Skip to content

Box3d: 3D collision resolution (#1476), plus two glTF colour fixes - #1583

Open
obiot wants to merge 6 commits into
masterfrom
platformer25d-1476
Open

Box3d: 3D collision resolution (#1476), plus two glTF colour fixes#1583
obiot wants to merge 6 commits into
masterfrom
platformer25d-1476

Conversation

@obiot

@obiot obiot commented Aug 11, 2026

Copy link
Copy Markdown
Member

Groundwork for #1476 — the engine side only. The example itself is not in this PR, so this does not close the issue.

Box3d — collision along Z

A body can now be resolved along Z as well as X and Y. Every other built-in shape is planar and goes through the 2D SAT narrowphase, whose minimum translation vector is a Vector2d — a depth gate could filter candidate pairs, but nothing could produce a Z pushback, so a character in a 2.5D scene could be pushed around the screen plane and never into or out of it.

  • Box3d — centre + half-extents, with a cached XY footprint so every 2D consumer (body bounds, broadphase pre-gate, debug draw) needs no 3D awareness
  • testBox3dBox3d — AABB-vs-AABB MTV, filling the new ResponseObject.overlapZ / overlapNZ
  • BodyvelZ, forceZ, frictionZ, maxVelZ
  • raycast3d — exact ray-vs-AABB slab test for bodies carrying a Box3d, replacing a bounding-sphere approximation whose radius came from a 2D bounds with no depth at all (a wide floor slab read as an enormous sphere and reported a hit well above its surface — useless for probing floor height)

The 2D path does not change

Z arrives as scalars beside overlapV / overlapN / body.vel rather than by widening them: Vector3d is not a subclass of Vector2d, so retyping would break every existing consumer. The MTV is a single axis, so a planar pair leaves every Z field at 0 and the added arithmetic is inert. Mixed pairs treat the planar shape as unbounded along Z, so introducing one Box3d body into an existing 2D game leaves its world shapes colliding exactly as before.

Two pre-existing glTF colour bugs, found on the way

  • baseColorFactor was never sRGB-encoded. The spec defines it in linear; a tint is 8-bit sRGB. The loader scaled by 255 and used it directly, so every untextured glTF material rendered washed out — an authored mid-green (linear 0.29) arrived as sRGB 0.58.
  • An indexed (palette) PNG rendered greyscale on Safari. The glTF parser decoded to HTMLImageElement, unlike the rest of the loader, which produces an ImageBitmap. Invisible for RGBA sources; for PNG colorType 3, WebKit's copyExternalImageToTexture uploads the raw palette indices, so the texture arrives with r == g == b. Other browsers normalise at decode, and WebGL's texImage2D is unaffected, so it presented as a WebGPU regression in one browser only. Nothing in the WebGPU API can fix this at upload time — palette expansion belongs to the decoder.

Also fixed along the way: Body.destroy() threw for any shape class not registered with the legacy pool (aborting teardown half-done and surfacing later as a TypeError in the broadphase), and an unknown shape-type pair crashed the physics step instead of warning.

Tests

240 files / 5925 passing. 66 new tests.

The differential sweeps compare testBox3dBox3d against a brute-force AABB test over 4000 seeded-random pairs, asserting both directions and reporting a rate rather than failing on the first mismatch. The MTV sweep separately checks the reported pushback actually separates the pair.

Each load-bearing test was verified to fail against the broken code — removing the Z separation check, flipping the Z MTV sign, disabling the box raycast path, and reverting each glTF fix all produce failures. The glTF sRGB suite tests the loader wiring, not just the helper: the helper-only tests still pass if the loader stops calling it.

AfterBurner example — two unrelated corrections

  • Ground shadows off. Its "ground" is a screen-space stroked line grid, not geometry, so a world-space blob has nothing correct to land on and drifts as the camera pitches. It was only casting them because the application default ships true.
  • antiAlias: true. The jet, enemies and props are low-poly models with long straight edges that aliased badly as they receded. This example composites through post effects, so it depends on capture targets being multisampled too (Tracking: WebGL 2 capabilities unblocked by the WebGL2-only baseline (#1509 scope C) #1556). Measured on the horizon band: intermediate-coverage pixels along the mountain silhouette rise from 8.5% to 27.0%.

Also

The unreleased CHANGELOG section is condensed in its own commit — 51 KB → 21 KB, no entry removed and no issue link dropped. Entries had grown into multi-paragraph essays; they are now one or two sentences each. Released sections are untouched.

Verified

Rendering checked on WebGL2, Chrome/WebGPU and WebKit/WebGPU — the Safari bug above is invisible to a SwiftShader harness, which silently falls back to WebGL2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QVjYzf76AEU3wJk766JAQi

obiot and others added 4 commits August 12, 2026 07:09
Adds a `Box3d` body shape so a body can be resolved along Z as well as X
and Y. Every other built-in shape is planar and goes through the 2D SAT
narrowphase, whose minimum translation vector is a `Vector2d` — a depth
gate can filter candidate pairs, but nothing could produce a Z pushback.

- `Box3d` (geometries/box3d.ts): centre + half-extents, with a cached XY
  footprint so every 2D consumer (body bounds, broadphase pre-gate, debug
  draw) needs no 3D awareness. The footprint is floored at a minimum edge
  because `Polygon.recalc` normalizes edges with no zero guard and would
  otherwise emit NaN normals for a zero-size box.
- `testBox3dBox3d` (physics/builtin/sat3d.js): AABB-vs-AABB MTV, filling
  the new `ResponseObject.overlapZ` / `overlapNZ`.
- `Body`: `velZ`, `forceZ`, `frictionZ`, `maxVelZ`, and a `hasDepth` flag.
- `raycast3d`: exact ray-vs-AABB slab test for bodies carrying a `Box3d`,
  replacing a bounding-sphere approximation whose radius came from a 2D
  bounds with no depth — useless for probing floor height.

The 2D path is unchanged. Z arrives as scalars beside `overlapV` /
`overlapN` / `body.vel` rather than by widening them: `Vector3d` is not a
subclass of `Vector2d`, so retyping would break every existing consumer.
The MTV is a single axis, so a planar pair leaves every Z field at 0 and
the added arithmetic is inert. Mixed pairs treat the planar shape as
unbounded along Z, so introducing one `Box3d` body into a 2D game leaves
its world shapes colliding exactly as before.

54 tests, including differential sweeps against a brute-force AABB test
(seeded LCG, reported as a rate) and a compat suite pinning that 2D-only
responses are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVjYzf76AEU3wJk766JAQi
`pbrMetallicRoughness.baseColorFactor` is defined by the glTF spec in
linear space; a melonJS tint is 8-bit sRGB, the same space as a CSS colour
or a PNG texel. The loader scaled the linear value by 255 and used it as
the tint, so every untextured glTF material rendered lighter and less
saturated than authored — a mid-green (linear 0.29) arrived as sRGB 0.58
and read as pale mint.

Adds `linearToSrgb8` and applies it at both tint sites. The helper clamps
its domain as well: an exporter emitting a slightly out-of-range factor
would previously have reached `Math.pow` with a negative base and NaN-ed
the whole tint.

Tested at both levels — the transfer function itself, and the loader
wiring (a `GLTFModel` built from a known factor must arrive at the encoded
tint). The wiring tests are the load-bearing ones: the helper tests alone
still pass if the loader stops calling it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVjYzf76AEU3wJk766JAQi
An indexed (palette) PNG texture in a glTF asset rendered greyscale on
Safari: geometry and animation intact, all colour gone (r == g == b).

The glTF parser decoded images to an `HTMLImageElement`, unlike the
ordinary image loader, which has always produced an `ImageBitmap`. That
difference is invisible for an RGBA source but not for an indexed one
(PNG colorType 3): WebKit's `copyExternalImageToTexture` uploads such an
image as its raw palette indices rather than expanding them through the
palette. Other browsers normalise the image at decode, and the WebGL
backend's `texImage2D` is unaffected, so it presented as a WebGPU
regression in one browser only.

Nothing in the WebGPU API can correct this at upload time — `flipY`,
`premultipliedAlpha` and `colorSpace` are the only knobs, and expanding a
palette belongs to the decoder. glTF images therefore now decode to an
`ImageBitmap`, which is RGBA by definition, bringing the glTF path in line
with the rest of the loader.

All three sources (embedded bufferView, data: URI, external file) fall
back to the previous element path on failure: `createImageBitmap` is
stricter about malformed sources, and a stricter loader would itself be a
regression — a first cut without the fallback broke 17 existing glTF
tests.

Regression test builds a real 2x2 indexed PNG (palette: red, blue) inside
a GLB and asserts both that the decode is an `ImageBitmap` and that the
palette actually expands.

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

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.

Copilot AI review requested due to automatic review settings August 11, 2026 23:17

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.

Copilot AI review requested due to automatic review settings August 11, 2026 23:32

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 and others added 2 commits August 12, 2026 07:36
Two unrelated corrections to the same example.

Ground shadows: this scene has no ground GEOMETRY to receive a blob — the
"ground" is a screen-space stroked line grid drawn in pixel coords at a
computed horizon, so a world-space shadow has nothing correct to land on
and drifts as the camera pitches. It was only casting them because the
application default ships `true`.

MSAA: the jet, enemies and terrain props are low-poly models with long
straight edges, which aliased badly as they receded. `antiAlias` was never
set here, so it defaulted off. The example composites through post
effects, so this depends on capture targets being multisampled too
(#1556). Measured on the horizon band: intermediate-coverage pixels along
the mountain silhouette rise from 8.5% to 27.0%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVjYzf76AEU3wJk766JAQi
Entries had grown into multi-paragraph essays — some single bullets ran to
4000 characters. Rewritten to one or two sentences each: what changed, why
it matters, and any migration-critical detail. Measurement tables kept
where the numbers are the point.

Unreleased section 51KB -> 21KB (58% shorter), average entry ~1000 -> ~450
characters. No entry removed and no issue link dropped. Released sections
are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVjYzf76AEU3wJk766JAQi
Copilot AI review requested due to automatic review settings August 11, 2026 23:37
@obiot
obiot force-pushed the platformer25d-1476 branch from b7e5620 to 81ae4c6 Compare August 11, 2026 23:37

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.

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.

2 participants