@driftengine/physics 4.8.2 → 4.8.4

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/src/exact.ts ADDED
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Transcendentals that give the same bits on every JavaScript engine.
3
+ *
4
+ * **Not more accurate than `Math`, and that is not what they are for.** These are within a couple
5
+ * of units in the last place of correctly rounded, which is about what a platform `sin` gives you.
6
+ * What they are is *reproducible*: ECMAScript declines to specify `sin`, `cos`, `exp`, `acos` and
7
+ * eighteen more, so two engines may differ by an ulp, and an ulp is enough to make two peers running
8
+ * the same simulation disagree — first invisibly, then completely.
9
+ *
10
+ * **In physics rather than core since 4.8.4**: a skinned cloth's damping needs `exp` and its bending
11
+ * `acos`, the simulation set may not call the platform's, and physics imports no other package. Core
12
+ * re-exports physics wholesale, so `exactExp` from `@driftengine/core` is the same function.
13
+ *
14
+ * `scripts/determinism.mjs` is the gate that refuses the unspecified ones inside the simulation set
15
+ * and names the twenty-two. Its header carries the other half of the argument: `+ - * /` are IEEE
16
+ * 754 operations by specification, JavaScript permits no fused-multiply-add contraction, and
17
+ * `Math.sqrt` is required correctly rounded and is *not* on that list. **So everything below uses
18
+ * only those five operations**, which is the whole design: reproducibility by construction rather
19
+ * than by hoping two engines chose the same algorithm.
20
+ *
21
+ * ---
22
+ *
23
+ * ## What is here, and what is deliberately not
24
+ *
25
+ * `exactSin`, `exactCos`, `exactExp`, `exactAcos` and `exactLog`. Five, because five is what the
26
+ * engine and its first consumer actually reach for: the terrain binding's slope angle needs `acos`,
27
+ * a lockstep fixture rotating anything needs `sin` and `cos`, exponential decay needs `exp`, and a
28
+ * detector's box prior is a logit of the patch grid, which needs `log`.
29
+ *
30
+ * **`atan2` is absent.** It is the obvious fifth and nothing here needs one: `shape.ts` sorts by a
31
+ * comparison key computed from two in-plane axes rather than by an angle, and `joints.ts` compares
32
+ * a quaternion component against a half-angle's sine. *Reversed by* a site that genuinely needs a
33
+ * signed angle from two components, at which point it arrives with its own golden vectors.
34
+ *
35
+ * ## Nothing in core imports these
36
+ *
37
+ * Deliberately. `core-only` is the one size figure in this repository that every consumer pays, and
38
+ * a module reachable from `createRenderer` is in it whether anybody calls it or not. These are
39
+ * reached by the script bindings and by consumers who ask for them, so tree-shaking keeps them out
40
+ * of a bundle that does not, which is measured rather than assumed.
41
+ *
42
+ * ## The coefficients
43
+ *
44
+ * The polynomial and rational coefficients are fdlibm's, which is the ancestor of most platform
45
+ * libms and is public domain. They are written as literals: **a literal is the same number on
46
+ * every engine, which is the only property being bought.** Their provenance matters less than their
47
+ * fixity — a coefficient somebody re-derived to one more digit is a different function, and
48
+ * `exact.test.ts`'s golden bit patterns are what would catch that.
49
+ */
50
+
51
+ /*
52
+ * π/2 in four pieces, most significant first, so a large multiple of it can be removed without
53
+ * losing the small remainder in the subtraction.
54
+ *
55
+ * **Two pieces are not enough and the measurement says so**: at an angle of 740 radians a two-part
56
+ * reduction leaves `sin` and `cos` 18.5 ulps out, because `k * PIO2_1` needs more than 53 bits once
57
+ * `k` grows. Three brings it under two ulps for the cost of two multiplies.
58
+ *
59
+ * **`PIO2_TAIL` and `PIO2_TAIL_LO` are one number in two halves, not two successive corrections.**
60
+ * fdlibm carries a fourth constant, `pio2_1t`, which is the tail for its *single*-iteration path;
61
+ * `pio2_2` is the same tail rounded differently, with `pio2_2t` as its own tail, and the two paths
62
+ * are alternatives. Refining the first with the second is not more accurate, it is wrong: it
63
+ * subtracts the tail twice and took `sin` to 2.2e11 ulps out, which is how this comment came to be
64
+ * here.
65
+ */
66
+ const PIO2_1 = 1.5707963267341256;
67
+ const PIO2_TAIL = 6.077100506303966e-11;
68
+ const PIO2_TAIL_LO = 2.0222662487959506e-21;
69
+ const TWO_OVER_PI = 0.6366197723675814;
70
+
71
+ const HALF_PI = 1.5707963267948966;
72
+ const PI = 3.141592653589793;
73
+
74
+ /* fdlibm's `__kernel_sin` coefficients. */
75
+ const S1 = -1.6666666666666632e-1;
76
+ const S2 = 8.33333333332249e-3;
77
+ const S3 = -1.984126982985795e-4;
78
+ const S4 = 2.7557313707070068e-6;
79
+ const S5 = -2.5050760253406863e-8;
80
+ const S6 = 1.5896909952115501e-10;
81
+
82
+ /* fdlibm's `__kernel_cos` coefficients. */
83
+ const C1 = 4.1666666666666602e-2;
84
+ const C2 = -1.388888888887411e-3;
85
+ const C3 = 2.480158728947673e-5;
86
+ const C4 = -2.7557314351390663e-7;
87
+ const C5 = 2.0875723212981748e-9;
88
+ const C6 = -1.1359647557788195e-11;
89
+
90
+ /* fdlibm's `asin` rational, numerator then denominator. */
91
+ const P0 = 1.6666666666666666e-1;
92
+ const P1 = -3.2556581862240092e-1;
93
+ const P2 = 2.0121253213486293e-1;
94
+ const P3 = -4.0055534500679411e-2;
95
+ const P4 = 7.9153499428981453e-4;
96
+ const P5 = 3.4793310759602117e-5;
97
+ const Q1 = -2.4033949117344142;
98
+ const Q2 = 2.0209457602335057;
99
+ const Q3 = -6.8828397160545329e-1;
100
+ const Q4 = 7.7038150555901935e-2;
101
+
102
+ /** ln 2, split for the same reason π/2 is. */
103
+ const LN2_HI = 0.6931471803691238;
104
+ const LN2_LO = 1.9082149292705877e-10;
105
+ const ONE_OVER_LN2 = 1.4426950408889634;
106
+
107
+ /** 2^(2^i), for i = 0 to 9. Every one is exact, so a product of them is exact. */
108
+ const POW2 = [
109
+ 2, 4, 16, 256, 65536, 4294967296, 1.8446744073709552e19, 3.402823669209385e38,
110
+ 1.157920892373162e77, 1.3407807929942597e154,
111
+ ];
112
+
113
+ /**
114
+ * Two to an integer power, by binary decomposition of the exponent.
115
+ *
116
+ * Multiplication by a power of two is exact in IEEE 754, so this is exact wherever it does not
117
+ * overflow — which is what makes it a legitimate substitute for the `Math.pow` a range reduction
118
+ * would otherwise reach for.
119
+ *
120
+ * A large negative exponent lands on zero instead of a subnormal, because the reciprocal is taken
121
+ * after the product overflows. That is `exp` underflowing to zero, which is where it was going.
122
+ */
123
+ function pow2(exponent: number): number {
124
+ let remaining = exponent < 0 ? -exponent : exponent;
125
+ let result = 1;
126
+ let index = 0;
127
+ while (remaining > 0 && index < POW2.length) {
128
+ if ((remaining & 1) === 1) result *= POW2[index] as number;
129
+ remaining >>= 1;
130
+ index += 1;
131
+ }
132
+ return exponent < 0 ? 1 / result : result;
133
+ }
134
+
135
+ /** sin on [-π/4, π/4]. */
136
+ function sinKernel(x: number): number {
137
+ const z = x * x;
138
+ const poly = S1 + z * (S2 + z * (S3 + z * (S4 + z * (S5 + z * S6))));
139
+ return x + x * z * poly;
140
+ }
141
+
142
+ /** cos on [-π/4, π/4]. */
143
+ function cosKernel(x: number): number {
144
+ const z = x * x;
145
+ const poly = C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6))));
146
+ return 1 - 0.5 * z + z * z * poly;
147
+ }
148
+
149
+ /**
150
+ * Fold an angle into [-π/4, π/4] and say which quadrant it came from.
151
+ *
152
+ * Cody-Waite: the multiple of π/2 is removed in pieces, so the subtraction that produces a small
153
+ * remainder from two large numbers does not lose the bits it is supposed to keep. One-piece
154
+ * reduction is accurate to about 1e-8 at an angle of a thousand radians, which for a simulation
155
+ * that accumulates rotation is the difference between an object facing forward and facing sideways
156
+ * after a few minutes.
157
+ */
158
+ function reduce(x: number): { r: number; quadrant: number } {
159
+ const k = Math.round(x * TWO_OVER_PI);
160
+
161
+ /* The leading piece, whose product with `k` is exact for every `k` a simulation reaches. */
162
+ const head = x - k * PIO2_1;
163
+
164
+ /*
165
+ * The tail, in two halves. `head - tail` loses the bits below its own last place, and
166
+ * `PIO2_TAIL_LO - (head - r - tail)` is exactly those bits recovered — the same
167
+ * subtract-and-recover fdlibm uses on its two-iteration path.
168
+ */
169
+ const tail = k * PIO2_TAIL;
170
+ let r = head - tail;
171
+ const lost = k * PIO2_TAIL_LO - (head - r - tail);
172
+ r = r - lost;
173
+
174
+ return { r, quadrant: ((k % 4) + 4) % 4 };
175
+ }
176
+
177
+ /** `Math.sin`, reproducibly. NaN in, NaN out; an infinity answers NaN, as `Math.sin` does. */
178
+ export function exactSin(x: number): number {
179
+ if (!Number.isFinite(x)) return Number.NaN;
180
+ const { r, quadrant } = reduce(x);
181
+ switch (quadrant) {
182
+ case 0:
183
+ return sinKernel(r);
184
+ case 1:
185
+ return cosKernel(r);
186
+ case 2:
187
+ return -sinKernel(r);
188
+ default:
189
+ return -cosKernel(r);
190
+ }
191
+ }
192
+
193
+ /** `Math.cos`, reproducibly. */
194
+ export function exactCos(x: number): number {
195
+ if (!Number.isFinite(x)) return Number.NaN;
196
+ const { r, quadrant } = reduce(x);
197
+ switch (quadrant) {
198
+ case 0:
199
+ return cosKernel(r);
200
+ case 1:
201
+ return -sinKernel(r);
202
+ case 2:
203
+ return -cosKernel(r);
204
+ default:
205
+ return sinKernel(r);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * `Math.exp`, reproducibly.
211
+ *
212
+ * Reduce by a multiple of ln 2 into [-ln2/2, ln2/2], sum the Taylor series there, and scale by a
213
+ * power of two. **A degree-14 series and not a shorter minimax**, because on that interval the
214
+ * fifteenth term is about 4e-20 — below the precision of a double — so the series is exact to the
215
+ * representation and there is nothing a fitted polynomial would buy.
216
+ */
217
+ export function exactExp(x: number): number {
218
+ if (Number.isNaN(x)) return Number.NaN;
219
+ if (x === Number.POSITIVE_INFINITY) return Number.POSITIVE_INFINITY;
220
+ if (x === Number.NEGATIVE_INFINITY) return 0;
221
+ if (x > 709.782712893384) return Number.POSITIVE_INFINITY;
222
+ if (x < -745.1332191019411) return 0;
223
+
224
+ const k = Math.round(x * ONE_OVER_LN2);
225
+ const r = x - k * LN2_HI - k * LN2_LO;
226
+
227
+ /* Horner over 1/n!, highest term first. */
228
+ let sum = 1 / 87178291200; // 1/14!
229
+ sum = 1 / 6227020800 + r * sum; // 1/13!
230
+ sum = 1 / 479001600 + r * sum; // 1/12!
231
+ sum = 1 / 39916800 + r * sum; // 1/11!
232
+ sum = 1 / 3628800 + r * sum; // 1/10!
233
+ sum = 1 / 362880 + r * sum; // 1/9!
234
+ sum = 1 / 40320 + r * sum; // 1/8!
235
+ sum = 1 / 5040 + r * sum; // 1/7!
236
+ sum = 1 / 720 + r * sum; // 1/6!
237
+ sum = 1 / 120 + r * sum; // 1/5!
238
+ sum = 1 / 24 + r * sum; // 1/4!
239
+ sum = 1 / 6 + r * sum; // 1/3!
240
+ sum = 0.5 + r * sum;
241
+ sum = 1 + r * sum;
242
+ sum = 1 + r * sum;
243
+
244
+ return sum * pow2(k);
245
+ }
246
+
247
+ /* fdlibm's `log` polynomial in s², for log(1 + f) around f = 0 by way of s = f / (2 + f). */
248
+ const LG1 = 6.66666666666673513e-1;
249
+ const LG2 = 3.999999999940941908e-1;
250
+ const LG3 = 2.857142874366239149e-1;
251
+ const LG4 = 2.222219843214978396e-1;
252
+ const LG5 = 1.818357216161805012e-1;
253
+ const LG6 = 1.531383769920937332e-1;
254
+ const LG7 = 1.479819860511658591e-1;
255
+ const TWO54 = 1.8014398509481984e16;
256
+ const SMALLEST_NORMAL = 2.2250738585072014e-308;
257
+
258
+ /* A double's two words, for reading and setting an exponent; one view, reused. */
259
+ const words = new DataView(new ArrayBuffer(8));
260
+
261
+ /**
262
+ * `Math.log`, reproducibly: fdlibm's algorithm, which is also V8's.
263
+ *
264
+ * Take the power of two out as `k` and leave a significand in [√2/2, √2), so that `f = m - 1` is
265
+ * small either side of zero; then log(1 + f) = f - f²/2 + s·(f²/2 + R(s²)) with s = f/(2 + f), and
266
+ * `k·ln 2` added back in two parts so the large part loses nothing. The exponent is read and the
267
+ * significand rescaled through the double's own words — integer operations, not transcendentals.
268
+ */
269
+ export function exactLog(x: number): number {
270
+ if (Number.isNaN(x) || x < 0) return Number.NaN;
271
+ if (x === 0) return Number.NEGATIVE_INFINITY;
272
+ if (x === Number.POSITIVE_INFINITY) return x;
273
+
274
+ let k = 0;
275
+ let value = x;
276
+ if (value < SMALLEST_NORMAL) {
277
+ k = -54;
278
+ value *= TWO54;
279
+ }
280
+ words.setFloat64(0, value);
281
+ let hx = words.getUint32(0);
282
+ k += (hx >>> 20) - 1023;
283
+ hx &= 0x000fffff;
284
+ /* Keep the significand at or above √2/2: past √2, halve it and count one more power of two. */
285
+ const i = (hx + 0x95f64) & 0x100000;
286
+ words.setUint32(0, hx | (i ^ 0x3ff00000));
287
+ k += i >>> 20;
288
+ const f = words.getFloat64(0) - 1;
289
+
290
+ if ((0x000fffff & (2 + hx)) < 3) {
291
+ /* |f| < 2^-20: two terms of the series are the whole answer. */
292
+ if (f === 0) return k === 0 ? 0 : k * LN2_HI + k * LN2_LO;
293
+ const r = f * f * (0.5 - 0.3333333333333333 * f);
294
+ return k === 0 ? f - r : k * LN2_HI - (r - k * LN2_LO - f);
295
+ }
296
+ const s = f / (2 + f);
297
+ const z = s * s;
298
+ const w = z * z;
299
+ const t1 = w * (LG2 + w * (LG4 + w * LG6));
300
+ const t2 = z * (LG1 + w * (LG3 + w * (LG5 + w * LG7)));
301
+ const r = t2 + t1;
302
+ /* Far enough from one that f²/2 is worth carrying apart from f. */
303
+ if (((hx - 0x6147a) | (0x6b851 - hx)) > 0) {
304
+ const half = 0.5 * f * f;
305
+ return k === 0
306
+ ? f - (half - s * (half + r))
307
+ : k * LN2_HI - (half - (s * (half + r) + k * LN2_LO) - f);
308
+ }
309
+ return k === 0 ? f - s * (f - r) : k * LN2_HI - (s * (f - r) - k * LN2_LO - f);
310
+ }
311
+
312
+ /** fdlibm's `asin` rational on `z = x²`, valid where `z` is small. */
313
+ function asinRational(z: number): number {
314
+ const numerator = z * (P0 + z * (P1 + z * (P2 + z * (P3 + z * (P4 + z * P5)))));
315
+ const denominator = 1 + z * (Q1 + z * (Q2 + z * (Q3 + z * Q4)));
316
+ return numerator / denominator;
317
+ }
318
+
319
+ /**
320
+ * `Math.acos`, reproducibly. Outside [-1, 1] this answers NaN, as `Math.acos` does.
321
+ *
322
+ * Three branches rather than one, and the split at ±0.5 is the reason it is accurate near the ends:
323
+ * `acos` has infinite derivative at ±1, so a polynomial in `x` loses its footing there. Substituting
324
+ * `s = sqrt((1 ∓ x)/2)` moves the awkward point to zero, where a polynomial is comfortable — which
325
+ * is also why `sqrt` being exactly rounded matters here rather than being a convenience.
326
+ */
327
+ export function exactAcos(x: number): number {
328
+ if (Number.isNaN(x) || x > 1 || x < -1) return Number.NaN;
329
+ if (x === 1) return 0;
330
+ if (x === -1) return PI;
331
+
332
+ if (x >= 0.5) {
333
+ const z = (1 - x) / 2;
334
+ const s = Math.sqrt(z);
335
+ return 2 * (s + s * asinRational(z));
336
+ }
337
+ if (x <= -0.5) {
338
+ const z = (1 + x) / 2;
339
+ const s = Math.sqrt(z);
340
+ return PI - 2 * (s + s * asinRational(z));
341
+ }
342
+ const z = x * x;
343
+ return HALF_PI - (x + x * asinRational(z));
344
+ }
package/src/index.ts CHANGED
@@ -116,6 +116,38 @@ export {
116
116
  } from './stallEscape.ts';
117
117
  export type { ClothOptions } from './cloth.ts';
118
118
  export { ClothBody, makeClothGrid } from './cloth.ts';
119
+ /*
120
+ * Cloth anchored to a skeleton rather than a world: a garment. The CPU solver is the reference and
121
+ * runs under WebGL2 and in Node; `@driftengine/core` runs the same kernels on WebGPU.
122
+ */
123
+ export { SkinnedCloth } from './skinnedCloth.ts';
124
+ /*
125
+ * The schedule both solvers share — poses, whole steps, resets and the blend — and the device
126
+ * interface the GPU solver implements, with what it reads: the targets' matrices and colliders,
127
+ * and the set-up checked and batched exactly as the CPU solver checks and batches it.
128
+ */
129
+ export { ClothControl } from './clothControl.ts';
130
+ export type { ClothDevice } from './clothControl.ts';
131
+ export type { ClothTargets } from './clothTargets.ts';
132
+ export { validateClothSetup } from './skinnedClothSetup.ts';
133
+ /*
134
+ * Transcendentals that give the same bits on every engine. Here rather than in core since 4.8.4,
135
+ * because the simulation needs them and this package imports nothing; core re-exports them with
136
+ * the rest of physics.
137
+ */
138
+ export { exactAcos, exactCos, exactExp, exactLog, exactSin } from './exact.ts';
139
+ export { batchesOf, colourConstraints } from './clothBatches.ts';
140
+ export type { ClothBatches } from './clothBatches.ts';
141
+ export type {
142
+ BendingConstraints,
143
+ ClothCollider,
144
+ ClothLimits,
145
+ DistanceConstraints,
146
+ ResolvedClothParameters,
147
+ SkinnedClothParameters,
148
+ SkinnedClothSetup,
149
+ TetherConstraints,
150
+ } from './skinnedClothSetup.ts';
119
151
  export type { Collider, ColliderBytes, ColliderGroup } from './colliderSet.ts';
120
152
  export { ColliderSet, boxCollider, colliderFromShape } from './colliderSet.ts';
121
153
  export { fingerprintBodies, fingerprintColliders } from './fingerprint.ts';
package/src/ragdoll.ts CHANGED
@@ -230,6 +230,11 @@ export function ragdollFromBones(
230
230
  * the bones meet.
231
231
  */
232
232
  const halfOf = new Float32Array(jointCount);
233
+ /**
234
+ * Each bone's rig offset over its length: 1 for a rig at unit scale, and what turns a length in
235
+ * metres back into the parent's own units where the rig is scaled.
236
+ */
237
+ const restScale = new Float32Array(jointCount).fill(1);
233
238
  let boneCount = 0;
234
239
  let firstBone = -1;
235
240
  /** Half the first bone's length, which is how far its head sits from the body's centre. */
@@ -273,6 +278,10 @@ export function ragdollFromBones(
273
278
  });
274
279
  bodyOf[j] = body;
275
280
  halfOf[j] = length / 2;
281
+ const rx = localRest[j * 3] ?? 0;
282
+ const ry = localRest[j * 3 + 1] ?? 0;
283
+ const rz = localRest[j * 3 + 2] ?? 0;
284
+ restScale[j] = Math.sqrt(rx * rx + ry * ry + rz * rz) / length;
276
285
  // What takes the body's rotation back to each of its two joints', captured at rest. Parents
277
286
  // come before children in a rig, so the start joint's rotation is already read.
278
287
  multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
@@ -288,13 +297,65 @@ export function ragdollFromBones(
288
297
  }
289
298
  }
290
299
 
300
+ /*
301
+ * **A bone too short for a body is transparent.** Each joint resolves to the nearest one at or
302
+ * above it that a body ends at, or to its root: what hangs from a collar sitting on the chest
303
+ * joint hangs from the bone that ends at the chest. A rig puts one joint exactly on another as
304
+ * often as not, and a bone under `minLength` gets no body, so until 4.8.3 the spine above such a
305
+ * collar was jointed to nothing and collided with every bone it met there — compared by index,
306
+ * they shared no end. **What it gives up** is the skipped bone's own length, under `minLength`,
307
+ * which the joints below absorb exactly at rest: see `restAnchor`.
308
+ */
309
+ const attach = new Int32Array(jointCount);
310
+ for (let j = 0; j < jointCount; j++) {
311
+ let a = j;
312
+ while ((bodyOf[a] ?? -1) < 0 && (parents[a] ?? -1) >= 0) a = parents[a] ?? -1;
313
+ attach[j] = a;
314
+ }
315
+ /** Where a bone's parent end really hangs: its parent joint, seen through any bone too short. */
316
+ const hangsFrom = (j: number): number => {
317
+ const p = parents[j] ?? -1;
318
+ return p < 0 ? -1 : (attach[p] ?? p);
319
+ };
320
+ /**
321
+ * Bone `j`'s body-frame point at joint `at`'s rest position, into `REST_ANCHOR`. Where `at` is
322
+ * the bone's own parent joint this is its head, `(0, -half, 0)`, exactly as before; through a
323
+ * short bone it is where the two bones meet at rest, so a joint anchored there moves nothing on
324
+ * the first tick.
325
+ */
326
+ const restAnchor = (j: number, at: number): void => {
327
+ const p = parents[j] ?? -1;
328
+ if (at === p) {
329
+ REST_ANCHOR[0] = 0;
330
+ REST_ANCHOR[1] = -(halfOf[j] ?? 0);
331
+ REST_ANCHOR[2] = 0;
332
+ return;
333
+ }
334
+ const body = bodyOf[j] ?? 0;
335
+ const dx = (worldMatrices[at * 16 + 12] ?? 0) - (world.bodies.posX[body] ?? 0);
336
+ const dy = (worldMatrices[at * 16 + 13] ?? 0) - (world.bodies.posY[body] ?? 0);
337
+ const dz = (worldMatrices[at * 16 + 14] ?? 0) - (world.bodies.posZ[body] ?? 0);
338
+ /* Into the body's frame: turned by the conjugate of its rotation. */
339
+ const ux = -(world.bodies.rotX[body] ?? 0);
340
+ const uy = -(world.bodies.rotY[body] ?? 0);
341
+ const uz = -(world.bodies.rotZ[body] ?? 0);
342
+ const w = world.bodies.rotW[body] ?? 1;
343
+ const cx = uy * dz - uz * dy;
344
+ const cy = uz * dx - ux * dz;
345
+ const cz = ux * dy - uy * dx;
346
+ REST_ANCHOR[0] = dx + 2 * (w * cx + (uy * cz - uz * cy));
347
+ REST_ANCHOR[1] = dy + 2 * (w * cy + (uz * cx - ux * cz));
348
+ REST_ANCHOR[2] = dz + 2 * (w * cz + (ux * cy - uy * cx));
349
+ };
350
+
291
351
  // Joint each bone to its parent's bone, at the shared end.
292
352
  for (let j = 0; j < jointCount; j++) {
293
353
  const body = bodyOf[j] ?? -1;
294
354
  if (body < 0) continue;
295
- const p = parents[j] ?? -1;
355
+ const p = hangsFrom(j);
296
356
  const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
297
357
  if (parentBody < 0) continue;
358
+ restAnchor(j, p);
298
359
  world.addJoint({
299
360
  type: JOINT_CONE_TWIST,
300
361
  bodyA: parentBody,
@@ -321,7 +382,9 @@ export function ragdollFromBones(
321
382
  * so the first tick still moves nothing.
322
383
  */
323
384
  anchorAY: halfOf[p] ?? 0,
324
- anchorBY: -(halfOf[j] ?? 0),
385
+ anchorBX: REST_ANCHOR[0],
386
+ anchorBY: REST_ANCHOR[1],
387
+ anchorBZ: REST_ANCHOR[2],
325
388
  });
326
389
  }
327
390
 
@@ -338,13 +401,18 @@ export function ragdollFromBones(
338
401
  for (let j = 0; j < jointCount; j++) {
339
402
  const body = bodyOf[j] ?? -1;
340
403
  if (body < 0) continue;
341
- const p = parents[j] ?? -1;
404
+ const p = hangsFrom(j);
342
405
  if (p < 0 || (bodyOf[p] ?? -1) >= 0) continue;
343
406
  const first = firstUnder[p] ?? -1;
344
407
  if (first < 0) {
345
408
  firstUnder[p] = j;
346
409
  continue;
347
410
  }
411
+ restAnchor(first, p);
412
+ const ax = REST_ANCHOR[0];
413
+ const ay = REST_ANCHOR[1];
414
+ const az = REST_ANCHOR[2];
415
+ restAnchor(j, p);
348
416
  world.addJoint({
349
417
  type: JOINT_CONE_TWIST,
350
418
  bodyA: bodyOf[first] ?? 0,
@@ -352,8 +420,12 @@ export function ragdollFromBones(
352
420
  swingCos: limitAt(swingCos, Math.SQRT1_2, j),
353
421
  twistSin: limitAt(twistSin, 0.3826834323650898, j),
354
422
  axisY: 1,
355
- anchorAY: -(halfOf[first] ?? 0),
356
- anchorBY: -(halfOf[j] ?? 0),
423
+ anchorAX: ax,
424
+ anchorAY: ay,
425
+ anchorAZ: az,
426
+ anchorBX: REST_ANCHOR[0],
427
+ anchorBY: REST_ANCHOR[1],
428
+ anchorBZ: REST_ANCHOR[2],
357
429
  });
358
430
  }
359
431
 
@@ -375,8 +447,9 @@ export function ragdollFromBones(
375
447
  world.ignorePair(body, other);
376
448
  continue;
377
449
  }
378
- const pj = parents[j] ?? -1;
379
- const pk = parents[k] ?? -1;
450
+ /* Seen through any bone too short for a body, which is where they really meet. */
451
+ const pj = hangsFrom(j);
452
+ const pk = hangsFrom(k);
380
453
  /* Jointed, either way round, or two bones hanging off the same parent joint: siblings share
381
454
  a head the way a parent and child share a point, so they overlap there too. */
382
455
  if (pk === j || pj === k || (pj === pk && pj >= 0)) world.ignorePair(body, other);
@@ -427,10 +500,32 @@ export function ragdollFromBones(
427
500
  copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
428
501
  }
429
502
  if (p < 0) continue;
430
- // Bones are rigid, so translation stays what the rig was built with.
431
- out.translation[j * 3] = localRest[j * 3] ?? 0;
432
- out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
433
- out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
503
+ /*
504
+ * **Placed along its own bone as the body holds it**, at the rig's length: the body's +y,
505
+ * in the parent's frame. For the first bone under a joint that is the rig's offset exactly,
506
+ * because the joint turns with that body; for a second or third — a thigh beside the spine,
507
+ * a clavicle beside the neck — it is what keeps the branch on its own body. Placed by the
508
+ * rig's offset, every branch but the first was swung by the first one's turn: reported as
509
+ * a foot 0.24 m and an arm 0.17 m from their bodies after a fall. A joint with no bone of
510
+ * its own keeps the rig's offset. **What would make it wrong** is a bone the body
511
+ * stretches, which a rigid capsule cannot.
512
+ */
513
+ const body = bodyOf[j] ?? -1;
514
+ if (body >= 0 && KNOWN[p] === 1) {
515
+ boneInParentFrame(
516
+ world,
517
+ body,
518
+ 2 * (halfOf[j] ?? 0) * (restScale[j] ?? 1),
519
+ WORLD_ROT,
520
+ p * 4,
521
+ out.translation,
522
+ j * 3,
523
+ );
524
+ } else {
525
+ out.translation[j * 3] = localRest[j * 3] ?? 0;
526
+ out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
527
+ out.translation[j * 3 + 2] = localRest[j * 3 + 2] ?? 0;
528
+ }
434
529
  out.scale[j * 3] = 1;
435
530
  out.scale[j * 3 + 1] = 1;
436
531
  out.scale[j * 3 + 2] = 1;
@@ -544,6 +639,8 @@ const WORLD_ROT = new Float32Array(256 * 4);
544
639
  /** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
545
640
  const KNOWN = new Uint8Array(256);
546
641
  const START_ROT = new Float32Array(4);
642
+ /** One joint anchor in a body's own frame, filled by `restAnchor` and read at once. */
643
+ const REST_ANCHOR = new Float64Array(3);
547
644
  const PARENT_ROT = new Float32Array(4);
548
645
  const TARGET_ROT = new Float32Array(4);
549
646
  const CURRENT_ROT = new Float32Array(4);
@@ -721,6 +818,40 @@ function multiplyByConjugate(
721
818
  out[3] = aw * bw - ax * bx - ay * by - az * bz;
722
819
  }
723
820
 
821
+ /**
822
+ * A body's own +y at `length`, turned by the body and then back by the inverse of a parent's world
823
+ * rotation: the bone as the body holds it, in the parent's frame.
824
+ */
825
+ function boneInParentFrame(
826
+ world: PhysicsWorld,
827
+ body: number,
828
+ length: number,
829
+ parent: Float32Array,
830
+ parentAt: number,
831
+ out: Float32Array,
832
+ outAt: number,
833
+ ): void {
834
+ const bx = world.bodies.rotX[body] ?? 0;
835
+ const by = world.bodies.rotY[body] ?? 0;
836
+ const bz = world.bodies.rotZ[body] ?? 0;
837
+ const bw = world.bodies.rotW[body] ?? 1;
838
+ /* The body's +y in the world, scaled. */
839
+ const vx = 2 * length * (bx * by - bw * bz);
840
+ const vy = length * (1 - 2 * (bx * bx + bz * bz));
841
+ const vz = 2 * length * (by * bz + bw * bx);
842
+ /* Turned by the conjugate of the parent's rotation: v + 2w(u×v) + 2u×(u×v), u = −q.xyz. */
843
+ const ux = -(parent[parentAt] ?? 0);
844
+ const uy = -(parent[parentAt + 1] ?? 0);
845
+ const uz = -(parent[parentAt + 2] ?? 0);
846
+ const w = parent[parentAt + 3] ?? 1;
847
+ const cx = uy * vz - uz * vy;
848
+ const cy = uz * vx - ux * vz;
849
+ const cz = ux * vy - uy * vx;
850
+ out[outAt] = vx + 2 * (w * cx + (uy * cz - uz * cy));
851
+ out[outAt + 1] = vy + 2 * (w * cy + (uz * cx - ux * cz));
852
+ out[outAt + 2] = vz + 2 * (w * cz + (ux * cy - uy * cx));
853
+ }
854
+
724
855
  /** `conj(a) · b` into `out`. */
725
856
  function multiplyConjugate(
726
857
  a: Float64Array,