Mesh Painters

From K-3D

Jump to: navigation, search


Historically the K-3D MeshInstance node performed two tasks:

  1. It associated a mesh with a transformation, positioning it within the virtual world.
  2. It used the OpenGL and RenderMan APIs (but not, interestingly, Yafray) to "render" the transformed mesh.

In its first capacity, MeshInstance makes it possible to efficiently create multiple visible "instances" of a single mesh within a scene (thus, the name). Unfortunately, the rendering code in MeshInstance grew to cause many problems:

  • As the number of options for rendering meshes grew, the amount of code in MeshInstance got out-of-hand, leading to maintenance problems.
  • The complexity of the MeshInstance code became a barrier to adding new rendering styles or capabilities (such as textured previews, use of GLSL, etc).
  • There was a corresponding growth in the number of properties to control rendering. Although these properties allowed fine-grained control over the appearance of a mesh in the viewport, it was impractical to setup MeshInstance nodes that shared some common (nonstandard) appearance.
  • Individual MeshInstance resource consumption became significant, reducing its value as a lightweight object that could be used to populate a scene with many copies of a mesh - think soldiers in ranks, or atoms in a molecule.
  • Lost caching opportunities - while an individual MeshInstance could cache data for efficient rendering (SDS cache, OpenGL display lists, etc), this data could not be shared between multiple MeshInstance nodes, even if they were are rendering the same mesh.

New Design

Beginning in K-3D 0.7, we define a new interface class - k3d::gl::imesh_painter:

namespace k3d

namespace gl

class imesh_painter :
  public iunknown
  virtual void paint_mesh(const mesh&, const painter_render_state&) = 0;
  virtual void select_mesh(const mesh&, const painter_render_state&, const painter_selection_state&) = 0;
  virtual void mesh_changed(const mesh&, iunknown* Hint) = 0;

} // namespace gl

} // namespace k3d

Then, we create document plugins that implement k3d::gl::imesh_painter, with each plugin implementing one drawing function, e.g:

  • One mesh painter plugin for drawing points.
  • One mesh painter plugin for drawing polygon faces.
  • One mesh painter plugin for drawing bilinear patches.
  • One mesh painter plugin for drawing bicubic patches.
  • And-so-on.

This cleanly separates the many types of drawing to be performed, and allows each mesh painter plugin to apply domain-specific optimizations. More specialized rendering can also be encapsulated in separate mesh painter plugins, including:

  • SDS preview.
  • Point proxy.
  • Bounding-box proxy.
  • Visualizing face normals.
  • Replacing the ShowComponentNumbering plugin, which didn't work correctly with transformed nodes.

Controlling the appearance of a mesh thus becomes a matter of control over the set of mesh painters used to draw it. The imesh_painter interface is designed so that "pass-through" calls make sense, one "parent" mesh painter might have multiple "child" mesh painters that are called when the parent is called. Such a "collection" mesh painter can provide support for an arbitrary number of children using user properties. Each MeshInstance node then maintains a pointer to a single mesh painter node.

Many MeshInstance nodes can (and typically do) reference a single mesh painter, allowing global control over render behavior (such as turning wireframe edges on-and-off for the entire scene). Conversely, different MeshInstance nodes can reference different mesh painters, allowing custom rendering styles on a MeshInstance-by-MeshInstance basis.

A "collection" mesh painter can also use logic to chose which children to call - a level-of-detail mesh painter could have pointers to mutally-exclusive children that render with varying degrees of fidelity, and dynamically choose which child to call at render-time based on distance or elapsed-drawing-time metrics.

New mesh painters can be authored, tested, and put into production without making any changes to MeshInstance. Alternative rendering algorithms, hardware-specific functionality, etc. can be developed and tested without affecting production code.

Note that each painter is responsible for handling both drawing and selection. This allows a painter to share implementation and cached data between drawing and selection operations. It further ensures that drawing and selection operations are symmetric and that no additional logic is required on the part of callers to determine when/if a painter supports selection operations.

Implementation Details

When the imesh_painter::paint_mesh(const mesh&, const painter_render_state&) method is called, the mesh painter implementation renders the supplied mesh. The OpenGL modelview matrix will already be setup by the caller prior to the call to paint_mesh(). The painter_render_state object is used to pass additional state which the painter may (or may not) use to alter its behavior. Because a single mesh painter may be used to draw zero-to-many meshes, any data that is cached must be stored in a mapping, keyed either to the mesh or some part of it (see Array Based Mesh Design).

The imesh_painter::select_mesh(const mesh&, const painter_render_state&, const painter_selection_state&) method handles rendering for interactive selection - typically it renders the supplied mesh making additional OpenGL calls used to supply metadata used by the selection mechanisms. Ideally, some select_mesh() implementations will be able to share cached data with their corresponding paint_mesh() implementations.

The imesh_painter::mesh_changed(const mesh&, iunknown*) method is called whenever a mesh is about to be modified (or destroyed). The mesh pointer implementation should release any cached data for that mesh.

See Also

Performance, Array Based Mesh Design, Negative Scale and Category:Painters.

Personal tools