clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

main
marbles / docs / collision-mesh-audit.md
collision-mesh-audit.md 12.3K source

Collision-to-mesh audit — piece bench

Audit date: 2026-08-09
Source: ?endless=1&peers=0&gallery=1
Inventory: 119/119 registry variants reviewed with collision wireframes enabled

The per-piece results are in collision-mesh-audit.csv. This was a static geometry audit: every ordinal was selected explicitly through the CUA-accessible numeric selector, the selected ordinal/name was validated, and the collision-on render was compared with the visible mesh. The yellow ring is only the selection marker and was ignored. No simulation outcome is inferred from a visual pass alone.

Result summary

Severity Pieces Meaning
blocker 1 Render mesh is missing/invalid, so comparison cannot be made
critical 18 Collision changes topology or leaves a major part of the mesh unsupported
high 12 Local mismatch is likely to change reachable motion or contacts
medium 88 Floor is broadly usable, but guard or edge fit is visibly inaccurate

The dominant issue classes are:

  • Over-conservative ribbon wall guard — 115 pieces. The floor is often close, but the collision profile rises far above the visible rim. The current profile reaches up=1.1 world units (CHANNEL_PROFILE in src/gen/endless/proxies.ts) while the rendered channel rim is much lower. This consumes tunnel/stack clearance and masks legitimate edge exits.
  • Openings and drop shafts — 13 pieces. straight-hole is completely capped by the ordinary ribbon. All twelve drop-stack variants use the same square five-slab tube/pan regardless of rounded vs square aperture, forward vs turning exit, or reversed direction.
  • Variable width — 5 pieces. straight-wide, both funnels, and both reversed funnels are rendered at up to ±1.7 but the bench shows a standard ribbon. There is also a diagnostic parity defect: production calls channelProxies(..., wideScalesOf(data)) in src/client/endless.ts, while src/client/pieceBench.ts omits the fourth argument. Extracting a shared segment-to-proxies helper is safer than fixing the bench call independently.
  • Turns and curved profiles — 62 pieces. Long curves, helixes, waves, and bumps broadly follow the visible floor. Error concentrates at inner/outer boundaries, tight corners, double bends, and short S-curve inflections. The current angular threshold bounds center-frame rotation, not the geometric error of the offset channel boundaries.
  • Junction composites — 4 pieces. cross covers only the active straight route. diamond-1..3 use thirteen slabs; the boxes visibly overshoot the sculpted split/merge deck and center wedge.
  • Invalid asset/bench state — 1 piece. cross-over reports null mesh bounds and focus remains on the prior cross, so its row is intentionally marked not-evaluable.

The CSV uses these short technique IDs.

T1 — Sweep the actual, variable cross-section

Keep the useful one-piece ribbon model, but make width and rim height functions of arc length and piece metadata. A path extrusion with explicit subdivision is established practice; Three.js exposes the same model through ExtrudeGeometry.extrudePath. For this project, the important change is to pass wScale in every environment and use a mesh-derived rim profile instead of one global CHANNEL_PROFILE.

T2 — Author apertures and junctions as topology, not drop points

Holes need boundary loops and open faces. Give straight-hole, drop-stack mouths, crosses, and split/merge pieces dedicated collision data in the GLB (or an adjacent compact collider asset). A procedural alternative is an outer contour plus explicit hole contours; Three.js Shape models this directly with Shape.holes. Do not infer a hole only from a near-vertical centerline tangent.

T3 — Use an error-bounded adaptive turn tessellation

Replace the global MAX_TURN rule with an error target in world units. For an offset boundary radius r, choose angular steps so the chord sagitta r * (1 - cos(delta / 2)) stays below the allowed collision-to-mesh error. Evaluate both inner and outer offsets and add mandatory rings at ports, inflections, curvature extrema, and width changes. This directly controls the over/undercut the player sees, rather than only controlling tangent rotation.

T4 — Cook a simplified static triangle mesh from the render source

These tracks are static environmental geometry, the strongest case for a cleaned/welded concave collider. PhysX triangle-mesh cooking validates, welds, builds adjacency, and constructs an acceleration structure (PhysX Geometry guide). Unity likewise recommends non-convex mesh colliders where high-fidelity static collision is essential and documents mesh cleaning/welding (Unity mesh cooking). Godot describes concave trimeshes as the most accurate static-level option, with the usual warning about fast small bodies (ConcavePolygonShape3D).

For Marbles, generate a low-poly collision surface during asset import, retain the exact aperture and rim boundaries, weld it, and keep Box3D's internal-edge identification enabled. The render mesh itself need not be the runtime collider.

T5 — Use compound convex pieces where a concave surface is unreliable

Drop tubes and split/junction walls can be a small, authored compound of convex parts. When automatic decomposition is useful, CoACD is specifically designed to preserve collision conditions with fewer convex components (CoACD project and SIGGRAPH 2022 paper). This is preferable to axis-aligned boxes that ignore a curved mouth or turning shaft, but authored parts will be more predictable for this small kit.

T6 — Separate geometric fidelity from solver forgiveness

Do not encode contact tolerance by making walls much taller or wider than the art. Keep collider geometry close to the mesh and apply small, measured solver margins. PhysX explicitly separates contactOffset and restOffset for this purpose (Advanced Collision Detection). For Box3D, establish the equivalent narrow margin through sphere radius/contact settings, then test at the maximum marble speed.

Add CI measurements per piece: signed floor-height error, lateral boundary error, aperture false-positive area, and minimum roof/stack clearance. Report max and percentile errors and fail on a piece-specific budget. This turns the gallery from a visual-only regression tool into a quantitative one.

T7 — Enforce asset and bench/runtime parity

  • Assert every selected render group has finite, non-empty bounds; this catches cross-over before a camera operation or audit.
  • Build collision through one shared function used by production, the bench, tests, and simulation. The current wide-scale argument mismatch proves that separately wired diagnostic paths can produce false conclusions.
  • Add stable piece number, piece id, family, mesh bounds, proxy composition, and production options to the accessible bench state (now present in the updated gallery UI).

Suggested order of work

  1. Fix cross-over asset/bounds and share the production proxy builder with the bench so the diagnostic is trustworthy.
  2. Replace straight-hole and drop-stack generic logic with authored aperture and shaft colliders.
  3. Make width a mandatory part of the swept profile and add parity tests for all five wide/funnel variants.
  4. Reduce the wall profile to the visible rim plus a small solver margin.
  5. Convert turn sampling from angle-bounded to boundary-error-bounded, starting with corner, bend, and the four short S-curve variants.
  6. Replace cross/diamond slabs with authored junction collision surfaces or a small compound-convex set.

Remediation — 2026-08-09 (same day, ready for re-audit)

All six work items landed; new kit truths were measured headlessly from assets/marble-kit.glb (scratch tooling patterned on tools/measure_ports.ts): wall inner face 0.287 grid, rim floor+0.25 grid, floor bore ROUND r 0.25 grid in straight-hole and BOTH end-hole flavors (flavors differ only in cup cosmetics), wide inner wall 0.575 grid → widening is an additive wall shift, not a section scale.

  1. Parity (T7) — segmentProxies in src/gen/endless/proxies.ts is now the ONE channel builder used by production, the bench, and the sims; gapSegmentsOf moved beside it (rail.ts re-exports). The bench asserts finite mesh bounds, frames empty plots safely, and renders cross-over (a deliberate noMesh phantom, not a broken asset) with its ghosted host mesh so the row is evaluable.
  2. Apertures (T2/T5) — PieceDef.holes → LoopSegment.holes opens a real bore in the ribbon with lip decks, side walls, and an under-deck guide ring (emitAperture). Drop-stacks get an authored measured shape: annulus deck floor, octagonal cup walls (r 1.0 world) with the entry arch open, octagonal shaft bore, receiver cup opened toward the true exit yaw, and the pan law kept.
  3. Width (T1) — width flows as a measured additive wall shift (0.576 world) through wideScalesOf; the bench passes def-authored widthIn/widthOut so isolated funnels taper. Parity tests: tests/collision_width_test.ts.
  4. Rim (T6) — CHANNEL_PROFILE now tops at 0.62 over the floor (measured rim 0.5 + 0.12 solver cap) with the wall face at 0.62 lateral (measured 0.574 + clearance); junction composite walls follow the same law.
  5. Turns (T3) — arc stations are chosen by outer-boundary chord-sagitta budget (BOUNDARY_ERR 0.008 world) instead of a fixed angle; ring-level crease rounding (kicker prevention) is unchanged and documented as deliberate forgiveness geometry.
  6. Junctions (T2) — the cross cell emits one authored 4-way composite (deck + corner wall Ls, all four port arches open) from the mesh visit; the phantom stays silent — previously the two same-height route ribbons walled each other off. Diamond/spur/braid composites tightened to the measured split-double wall faces (branch ± 0.574 + clearance).

Quantitative CI (the audit's T6 closing ask) — tests/collision_metrics_test.ts measures every registry variant: signed ring-floor error vs centerline (budget 0.05, worst measured 0.041 at the slant-d crest rounding), ring lateral extents, rim cap band, bore openness probes for all 13 hole/drop bores, and cross junction coverage probes. It found and fixed one real defect during bring-up: approach ribbons overhanging drop bores (now lip-trimmed in the proxy law).

Re-audit gap closure (2026-08-09, after the fresh CUA pass in collision-mesh-reaudit.md) — the CI now also measures against the RENDER MESH itself, not just centerline + constants:

  • Render-to-collision directed distance: every GLB channel-surface vertex (placement-transformed, corridor-filtered to the driveable band; port-face bands excluded — those are shared window boundaries covered by the neighbor's ring) must lie within 0.2 world of the collision surface (point-to-slab + point-to-ribbon-triangle). 40k+ vertices across all variants; worst 0.19. Bringing this up hardened the colliders further: straight-hole gained its bore side-sliver decks (collar verts were 0.24 adrift) and the cross corner Ls became solid corner posts matching the kit's blocks (0.29 adrift at the post corners).
  • Drop compound probes (all 12): entry arch open, far/lateral cup walls present, shaft bore guide present, receiver exit mouth open toward the true exit heading with its far wall present, pan present.
  • Diamond compound probes (all 3): branch decks present, outer and wedge walls at the measured lines, branch lanes and entry throat clear.
  • Coverage accounting: bench-build failures fail the suite; every variant must be evaluated by ≥1 metric family (rings 113, render-dist 101, probes 17, paired-by-design 1 — cross-over, whose collision is the co-located cross composite).

Suite: 174 tests green.