Skip to content

feat(broadphase): Octree + Sphere geometry + math.damp + AfterBurner dogfood (#1469)#1473

Open
obiot wants to merge 1 commit into
masterfrom
feat/octree
Open

feat(broadphase): Octree + Sphere geometry + math.damp + AfterBurner dogfood (#1469)#1473
obiot wants to merge 1 commit into
masterfrom
feat/octree

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented May 30, 2026

Summary

Implements the Octree-as-sibling-to-QuadTree proposal from #1469 and extends the engine with a 3D Sphere primitive, frame-rate-independent math.damp, and a refactored AfterBurner showcase that dogfoods all three.

Highlights

  • Octree broadphase under physics/broadphase/ — sibling to QuadTree (now also moved + TS-converted). World swaps the active broadphase reactively when sortOn crosses the 2D↔3D boundary, so loading-screen Camera2d → in-game Camera3d transitions are transparent. Query surface: retrieve, queryAABB, querySphere(cx, cy, cz, r) / querySphere(sphere), queryFrustum(planes), queryRay(from, dir, tMax). Pool reuse, scratch reuse, subtree-count short-circuit, collapse-on-removal.
  • Sphere geometry primitive — first-class 3D shape under geometries/ (sibling to Rect / Ellipse / Polygon). contains, overlaps, overlapsAABB, getBounds, setShape, clone. Pooled. Used as the canonical 3D query shape across the engine.
  • AABB3d — minimal 3D bounding box. Used by Octree internally; exposed for user code.
  • PhysicsAdapter.raycast3d? + querySphere? — capability-gated (raycasts3d: boolean); BuiltinAdapter implements via Octree + per-candidate narrow phase. Matter/Planck declare raycasts3d: false and omit (2D-only).
  • Camera3d.queryVisible(world) — bulk frustum cull via Octree.queryFrustum.
  • math.lerp + math.damp — scalar primitives; damp is frame-rate-independent exponential damping (Three.js MathUtils.damp parity). Fixes the "lerp smoothing is broken" footgun. Vector2d/3d/Observable*.damp per-component wrappers + lerp refactored to call through math.lerp for a single source of truth.
  • AfterBurner refactorupdate(dt) decomposed from 230 lines into a 14-line orchestrator + named tick helpers. Bullet × enemy collision now goes through world.adapter.querySphere?(sphere) + __kind tag filter. Enemy barrel roll → self-rescheduling Tween. Player bank/pitch smoothing → math.damp. Position clamping → math.clamp. Random ranges → math.randomFloat.

Correctness bugs found + fixed in review

  • raycast3d from-inside-sphere: missed hits when sphere fully covered the ray segment (t0 < 0, t1 > 1). Hit-table guard t1 ≤ 1 was wrong; fixed to t0 < 0 && t1 ≥ 0.
  • Octree.getIndex isFloating items: UI overlays under Camera3d were misclassified by hundreds of units. Now mirrors QuadTree's viewport.localToWorld branch.
  • AfterBurner bullet broadphase membership: Sprite defaults isKinematic = true so the broadphase walk skipped bullets entirely. Now flipped to false at spawn + tagged __kind = \"bullet\".

Tests

  • 3946 melonjs tests passing (+96 across octree.spec.js, sphere.spec.ts, math.spec.js, vector{2,3}d.spec.ts, obsvervableVector3d.spec.ts).
  • 177 matter-adapter + 169 planck-adapter unchanged (capabilities-shape pin extended for raycasts3d).
  • Adversarial coverage: from-inside-sphere raycast, isFloating regression, pool-reuse-after-collapse, frame-rate-independence at 60/30/144 Hz, IEEE 754 edge cases (Inf − Inf = NaN), zero-volume bounds, NaN propagation, sphere-AABB cross-checks, sphere overload routing through Octree's internal entry point.

Versions + CHANGELOGs

  • melonjs CHANGELOG (19.7.0 unreleased): Added entries for Sphere, AABB3d, Camera3d.queryVisible, math.lerp/damp, expanded Octree query surface, adapter.querySphere.
  • @melonjs/matter-adapter 1.0.0 → 1.1.0, peer melonjs: \">=19.5.0\"\">=19.7.0\".
  • @melonjs/planck-adapter same bump.
  • (Minimum bump because AdapterCapabilities gained the required raycasts3d field in 19.7.)

Pre-existing lint/TS fixes

  • matter/planck adapter tsconfig.json: added skipLibCheck: true + \"types\": [\"node\"].
  • matter-adapter.spec.ts: added override modifier on 5 onCollision overrides.
  • matter/planck parity.spec.ts: coin.ancestor cast to Container for removeChildNow.

Test plan

  • Local: pnpm vitest --root packages/melonjs run → 3946 pass
  • Local: cd packages/matter-adapter && pnpm vitest run → 177 pass
  • Local: cd packages/planck-adapter && pnpm vitest run → 169 pass
  • Local: examples build (pnpm --filter examples build) → clean
  • Visual: AfterBurner example plays as before (motion, bank, barrel rolls, bullet hits, enemy hits, game-over freeze, restart)
  • CI: matrix lint + test + windows
  • CodeQL + Analyze (javascript) + Analyze (actions)

🤖 Generated with Claude Code

…dogfood (#1469)

Implements the Octree-as-sibling-to-QuadTree proposal from #1469 and
extends the engine with a 3D `Sphere` primitive, frame-rate-independent
`math.damp`, and a refactored AfterBurner that dogfoods all three.

## Broadphase (issue #1469)

- New `physics/broadphase/` folder. `QuadTree` moved from
  `physics/builtin/quadtree.js` and rewritten in TypeScript — public
  surface byte-identical so every existing `world.broadphase.*` call
  keeps working.
- `Octree` — 8-octant sibling. Mirrors QuadTree's full surface
  (insert, remove, retrieve, insertContainer, removeContainer, clear,
  getIndex, isPrunable, hasChildren) plus 3D-only region queries:
  `queryAABB(aabb)`, `querySphere(cx, cy, cz, r)` / `querySphere(sphere)`,
  `queryFrustum(planes)`, `queryRay(from, dir, tMax)`. Pool reuse,
  scratch reuse, subtree-count short-circuit, collapse-on-removal —
  same optimizations as QuadTree.
- `AABB3d` — 3D AABB primitive (min/max, contains, overlaps,
  overlapsSphere, isFinite). Used by Octree internally and by
  `Camera3d.queryVisible` callers.
- `World.broadphase` is now sortOn-reactive: the `sortOn` setter swaps
  the broadphase across 2D↔3D crossings, so loading-screen Camera2d →
  in-game Camera3d transitions are transparent. `Camera3d.defaultSortOn
  = "depth"` triggers the swap automatically.
- Out-of-bounds robustness in `Octree.getIndex` — items outside the
  root AABB classify as -1 (stay at root.objects) so spatial pruning
  in queryFrustum/queryAABB/querySphere never silently drops them.
  `isFloating` handling matches QuadTree's viewport.localToWorld
  branch.

## Adapter surface

- `PhysicsAdapter.raycast3d?(from, to)` — capability-gated by new
  `raycasts3d: boolean`. `BuiltinAdapter` implements via `Octree.queryRay`
  + ray-vs-bounding-sphere narrow phase (sphere uses bounds half-
  diagonal — same circumradius as `Camera3d.isVisible`). Matter and
  Planck declare `raycasts3d: false` and omit the method (2D-only).
- `PhysicsAdapter.querySphere?` — two call shapes: `(center: Vector3d,
  radius)` (loose) and `(sphere: Sphere)` (packaged). `BuiltinAdapter`
  implements both via `Octree.querySphere` + per-candidate centre-
  distance narrow phase.
- Adapter is the user-facing surface for both. `world.raycast3d` /
  `world.querySphere` were briefly added then dropped — `adapter.*`
  is the canonical path.

## Camera3d.queryVisible(world, out?)

Bulk frustum cull via `Octree.queryFrustum`. Returns every renderable
whose octant overlaps the current frustum — use as a broadphase pass
before per-renderable `isVisible` narrow culling. Returns `[]` under a
2D broadphase so call sites don't need a sortOn guard.

## Sphere geometry primitive

First-class 3D shape under `geometries/` — sibling to Rect, Ellipse,
Polygon. `new Sphere(x, y, z, r)`. Methods: contains(point),
overlaps(other), overlapsAABB(aabb), getBounds() (lazy AABB3d cache),
setShape, clear, clone. Pooled via `spherePool`. Not added to the
`BodyShape` union — 3D physics is out of scope today; Sphere is a
math/geometry primitive used by `adapter.querySphere(sphere)` and
`Octree.querySphere(sphere)`.

## math.damp + math.lerp

- `math.damp(current, target, lambda, dt)` — frame-rate-independent
  exponential damping (Three.js `MathUtils.damp` parity). Fixes the
  "lerp smoothing is broken" footgun: same convergence after the
  same total elapsed time regardless of how `dt` was split across
  frames. AfterBurner's bank smoothing migrated.
- `math.lerp(a, b, t)` — scalar linear interpolation. Vector2d /
  Vector3d / ObservableVector*.lerp routed through it for a single
  source of truth.
- `Vector2d/3d.damp(target, lambda, dt)` — per-component vector
  damping wrappers (including ObservableVector overrides — change
  callback fires exactly once per damp call via the underlying set).

## AfterBurner dogfood

- `update(dt)` decomposed from 230 lines into a 14-line orchestrator
  + `tickPlayerInput`, `tickInvulnBlink`, `tickFireAndSpawn`,
  `tickBullets`, `tickEnemyBullets`, `tickEnemies`, `tickEnemyFire`,
  `enemyHitByPlayerBullet`, `scoreEnemyKill`, `withinPlayerHitRadius`.
- Bullet × enemy collision via `world.adapter.querySphere?(sphere)`
  + `__kind` tag filter — replaces the inline O(K×M) sphere-distance
  loop with a broadphase-driven candidate set.
- Enemy barrel roll → self-rescheduling `Tween` (no more hand-rolled
  `rollTimeMs` / `rollDurationMs` / `nextRollMs` state machine).
- Player position clamp → `math.clamp`. Player bank/pitch smoothing →
  `math.damp`. Random ranges → `math.randomFloat` (4 inline copies
  replaced; existing helper was just under-used).
- `removeEnemy` stops the roll tween before detaching the mesh.
  `setGameOver` stops every in-flight roll tween so enemies freeze
  fully (tweens run on the engine's tween clock, independent of our
  `update` loop's gameOver early-return).
- Bullet bug fix: `Sprite` defaults `isKinematic = true` so the
  broadphase walk skipped them entirely. Now flips to `false` at
  spawn so `querySphere` actually finds them as candidates.

## Correctness bugs found + fixed in review

- `raycast3d` missed hits when the sphere fully covered the ray
  segment (`t0 < 0`, `t1 > 1`). The `t1 ≤ 1` guard was wrong;
  inside-sphere case now `t0 < 0 && t1 ≥ 0`.
- `Octree.getIndex` didn't handle `isFloating` items — UI overlays
  under Camera3d would have been misclassified by hundreds of units.
  Now mirrors QuadTree's `viewport.localToWorld` branch.

## Tests

- 3946 melonjs tests passing (+96 new across octree.spec.js,
  sphere.spec.ts, math.spec.js, vector{2,3}d.spec.ts,
  observableVector3d.spec.ts).
- 177 matter-adapter + 169 planck-adapter unchanged (capabilities-shape
  pin extended for raycasts3d).
- Adversarial coverage: from-inside-sphere raycast, isFloating,
  pool-reuse-after-collapse, frame-rate-independence at 60/30/144 Hz,
  IEEE 754 edge cases (Inf − Inf = NaN), zero-volume bounds, NaN
  propagation, sphere-AABB cross-checks.

## Pre-existing lint/TS fixes

- matter/planck adapter `tsconfig.json`: added `skipLibCheck: true` +
  `"types": ["node"]` so `scripts/clean.ts` + `scripts/build.ts`
  typecheck.
- matter-adapter.spec.ts: added `override` modifier on 5
  `onCollision` overrides.
- matter/planck parity.spec.ts: `coin.ancestor` cast to `Container`
  for `removeChildNow` (ancestor is typed `Entity | Container`, only
  Container exposes it).

## Versions + CHANGELOGs

- melonjs CHANGELOG.md (19.7.0 Added section): Sphere, AABB3d,
  Camera3d.queryVisible, math.lerp/damp, expanded Octree query
  surface, adapter.querySphere.
- `@melonjs/matter-adapter` 1.0.0 → 1.1.0, peer dep
  `melonjs: ">=19.5.0"` → `">=19.7.0"` (AdapterCapabilities gained
  required `raycasts3d`).
- `@melonjs/planck-adapter` same bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 30, 2026 09:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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