Skip to content

P7: orthographic cascades, level draw order, particle and splat fog - #77

Open
dzolotov wants to merge 6 commits into
0.9.0from
wf/p7-ortho-coverage-order
Open

dzolotov wants to merge 6 commits into
0.9.0from
wf/p7-ortho-coverage-order

Conversation

@dzolotov

@dzolotov dzolotov commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

The rest of P7 (§4.1), apart from the parts listed under "Left".

What changed

Shadow cascades under an orthographic camera. The cascades were split by distance from the eye. An orthographic camera often sits tens of metres back, so the near cascades covered empty air in front of the lens and every shadow came from the whole-level map. With an orthographic camera, the renderer now finds the depths where the view box meets the casters' bounds, caps that range at viewDistance, and cuts it into equal slabs, one per near cascade. Each slab's sphere is fitted to the corners of that slab's part of the box and the bounds (orthographic_cascades.dart). The last cascade still covers the whole scene, and the reach back towards the light is kept. Through that lens ShadowFactor picks a cascade by depth along the view axis. The light shafts and the volumetric fog pick by distance travelled along the ray, which through a perspective lens is the same as before. The software backend mirrors all three. Adds the orthographic-shadow golden to all four sets.

Draw order in the level format. New field Brush.drawOrder. It is optional and left out of the document when it is 0, so existing levels keep their bytes and their digest. The validator rejects values outside −128..127. Brush surfaces are batched by it and LevelScene sets MeshNode.drawOrder. Breaches and duplicates keep it. An edit that only changes the order is look-only: it shows up in LevelDiff.brushOrder, both from diffLevel and from a LevelPatch.

Particles' and splats' fog and facing (this also closes the P5 tail about height fog on particles). These stages now declare the whole FogInfo block, in lib/particle_fog.glsl. With an orthographic camera they fog by depth, and a mesh particle faces the view axis. Height fog is now integrated along the ray the same way as in ApplyFog. Both particle contributors and the splat contributor bind the new members, and the CPU stages mirror them. The uniform tables and the web shader tables are regenerated.

Splat sort by depth along the axis under an orthographic camera. The cloud is re-sorted when the axis turns.

How it was tested

  • New tests, each checked by mutating the code:
    • orthographic_cascade_test.dart
    • orthographic_cascade_pick_test.dart
    • brush_draw_order_test.dart
    • level_loader_test.dart (draw order)
    • particle_fog_test.dart
    • splat_fog_test.dart (orthographic)
    • particle_mesh_facing_test.dart
    • splat_sort_test.dart (axis)
  • One exception, stated in the test itself: the "three cascades shadow what one does" check has no mutation that makes it fail, because a miss falls through to the whole-level map.
  • Full suites pass: flutter3d (1760), flutter3d_core (835), flutter3d_cpu (549), flutter3d_sim (732), flutter3d_particles (105), flutter3d_app (168), flutter3d_editor_core (153), flutter3d_game (324), flutter3d_impeller (171), flutter3d_hardware (70), flutter3d_shaders (5). Cross-backend tests also pass for cpu, webgl and webgpu.
  • Goldens:
    • All 91 Impeller references pass after both shader changes.
    • Software set: the particle, splat, fog and orthographic scenes pass.
    • WebGL2 and WebGPU: 14 affected scenes, all at 0 differing pixels.
    • orthographic-shadow against Impeller: WebGPU 0 pixels, WebGL 0.394%, software 1.043%. The differences are on edges, which Impeller multisamples and the others do not.
  • tool/structure.dart: all 35 rules hold. tool/verify_plan.dart is clean.

Left

  • A near plane at or behind the eye. The surface buffer reads a depth of 0 or less as sky, and every pass that reads the buffer would need changing.
  • The draw order in the editor inspector. This waits for the inspector rework.
  • The example's mesh-overlay golden still passes a made-up perspective to MeshOverlay.lookFrom.
  • The MCP tools.
  • No golden draws particles or splats under an orthographic camera or in height fog. Those paths are covered only by the software tests.

The cascades were split by distance from the eye, half way to logarithmic,
which is right through a perspective lens and wrong through an orthographic
one: there every metre of depth gets the same texels on screen, and the eye
is only where the camera was put, often tens of metres back. The near
cascades covered air in front of the lens and every shadow came from the
last, whole-level map.

Under an orthographic camera the depths where the view box meets the
casters' bounds, capped at ShadowSettings.viewDistance, are now cut into
equal slabs, one per near cascade, each fitted to the corners of its own
share of the box and the bounds. The last cascade is still the whole scene
and the reach back towards the light is kept. ShadowFactor picks a cascade
by depth along the view axis through that lens; the light shafts and the
volumetric fog pick by the way along the ray from where depth is nought,
which is the same distance as before through a perspective lens. The
software backend mirrors all three.

orthographic-shadow is in all four golden sets; the other ninety Impeller
references still pass unchanged.
MeshNode.drawOrder had no word in the level format, so a stripe painted on a
floor of the same stone fought the floor pixel by pixel and the only fix was
to patch the node after loading. Brush.drawOrder is that word: optional, left
out of the document at nought so every level keeps its bytes and its digest,
and refused by the validator outside -128 to 127, the range the renderer's
sort key holds, rather than clamped into a tie.

Brush surfaces are batched by it, since a batch is drawn as one node with one
place in the order, and LevelScene sets it on the node. A cut brush's pieces
and a duplicated brush keep it. Nothing the simulation reads depends on it,
so an edit of the order alone is look-only: LevelDiff.brushOrder names the
brushes, from diffLevel and from a LevelPatch alike, and simulation stays
empty. Any other change to the same brush still goes to simulation.
Their stages declared the fog block with two of its four members, the
colour and the eye. They could not tell an orthographic camera from a
perspective one, so they fogged in rings round the eye's point and faced a
mesh particle towards it, and they could not read the height fog's falloff,
so a puff of smoke fogged at the camera's density whatever its height.

The block is now declared whole in lib/particle_fog.glsl, shared by every
particle and splat stage: depth from the eye's plane through an orthographic
lens, the view axis for a mesh particle's facing there, and the height fog
integrated along the ray as ApplyFog does. Both particle contributors and
the splat contributor bind the axis, the lens and the falloff, and the
software stages mirror it. Through a perspective lens with a flat fog
nothing moves: every particle and splat golden holds in all four sets.

Splats under an orthographic camera are also sorted by depth along the view
axis rather than by distance from the eye's point, and resorted when the
axis turns.
The section 4.1 row still said orthography, alpha to coverage and the order
were left. All three landed; this records the level format's draw order, the
orthographic shadow cascades, the particles' and splats' fog and facing, and
the splats' depth sort, with their tests and golden budgets. Left: a near
plane at or behind the eye, the order in the editor's inspector, the example
overlay's made-up perspective, the MCP tools, and goldens for particles and
splats under an orthographic camera or in a height fog. P5 no longer lists
height fog on particles and splats.
The near cascades under an orthographic camera were fitted to the corners
of each slab of the view box and the casters' bounds, so their radii and
slab ends moved with every pan and every caster that rose or fell. The
shadow pass snaps each map to whole texels of its radius, so that grid
rescaled every frame and shadow edges crawled, and a still tile was
redrawn because its scale had changed instead of being scrolled. The slab
ends and radii now round onto steps a sixteenth of their power of two;
orthographic-shadow is re-recorded in all four sets (shadow outlines move
by a texel).

Tests: a centimetre's pan and a caster rising five centimetres leave the
near radii and slab ends unchanged; the software shafts and volumetric fog
at the edge of an orthographic frame read the near tile; a breach and an
editor duplicate keep a brush's drawOrder. The shadow pass's aspect falls
back to one for a viewport with no height.
The step was taken from end - start, which in doubles can come back a
bit either side of the reach as start moves; at a reach that is a power
of two that doubles the step and moves every slab end and near radius.
The step now comes from min(far - start, reach) itself, and the power of
two is found by doubling and halving instead of a logarithm, so it is
exact by construction. Vector3's float corners keep the sum exact in
practice, so no test reaches the old case through the engine.
@dzolotov

dzolotov commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review of 1b509d2: ok. One follow-up pushed: the rounding step now comes from the depth range's length rather than end - start, and the power of two is found exactly, so a power-of-two viewDistance cannot double the step from rounding noise. Known limit kept: there is no hysteresis, so a fit sitting on a step boundary can toggle one step; the far cascade's radius is not rounded (older than this PR).

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