Skip to content

Container: order floating children by depth alone, not by screen position - #1629

Merged
obiot merged 1 commit into
masterfrom
fix/floating-screen-space-depth
Aug 31, 2026
Merged

Container: order floating children by depth alone, not by screen position#1629
obiot merged 1 commit into
masterfrom
fix/floating-screen-space-depth

Conversation

@obiot

@obiot obiot commented Aug 31, 2026

Copy link
Copy Markdown
Member

The bug

Container.draw gives a floating child resetTransform() and the camera's screen projection, so its pos.x/y are canvas pixels — not a place in the world. _sortDepth fed those to a world-space distance and subtracted the camera position on top of that.

Two consequences, both under a Camera3d:

A HUD's layering depended on where it sat on the screen.

screen pos contributed to the distance result
score, in a corner (20, 16) 656 sorted near → drew on top
banner, centred (512, 200) 302144 sorted far → scenery drew over it

Same floating, same depth. Only the screen position differed.

And it drifted as the camera travelled, via the (z - camZ)² term — a HUD correct at the start of a level was buried by the end of it, which is the kind of thing that gets papered over with a per-frame hud.depth = camera.pos.z + n workaround.

The fix

A floating child is ordered by |pos.z| alone. World children are untouched.

Magnitude is what the screen-space idioms in this repo already encode, and the old key squared the distance, so the sign never carried meaning:

world.addChild(hud, -150);        // small -> near -> in front of the scene
world.addChild(backdrop, -10000); // large -> far  -> behind the scene
world.addChild(backdrop, 100000); // equally far; the sign is not read

That is the flight demo's HUD at -150, and the glTF Scene, glTF Animated Model, Billboard and Night City examples parking a floating sky at -10000, and Instanced Forest at 100000. Treating a negative depth as "nearest" pulls those four skies in front of their own scenes and leaves nothing but a gradient — so the sign is ignored.

Tests

tests/floating-depth-sort.spec.js, 21 tests: both idioms at extreme camera positions, a backdrop with the camera sitting on its own depth, an overlay after 8000 units of travel, sign symmetry, and the world path asserted unchanged.

Checked against four mutations, each of which is a fix someone could plausibly write:

mutation tests failing
the pre-fix key 10 / 21
floating forced to the front 11 / 21
a signed key 5 / 21
keeping the screen coordinates, dropping only the camera term 6 / 21

Verification

  • 267 files / 6480 tests pass; lint, types and biome clean
  • every 3D example opened and inspected: glTF Scene, glTF Animated Model, Billboard, Night City, Instanced Forest, AfterBurner, Per-material Textures
  • 2D examples are unaffected — their worlds sort on "z", not "depth"

Supersedes #1628, which forced floating children to the front and blanked three examples.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N

…tion

`Container.draw` gives a `floating` child `resetTransform()` and the
camera's screen projection, so its `pos.x/y` are canvas pixels rather
than a place in the world. `_sortDepth` fed those to a world-space
distance and subtracted the camera position on top of that, with two
consequences under a `Camera3d`:

  - a HUD's layering depended on where it sat on the SCREEN. A score in
    a corner scored 20² + 16² and floated above the scene; the same text
    centred scored 512² + 200² and sank behind it.
  - it drifted as the camera travelled, via the (z - camZ)² term, so a
    HUD that was correct at the start of a level was buried by the end.

A floating child is now ordered by |pos.z| alone. Magnitude is what the
screen-space idioms already encode — the flight demo's HUD sits at -150,
and the glTF, Billboard, Night City and Instanced Forest examples park a
floating sky at -10000 or 100000 — and the old key squared the distance,
so the sign never carried meaning. Reading it as meaningful would put
those four skies in front of their own scenes; the new key ignores it.

World children are untouched.

Tests cover both idioms at extreme camera positions, and are checked
against four mutations: the pre-fix key (10 fail), floating forced to
the front (11), a signed key (5), and keeping the screen coordinates (6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
Copilot AI lite review requested due to automatic review settings August 31, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot obiot added the Bug label Aug 31, 2026
@obiot
obiot merged commit b0a512e into master Aug 31, 2026
6 checks passed
@obiot
obiot deleted the fix/floating-screen-space-depth branch August 31, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants