Skip to content

feat: mesh materials and mesh shadows - #429

Merged
oarriaga merged 6 commits into
paz-jaxfrom
refactor/renderer-style
Aug 9, 2026
Merged

feat: mesh materials and mesh shadows#429
oarriaga merged 6 commits into
paz-jaxfrom
refactor/renderer-style

Conversation

@oarriaga

@oarriaga oarriaga commented Aug 9, 2026

Copy link
Copy Markdown
Owner

#425 merged into refactor/renderer-style rather than paz-jax, because that was
its base as a stacked PR. The five mesh commits are therefore sitting on
refactor/renderer-style and never reached paz-jax. This lands them.

Contents are exactly what #425 reviewed: the renderer package split, plus
meshes honouring reflective/transparency/refractive_index, casting shadows and
receiving them. No new commits since that review.

Merge base is 481f4f3 and the branch diff is only these changes, so it applies
cleanly; a fast-forward is impossible only because #424's merge commit was
created on paz-jax.

renderer.py becomes paz/graphics/renderer/, one module per stage of the
pipeline: rays (the bounce loop), tiling, intersect, shade, shadow,
optics, material. The loop body now reads intersect -> shade -> advance
instead of burying the scatter step inside update_state.

Avoids "trace", which in a JAX-first library already means jit tracing:
trace_bounces is rays.render, trace_chunks is rays.render_chunks.

The four shape-rendering examples patched
paz.graphics.renderer.compute_soft_occlusion, which the split would have
silently turned into a no-op; they now patch renderer.shadow directly.
The tiling module is not called "tiles" because render() takes a public
tiles= keyword that examples pass by name.

No behaviour change. 106 tests pass in renderer_test.py and mesh_test.py
including all snapshots; all 16 benchmark gradients are bit-identical to
the pre-split values.
scene.stack_materials already compiles reflective, transparency and
refractive_index for every mesh, and both Material and
CookTorranceMaterial declare all three. The renderer threw them away and
substituted a hardcoded (0.0, 0.0, 1.0) row for all triangles, so a
mirror or glass mesh rendered as matte and opaque.

Gather the real values per primitive via triangle_hit.primitive and
select between shape and triangle values on the closest hit.

Meshes that leave these fields at their defaults render identically, so
existing scenes are unaffected. Two new mesh_test cases cover a
reflective and a transparent mesh; both fail before this change.
Shadow rays only ever intersected shapes, so a mesh lit from above cast
nothing onto the floor beneath it. Shadow rays now also traverse the
triangles and join the shape blockers as one more caster row before the
soft-occlusion reduction.

select_meshes never received shadow_mask, so the public "this node does
not cast" control had no mesh equivalent. CompiledScene gains
triangle_shadow_mask and scene.compile fills it, which keeps that
control working for meshes instead of silently ignoring it.

The shape path rejects a shadow ray re-hitting its own surface by shape
identity. Triangles have no such index, so they use a distance
threshold instead; meshes do not receive shadows yet, so today the
receiver is always a shape and the threshold only guards shape
receivers close to a mesh.

Two mesh_test cases cover casting and the mask suppressing it; both fail
before this change.
The invented TRIANGLE_SELF_HIT_EPSILON was unexercised: sweeping it over
five orders of magnitude across three scene layouts produced
byte-identical renders, because with only shape receivers a shadow ray
can reach a triangle at near-zero depth only if the shape is coincident
with the mesh. Use the EPSILON the shape path already applies to
non-self hits instead of a new magic number.
compute_triangle_colors shaded with compute_colors and no occlusion, so
a mesh was never darkened by anything and a mesh-only scene had no
shadows at all regardless of the shadows flag. Triangles now take the
same per-light occlusion the shape path uses.

compute_occlusion takes a Receiver (points, normals, shape indices)
instead of a Hit, so triangle hit points can be fed in; a triangle
receiver uses NO_SHAPE so the shape self-hit rejection never matches it.
Shape blockers are now skipped when a scene has no shapes, which a
mesh-only scene with shadows would previously have crashed on.

Reinstates TRIANGLE_SELF_HIT_EPSILON, this time with evidence: on a
sphere mesh self-shadowing at its terminator, values at or below 1e-5
leak self-hits, the effect saturates at 1e-3, and 1e-3 to 5e-2 are flat.
The spurious self-shadow it removes is worth up to 9 grey levels.
@oarriaga
oarriaga merged commit 7d8c498 into paz-jax Aug 9, 2026
@oarriaga
oarriaga deleted the refactor/renderer-style branch August 9, 2026 05:21
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