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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions