Skip to content

Matplotlib mesh renderer: walkthrough & observations #19

@jeromeetienne

Description

@jeromeetienne

How the matplotlib mesh renderer works — src/gsp_matplotlib/renderer/matplotlib_renderer_mesh.py.

Pipeline

RendererMesh.render(renderer, viewport, mesh, model_matrix, camera) returns a list of matplotlib Artists.

  1. Pull buffers (matplotlib_renderer_mesh.py:45-71) — TransBufUtils.to_buffer then Bufferx.to_numpy on positions, indices, model/view/projection matrices, face colors, edge colors, edge widths. Colors are normalized from 0-2550-1 (/ 255.0).

  2. Per-vertex → per-face broadcast (matplotlib_renderer_mesh.py:79-95) — matplotlib's PolyCollection wants one color/width per face. A local _to_per_face helper handles three cases:

    • already per-face → leave alone
    • per-vertex → pick the value at the first vertex of each triangle (array[indices[:, 0]])
    • length-1 → broadcast to face_count
  3. MVP transform (matplotlib_renderer_mesh.py:110-113) — MathUtils.compute_mvp_matrix(...) then apply_transform_matrix(vertices, mvp) → NDC. Reshape into (face_count, 3, 3) triangles.

  4. Drop Z (matplotlib_renderer_mesh.py:120) — faces_vertices_2d = faces_vertices_ndc[..., :2]. Z is kept around for sorting only.

  5. Painter's sort (matplotlib_renderer_mesh.py:146-156) — when face_sorting=True, sort faces by mean NDC z descending (far → near), and re-index colors/edge_colors/edge_widths with the same permutation. Comment notes the limitation: this works within the artist; cross-object ordering would need set_zorder and is currently disabled.

  6. Face culling (matplotlib_renderer_mesh.py:162-168) — delegates to RendererUtils.compute_faces_visible which uses the 2D cross product of edges. Sign decides CCW vs CW; magnitude < 1e-6 is treated as degenerate. FrontSide/BackSide/BothSides each pick a different predicate. Filter mask is applied to all per-face arrays.

  7. Artist cache (matplotlib_renderer_mesh.py:173-185) — one PolyCollection per mesh uuid, stored in renderer._artists. Created hidden on first render, then set_visible(True).

  8. Update artist (matplotlib_renderer_mesh.py:199-202) — set_verts, set_facecolor, set_edgecolor, set_linewidth.

Things to notice

  • uvs and normals are completely ignored — the matplotlib path is "flat triangles in screen space"; no shading, no texturing. That fits MeshBasicMaterial's name, but reinforces the open question from Mesh implementation: overview & rough edges #18 about whether those buffers should be required at construction.
  • Per-vertex color collapses to the first vertex of each triangle — no per-face averaging, no gouraud interpolation. So an OBJ with per-vertex colors will look blocky and asymmetric (rotating the index order changes the look).
  • Culling uses the screen-space cross product, not a real normal. This is correct for triangles after a perspective divide (the sign of the 2D cross product matches the front-facing orientation), but it diverges from any culling that would use geometry.normals.
  • Mesh.sanity_check_attributes_buffer is called at matplotlib_renderer_mesh.py:101, but that method is a pass — so the call is currently free, but also free of safety. Same observation as Mesh implementation: overview & rough edges #18.
  • Z-order across objects is commented out (matplotlib_renderer_mesh.py:187-192) — RendererUtils.update_single_artist_zorder exists but is disabled, so two overlapping meshes will paint in registration order, not depth order.
  • Camera position is never directly used — culling depends only on post-MVP geometry, so the camera enters only through the view/projection matrices.

Links pinned to commit 6f3b86c.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions