feat(broadphase): Octree + Sphere geometry + math.damp + AfterBurner dogfood (#1469)#1473
Open
obiot wants to merge 1 commit into
Open
feat(broadphase): Octree + Sphere geometry + math.damp + AfterBurner dogfood (#1469)#1473obiot wants to merge 1 commit into
obiot wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the
Octree-as-sibling-to-QuadTreeproposal from #1469 and extends the engine with a 3DSphereprimitive, frame-rate-independentmath.damp, and a refactored AfterBurner showcase that dogfoods all three.Highlights
physics/broadphase/— sibling toQuadTree(now also moved + TS-converted). World swaps the active broadphase reactively whensortOncrosses 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.Spheregeometry primitive — first-class 3D shape undergeometries/(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 declareraycasts3d: falseand omit (2D-only).Camera3d.queryVisible(world)— bulk frustum cull viaOctree.queryFrustum.math.lerp+math.damp— scalar primitives;dampis frame-rate-independent exponential damping (Three.jsMathUtils.dampparity). Fixes the "lerp smoothing is broken" footgun.Vector2d/3d/Observable*.dampper-component wrappers +lerprefactored to call throughmath.lerpfor a single source of truth.update(dt)decomposed from 230 lines into a 14-line orchestrator + named tick helpers. Bullet × enemy collision now goes throughworld.adapter.querySphere?(sphere)+__kindtag filter. Enemy barrel roll → self-reschedulingTween. Player bank/pitch smoothing →math.damp. Position clamping →math.clamp. Random ranges →math.randomFloat.Correctness bugs found + fixed in review
raycast3dfrom-inside-sphere: missed hits when sphere fully covered the ray segment (t0 < 0,t1 > 1). Hit-table guardt1 ≤ 1was wrong; fixed tot0 < 0 && t1 ≥ 0.Octree.getIndexisFloatingitems: UI overlays under Camera3d were misclassified by hundreds of units. Now mirrors QuadTree'sviewport.localToWorldbranch.SpritedefaultsisKinematic = trueso the broadphase walk skipped bullets entirely. Now flipped tofalseat spawn + tagged__kind = \"bullet\".Tests
octree.spec.js,sphere.spec.ts,math.spec.js,vector{2,3}d.spec.ts,obsvervableVector3d.spec.ts).raycasts3d).Inf − Inf = NaN), zero-volume bounds, NaN propagation, sphere-AABB cross-checks, sphere overload routing through Octree's internal entry point.Versions + CHANGELOGs
melonjsCHANGELOG (19.7.0 unreleased): Added entries for Sphere, AABB3d, Camera3d.queryVisible, math.lerp/damp, expanded Octree query surface, adapter.querySphere.@melonjs/matter-adapter1.0.0 → 1.1.0, peermelonjs: \">=19.5.0\"→\">=19.7.0\".@melonjs/planck-adaptersame bump.AdapterCapabilitiesgained the requiredraycasts3dfield in 19.7.)Pre-existing lint/TS fixes
tsconfig.json: addedskipLibCheck: true+\"types\": [\"node\"].overridemodifier on 5onCollisionoverrides.coin.ancestorcast toContainerforremoveChildNow.Test plan
pnpm vitest --root packages/melonjs run→ 3946 passcd packages/matter-adapter && pnpm vitest run→ 177 passcd packages/planck-adapter && pnpm vitest run→ 169 passpnpm --filter examples build) → clean🤖 Generated with Claude Code