Skip to content

Split shadow cascades through an orthographic camera's box - #76

Closed
dzolotov wants to merge 3 commits into
0.9.0from
wf/ortho-shadow-cascades
Closed

dzolotov wants to merge 3 commits into
0.9.0from
wf/ortho-shadow-cascades

Conversation

@dzolotov

@dzolotov dzolotov commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Under an orthographic camera the directional shadow cascades were split by distance from the eye, mostly logarithmically, the way a perspective camera wants them. Through an orthographic lens the eye is just wherever the camera was put along its axis, usually tens of metres short of anything. So the near cascades covered empty air in front of the board, and every shadow ended up in the last cascade, which spans the whole level. On a big isometric board that means thin casters throw no shadow you can see.

What changed

  • _renderShadowMap (flutter3d_core) checks isOrthographic on the camera's matrix. When it's orthographic, the near cascades split evenly the depth that the camera's box and the casters' bounds have in common. An orthographic pixel needs the same texel size at every depth, so the split is even, not logarithmic. The common depth range comes from clipping each shape's edges against the other's half-spaces. On an isometric board that range runs from where the box's long edges hit the ground, not from the near and far planes.
  • The shading still picks a cascade by distance from the eye, so the thresholds are distances too. Each near cascade is a sphere fitted to the slab of the box its threshold can send to it. The last near threshold reaches the box's farthest corner. Radii round up in steps of an eighth of an octave, so panning over uneven ground doesn't keep resizing texels.
  • The last cascade still covers the whole scene, as a fallback for mirrors and probes, and the caster reach-back is unchanged. A near cascade wider than the scene sphere is replaced by the scene sphere. Perspective cameras split exactly as before.
  • The shadow node now gets the first view's aspect ratio so it can build the box. Renderer.debugCascadeSplits exposes the thresholds for tests.
  • ShadowSettings.cascadeSplit and viewDistance document how they behave under an orthographic camera.
  • New golden orthographic-shadow: a strategy board seen through an isometric orthographic camera from 60 m back. The floor is 120 m, far larger than the box. Blocks stand in for buildings and 12 cm posts stand in for units. Before this change the posts cast no visible shadow. With it they're as sharp at the top of the frame as at the bottom. orthographic-metal has shadows off and didn't move.

How it was tested

  • flutter3d_cpu/test/orthographic_test.dart, three new tests:
    • The first split falls inside the casters' depth range within the box. Before, it sat at 8.9 m, in front of everything, with the board at about 55 m and beyond.
    • A near post and a far post both land in near cascades, in different ones, with radii within 1.5x of each other and under 0.7 of the whole-scene radius.
    • Moving the eye along its axis changes a radius by at most one rounding step.
  • Mutation checks: with the orthographic branch removed, the first two tests fail. With an uneven first split (at 30% of the depth) the texel test fails.
  • orthographic-shadow is recorded in all four sets (tool/golden.sh, tool/golden.sh --cpu, tool/golden_web.sh, tool/golden_web.sh --backend=webgpu). Distance from Impeller: WebGPU 0 pixels, WebGL 0.433%, software 1.117%, all on edges (the posts are almost all edge). Budgets are set just above those numbers.
  • Full suites that pass: flutter3d_core (835), flutter3d_cpu (547, 1 skipped), flutter3d (1757), flutter3d_webgl VM (105), flutter3d_webgpu VM (322). tool/structure.dart: 35 rules, all held. Golden-scene and test counts in the docs are updated (91 scenes).

What is left

  • The shading still picks a cascade by distance from the eye, not by depth along the view axis. To keep the box's rim covered, the second cascade is fitted a bit wider, and an eye close to the board costs up to one rounding step of that radius. Picking by ViewDepth under an orthographic camera in shadow.glsl, the volumetric fog, the light shafts and the reflections (and their CPU mirrors) would remove that cost. That's a shader change on all four backends and isn't in this PR.
  • The browser (--platform chrome) suites of flutter3d_webgl and flutter3d_webgpu weren't run here. Neither package's code changed, only the count wording in one test comment and in the golden script.
  • The rest of P7's Left list is untouched: render order in the level format and the editor, particles' and splats' fog and facing, a near plane at or behind the eye, splat sorting, and the example overlay's lookFrom.

The cascades were split by distance from the eye, mostly logarithmically,
which is what a perspective camera wants. Through an orthographic lens
the eye is only where the camera was put along its axis, often tens of
metres short of anything, so the near cascades covered air in front of
the board and every shadow fell to the last cascade, the whole level.

When isOrthographic reads the camera's matrix, the near cascades now
share evenly the depth the camera's box and the casters' bounds have in
common, since a pixel needs the same texel at every depth. The shading
still picks a cascade by distance from the eye, so the thresholds are
distances and each sphere is fitted to the slab of the box its threshold
can send to it. Radii are rounded up an eighth of an octave at a time.
The last cascade stays the whole scene and the caster reach-back is
kept. The shadow node is handed the first view's aspect to build the box.

Renderer.debugCascadeSplits exposes the thresholds for tests.
orthographic_test.dart: the first split lies inside the casters' depth
in the box, near and far posts land in near cascades of similar radius,
and the eye's place along the axis moves a radius by one step at most.
A strategy board through an isometric orthographic camera: a floor far
larger than the box the camera sees, blocks for buildings and thin posts
for units from the near edge of the view to the far one. With the
cascades split from the eye the posts cast no shadow that could be seen;
split through the box they are as sharp at the top of the frame as at
the bottom.

WebGPU lands at 0 pixels from Impeller, WebGL at 0.433% and the software
set at 1.117%, all on edges; the budgets are set just above. The golden
scene and test counts in the documents follow.
What was done and how it was tested, and what stays left: the shading
still picks a cascade by distance from the eye rather than by depth
along the view axis, which costs an eye close to the board up to one
rounding step of the second cascade's radius.
@dzolotov

dzolotov commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Closed in favour of #77, which makes the same orthographic cascade change and also covers the cascade pick in the fog and shafts, the level's draw order, and fog for particles and splats.

@dzolotov dzolotov closed this Oct 3, 2026
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