Skip to content

Split MeshData into shared MeshGeometry, MeshInstance and BasicShading - #25

Merged
mtao merged 5 commits into
feature/controls-modelfrom
refactor/mesh-geometry-shading
Oct 14, 2026
Merged

mtao merged 5 commits into
feature/controls-modelfrom
refactor/mesh-geometry-shading

Conversation

@mtao

@mtao mtao commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

MeshData combined render-agnostic mesh data with one specific render setup: the position/normal/scalar roles and render state of the built-in lit, colour-mapped shader. This PR separates the two: geometry becomes a shared resource, and how an object renders it becomes a feature. Other shading contexts (textured, points, volumes, …) can later use the same geometry, and several objects can show one geometry.

New types

  • MeshGeometry (a plain class, not a feature) holds named, typed attributes on vertices/edges/faces. Its version() bumps on any geometry change. It knows nothing about rendering. owns(handle) is public so shadings can check that an attribute belongs to it.
    • Topology is stored as attributes: .face_vertices (Face domain, uint32, 3 × F) and .edge_vertices (Edge domain, 2 × E, derived from the triangles unless set explicitly). The face and edge counts are those attributes' counts, so face/edge attributes have a single source for their element count; a mismatch is kept but logged as a warning.
    • Reserved names: names starting with . are reserved. The public add_attribute/remove_attribute reject them, so topology changes only through set_triangle_indices / set_edge_indices.
    • Typed index views: triangle_indices() / edge_indices() return column-major mdspan<const uint32_t, extents<K, dyn>> views (one column per element), so the shape is in the type.
    • Copying: it isn't copyable, since a default copy would share attribute instances; copy_from makes a deep copy.
    • Converting index input: input in either orientation and any integer type is converted with zipper (cast, plus transpose for N × K) instead of a hand-written copy loop. A static extent of K decides the orientation; rows win only when both extents are dynamic.
  • MeshInstance (feature) holds a std::shared_ptr<MeshGeometry>: the geometry an Object shows. Several objects can show the same geometry, and the geometry outlives them. set_geometry switches it.
  • BasicShading (feature) holds the position/normal/scalar role assignments and MeshRenderState. It reads its geometry through the Object's MeshInstance (geometry()).
    • Lazy resolution: bindings are resolved against the geometry whenever the geometry's version changes or the instance shows different geometry. Attributes replaced in place are followed, removed attributes unbind their roles, and magnitude bindings are recomputed. This replaces the inline bookkeeping MeshData did when attributes were added or removed.
    • Auto-binding: a role you haven't set yourself binds vertex_positions / vertex_normals automatically. A role you explicitly cleared stays cleared when the geometry changes.
    • Versioning: version() bumps only on role changes. Geometry changes are covered by MeshGeometry::version().
    • Missing geometry: without a geometry (no MeshInstance, an instance with no geometry, or not attached to an Object yet), all roles are unbound and assignments fail.
  • MeshAttribute/MeshAttributeHandle and RoleBinding move to their own headers. RoleBinding gains whole / component / magnitude factories.

Callers

  • VulkanMeshDrawable re-uploads buffers when either version changes, and reads render state from BasicShading.
  • MeshScene::add_mesh creates an Object with a MeshInstance (showing a new, empty geometry), a BasicShading and a drawable. ensure_drawable applies to objects with a MeshInstance, and adds a BasicShading if one is missing.
  • Control panels: PanelContext::active_mesh_data() is replaced by active_geometry() / active_shading().
    • The Data and Material panels edit the shading; their attribute lists come from the shading's geometry.
    • The Object tab's Mesh info shows the vertex count from the shading and the triangle/edge counts from the geometry.
  • to_mesh_data becomes to_mesh_geometry. normalize_to_unit_box takes the BasicShading.
  • The Qt and ImGui scene widgets and both viewer tools are updated.
  • Duplicate in the ImGui scene panel now works. Before, the copy had empty geometry and never got a drawable, so it didn't render.
    • The copy shows the same geometry (instancing), with its own shading copied by BasicShading::copy_from. That copies the render state and role assignments; each assigned role points at the same-named attribute of the copy's geometry, and explicitly cleared roles stay cleared.
    • The copy and its descendants get drawables through MeshScene::ensure_drawable.
    • MeshGeometry::copy_from (a deep copy with new attribute instances) is available when an independent copy is wanted.
  • Small addition: AbstractFeature::is_attached().
  • magnitude_of evaluates colwise().norm() directly into an owning vector now that colwise()/rowwise() don't work on temporary expressions zipper#42 is fixed.
  • The attribute role pickers in the control panels skip reserved (topology) attributes.

Tests

  • Pure geometry tests (edges, topology, index layouts, attribute storage) use a bare MeshGeometry. Role tests use an Object with both features.
  • New coverage:
    • geometry and shading version counters bump independently
    • a cleared role stays cleared after geometry changes
    • a shading without geometry, and geometry added to its Object later
    • attributes from another geometry are rejected
    • a magnitude binding is recomputed when its attribute is replaced, and unbinds on removal
    • normalization works from double positions
    • copy_from produces independent attributes, re-targets roles by name, and keeps cleared roles cleared
    • two objects sharing one geometry with independent role bindings; geometry changes visible through both; the geometry outliving its objects; switching an instance to other geometry
    • topology stored as attributes: domains, types and shapes, rejection of reserved names, and copying
    • a static 3 × 3 index input treats rows as triangles; a 2 × N input with N = 2 keeps one column per edge
  • meson test: 13/13 suites pass. test_scene_graph: 547 assertions in 62 test cases.

mtao added 5 commits October 12, 2026 22:48
MeshData mixed render-agnostic mesh data with one fixed render setup.

- MeshGeometry (feature): named, typed attributes plus triangle/edge
  topology; version() bumps on any geometry change.  No roles.
- BasicShading (feature on the same Object): position/normal/scalar role
  assignments and the MeshRenderState.  Bindings are resolved lazily
  against the Object's MeshGeometry whenever its version changes:
  replaced attributes are followed, removed ones dropped, magnitudes
  recomputed, and roles not set explicitly bind vertex_positions /
  vertex_normals.  version() bumps on role changes only.
- MeshAttribute and RoleBinding move to their own headers; RoleBinding
  gains whole/component/magnitude factories.
- VulkanMeshDrawable re-uploads when either version changes; MeshScene,
  the Qt/ImGui scene widgets, the control panels (PanelContext gets
  active_geometry()/active_shading()), normalize_to_unit_box (takes the
  shading) and to_mesh_geometry follow.
- AbstractFeature::is_attached().
- magnitude_of uses colwise() on the cast expression directly now that
  zipper#42 is fixed.
- MeshGeometry::copy_from: deep copy of attributes (new instances) and
  topology.
- BasicShading::copy_from: render state plus role assignments,
  re-targeted by attribute name to this shading's geometry; explicitly
  cleared roles stay cleared.
- ImGui Duplicate uses both and ensures drawables on the copy and its
  descendants (previously duplicates had empty geometry and were never
  drawn); deep_duplicate drops its unused MeshScene parameter.
With zipper#42 fixed the norms no longer need a hand-allocated buffer and
span view; eval() the expression and build the attribute from its mdspan.
- Triangles and edges are uint32 attributes on the Face/Edge domains:
  .face_vertices (3 x F) and .edge_vertices (2 x E, derived from the
  triangles unless set explicitly).  Names starting with '.' are reserved:
  add_attribute/remove_attribute reject them, and Edge/Face attributes
  whose count differs from the edge/face count get a warning.
- triangle_indices()/edge_indices() return column-major
  mdspan<const uint32_t, extents<K, dyn>> views; counts are extent(1).
- Index input in either orientation is converted with zipper (cast, plus
  transpose for N x K) instead of a hand-written copy; a static extent of
  K decides the orientation, rows win when both are dynamic.
- Role pickers in the controls skip reserved attributes.
Geometry is data that Objects refer to, not a component of one Object:
- MeshGeometry no longer derives from AbstractFeature (and is no longer
  copyable; copy_from makes deep copies).
- New MeshInstance feature holds std::shared_ptr<MeshGeometry>, so several
  Objects can show one geometry, each with its own shading.
- BasicShading reads the geometry through the Object's MeshInstance and
  re-resolves when it shows different geometry.
- MeshScene/Qt add an Object with a MeshInstance showing new geometry;
  ImGui Duplicate shares the source's geometry and copies its shading;
  VulkanMeshDrawable, panels and tools read geometry via the instance.
@mtao mtao changed the title Split MeshData into MeshGeometry and BasicShading Split MeshData into shared MeshGeometry, MeshInstance and BasicShading Oct 14, 2026
@mtao
mtao merged commit 286abf7 into feature/controls-model Oct 14, 2026
2 checks passed
@mtao
mtao deleted the refactor/mesh-geometry-shading branch October 14, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant