Skip to content

Lit meshes under a Camera2d: populate normals so directional/ambient lighting works #1576

Description

@obiot

Summary

lit: true only does anything under a Camera3d. On the 2D-camera (accumulated) path the engine never writes Mesh#normals — the array is documented as "world-space normals for the current draw, recomputed from originalNormals along the Camera3d path" — so the lit batcher pushes zeros and the shader has no surface to shade against.

Until recently that rendered black (normalizing a zero vector gives NaN). That is now guarded: a lit mesh with no usable normals degrades to unlit on both backends, which is recognisably the model rather than a hole. This ticket is about making it actually light.

Why it is not just "turn it on"

1. Normals need a CPU transform on that path. The Camera3d path rotates them on the GPU (mat3(uModelMatrix) * aNormal). The accumulated path has no model matrix at draw time, so normals would have to be rotated per vertex on the CPU. That is acceptable on this path specifically — it already CPU-projects every vertex, so it fits its own cost model rather than adding a new penalty. It is precisely the work #1507's retained path exists to avoid, which is presumably why it was never wired up.

2. Positional lights cannot work as-is — only directional and ambient. Lighting is evaluated in world space: the fragment stage compares vWorldPos against each light's world position for point/spot falloff. Under a 2D camera the vertices are the projected output, so vWorldPos would carry screen-ish coordinates and a point light's range would attenuate against projected pixel distances. That is not merely approximate, it is meaningless — and worse, it would look plausible.

Proposal

  • Rotate originalNormals by the mesh's model rotation into normals on the accumulated path, so directional + ambient shading works.
  • Make point and spot lights explicitly not apply there — ignored with a one-shot warning naming the camera, rather than silently shading against a bogus position.
  • Optionally, later: thread a genuine world-space position through as a separate varying to lift that restriction, at the cost of another per-vertex attribute on a path that is already CPU-bound. Worth measuring before committing to it.

Acceptance

  • An OBJ or raw-geometry mesh with lit: true under a Camera2d shades from a Light3d directional light plus ambient, and matches the Camera3d result for the same orientation.
  • A point/spot light warns once and does not contribute, instead of producing a wrong result.
  • A mesh with no normals at all still renders unlit (the current guard) rather than black.

References

src/renderable/mesh.js (originalNormals / normals, _projectVertices), src/video/webgl/batchers/lit_mesh_batcher.js (_pushVertex reads mesh.normals), src/video/webgl/shaders/mesh-lit.frag and src/video/webgpu/shaders/mesh-lit.wgsl (the zero-normal guard). Related: #1572 (OBJ normals), #1507 (retained path).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions