reze-engine 0.36.0 → 0.36.1

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/README.md CHANGED
@@ -333,9 +333,10 @@ In-house sequential-impulse rigid-body solver for PMX rigs (sphere / box / capsu
333
333
  - **Implicit spring-dampers** — each sprung axis solves the backward-Euler soft constraint `relVel⁺ + (k/γ)·err + s·λ = 0` (`γ = c + h·k`, `s = 1/(h·γ)`), unconditionally stable for any authored stiffness, with damping `c = 2ζ√(k·m_eff)` intrinsic to the row. Resting cloth genuinely settles: the previous clamped velocity-drive could inject energy far from equilibrium but not absorb it near it, so static dresses slowly "boiled" — measured 15× lower resting velocity after the change, with authored stiffness now delivered in full (no deadbeat clamp).
334
334
  - **Angular limits** — hybrid: small violations (< 0.5 rad, the resting-cloth regime) use per-axis Euler stop rows, which converge cleanly and keep resting cloth still; larger violations switch to a single geodesic row toward the Euler-clamped target rotation, because per-axis Euler rows chase phantom errors near the ±90° singularity and pump energy. Ranged stops are unilateral (accumulated impulse clamped to the corrective sign) so a limit pushes back into range but never brakes natural recovery; locked axes stay bilateral equality joints. Spring rows stay per-axis, with stiffness clamped to the `k·dt² ≤ ¼` stability bound.
335
335
  - **Narrowphase** — analytical sphere-sphere / -capsule / -box, capsule-capsule / -box, and box-box (SAT + face clipping, up to 4 points per manifold). MMD dress rigs are built from box panels, and box-box is the majority of collidable pairs on those models — without it cloth passes through cloth. Capsule-capsule emits multiple contacts along near-parallel axes so cloth can't pivot around a single closest point.
336
- - **Speculative contacts** (`margin 0.04`) fire at near-touch and cancel only the approach velocity in excess of what closes the remaining gap in one substep (`gap/dt`), so they stop fast bodies tunneling thin surfaces while staying inert until the body would actually arrive. The push-only clamp does not achieve that on its own it forbids a pulling impulse, not a large pushing one on a body still in mid-air.
337
- - **Contact relaxation** — each contact row's gain is `1/max(rows on A, rows on B)` over dynamic bodies (floor 0.12). Every row drives the relative velocity at its own point to zero, so a dress panel carrying dozens at once is over-braked, differently each substep; a body on a lone contact keeps full gain and still settles exactly. A flat factor damps the well-conditioned rows too and leaves single-contact bodies creeping instead of settling.
338
- - **Split-impulse correction** resolves penetration by a mass-weighted translation _outside_ the velocity solver, so joint pulls can't fight separation.
336
+ - **Penetration and separation are one term**, ported from Bullet 2.75 (`m_erp` 0.2), the build MMD's own physics runs: the contact row's target carries `depth · erp / dt`. Penetrating, it pushes apart; separated — a speculative row inside the `0.04` margin it *allows* the approach that closes the gap, which is what keeps those rows inert until the body would really arrive. A push-only clamp does not achieve that on its own: it forbids a pulling impulse, not a large pushing one on a body still in mid-air.
337
+ - **Warm starting** (`0.85`) over persistent per-pair manifolds, up to 4 points matched in each body's own local frame, so a resting stack starts each substep already holding roughly the load it needs. When a 5th point arrives the one dropped is whichever leaves the largest quadrilateral area is what stops a resting box pivoting.
338
+ - **Contact ordering is reshuffled between iterations.** Projected Gauss-Seidel is order-biased, rows solved first win, and on a cross-linked skirt lattice that reads as chatter. The shuffle is re-seeded per substep, so a scene still replays identically.
339
+ - **Split impulse** resolves deep overlap on a pseudo-velocity channel with its own full iteration pass, integrated straight into the transform. Recovering penetration therefore adds no real momentum for the joint springs to hand back — the property MMD depends on, given how constantly its hair and layered skirts clip.
339
340
  - **Kinematic advancement** — bone-driven bodies move toward the frame's bone pose incrementally per substep, with velocities derived over the fixed step, so the solver never sees more than one 60 Hz step of anchor motion regardless of render dt.
340
341
  - **Discontinuity guards** — a bone-pose jump beyond continuous motion (timeline scrub, long stall) rigidly carries each dynamic body along with its kinematic root's transform delta and zeroes momentum instead of dragging cloth across the gap; correction velocities are clamped (120 u/s linear, 30 rad/s angular), per-step travel is capped, and any body that still goes non-finite is restored to its previous substep pose.
341
342
  - Sleeping is off (cloth must always react); resting bodies bleed micro-velocity via per-PMX damping.
@@ -26,9 +26,18 @@ export interface Contact {
26
26
  cBzN: number;
27
27
  jacInvN: number;
28
28
  bounceVel: number;
29
- /** Per-contact relaxation gain, 1/max(rows on A, rows on B). See CONTACT_SOR_MIN. */
30
- sorGain: number;
31
- allowedApproachVel: number;
29
+ /** Substeps this contact point has persisted (from the manifold cache).
30
+ * Bullet kills restitution past m_restingContactRestitutionThreshold = 2. */
31
+ age: number;
32
+ /** Penetration term routed to the SPLIT (pseudo-velocity) channel instead of
33
+ * the real one, when the contact is deeper than the split threshold. */
34
+ rhsPenetration: number;
35
+ /** Accumulated impulse on the split channel — Bullet's m_appliedPushImpulse. */
36
+ appliedPushImpulse: number;
37
+ /** Baumgarte bias, depth·ERP/dt — Bullet 2.75's positionalError. Positive
38
+ * when penetrating (pushes apart), negative when separated (allows the
39
+ * approach that closes the gap). See CONTACT_ERP. */
40
+ biasVel: number;
32
41
  t1x: number;
33
42
  t1y: number;
34
43
  t1z: number;
@@ -1 +1 @@
1
- {"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAa5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAI/B,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAA;IAGf,kBAAkB,EAAE,MAAM,CAAA;IAE1B,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAEhB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAgCD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,KAAK,SAAI;IAET,OAAO,IAAI,OAAO;IAelB,KAAK,IAAI,IAAI;IAGb,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;CAGxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAahF;AAwoCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAgDrG;AAiCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsB3E"}
1
+ {"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAa5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAI/B,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB;kFAC8E;IAC9E,GAAG,EAAE,MAAM,CAAA;IACX;6EACyE;IACzE,cAAc,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAA;IAC1B;;0DAEsD;IACtD,OAAO,EAAE,MAAM,CAAA;IAEf,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAEhB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAkCD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,KAAK,SAAI;IAET,OAAO,IAAI,OAAO;IAgBlB,KAAK,IAAI,IAAI;IAGb,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;CAGxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAahF;AAwoCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAgDrG;AAiCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsB3E"}
@@ -35,8 +35,10 @@ function makeContact() {
35
35
  cBxN: 0, cByN: 0, cBzN: 0,
36
36
  jacInvN: 0,
37
37
  bounceVel: 0,
38
- sorGain: 1,
39
- allowedApproachVel: 0,
38
+ age: 0,
39
+ rhsPenetration: 0,
40
+ appliedPushImpulse: 0,
41
+ biasVel: 0,
40
42
  t1x: 0, t1y: 0, t1z: 0,
41
43
  cAxT1: 0, cAyT1: 0, cAzT1: 0,
42
44
  cBxT1: 0, cByT1: 0, cBzT1: 0,
@@ -59,6 +61,7 @@ export class ContactPool {
59
61
  c.appliedNormalImpulse = 0;
60
62
  c.appliedFrictionImpulse1 = 0;
61
63
  c.appliedFrictionImpulse2 = 0;
64
+ c.appliedPushImpulse = 0;
62
65
  this.count++;
63
66
  return c;
64
67
  }
@@ -1,13 +1,34 @@
1
- import type { ContactPool } from "./contact";
2
- export declare class ImpulseCache {
3
- private a;
4
- private b;
5
- /** Seed this substep's contacts with what the same rows converged to last
6
- * substep. Contacts with no history start at zero, as new contacts should. */
7
- seed(pool: ContactPool): void;
8
- /** Store the converged impulses, then swap: this substep's results become the
9
- * next one's history. Double-buffered so a pair that stops touching simply
10
- * falls out rather than needing an expiry sweep. */
11
- writeback(pool: ContactPool): void;
1
+ interface Point {
2
+ /** Contact point in each body's local frame (lever arms are world-space and
3
+ * rotate with the body, so they cannot be compared across substeps). */
4
+ lax: number;
5
+ lay: number;
6
+ laz: number;
7
+ lbx: number;
8
+ lby: number;
9
+ lbz: number;
10
+ normalImpulse: number;
11
+ frictionImpulse1: number;
12
+ frictionImpulse2: number;
13
+ /** Substeps this point has survived — Bullet disables restitution past
14
+ * m_restingContactRestitutionThreshold (2) so resting contacts stop bouncing. */
15
+ age: number;
16
+ /** Marks points seen this substep; unseen ones are dropped. */
17
+ seen: boolean;
12
18
  }
19
+ export declare class ManifoldCache {
20
+ private pairs;
21
+ private touched;
22
+ private static key;
23
+ /** Look up what this contact point converged to last substep. Returns null
24
+ * when it is new. */
25
+ find(a: number, b: number, lax: number, lay: number, laz: number): Point | null;
26
+ /** Record what this point converged to, for the next substep to start from. */
27
+ store(a: number, b: number, lax: number, lay: number, laz: number, lbx: number, lby: number, lbz: number, normalImpulse: number, frictionImpulse1: number, frictionImpulse2: number, age: number): void;
28
+ /** Drop every point not re-seen this substep, and every pair left empty.
29
+ * Without this a separated pair keeps handing back a stale impulse. */
30
+ endStep(): void;
31
+ clear(): void;
32
+ }
33
+ export {};
13
34
  //# sourceMappingURL=manifold.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifold.d.ts","sourceRoot":"","sources":["../../src/physics/manifold.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAK5C,qBAAa,YAAY;IACvB,OAAO,CAAC,CAAC,CAAkC;IAC3C,OAAO,CAAC,CAAC,CAAkC;IAE3C;mFAC+E;IAC/E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAmB7B;;yDAEqD;IACrD,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;CAgBnC"}
1
+ {"version":3,"file":"manifold.d.ts","sourceRoot":"","sources":["../../src/physics/manifold.ts"],"names":[],"mappings":"AAuBA,UAAU,KAAK;IACb;6EACyE;IACzE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB;sFACkF;IAClF,GAAG,EAAE,MAAM,CAAA;IACX,+DAA+D;IAC/D,IAAI,EAAE,OAAO,CAAA;CACd;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,OAAO,CAAoB;IAEnC,OAAO,CAAC,MAAM,CAAC,GAAG;IAIlB;0BACsB;IACtB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAc/E,+EAA+E;IAC/E,KAAK,CACH,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EACrC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EACrC,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EACzE,GAAG,EAAE,MAAM,GACV,IAAI;IAoCP;4EACwE;IACxE,OAAO,IAAI,IAAI;IAgBf,KAAK,IAAI,IAAI;CAId"}
@@ -1,68 +1,148 @@
1
- // Contact impulse history — the cache that makes warm starting possible.
1
+ // Persistent contact manifolds — the cache that makes warm starting possible.
2
2
  //
3
- // Bullet keeps a btPersistentManifold per body pair and carries each point's
4
- // m_appliedImpulse into the next substep at m_warmstartingFactor (0.85), so a
5
- // resting contact starts from the answer it converged to last time instead of
6
- // from zero. Without it a sustained contact is rediscovered every substep:
7
- // it overshoots, releases, re-forms which is chatter. Measured here, contacts
8
- // account for ALL of the visible shake under animation (25 shaking bodies with
9
- // contacts on, 0 with them off), so this is the mechanism that matters.
3
+ // Bullet 2.75 keeps a btPersistentManifold per body pair holding up to 4 points,
4
+ // each carrying the impulse it converged to last step. At setup the solver seeds
5
+ // each row with `cp.m_appliedImpulse * m_warmstartingFactor` (0.85) and applies
6
+ // it immediately, so a resting stack starts the substep already holding roughly
7
+ // the load it needs instead of rediscovering it from zero every time.
10
8
  //
11
- // Identity is by ORDINAL within the pair, not by proximity. Our narrowphase is
12
- // deterministic the same pair walks the same code path and emits its contacts
13
- // in the same order every substep so "the third contact of this pair" is a
14
- // stabler key than "the cached point nearest this one", which needs a distance
15
- // threshold and mis-pairs whenever two contacts fall inside it.
16
- const MAX_PER_PAIR = 8;
17
- const STRIDE = 3; // normal, friction1, friction2
18
- export class ImpulseCache {
9
+ // That is not a nicety here: penetration recovery rides in the contact velocity
10
+ // row as a Baumgarte term, and a row rebuilt from zero each substep overshoots
11
+ // it. Warm starting on its own was measured WORSE on this engine but that was
12
+ // against a solver with no bias term and a position-correction pass, which is a
13
+ // different system. The two are one design in Bullet and are ported as one.
14
+ //
15
+ // Points are matched by proximity in each body's own local frame, which is what
16
+ // btPersistentManifold does — a world-space match would drift with the body.
17
+ const MAX_POINTS = 4;
18
+ /** Match radius, in model units. Bullet's gContactBreakingThreshold is 0.02;
19
+ * ours is the contact margin, so a point that merely slid along a face is
20
+ * still recognised as the same point rather than dropped and rebuilt. */
21
+ const MATCH_DIST_SQ = 0.04 * 0.04;
22
+ export class ManifoldCache {
19
23
  constructor() {
20
- this.a = new Map();
21
- this.b = new Map();
24
+ this.pairs = new Map();
25
+ this.touched = new Set();
22
26
  }
23
- /** Seed this substep's contacts with what the same rows converged to last
24
- * substep. Contacts with no history start at zero, as new contacts should. */
25
- seed(pool) {
26
- const ord = new Map();
27
- for (let i = 0; i < pool.count; i++) {
28
- const c = pool.get(i);
29
- const key = c.bodyA * 65536 + c.bodyB;
30
- const n = ord.get(key) ?? 0;
31
- ord.set(key, n + 1);
32
- c.manifoldKey = key;
33
- c.manifoldSlot = n;
34
- if (n >= MAX_PER_PAIR)
35
- continue;
36
- const prev = this.a.get(key);
37
- if (!prev)
38
- continue;
39
- const o = n * STRIDE;
40
- c.appliedNormalImpulse = prev[o];
41
- c.appliedFrictionImpulse1 = prev[o + 1];
42
- c.appliedFrictionImpulse2 = prev[o + 2];
27
+ static key(a, b) {
28
+ return a < b ? a * 65536 + b : b * 65536 + a;
29
+ }
30
+ /** Look up what this contact point converged to last substep. Returns null
31
+ * when it is new. */
32
+ find(a, b, lax, lay, laz) {
33
+ const list = this.pairs.get(ManifoldCache.key(a, b));
34
+ if (list === undefined)
35
+ return null;
36
+ let best = null;
37
+ let bestD = MATCH_DIST_SQ;
38
+ for (let i = 0; i < list.length; i++) {
39
+ const p = list[i];
40
+ const dx = p.lax - lax, dy = p.lay - lay, dz = p.laz - laz;
41
+ const d = dx * dx + dy * dy + dz * dz;
42
+ if (d < bestD) {
43
+ bestD = d;
44
+ best = p;
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+ /** Record what this point converged to, for the next substep to start from. */
50
+ store(a, b, lax, lay, laz, lbx, lby, lbz, normalImpulse, frictionImpulse1, frictionImpulse2, age) {
51
+ const k = ManifoldCache.key(a, b);
52
+ this.touched.add(k);
53
+ let list = this.pairs.get(k);
54
+ if (list === undefined) {
55
+ list = [];
56
+ this.pairs.set(k, list);
57
+ }
58
+ // Replace the nearest existing point, else append; past MAX_POINTS drop the
59
+ // shallowest-held one so the manifold keeps the load-bearing corners.
60
+ let best = -1;
61
+ let bestD = MATCH_DIST_SQ;
62
+ for (let i = 0; i < list.length; i++) {
63
+ const p = list[i];
64
+ const dx = p.lax - lax, dy = p.lay - lay, dz = p.laz - laz;
65
+ const d = dx * dx + dy * dy + dz * dz;
66
+ if (d < bestD) {
67
+ bestD = d;
68
+ best = i;
69
+ }
70
+ }
71
+ if (best < 0) {
72
+ if (list.length < MAX_POINTS) {
73
+ list.push({ lax, lay, laz, lbx, lby, lbz, normalImpulse, frictionImpulse1, frictionImpulse2, age, seen: true });
74
+ return;
75
+ }
76
+ // btPersistentManifold::sortCachedPoints — when a 5th point arrives, drop
77
+ // whichever of the 5 leaves the largest quadrilateral. Area is what keeps
78
+ // a resting box from pivoting; dropping the shallowest instead can leave
79
+ // four nearly-collinear points that pin position but not orientation.
80
+ best = worstAreaIndex(list, lax, lay, laz);
43
81
  }
82
+ const p = list[best];
83
+ p.lax = lax;
84
+ p.lay = lay;
85
+ p.laz = laz;
86
+ p.lbx = lbx;
87
+ p.lby = lby;
88
+ p.lbz = lbz;
89
+ p.normalImpulse = normalImpulse;
90
+ p.frictionImpulse1 = frictionImpulse1;
91
+ p.frictionImpulse2 = frictionImpulse2;
92
+ p.age = age;
93
+ p.seen = true;
44
94
  }
45
- /** Store the converged impulses, then swap: this substep's results become the
46
- * next one's history. Double-buffered so a pair that stops touching simply
47
- * falls out rather than needing an expiry sweep. */
48
- writeback(pool) {
49
- this.b.clear();
50
- for (let i = 0; i < pool.count; i++) {
51
- const c = pool.get(i);
52
- if (c.manifoldSlot >= MAX_PER_PAIR)
95
+ /** Drop every point not re-seen this substep, and every pair left empty.
96
+ * Without this a separated pair keeps handing back a stale impulse. */
97
+ endStep() {
98
+ for (const [k, list] of this.pairs) {
99
+ if (!this.touched.has(k)) {
100
+ this.pairs.delete(k);
53
101
  continue;
54
- let arr = this.b.get(c.manifoldKey);
55
- if (!arr) {
56
- arr = new Float32Array(MAX_PER_PAIR * STRIDE);
57
- this.b.set(c.manifoldKey, arr);
58
102
  }
59
- const o = c.manifoldSlot * STRIDE;
60
- arr[o] = c.appliedNormalImpulse;
61
- arr[o + 1] = c.appliedFrictionImpulse1;
62
- arr[o + 2] = c.appliedFrictionImpulse2;
103
+ let w = 0;
104
+ for (let i = 0; i < list.length; i++) {
105
+ const p = list[i];
106
+ if (!p.seen)
107
+ continue;
108
+ p.seen = false;
109
+ list[w++] = p;
110
+ }
111
+ list.length = w;
112
+ if (w === 0)
113
+ this.pairs.delete(k);
114
+ }
115
+ this.touched.clear();
116
+ }
117
+ clear() {
118
+ this.pairs.clear();
119
+ this.touched.clear();
120
+ }
121
+ }
122
+ /** Which of the 4 cached points to replace so the surviving quad keeps the most
123
+ * area once the new point joins it. */
124
+ function worstAreaIndex(list, nx, ny, nz) {
125
+ let bestIdx = 0;
126
+ let bestArea = -1;
127
+ for (let drop = 0; drop < list.length; drop++) {
128
+ // The quad is: the new point plus the three survivors.
129
+ const pts = [[nx, ny, nz]];
130
+ for (let i = 0; i < list.length; i++)
131
+ if (i !== drop)
132
+ pts.push([list[i].lax, list[i].lay, list[i].laz]);
133
+ if (pts.length < 4)
134
+ continue;
135
+ // |d0 × d1| over the diagonals — Bullet's area proxy.
136
+ const d0x = pts[0][0] - pts[2][0], d0y = pts[0][1] - pts[2][1], d0z = pts[0][2] - pts[2][2];
137
+ const d1x = pts[1][0] - pts[3][0], d1y = pts[1][1] - pts[3][1], d1z = pts[1][2] - pts[3][2];
138
+ const cx = d0y * d1z - d0z * d1y;
139
+ const cy = d0z * d1x - d0x * d1z;
140
+ const cz = d0x * d1y - d0y * d1x;
141
+ const area = cx * cx + cy * cy + cz * cz;
142
+ if (area > bestArea) {
143
+ bestArea = area;
144
+ bestIdx = drop;
63
145
  }
64
- const t = this.a;
65
- this.a = this.b;
66
- this.b = t;
67
146
  }
147
+ return bestIdx;
68
148
  }
@@ -1,7 +1,11 @@
1
1
  import type { RigidBodyStore } from "./body";
2
2
  import type { SixDofSpringConstraint } from "./constraint";
3
3
  import type { ContactPool } from "./contact";
4
- export declare function solveConstraints(store: RigidBodyStore, constraints: SixDofSpringConstraint[], cache: SolverCache, contacts: ContactPool, dt: number, iterations: number): void;
4
+ import type { ManifoldCache } from "./manifold";
5
+ export declare function solveConstraints(store: RigidBodyStore, constraints: SixDofSpringConstraint[], cache: SolverCache, contacts: ContactPool, dt: number, iterations: number, manifolds?: ManifoldCache | null): void;
6
+ /** Integrate the accumulated push/turn velocity straight into the transform —
7
+ * btSolverBody::writebackVelocity(timeStep). Never touches real momentum. */
8
+ export declare function applySplitImpulsePush(store: RigidBodyStore, dt: number): void;
5
9
  export declare class SolverCache {
6
10
  readonly F: Float32Array;
7
11
  readonly I: Int32Array;
@@ -10,4 +14,6 @@ export declare class SolverCache {
10
14
  readonly D: Float64Array;
11
15
  constructor(constraints: SixDofSpringConstraint[]);
12
16
  }
17
+ /** Store every solved row back into the manifold for the next substep. */
18
+ export declare function saveContactImpulses(store: RigidBodyStore, contacts: ContactPool, manifolds: ManifoldCache): void;
13
19
  //# sourceMappingURL=solver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAgFrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CA2CN;AAyCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;IACtB;sEACkE;IAClE,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;gBACZ,WAAW,EAAE,sBAAsB,EAAE;CAUlD"}
1
+ {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAmJ/C,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,aAAa,GAAG,IAAW,GACrC,IAAI,CA+FN;AAgDD;8EAC8E;AAC9E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CA8B7E;AAyCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;IACtB;sEACkE;IAClE,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;gBACZ,WAAW,EAAE,sBAAsB,EAAE;CAUlD;AAs6BD,0EAA0E;AAC1E,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,aAAa,GACvB,IAAI,CAcN"}