reze-engine 0.36.0 → 0.37.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.
@@ -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"}