@woosh/meep-engine 3.25.0 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +59 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +285 -0
- package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
- package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
- package/src/engine/physics/cloth/MEASUREMENTS.md +582 -0
- package/src/engine/physics/cloth/PLAN.md +330 -49
- package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
- package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
- package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +65 -7
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothInstance.js +72 -8
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +25 -4
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothSystem.js +75 -8
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +41 -4
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothWorld.js +207 -63
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +10 -0
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +17 -1
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +27 -3
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +28 -3
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +75 -10
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +131 -12
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
- package/src/engine/physics/cloth/playground/collide_main.js +1 -1
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +12 -2
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_solver_constants.js +12 -2
- package/src/engine/physics/cloth/solver/cloth_step_instance.js +1 -1
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
- package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +23 -9
- package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -1
- package/src/engine/physics/cloth/wind/AbstractClothWind.js +23 -9
- package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +7 -5
- package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -1
- package/src/engine/physics/cloth/wind/ClothAmbientWind.js +12 -9
- package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
- package/src/engine/physics/cloth/wind/ClothFluidWind.js +5 -2
- package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +7 -2
- package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
- package/src/engine/physics/cloth/wind/cloth_sample_air.js +12 -5
|
@@ -7,6 +7,7 @@ import { min2 } from "../../../math/min2.js";
|
|
|
7
7
|
import { seededRandom } from "../../../math/random/seededRandom.js";
|
|
8
8
|
import { number_compare_ascending } from "../../../primitives/numbers/number_compare_ascending.js";
|
|
9
9
|
import { v3_dot } from "../../vec3/v3_dot.js";
|
|
10
|
+
import { Ray3 } from "../ray/Ray3.js";
|
|
10
11
|
import { AbstractShape3D } from "./AbstractShape3D.js";
|
|
11
12
|
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
|
|
12
13
|
|
|
@@ -326,6 +327,69 @@ export class UnionShape3D extends AbstractShape3D {
|
|
|
326
327
|
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Nearest hit across the children, which all share the union's own frame — so, unlike the other
|
|
332
|
+
* two wrappers, there is no ray to re-frame and no answer to map back.
|
|
333
|
+
*
|
|
334
|
+
* Each child is handed the best distance so far as its `tMax`, so a child can only report a hit by
|
|
335
|
+
* beating every child before it. That is what makes it safe to let the children write straight into
|
|
336
|
+
* `hit`: a child that writes is a child that won, and a child that misses leaves `hit` untouched by
|
|
337
|
+
* the family convention, so the standing answer survives it.
|
|
338
|
+
*
|
|
339
|
+
* The distance to narrow on is recovered from the point the child wrote, as
|
|
340
|
+
* `(p − origin)·direction` — the shape-level API hands back a point where the narrowphase's
|
|
341
|
+
* `refine_ray_union` has the distance already. Two children closer together than a float32 ulp
|
|
342
|
+
* of the coordinates involved are therefore a tie, settled by whichever is listed last.
|
|
343
|
+
*
|
|
344
|
+
* That ulp is the API's, not this method's: a child writes `origin + direction · t` into a float32
|
|
345
|
+
* `hit` before anything here reads it, so the resolution is already spent by the time a LONE shape
|
|
346
|
+
* answers. Measured against the narrowphase over 4,000 rays per scale, this union's error tracks a
|
|
347
|
+
* lone sphere's within a factor of two everywhere — 3e-6 near the origin, 1e-3 at 1e4 out, 0.1 at
|
|
348
|
+
* 1e6, where one float32 ulp IS 0.06 world units. A compound collider that far out wants a float64
|
|
349
|
+
* ray, which is what the narrowphase path already gives it; it does not want a different union.
|
|
350
|
+
*
|
|
351
|
+
* DIVERGENCE from `refine_ray_union`: that one carries a sentinel for a shape it cannot answer and
|
|
352
|
+
* poisons the entire union with it, because reporting the nearest of the children it COULD answer
|
|
353
|
+
* would be a hit confidently too far. There is no sentinel in this API — a child that does not
|
|
354
|
+
* implement `raycast` throws from {@link AbstractShape3D#raycast} — but the effect is the one that
|
|
355
|
+
* matters: every child is visited unconditionally, so such a child is never quietly skipped, and
|
|
356
|
+
* the throw leaves the union with no answer rather than a partial one.
|
|
357
|
+
*
|
|
358
|
+
* @param {Float32Array|number[]} hit out: [px, py, pz, nx, ny, nz]
|
|
359
|
+
* @param {Ray3} ray union-frame ray (origin in [0..2], unit direction in [3..5], tMax in [6])
|
|
360
|
+
* @returns {boolean} whether anything was hit
|
|
361
|
+
*/
|
|
362
|
+
raycast(hit, ray) {
|
|
363
|
+
const children = this.children;
|
|
364
|
+
const child_count = children.length;
|
|
365
|
+
|
|
366
|
+
const ox = ray[0], oy = ray[1], oz = ray[2];
|
|
367
|
+
const dx = ray[3], dy = ray[4], dz = ray[5];
|
|
368
|
+
|
|
369
|
+
// Allocated per call rather than kept as a module scratch: a union may hold another wrapper,
|
|
370
|
+
// and every level of that recursion needs its own narrowing ray alive across the loop below.
|
|
371
|
+
const child_ray = new Ray3();
|
|
372
|
+
|
|
373
|
+
child_ray[0] = ox; child_ray[1] = oy; child_ray[2] = oz;
|
|
374
|
+
child_ray[3] = dx; child_ray[4] = dy; child_ray[5] = dz;
|
|
375
|
+
child_ray[6] = ray[6];
|
|
376
|
+
|
|
377
|
+
let found = false;
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < child_count; i++) {
|
|
380
|
+
if (!children[i].raycast(hit, child_ray)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The direction is unit, so the hit's projection onto it IS the distance travelled.
|
|
385
|
+
child_ray[6] = (hit[0] - ox) * dx + (hit[1] - oy) * dy + (hit[2] - oz) * dz;
|
|
386
|
+
|
|
387
|
+
found = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return found;
|
|
391
|
+
}
|
|
392
|
+
|
|
329
393
|
/**
|
|
330
394
|
*
|
|
331
395
|
* @param {UnionShape3D} other
|
|
@@ -3096,6 +3096,14 @@ construction rather than by arrangement. It sits **above** the sleep tests
|
|
|
3096
3096
|
deliberately: a world whose every cloth is asleep still has weather, and §10's
|
|
3097
3097
|
"sleep wakes on a wind change" is the whole reason it needs to.
|
|
3098
3098
|
|
|
3099
|
+
> **Corrected by §57.** Everything above is right about a scene with *one* cloth
|
|
3100
|
+
> in it, which is the scene this was measured in. A second cloth registering
|
|
3101
|
+
> while the first is running spends its registration ticks in a world whose clock
|
|
3102
|
+
> is moving, so the world clock — however it counts — hands it a different gust
|
|
3103
|
+
> under the two systems. The gust now reads the asking cloth's own age and
|
|
3104
|
+
> `ClothWorld#time` keeps only the job below: telling a source that reads the
|
|
3105
|
+
> scene when to walk it.
|
|
3106
|
+
|
|
3099
3107
|
### The number
|
|
3100
3108
|
|
|
3101
3109
|
```
|
|
@@ -3287,3 +3295,577 @@ stands and M6 adds one clause to it: re-measure the absolute column on a quiet
|
|
|
3287
3295
|
machine, take a paired reading even then, and if the answer is that the library
|
|
3288
3296
|
is genuinely at its ceiling, the lever to spend is the one §17 prices rather than
|
|
3289
3297
|
the feature that happened to be measured last.
|
|
3298
|
+
|
|
3299
|
+
---
|
|
3300
|
+
|
|
3301
|
+
# Cloth — post-M6 measurements
|
|
3302
|
+
|
|
3303
|
+
> Written 2026-09-13, against the fixes listed in §17's post-M6 entry.
|
|
3304
|
+
> Everything above this line is M0's through M6's and is unchanged, except where
|
|
3305
|
+
> a section below is named *inside* the one it corrects.
|
|
3306
|
+
>
|
|
3307
|
+
> The scripts are the ones the milestones already carry; where a number here
|
|
3308
|
+
> came from something else, the section says what.
|
|
3309
|
+
|
|
3310
|
+
---
|
|
3311
|
+
|
|
3312
|
+
## 53. A cloth resting on a collider never slept, and the speed was not the thing to read
|
|
3313
|
+
|
|
3314
|
+
§7 P5's gate is free-hanging chains, and every library fabric passes it. Put the
|
|
3315
|
+
same chain on a floor and four of the seven never sleep at all.
|
|
3316
|
+
|
|
3317
|
+
```
|
|
3318
|
+
8-link rope, 10 cm links, 2 % of lateral lean, dropped for 2,400 steps
|
|
3319
|
+
"-1" is "still awake when the run ended"
|
|
3320
|
+
|
|
3321
|
+
entry free floor mu=0 floor mu=0.5 sphere mu=0
|
|
3322
|
+
SILK 386 1029 398 631
|
|
3323
|
+
COTTON 236 -1 -1 362
|
|
3324
|
+
DENIM 101 198 164 220
|
|
3325
|
+
LEATHER 139 192 -1 2312
|
|
3326
|
+
ROPE 283 911 -1 427
|
|
3327
|
+
CHAIN 144 419 245 293
|
|
3328
|
+
HAIR 143 271 2129 280
|
|
3329
|
+
```
|
|
3330
|
+
|
|
3331
|
+
A cloth that sleeps in 283 steps hanging free and never sleeps lying on the
|
|
3332
|
+
ground is not a cloth that is behaving differently; it is a **test** that is
|
|
3333
|
+
reading the wrong quantity.
|
|
3334
|
+
|
|
3335
|
+
### What is actually happening
|
|
3336
|
+
|
|
3337
|
+
Traced on cotton on a frictionless floor, six hundred steps after it has come to
|
|
3338
|
+
rest, the configuration is a clean **period-4 limit cycle**:
|
|
3339
|
+
|
|
3340
|
+
```
|
|
3341
|
+
step peak speed peak travel contact penalty, four live slots
|
|
3342
|
+
0 6.806e-3 5.801e-5 628.4 1544.9 1553.7 561.1
|
|
3343
|
+
1 9.568e-3 5.328e-5 628.9 1545.1 1554.0 561.6
|
|
3344
|
+
2 1.443e-2 6.116e-5 629.3 1545.7 1554.3 562.1
|
|
3345
|
+
3 1.229e-2 1.061e-4 629.8 1546.3 1554.8 562.6
|
|
3346
|
+
4 6.806e-3 5.801e-5 630.2 1546.6 1555.3 563.2 <- repeats
|
|
3347
|
+
```
|
|
3348
|
+
|
|
3349
|
+
The cloth travels **a tenth of a millimetre a step**, which nobody can see, at a
|
|
3350
|
+
peak particle speed of **1.5e-2 m/s**, which is **eighteen times** the sleep
|
|
3351
|
+
threshold of 8.0e-4 m/s. Both numbers are correct and they disagree about
|
|
3352
|
+
whether the cloth is at rest, because a limit cycle has bounded displacement and
|
|
3353
|
+
un-bounded-below speed.
|
|
3354
|
+
|
|
3355
|
+
The penalty column says where the cycle gets its energy: it climbs
|
|
3356
|
+
monotonically — `k + β·|C|` in `cloth_contact_dual_update` — and **never comes
|
|
3357
|
+
back down**, because `γ = 1` and `clamp_band`'s floor is the contact's own
|
|
3358
|
+
`0.25·(m/h²)`. Two of the four slots are already at the ceiling of `8·(m/h²)`,
|
|
3359
|
+
which is exactly the ratio §6 measured a row adding energy above.
|
|
3360
|
+
|
|
3361
|
+
### Two candidates, and only the second survives
|
|
3362
|
+
|
|
3363
|
+
The *friction re-anchor* — `cloth_contact_row.js` moving the anchor to the
|
|
3364
|
+
particle whenever the row's force clamps out — is **not** it, and no experiment
|
|
3365
|
+
is needed to say so: cotton fails on a floor whose friction is **zero**, where
|
|
3366
|
+
`friction <= 0` zeroes the friction multiplier before the anchor is ever read.
|
|
3367
|
+
It is ruled out by the μ = 0 column of the table above.
|
|
3368
|
+
|
|
3369
|
+
The *penalty ramp* is it, and the fix for it is not in scope: `γ` is
|
|
3370
|
+
unmeasured-by-decision (§5.2), a `γ < 1` would have to be re-measured against
|
|
3371
|
+
every hard row in the system, and the motion it sustains is a tenth of a
|
|
3372
|
+
millimetre. **The defect worth fixing is that the sleep test disagrees with the
|
|
3373
|
+
plan.** §7 P5 has said "maximum particle displacement over a window" since M0 and
|
|
3374
|
+
M1 implemented an instantaneous speed.
|
|
3375
|
+
|
|
3376
|
+
### The rule, and the threshold
|
|
3377
|
+
|
|
3378
|
+
Displacement from an anchor taken when the window opened, re-anchored the moment
|
|
3379
|
+
any particle leaves the threshold; `CLOTH_SLEEP_STEPS` consecutive steps inside
|
|
3380
|
+
it is sleep. Swept over the ratio, every entry × every scene, "steps to sleep":
|
|
3381
|
+
|
|
3382
|
+
```
|
|
3383
|
+
ratio of reach worst where any awake at 2,400?
|
|
3384
|
+
1e-3 (as speed) -1 COTTON on a floor 4 of 28
|
|
3385
|
+
1e-3 872 SILK on a frictionless floor no
|
|
3386
|
+
5e-4 961 SILK on a frictionless floor no
|
|
3387
|
+
2e-4 965 SILK on a frictionless floor no
|
|
3388
|
+
1e-4 1773 COTTON on a frictionless floor no
|
|
3389
|
+
```
|
|
3390
|
+
|
|
3391
|
+
**5e-4 is the value**, and it is chosen to be *no looser* than the speed test it
|
|
3392
|
+
replaces rather than to be the fastest: over a 30-step window at the engine's
|
|
3393
|
+
60 Hz fixed step, half a thousandth of a reach per window is the same millimetre
|
|
3394
|
+
a second per step that `CLOTH_SLEEP_SPEED_RATIO` named. 1e-3 would sleep sooner
|
|
3395
|
+
and would be a real relaxation of the gate; 1e-4 costs nearly twice the worst
|
|
3396
|
+
case and buys nothing visible.
|
|
3397
|
+
|
|
3398
|
+
With it in place, the table at the top of this section reads:
|
|
3399
|
+
|
|
3400
|
+
```
|
|
3401
|
+
entry free floor mu=0 floor mu=0.5 sphere mu=0
|
|
3402
|
+
SILK 325 961 309 573
|
|
3403
|
+
COTTON 180 361 277 325
|
|
3404
|
+
DENIM 92 195 128 194
|
|
3405
|
+
LEATHER 127 175 109 151
|
|
3406
|
+
ROPE 235 840 941 393
|
|
3407
|
+
CHAIN 143 425 201 261
|
|
3408
|
+
HAIR 132 260 177 246
|
|
3409
|
+
```
|
|
3410
|
+
|
|
3411
|
+
Nothing is awake, the worst case is 961 steps and the free-hanging column has
|
|
3412
|
+
not got worse — it is uniformly *faster*, because a chain settling through a
|
|
3413
|
+
swing covers less ground per window than its instantaneous speed suggested.
|
|
3414
|
+
|
|
3415
|
+
### What it costs
|
|
3416
|
+
|
|
3417
|
+
One compare-only pass over the particle positions per cloth per step, on the
|
|
3418
|
+
main thread, in `ClothWorld#finish`, with no square root — the displacements are
|
|
3419
|
+
compared squared — and one `Float32Array` of `3·particle_count` per instance. It
|
|
3420
|
+
does not enter the step, so `cloth_library_bench.mjs` cannot see it and does not:
|
|
3421
|
+
the library column is unchanged across this commit.
|
|
3422
|
+
|
|
3423
|
+
*Reproduce:* `cloth_pathology.spec.js`, the two P5 sleep tests. The table above
|
|
3424
|
+
is the same scenario driven from a script rather than through vitest.
|
|
3425
|
+
|
|
3426
|
+
---
|
|
3427
|
+
|
|
3428
|
+
## 54. A collider that does not move is not a collider a cloth cannot hit
|
|
3429
|
+
|
|
3430
|
+
§20 measured the discovery reach missing a **moving collider** and fixed it by
|
|
3431
|
+
folding the collider's travel into the reach. The mirror case was argued away in
|
|
3432
|
+
the same docblock rather than measured: a fixed margin of `2·(thickness +
|
|
3433
|
+
inflation)` "covers half a metre a second of approach", and anything faster is
|
|
3434
|
+
"one the velocity clamp of §7 P1 is already the bound on".
|
|
3435
|
+
|
|
3436
|
+
**It is not.** That clamp is `4 · shortest_row · substeps · iterations / dt`,
|
|
3437
|
+
which for a garment's rows at 2 × 4 is about **154 m/s** — three orders of
|
|
3438
|
+
magnitude above the half metre a second the margin actually covers. Everything
|
|
3439
|
+
between is uncovered, and a character walks through it.
|
|
3440
|
+
|
|
3441
|
+
### The reproduction
|
|
3442
|
+
|
|
3443
|
+
One particle at 8 mm thickness, thrown along +X at a **static** 3 cm capsule,
|
|
3444
|
+
no gravity so that nothing but the contact can change its velocity, 2 substeps ×
|
|
3445
|
+
4 iterations. "Warned" is whether a row existed by the time the particle was
|
|
3446
|
+
first sampled inside the contact offset; "far side" is whether it ended past the
|
|
3447
|
+
capsule rather than bounced off it.
|
|
3448
|
+
|
|
3449
|
+
```
|
|
3450
|
+
with |v|·dt in the reach as M2 shipped
|
|
3451
|
+
speed warned far side x_final warned far side x_final
|
|
3452
|
+
1 m/s yes no -0.672 no no -1.464
|
|
3453
|
+
2 yes no -1.268 no no -2.058
|
|
3454
|
+
3 yes no -1.371 no YES +13.315
|
|
3455
|
+
4 yes no -2.220 no YES +14.294
|
|
3456
|
+
5 yes no -0.532 no YES +25.516
|
|
3457
|
+
6 yes no -1.287 no YES +19.800
|
|
3458
|
+
8 yes YES +26.285 — YES +26.467
|
|
3459
|
+
10 yes YES +31.285 no YES +33.133
|
|
3460
|
+
```
|
|
3461
|
+
|
|
3462
|
+
Two separate readings. **`warned` is never true without the term, at any
|
|
3463
|
+
speed** — the first row a particle ever gets is the one that finds it already
|
|
3464
|
+
inside, which is the opposite of what a speculative contact is for. And from
|
|
3465
|
+
**3 m/s** the particle is not stopped at all: the row the next step finally
|
|
3466
|
+
creates is evaluated where the particle now is, which is on the far side, and
|
|
3467
|
+
its normal points away from the collider, so the first thing the contact does is
|
|
3468
|
+
push it the rest of the way out. §7 P4(a) chose orientation over history
|
|
3469
|
+
deliberately and this is the price of that choice when discovery is late.
|
|
3470
|
+
|
|
3471
|
+
### What the fix does and does not buy
|
|
3472
|
+
|
|
3473
|
+
Swept, in half-metre steps from 1 to 12 m/s, whether the particle is stopped:
|
|
3474
|
+
|
|
3475
|
+
```
|
|
3476
|
+
substeps stopped up to through from
|
|
3477
|
+
2 6.0 m/s 6.5 m/s
|
|
3478
|
+
4 12.0 m/s — nothing in range
|
|
3479
|
+
```
|
|
3480
|
+
|
|
3481
|
+
A clean threshold, and it is `substeps` rather than discovery: a row that exists
|
|
3482
|
+
is still evaluated at substep positions, so a particle that crosses a whole
|
|
3483
|
+
collider inside one substep has a row that never sees it touching. §7 P3 has
|
|
3484
|
+
always said "more substeps in the affected `ClothDynamics`"; this is the first
|
|
3485
|
+
measurement of where that lands. Discovery being right is necessary and not
|
|
3486
|
+
sufficient, and the two are now separable — unswept at four substeps still
|
|
3487
|
+
tunnels at 8 m/s.
|
|
3488
|
+
|
|
3489
|
+
### What it costs
|
|
3490
|
+
|
|
3491
|
+
Interleaved A/B in one process, alternating which side runs first, `dt` against
|
|
3492
|
+
zero — zero being exactly the reach M2 shipped:
|
|
3493
|
+
|
|
3494
|
+
```
|
|
3495
|
+
1,024 particles, one ground plane, best of eight interleaved pairs
|
|
3496
|
+
|
|
3497
|
+
approach with travel as M2 shipped ratio
|
|
3498
|
+
at rest 45.16 ns 45.35 ns 0.996
|
|
3499
|
+
falling at 3 m/s 92.32 ns 55.88 ns 1.652
|
|
3500
|
+
```
|
|
3501
|
+
|
|
3502
|
+
**At rest it is free** — the square root disappears into the measurement's own
|
|
3503
|
+
noise. Moving, it is 36 ns a particle a step, and that is not the arithmetic: it
|
|
3504
|
+
is the records a wider reach lets past the AABB reject, which is the feature
|
|
3505
|
+
rather than its overhead. Against the 4.5 ms §23 measures for this workload it
|
|
3506
|
+
is **0.8 % of a step**, and `cloth_library_bench.mjs` cannot see it at all,
|
|
3507
|
+
because the bench drives `cloth_step` with no collider table — the library
|
|
3508
|
+
column is unchanged across this commit, which is the regression check rather
|
|
3509
|
+
than the measurement.
|
|
3510
|
+
|
|
3511
|
+
### And it moved a sleep number, which is a coupling worth naming
|
|
3512
|
+
|
|
3513
|
+
`cloth_contact_dual_update` ramps the penalty by `β·|C|` on the **unsigned**
|
|
3514
|
+
residual, so a row that is not touching ramps too. That was nearly invisible
|
|
3515
|
+
while rows only appeared within 16 mm; with the reach carrying a falling cloth's
|
|
3516
|
+
travel, rows now appear tens of centimetres out and reach `k_max` long before
|
|
3517
|
+
contact, and `γ = 1` never brings them down again. §53's table moves with it:
|
|
3518
|
+
most entries settle *sooner*, and hair on a floor with friction goes from 177
|
|
3519
|
+
steps to **2,202** — a creep at 1.6 times the sleep window's threshold rather
|
|
3520
|
+
than a cloth that has stopped.
|
|
3521
|
+
|
|
3522
|
+
Making the ramp one-sided is the obvious repair and **the measurement says not
|
|
3523
|
+
to**:
|
|
3524
|
+
|
|
3525
|
+
```
|
|
3526
|
+
stopped up to, half-metre steps from 1 to 12 m/s
|
|
3527
|
+
|
|
3528
|
+
2 substeps 4 substeps
|
|
3529
|
+
ramp on |C| 6.0 m/s 12.0 m/s, no holes
|
|
3530
|
+
ramp on max(C, 0) 6.0 m/s, hole at 5 9.5 m/s, holes at 10–11.5
|
|
3531
|
+
```
|
|
3532
|
+
|
|
3533
|
+
The pre-stiffening of a speculative row is what gives a fast arrival something
|
|
3534
|
+
solid to hit; removing it trades a visible failure — cloth through a character —
|
|
3535
|
+
for an invisible one — a hair rope taking thirty-seven seconds to stop creeping
|
|
3536
|
+
instead of three. So the ramp is left alone and the coupling is recorded in §18.
|
|
3537
|
+
|
|
3538
|
+
---
|
|
3539
|
+
|
|
3540
|
+
## 55. A collider arriving and a collider retiring, and what each one was not told
|
|
3541
|
+
|
|
3542
|
+
Two defects at the two ends of a collider's life, both of them a *set* question
|
|
3543
|
+
answered with a *pose*.
|
|
3544
|
+
|
|
3545
|
+
### Arriving
|
|
3546
|
+
|
|
3547
|
+
§7 P5's wake test compares each overlapping collider's previous pose with its
|
|
3548
|
+
current one. `ClothColliderSystem#link` calls `placePose`, which sets both — on
|
|
3549
|
+
purpose, so a new collider's first step sweeps nothing, because it did not move,
|
|
3550
|
+
it appeared. The two facts together say a collider that appears inside a
|
|
3551
|
+
sleeping cloth has not moved.
|
|
3552
|
+
|
|
3553
|
+
```
|
|
3554
|
+
6-link rope, 10 cm links, left to settle: asleep at step 236
|
|
3555
|
+
a sphere of radius 0.2 spawned around its lower half, never moved
|
|
3556
|
+
|
|
3557
|
+
300 steps later: asleep links inside the sphere
|
|
3558
|
+
before yes 4 of 6
|
|
3559
|
+
after no 0 of 6
|
|
3560
|
+
```
|
|
3561
|
+
|
|
3562
|
+
§1's first commitment names this case in its own words, and the gate for it was
|
|
3563
|
+
`cloth_collision.spec.js`'s "a collider that appeared but has not moved does not
|
|
3564
|
+
wake anything" — a test that was passing because the thing it asserted was the
|
|
3565
|
+
defect.
|
|
3566
|
+
|
|
3567
|
+
The fix is the **handle set**: `ClothInstance` keeps the handles the last step
|
|
3568
|
+
gathered, and the wake test compares this step's against them before it compares
|
|
3569
|
+
poses. One integer per record, on a path that was already doing a broadphase
|
|
3570
|
+
query, against a step three orders of magnitude more expensive.
|
|
3571
|
+
|
|
3572
|
+
Two details it turns on. The wake test's query has to use the **same reach** the
|
|
3573
|
+
last step's gather used, or it is comparing the answers to two different
|
|
3574
|
+
questions and wakes on the margin rather than on the world; `gather_reach` is
|
|
3575
|
+
recorded beside the set. And a `PlaneShape3D`'s AABB is unbounded, so a plane is
|
|
3576
|
+
in *every* cloth's set the moment it exists: adding one anywhere wakes every
|
|
3577
|
+
sleeping cloth once, which costs the thirty steps it takes to find nothing and
|
|
3578
|
+
go back to sleep. That is stated rather than worked around — a bounded collider
|
|
3579
|
+
five metres away does not wake anything, and the spec pins both.
|
|
3580
|
+
|
|
3581
|
+
### Retiring
|
|
3582
|
+
|
|
3583
|
+
`ClothColliderIndex#add` popped a retired slot immediately. A handle is a slot
|
|
3584
|
+
index, and `cloth_contact_find` matches warm-started contacts on the handle
|
|
3585
|
+
alone — so a collider removed and another added within one tick are, to every
|
|
3586
|
+
cloth in the scene, the same collider.
|
|
3587
|
+
|
|
3588
|
+
```
|
|
3589
|
+
a rope settled on a floor for 400 steps, then in one step:
|
|
3590
|
+
remove the floor, add a different collider
|
|
3591
|
+
|
|
3592
|
+
the floor's handle: 0
|
|
3593
|
+
the replacement's handle: 0 <- the cloth cannot tell
|
|
3594
|
+
the particle's multiplier: 0.0981 before, 0.0970 after
|
|
3595
|
+
the friction anchor: carried, still in the old floor's frame
|
|
3596
|
+
```
|
|
3597
|
+
|
|
3598
|
+
Both index docblocks claimed the opposite — "any cloth holding warm-started
|
|
3599
|
+
contacts against it drops them on its next step" — and the claim is true only if
|
|
3600
|
+
the handle is *absent* for a step, which nothing made it. `remove` now retires
|
|
3601
|
+
into a list `advance` drains one step later. The cost is one slot held back for
|
|
3602
|
+
one step; the alternative, a generation word beside the handle, is the same
|
|
3603
|
+
guarantee threaded through the per-cloth table, the key array and the warm-start
|
|
3604
|
+
scan.
|
|
3605
|
+
|
|
3606
|
+
### They compose, and that is not a coincidence
|
|
3607
|
+
|
|
3608
|
+
The deferral makes the handle absent for a step; the set comparison is what makes
|
|
3609
|
+
a **sleeping** cloth notice that absence, wake, and clear its slots before
|
|
3610
|
+
anything else can take the number. Either alone leaves a case open: without the
|
|
3611
|
+
deferral the slot is reused before the cloth steps, and without the set test a
|
|
3612
|
+
sleeping cloth never steps at all.
|
|
3613
|
+
|
|
3614
|
+
*Reproduce:* `cloth_collision.spec.js` — the far collider, the overlapping
|
|
3615
|
+
collider, the leaving collider and the replaced collider — and
|
|
3616
|
+
`ClothColliderIndex.spec.js`'s retirement test.
|
|
3617
|
+
|
|
3618
|
+
---
|
|
3619
|
+
|
|
3620
|
+
## 56. The parity gate had one cloth in it, and a second cloth is a different gate
|
|
3621
|
+
|
|
3622
|
+
§40 made the gate structural: one parameter block, one `cloth_step_instance`,
|
|
3623
|
+
neither system computing a number of its own. Every scene it was measured on has
|
|
3624
|
+
exactly one cloth in it, and two of the three things that can differ between the
|
|
3625
|
+
systems only exist when there are two.
|
|
3626
|
+
|
|
3627
|
+
### A nested pair
|
|
3628
|
+
|
|
3629
|
+
`ClothSystem` ran advance, step and finish **per cloth**, walking
|
|
3630
|
+
`ClothWorld#instances` in hierarchy-depth order — a sort that existed precisely
|
|
3631
|
+
so a nested cloth would advance against an anchor the cloth above it had already
|
|
3632
|
+
written this step. `WorkerClothSystem` cannot do that: the step it writes back at
|
|
3633
|
+
the top of a tick happened on another thread during the last one, so its `apply`
|
|
3634
|
+
writes T−1 and its `collect` then advances everything for T.
|
|
3635
|
+
|
|
3636
|
+
```
|
|
3637
|
+
a 4-link rope hung from link 3 of a 6-link rope, no wind, 120 steps
|
|
3638
|
+
|
|
3639
|
+
outer cloth: identical, every step
|
|
3640
|
+
inner cloth: differs at step 1, byte 24
|
|
3641
|
+
```
|
|
3642
|
+
|
|
3643
|
+
Byte 24 is inside the particle positions, which is the honest place for it: the
|
|
3644
|
+
child is simulating against a different anchor, not reporting a different number
|
|
3645
|
+
about the same one.
|
|
3646
|
+
|
|
3647
|
+
**Three loops on both sides** — advance every cloth, then step every cloth, then
|
|
3648
|
+
finish every cloth — and the depth sort has nothing left to decide, so it is
|
|
3649
|
+
gone. What a nested cloth reads is its parent one fixed step stale, which is the
|
|
3650
|
+
same trade §4.1 already makes for an anchor and measures at a few centimetres.
|
|
3651
|
+
|
|
3652
|
+
**That was necessary and not sufficient**, and the second half is more
|
|
3653
|
+
interesting than the first. `cloth_write_back` composed every node's world pose
|
|
3654
|
+
as `anchor ⊗ local particle position`, reading the anchor from the live
|
|
3655
|
+
`Transform64`. For `ClothSystem` that is the anchor the step ran on, because the
|
|
3656
|
+
step happened between the two. For the worker it is not: it writes step T−1's
|
|
3657
|
+
particles against tick T's anchor.
|
|
3658
|
+
|
|
3659
|
+
```
|
|
3660
|
+
the inner cloth's anchor, as each system's write-back left it
|
|
3661
|
+
|
|
3662
|
+
step reference worker (before) worker (after)
|
|
3663
|
+
2 0.038945 0.069849 0.038945
|
|
3664
|
+
3 0.038341 0.069079 0.038341
|
|
3665
|
+
4 0.037680 0.068171 0.037680
|
|
3666
|
+
```
|
|
3667
|
+
|
|
3668
|
+
The worker's column before the fix is the parent's *current* anchor plus a
|
|
3669
|
+
one-step-old shape — a pose that describes neither step. Nothing rendered can
|
|
3670
|
+
tell, because the attachment cascade re-derives position from the anchor and the
|
|
3671
|
+
local transforms either way (§4.1); a nested cloth reading that pose as its own
|
|
3672
|
+
anchor can. The write-back now uses `ClothInstance#anchor_translation` and
|
|
3673
|
+
`#anchor_rotation`, which `advance` already caches and which the collider gather
|
|
3674
|
+
already reads for the same reason.
|
|
3675
|
+
|
|
3676
|
+
### What the gate is now
|
|
3677
|
+
|
|
3678
|
+
`WorkerClothSystem.spec.js` compares the whole buffer of **both** cloths at every
|
|
3679
|
+
one of 120 steps, with the parent's anchor walked through a turn so the child has
|
|
3680
|
+
something to be wrong about, and self-checks that the child actually moved.
|
|
3681
|
+
|
|
3682
|
+
---
|
|
3683
|
+
|
|
3684
|
+
## 57. A world clock is right for the first cloth in a scene and wrong for the second
|
|
3685
|
+
|
|
3686
|
+
§50 put the wind clock on steps rather than ticks so that a cloth's first step
|
|
3687
|
+
got the same weather under both systems. It did, and the scene it was measured
|
|
3688
|
+
in had one cloth.
|
|
3689
|
+
|
|
3690
|
+
With cloth A already running, cloth B's registration takes k ticks — and A is
|
|
3691
|
+
advanced on every one of them, so the world clock moves while B waits. B
|
|
3692
|
+
therefore reaches **its own first step** at a different clock value under the two
|
|
3693
|
+
systems:
|
|
3694
|
+
|
|
3695
|
+
```
|
|
3696
|
+
A primed and running, B spawned, a two-tick delivery for B's registration
|
|
3697
|
+
|
|
3698
|
+
B's own step 1: reference clock 0.1000 s
|
|
3699
|
+
worker clock 0.1333 s
|
|
3700
|
+
|
|
3701
|
+
differing bytes in B's buffer: step 1, byte 0, and every step after
|
|
3702
|
+
the same run with gust = 0: none
|
|
3703
|
+
```
|
|
3704
|
+
|
|
3705
|
+
The `gust = 0` row is what identifies the cause. Latency is not the problem —
|
|
3706
|
+
§39 already made an unacknowledged cloth neither stepped nor advanced, so B's
|
|
3707
|
+
step *sequence* is identical — the problem is that a shared clock is not a
|
|
3708
|
+
property of the cloth asking.
|
|
3709
|
+
|
|
3710
|
+
**The clock is now the asking cloth's own age**, `ClothInstance#age`, seconds
|
|
3711
|
+
*this instance* has stepped, handed to `AbstractClothWind#sample` per call. The
|
|
3712
|
+
source holds no clock: `ClothAmbientWind#__time` is gone and `begin` keeps only
|
|
3713
|
+
the job it can do correctly, which is telling a source that reads the scene —
|
|
3714
|
+
`ClothFluidWind`'s field walk — when to walk it. `ClothWorld#time` stays for
|
|
3715
|
+
exactly that.
|
|
3716
|
+
|
|
3717
|
+
*What it changes behaviourally:* two garments spawned a second apart are a second
|
|
3718
|
+
apart in the gust rather than in phase with it. For a bounded sum of three
|
|
3719
|
+
sinusoids with no spatial structure (§48) that is not a weaker model, it is the
|
|
3720
|
+
same model sampled at a different phase — and a scene in which every banner
|
|
3721
|
+
gusts in lockstep was the odder of the two pictures.
|
|
3722
|
+
|
|
3723
|
+
*What it does not change:* a fluid field's velocities are a snapshot of the tick,
|
|
3724
|
+
not a function of a clock, so `ClothFluidWind` passes the age through to its
|
|
3725
|
+
ambient fallback and ignores it otherwise.
|
|
3726
|
+
|
|
3727
|
+
*Reproduce:* `WorkerClothSystem.spec.js`, the staggered-spawn parity test.
|
|
3728
|
+
|
|
3729
|
+
---
|
|
3730
|
+
|
|
3731
|
+
## 58. The library, re-measured, and what the bench can and cannot see
|
|
3732
|
+
|
|
3733
|
+
`cloth_library_bench.mjs`, before this pass and after it, on the same machine
|
|
3734
|
+
within the hour:
|
|
3735
|
+
|
|
3736
|
+
```
|
|
3737
|
+
entry budget before after delta
|
|
3738
|
+
SILK 2x4 1.561 ms 1.649 ms +5.6%
|
|
3739
|
+
COTTON 2x4 1.655 ms 1.546 ms -6.6%
|
|
3740
|
+
DENIM 2x2 1.261 ms 1.343 ms +6.5%
|
|
3741
|
+
LEATHER 2x2 1.346 ms 1.246 ms -7.4%
|
|
3742
|
+
ROPE 2x4 0.043 ms 0.042 ms -2.3%
|
|
3743
|
+
CHAIN 2x6 0.063 ms 0.063 ms +0.0%
|
|
3744
|
+
HAIR 2x4 1.537 ms 1.702 ms +10.7%
|
|
3745
|
+
```
|
|
3746
|
+
|
|
3747
|
+
**Both signs, no pattern, ±11%** — which is §42's and §52's lesson arriving a
|
|
3748
|
+
third time, and here it is not merely a disclaimer: **the bench cannot see any
|
|
3749
|
+
of this pass's solver work**, and reading the code is what says so rather than
|
|
3750
|
+
reading the column. It builds a `ClothState` and calls `cloth_step` with a null
|
|
3751
|
+
collider table, so `cloth_contact_find` — the one function §54 changed — never
|
|
3752
|
+
runs; and it never constructs a `ClothWorld`, so the sleep window, the wake test,
|
|
3753
|
+
the write-back's anchor and the wind clock are all outside it. The column is a
|
|
3754
|
+
**regression check** on the paths this pass did not touch, and it passes: no
|
|
3755
|
+
entry moves outside the spread three runs of the unchanged bench already produce
|
|
3756
|
+
(§52 put the §17 baseline row at 3.49, 3.83 and 4.26 ms within an hour).
|
|
3757
|
+
|
|
3758
|
+
What *is* priced, and is priced by a paired measurement rather than by this
|
|
3759
|
+
column, is §54's discovery reach: free at rest, 1.65× on the discovery pass for a
|
|
3760
|
+
cloth falling at 3 m/s, which is 0.8% of a step. §53's sleep window is one
|
|
3761
|
+
compare-only pass over positions on the main thread, in `ClothWorld#finish`,
|
|
3762
|
+
which the bench also cannot see and which sits beside a write-back that already
|
|
3763
|
+
walks every node.
|
|
3764
|
+
|
|
3765
|
+
**No entry is re-sized.** Three passes have now said the same thing about this
|
|
3766
|
+
column and the advice does not change: re-measure it on a quiet machine, take a
|
|
3767
|
+
paired reading even then, and check the process count first.
|
|
3768
|
+
|
|
3769
|
+
---
|
|
3770
|
+
|
|
3771
|
+
## 59. The broadphase was a scan, and the margin it scanned with was metres wide
|
|
3772
|
+
|
|
3773
|
+
Two defects, found from one downstream console line: a cape overlapping **80**
|
|
3774
|
+
cloth colliders against a per-cloth table of 64. Both are about the gather, and
|
|
3775
|
+
only one of them is the one the warning names.
|
|
3776
|
+
|
|
3777
|
+
### The margin
|
|
3778
|
+
|
|
3779
|
+
`cloth_gather_colliders` fattened the cloth's particle bounds by the §7 P1
|
|
3780
|
+
velocity clamp read as a distance — `4 · shortest_row · substeps · iterations`.
|
|
3781
|
+
That is not a distance the cloth can travel; it is a bound on what the solver
|
|
3782
|
+
could undo. Over the shipped library at 60 Hz:
|
|
3783
|
+
|
|
3784
|
+
```
|
|
3785
|
+
entry sub x it row 2cm row 4cm row 8cm
|
|
3786
|
+
SILK 2 x 4 0.64 m 1.28 m 2.56 m
|
|
3787
|
+
COTTON 2 x 4 0.64 m 1.28 m 2.56 m
|
|
3788
|
+
DENIM 2 x 2 0.32 m 0.64 m 1.28 m
|
|
3789
|
+
CHAIN 2 x 6 0.96 m 1.92 m 3.84 m
|
|
3790
|
+
```
|
|
3791
|
+
|
|
3792
|
+
The fattening is routinely larger than the garment. `cloth_contact_find` had
|
|
3793
|
+
already reached this conclusion for itself at §54 — it calls the same clamp
|
|
3794
|
+
"about 154 m/s … three orders of magnitude" past the speeds that matter and
|
|
3795
|
+
reaches with `|v_i|·dt` instead — and the gather was never brought along.
|
|
3796
|
+
|
|
3797
|
+
Being enormous also hid what it was **missing**. The narrowphase reaches
|
|
3798
|
+
`(radius + inflation) · (1 + CLOTH_CONTACT_MARGIN) + collider travel +
|
|
3799
|
+
|v_i|·dt`; the gather's fixed term was `2 · contact_radius` and nothing else, so
|
|
3800
|
+
a wide enough `ClothCollider#inflation` was a contact the gather never handed
|
|
3801
|
+
over. `cloth_gather_reach` is now that expression, maximised over the records
|
|
3802
|
+
the gather cannot yet know it will get — which is what `ClothColliderIndex`'s
|
|
3803
|
+
`max_inflation` and `max_collider_travel` are for. It is a **bound** and not an
|
|
3804
|
+
estimate: the gather reads velocity before the step and `cloth_contact_find`
|
|
3805
|
+
reads it after `damp`, which only ever scales down.
|
|
3806
|
+
|
|
3807
|
+
Measured on the rope of §20 — eight 10 cm links at 2 × 4, five colliders placed
|
|
3808
|
+
2 m away and one against the tail:
|
|
3809
|
+
|
|
3810
|
+
```
|
|
3811
|
+
gather margin, hanging 3.2006 m -> 0.0209 m
|
|
3812
|
+
colliders gathered 6 -> 1
|
|
3813
|
+
```
|
|
3814
|
+
|
|
3815
|
+
The 3.2006 m does not move when the rope is thrown at 9 m/s. The new one does,
|
|
3816
|
+
which is the entire point of it.
|
|
3817
|
+
|
|
3818
|
+
### The scan
|
|
3819
|
+
|
|
3820
|
+
`ClothColliderIndex#query` was a linear scan over every live record, run once
|
|
3821
|
+
per cloth per step — `clothes × colliders`. The docblock defended it on two
|
|
3822
|
+
grounds and both were wrong. The set size ("twenty or so, and never the whole
|
|
3823
|
+
world") was §41's count of colliders overlapping **one garment**, which is a
|
|
3824
|
+
narrowphase input being used to size a world-wide structure. The churn argument
|
|
3825
|
+
("a tree over twenty leaves that all move every frame costs more to keep than to
|
|
3826
|
+
skip") is answered by `PhysicsSystem`, which keeps two dynamic BVHs over bodies
|
|
3827
|
+
that all move every frame and pays for it with `compute_fat_world_aabb`'s refit
|
|
3828
|
+
skip. The third ground, `PlaneShape3D`'s unbounded AABB, is real and is not a
|
|
3829
|
+
reason for anything: the half-space is constructed in exactly one place in this
|
|
3830
|
+
engine — the JSON shape adapter — and no scene builds one, so the planes sit in
|
|
3831
|
+
a list beside the tree that is empty in every scene that has ever run.
|
|
3832
|
+
|
|
3833
|
+
Colliders drifting 0.4 to 2 mm a step, every cloth gathering every step, paired
|
|
3834
|
+
runs with the order alternated:
|
|
3835
|
+
|
|
3836
|
+
```
|
|
3837
|
+
colliders cloths scan pose scan gather tree pose tree gather gather
|
|
3838
|
+
64 8 0.012 0.001 0.010 0.004 0.4x
|
|
3839
|
+
256 16 0.047 0.009 0.046 0.011 0.8x
|
|
3840
|
+
1024 32 0.207 0.073 0.205 0.029 2.5x
|
|
3841
|
+
4096 64 0.961 1.090 0.912 0.076 14.4x
|
|
3842
|
+
16384 128 4.684 9.930 4.293 0.205 48.5x
|
|
3843
|
+
```
|
|
3844
|
+
|
|
3845
|
+
`pose` is `advance` plus a `setPose` per collider, which is O(colliders) once a
|
|
3846
|
+
step in both and is unchanged — tree maintenance is inside the noise. `gather`
|
|
3847
|
+
is the `clothes × colliders` term, and it is gone. Below a few hundred
|
|
3848
|
+
colliders the scan is very slightly ahead, on numbers too small to matter.
|
|
3849
|
+
|
|
3850
|
+
### Two things the tree got wrong first, both measured
|
|
3851
|
+
|
|
3852
|
+
**A leaf inserted before its pose homes at the origin.** `add` built the leaf
|
|
3853
|
+
and `link` placed it immediately afterwards — but `insert_leaf` picks a sibling
|
|
3854
|
+
from where the box *is*, and `node_move_aabb` refits ancestors without ever
|
|
3855
|
+
asking whether the leaf still belongs under them. So a scene added-then-placed
|
|
3856
|
+
built its whole tree out of one coincident point and never rebuilt it: a root 21
|
|
3857
|
+
deep whose every branch overlapped every query, **128 gathers at 128 ms against
|
|
3858
|
+
0.2 ms** for the same leaves inserted at their real positions. The leaf is now
|
|
3859
|
+
created on the first `setPose`, and a leaf that escapes its slack is removed and
|
|
3860
|
+
re-inserted rather than refit.
|
|
3861
|
+
|
|
3862
|
+
**A `Float32` leaf over a `Float64` world loses a bound.** The tree's nodes are
|
|
3863
|
+
`Float32` and the table is `Float64` precisely because a world coordinate can be
|
|
3864
|
+
far from the origin. One `Float32` ulp is a proportion of the magnitude — a
|
|
3865
|
+
millimetre at 10 km, a whole metre at 10,000 km — so a box widened by a fixed
|
|
3866
|
+
5 cm and then stored can come back **45 cm inside its own record**, and a leaf
|
|
3867
|
+
smaller than its record is a collider the tree silently stops answering with.
|
|
3868
|
+
The slack now carries a relative term of two ulps as well as the constant. It
|
|
3869
|
+
fails without it, and only for a cloth grazing a face: a query box as wide as
|
|
3870
|
+
the collider survives losing one side, which is why the first spec written for
|
|
3871
|
+
this passed and pinned nothing.
|