Three complaints from looking at the running app, all one problem: the world
didn't behave like a place.
COLLIDERS COME FROM EACH PROP'S OWN PRIMITIVES, not its AABB. Kenney authors a
house as walls + roof + door frame and a tree as trunk + canopy, so
StaticModel::parts records per-primitive bounds during the existing vertex
bake (they were being merged away). collider_parts() drops boxes under 10% of
the model's span, merges near-coincident ones, caps at 8 — low-res by design.
Policy falls out of the decomposition: buildings/fences/rocks take every
qualifying box; trees keep only parts both narrow and low, so the trunk blocks
and you walk under branches; lamps and decals take none. A prop whose parts all
filter out gets a synthesised box (trees a narrow post), because silently
reverting to walk-through scenery is the bug being fixed — a real catch, since
that fallback first shipped for Solid only and colliders dropped 39 -> 20 when
single-mesh pines found no trunk.
`hidden` rather than `visible`, deliberately: Entity derives Default, so the
field defaulting to false must be the UNUSUAL case. A `visible` flag would make
every default-constructed entity invisible — the same trap as the zero-seed rng
and the zero-gravity bodies this codebase has already been bitten by twice.
Proven by test, not by eye: a walker stops at a house wall but passes through
its DOORWAY (this fails with a single AABB), a trunk blocks while its canopy
doesn't, hidden colliders still block. One test initially "failed" because 120
ticks at 4 u/s travels exactly 8 units — it was measuring the tick budget, not
the collider.
STATIC PROPS NOW CAST. rebuild_static_shadows only walked entities, and props
are ModelInstances whose colliders are hidden, so trees and houses cast
nothing. Placed models feed the same baked layer, caster points sampled from
the model mesh (extremes always kept, then strided to ~48 — a stride alone
misses roof ridges) so a pine's shadow tapers. Cached on (render_rev,
bake_generation, models_rev), merged into one geometry, one draw.
THE SCENE IS COMPOSED: five suburban houses set back from a road all FACING it
(uniform facing is the point — random yaw reads as debris), lamps on one verge,
benches on the other, a fence line, three separated tree stands rather than
uniform sprinkling, and the physics demo gathered into a builder's yard. Props
scale to a target height from their own bounds, since a fixed multiplier gives
a 12-unit bench beside a 2-unit house. Exhaust only emits above 3 u/s (a parked
car under its own smoke column read as a bug).
44 props, 39 colliders, 7 draw items, 15.8k triangles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs, one visible symptom — the Knight strolling through the crate stack.
1. Rigid bodies were absent from the mover sweep set and from raycast /
camera-boom queries. When box3d dynamics landed, movers kept sweeping only
against Static|Kinematic, so a character (and a bullet, and the chase
camera) passed straight through every crate. Rigid poses are read back from
box3d at the end of the previous tick, so at snapshot time a rigid is as
settled as a kinematic and belongs in exactly the same set.
2. Fixing (1) exposed a deeper one. Clamping left the two boxes EXACTLY flush
(|d| == sum of halves), where float error decides the next axis' overlap
test either way — and a "yes" sent the falling mover UP onto the crate,
straight through the documented 0.55 step-up limit. It then walked along
the crate top. CONTACT_SKIN (1e-3) makes resting contact stop a hair short
of flush, so contact is unambiguous instead of borderline. Verified: a
walker into a 1.0-tall crate now stops at its face (x 1.20, y 0.50) rather
than climbing to y 1.50.
mover_is_blocked_by_a_rigid_body pins the behaviour. The mover golden hash is
re-baselined once, deliberately — the skin shifts every clamped position by
1e-3, and the movement it now describes is correct rather than merely
different. The reason is recorded at the assertion so a future change to that
hash needs the same justification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
step_world cloned two things per tick purely to dodge a borrow: the whole
Terrain (heights AND colors) and every static/kinematic Entity at 208 bytes
each. Terrain dominated — a 257^2 field is 1.3 MB/tick, 79 MB/s of memcpy at
60 Hz, on a world containing seven entities.
- Terrain: copy -> borrow, splitting the struct borrow the way the bottom of
the same function already did
- Statics: 208-byte Entity -> 48-byte Solid. This one MUST stay a copy —
movers sweep against kinematic poses from BEFORE this tick's integration
and that ordering is load-bearing — but it only ever needed
id/kind/pos/half/vel
- owner_pose: skip building the table when nothing is attached (most worlds)
scene ms/tick B/tick
demo 0.002 -> 0.003 15,140 -> 4,796 (-68%)
racing-ish (129) 0.007 -> 0.002 362,316 -> 8,576 (-98%)
terrain 257 0.019 -> 0.001 1,323,964 -> 896 (-99.93%)
large (500 static) 0.063 -> 0.056 591,386 -> 82,382 (-86%)
stress (2000 static) 0.583 -> 0.457 2,353,936 -> 327,812 (-86%)
Result-neutrality proven, not assumed: new mover_golden.rs covers what
rigid_dynamics.rs doesn't reach (terrain cliffs/floors, sweeps, platform
carry, attach pin, projectile lifetimes, auto-face) and its golden hash is
identical before and after — verified by stashing only the source changes
and re-running, not by re-baselining. Also includes a test pinning the
pre-integration snapshot ordering, so a future "obvious" simplification that
reads live positions gets caught.
Leak check: 36,000 ticks (10 simulated minutes) of a busy world with
projectiles spawning and expiring — RSS flat at 3.8 MB, +0.4% drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>