@woosh/meep-engine 2.141.0 → 2.142.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/engine/control/first-person/prototype_first_person_controller.js +5 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
- package/src/engine/physics/PLAN.md +705 -578
- package/src/engine/physics/REVIEW_003.md +166 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +402 -165
- package/src/engine/physics/ecs/Joint.d.ts +115 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +168 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
- package/src/engine/physics/queries/raycast.d.ts +11 -9
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +108 -159
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
- package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
|
@@ -18,20 +18,46 @@ import { quat3_multiply } from "../../../core/geom/3d/quaternion/quat3_multiply.
|
|
|
18
18
|
*
|
|
19
19
|
* A joint is a configurable 6-DOF constraint ({@link Joint}); each relative
|
|
20
20
|
* degree of freedom is independently locked / free / limited / springy /
|
|
21
|
-
* motorised. This module implements **LOCKED
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* per-DOF
|
|
21
|
+
* motorised. This module implements **LOCKED** and **LIMITED** DOFs (linear
|
|
22
|
+
* and angular). LOCKED + FREE express the ball-socket, hinge, weld, and
|
|
23
|
+
* prismatic joints; LIMITED adds slider end-stops and joint range-of-motion.
|
|
24
|
+
* Springs and motors slot into the same per-DOF row model as they land.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
26
|
+
* ## One row model, parameterised by mode
|
|
27
|
+
*
|
|
28
|
+
* Every DOF resolves to the same scalar velocity row, contact-shaped: for a
|
|
29
|
+
* unit axis `d` and lever arms `rA = anchorA − comA`, `rB = anchorB − comB`,
|
|
30
|
+
* `K = invMA + invMB + (rA×d)·Iw_A·(rA×d) + (rB×d)·Iw_B·(rB×d)` (linear)
|
|
31
|
+
* `K = d·Iw_A·d + d·Iw_B·d` (angular)
|
|
32
|
+
* `vrel = (vA + ωA×rA − vB − ωB×rB) · d` (linear, A−B)
|
|
33
|
+
* `vrel = (ωB − ωA) · d` (angular, B−A)
|
|
34
|
+
* `λ = clamp(λ_acc − (1/K)·(vrel + bias)) − λ_acc`,
|
|
35
|
+
* applied as `±λ·d`. The **mode** picks only two row parameters:
|
|
36
|
+
* - LOCKED → `bias = β/h · error`, impulse clamp `(−∞, +∞)` (bilateral).
|
|
37
|
+
* - LIMITED → row active only when the DOF position is at/beyond a bound;
|
|
38
|
+
* `bias = β/h · (position − bound)`, impulse clamp one-sided
|
|
39
|
+
* (`[0, +∞)` at the lower stop, `(−∞, 0]` at the upper). The clamp makes
|
|
40
|
+
* it a unilateral push-out-of-violation constraint, exactly like a contact
|
|
41
|
+
* normal — the limit can only resist crossing the stop, never pull the DOF
|
|
42
|
+
* back into range.
|
|
43
|
+
* The warm-start and the iteration loop are mode-agnostic; they read the
|
|
44
|
+
* per-DOF `(active, effMass, bias, clampLo, clampHi)` filled by the mode-aware
|
|
45
|
+
* setup. Geometry (anchors / lever arms / position error) is recomputed from
|
|
46
|
+
* the current pose every substep, so the joint tracks the bodies as they move.
|
|
47
|
+
*
|
|
48
|
+
* The "position" along a DOF is `C · axis` for linear (signed anchor offset in
|
|
49
|
+
* metres) and `e · axis` for angular, where `e = 2·sign(qD.w)·qD.xyz` is the
|
|
50
|
+
* small-angle rotation vector of the relative rotation `qD = conj(qA)⊗qB`,
|
|
51
|
+
* expressed in frame-A-local coordinates (so error component `k` pairs with
|
|
52
|
+
* frame axis `k`). The angular position is therefore a first-order (small
|
|
53
|
+
* angle) measure: accurate for the modest ranges of slider/hinge end-stops,
|
|
54
|
+
* but it under-reports large angles (`2·sin(θ/2)` vs `θ`). Accurate wide-cone
|
|
55
|
+
* range-of-motion (ragdoll shoulders) needs the swing-twist decomposition,
|
|
56
|
+
* tracked as the follow-up; angular LIMITED here is the moderate-range cut.
|
|
57
|
+
* The decomposition itself is already implemented — reuse
|
|
58
|
+
* `Quaternion.computeSwingAndTwist(axis, swing, twist)` /
|
|
59
|
+
* `computeTwistAngle(axis)` (`core/geom/Quaternion.js`) on the relative
|
|
60
|
+
* rotation `qD` rather than writing a new one.
|
|
35
61
|
*
|
|
36
62
|
* @author Alex Goldring
|
|
37
63
|
* @copyright Company Named Limited (c) 2026
|
|
@@ -61,6 +87,24 @@ const scratch_anchor = new Float64Array(3);
|
|
|
61
87
|
const scratch_frame_a = new Float64Array(9);
|
|
62
88
|
/** Scratch quaternion for frame composition / relative rotation. */
|
|
63
89
|
const scratch_q = new Float64Array(4);
|
|
90
|
+
/** Angular position error `e` per frame axis (frame-A-local small-angle vector). */
|
|
91
|
+
const scratch_ang_err = new Float64Array(3);
|
|
92
|
+
|
|
93
|
+
// Per-DOF row parameters, filled per joint by the mode-aware setup and consumed
|
|
94
|
+
// by the mode-agnostic warm-start + iteration. Indexed 0..2 (the three frame
|
|
95
|
+
// axes); the linear set drives imp[0..2], the angular set drives imp[3..5].
|
|
96
|
+
const lin_active = new Uint8Array(3);
|
|
97
|
+
const lin_eff = new Float64Array(3);
|
|
98
|
+
const lin_bias = new Float64Array(3);
|
|
99
|
+
const lin_clo = new Float64Array(3);
|
|
100
|
+
const lin_chi = new Float64Array(3);
|
|
101
|
+
const lin_gamma = new Float64Array(3);
|
|
102
|
+
const ang_active = new Uint8Array(3);
|
|
103
|
+
const ang_eff = new Float64Array(3);
|
|
104
|
+
const ang_bias = new Float64Array(3);
|
|
105
|
+
const ang_clo = new Float64Array(3);
|
|
106
|
+
const ang_chi = new Float64Array(3);
|
|
107
|
+
const ang_gamma = new Float64Array(3);
|
|
64
108
|
|
|
65
109
|
/**
|
|
66
110
|
* Angular effective-mass contribution of one body about a unit world axis:
|
|
@@ -151,10 +195,227 @@ function inv_mass_of(rb) {
|
|
|
151
195
|
return rb.mass > 0 ? 1 / rb.mass : 0;
|
|
152
196
|
}
|
|
153
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Clamp the SPOOK position bias to ±{@link MAX_JOINT_BIAS}.
|
|
200
|
+
* @param {number} b @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
function clamp_bias(b) {
|
|
203
|
+
if (b > MAX_JOINT_BIAS) return MAX_JOINT_BIAS;
|
|
204
|
+
if (b < -MAX_JOINT_BIAS) return -MAX_JOINT_BIAS;
|
|
205
|
+
return b;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Swing-twist decomposition of the relative rotation `qD = conj(qA)⊗qB`, giving
|
|
210
|
+
* the per-frame-axis angular positions used by wide-cone angular DOFs.
|
|
211
|
+
*
|
|
212
|
+
* Decomposes `qD = swing ⊗ twist`, with **twist** the rotation about frame
|
|
213
|
+
* axis X (the bone / twist axis) and **swing** the residual tilt (a rotation
|
|
214
|
+
* about an axis in the YZ plane that carries X to its new direction). Writes,
|
|
215
|
+
* into `out`:
|
|
216
|
+
* - `out[0]` = signed twist angle about X, in `(−π, π]` — *exact*, so a twist
|
|
217
|
+
* limit engages at the true angle, not the `2·sin(θ/2)` small-angle proxy;
|
|
218
|
+
* - `out[1]`, `out[2]` = the swing angle distributed over Y and Z
|
|
219
|
+
* (`φ·axis`, φ the true swing angle), so independent Y/Z swing limits form
|
|
220
|
+
* an accurate (box-shaped) cone that holds at large tilt.
|
|
221
|
+
* Reduces continuously to the small-angle vector near identity. Pure scratch /
|
|
222
|
+
* locals — no allocation, suitable for the per-substep hot loop. (The Quaternion
|
|
223
|
+
* class has an object-based `computeSwingAndTwist`; this is the inlined,
|
|
224
|
+
* allocation-free form — see the bench that justifies it.)
|
|
225
|
+
*
|
|
226
|
+
* @param {number} dx @param {number} dy @param {number} dz @param {number} dw qD
|
|
227
|
+
* @param {Float64Array} out length-3 destination (frame-axis positions)
|
|
228
|
+
*/
|
|
229
|
+
export function swing_twist_error(dx, dy, dz, dw, out) {
|
|
230
|
+
// Shortest arc: work with the hemisphere where dw ≥ 0.
|
|
231
|
+
if (dw < 0) { dx = -dx; dy = -dy; dz = -dz; dw = -dw; }
|
|
232
|
+
|
|
233
|
+
const nxt = Math.sqrt(dx * dx + dw * dw); // |twist| (before normalising)
|
|
234
|
+
let sy, sz, sw;
|
|
235
|
+
if (nxt < 1e-12) {
|
|
236
|
+
// dw ≈ 0 and dx ≈ 0: a ~180° pure swing — twist is undefined, take 0.
|
|
237
|
+
out[0] = 0;
|
|
238
|
+
// Swing is qD itself; distribute its angle over Y/Z below.
|
|
239
|
+
sy = dy; sz = dz; sw = dw;
|
|
240
|
+
} else {
|
|
241
|
+
out[0] = 2 * Math.atan2(dx, dw); // exact signed twist about X
|
|
242
|
+
const tx = dx / nxt, tw = dw / nxt; // normalised twist (tx,0,0,tw)
|
|
243
|
+
// swing = qD ⊗ conj(twist); its X component is identically 0.
|
|
244
|
+
sy = dy * tw - dz * tx;
|
|
245
|
+
sz = dy * tx + dz * tw;
|
|
246
|
+
sw = dw * tw + dx * tx;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (sw < 0) { sy = -sy; sz = -sz; sw = -sw; }
|
|
250
|
+
const syz = Math.sqrt(sy * sy + sz * sz);
|
|
251
|
+
if (syz < 1e-9) {
|
|
252
|
+
// Near-zero swing: small-angle vector (continuous with φ·axis as φ→0).
|
|
253
|
+
out[1] = 2 * sy;
|
|
254
|
+
out[2] = 2 * sz;
|
|
255
|
+
} else {
|
|
256
|
+
const phi = 2 * Math.atan2(syz, sw); // exact swing angle in [0, π]
|
|
257
|
+
const s = phi / syz;
|
|
258
|
+
out[1] = sy * s;
|
|
259
|
+
out[2] = sz * s;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fill the row parameters for one DOF given its mode, signed position and
|
|
265
|
+
* signed velocity.
|
|
266
|
+
*
|
|
267
|
+
* Writes `active / eff / bias / clampLo / clampHi` at index `k` of the supplied
|
|
268
|
+
* arrays and, for LIMITED, resets the warm-start impulse when the active bound
|
|
269
|
+
* flipped (a stale opposite-side impulse must not warm-start).
|
|
270
|
+
*
|
|
271
|
+
* ## LIMITED is a speculative (β = 1) one-sided velocity constraint
|
|
272
|
+
*
|
|
273
|
+
* Rather than waiting for the DOF to cross a stop and then pushing back with a
|
|
274
|
+
* Baumgarte bias — which injects real velocity and makes the stop *bounce* when
|
|
275
|
+
* nothing else loads the joint — the limit row is always live and uses the full
|
|
276
|
+
* `(position − bound)/h` as its bias. With the impulse clamped to one side this
|
|
277
|
+
* removes exactly the approach velocity that would overshoot, so the DOF
|
|
278
|
+
* **lands on the bound** with zero relative velocity (an inelastic stop, no
|
|
279
|
+
* penetration to recover, no rebound). When the DOF is far from the bound the
|
|
280
|
+
* large same-signed bias drives the clamp to zero, so the row is a no-op — it
|
|
281
|
+
* self-gates on approach. At a stop held by a sustained load (gravity on a
|
|
282
|
+
* slider) the per-substep load and the per-substep warm-start cancel, so the
|
|
283
|
+
* DOF rests exactly on the bound, mirroring a resting contact.
|
|
284
|
+
*
|
|
285
|
+
* Only the *push-out* side of the bias is clamped (to {@link MAX_JOINT_BIAS}),
|
|
286
|
+
* so a body teleported deep past a stop is eased out rather than yanked; the
|
|
287
|
+
* gating side is left unbounded.
|
|
288
|
+
*
|
|
289
|
+
* The active bound is the violated one, else — within range — the one the DOF
|
|
290
|
+
* is moving toward (`vel` sign). LOCKED is unchanged: a bilateral row with a
|
|
291
|
+
* `β/h` Baumgarte bias and no clamp.
|
|
292
|
+
*
|
|
293
|
+
* ## MOTOR drives the relative velocity toward a target
|
|
294
|
+
*
|
|
295
|
+
* A motor is a velocity row biased to the target (`bias = −target`, so the
|
|
296
|
+
* solve drives the relative velocity to the target) with the impulse clamped to
|
|
297
|
+
* `±maxForce·h` — a force-limited servo. With an unbounded force budget it is a
|
|
298
|
+
* perfect velocity drive; with a finite one it pulls toward the target as hard
|
|
299
|
+
* as it can (a weak motor cannot overcome a heavier load). The target follows
|
|
300
|
+
* the row's sign convention: linear is A−B along the frame axis, angular is
|
|
301
|
+
* B−A about it.
|
|
302
|
+
*
|
|
303
|
+
* ## SPRING is a SPOOK soft (compliant) constraint
|
|
304
|
+
*
|
|
305
|
+
* A spring drives the DOF position toward zero with stiffness `k` (N/m or
|
|
306
|
+
* N·m/rad) and damping `c`, as a *regularised* row rather than a hard one. The
|
|
307
|
+
* implicit-Euler soft constraint (Catto, "Soft Constraints") gives, per substep
|
|
308
|
+
* `h`, with `denom = c + h·k`:
|
|
309
|
+
* `γ = 1 / (h·denom)` the compliance / regularisation (inverse mass),
|
|
310
|
+
* `bias = (k/denom)·C` the restoring velocity, and a softened effective
|
|
311
|
+
* `effMass = 1/(K + γ)` mass — so the row solves
|
|
312
|
+
* `λ = −effMass·(vrel + bias + γ·λ_accum)`.
|
|
313
|
+
* The `γ·λ_accum` term is what makes it compliant (the constraint yields in
|
|
314
|
+
* proportion to the force it carries), so a spring under load settles at a
|
|
315
|
+
* deflection rather than snapping rigid. Stiffness-only (`c = 0`) oscillates
|
|
316
|
+
* undamped; damping-only (`k = 0`) is a pure damper; both zero is inert. The
|
|
317
|
+
* row is bilateral (no clamp). This is the suspension / bungee / soft-return
|
|
318
|
+
* element, and the soft basis for cone-twist later. The angular position uses
|
|
319
|
+
* the same small-angle measure as LIMITED (good for modest ranges).
|
|
320
|
+
*
|
|
321
|
+
* @param {Joint} joint the joint (per-DOF config + warm-start impulse read here)
|
|
322
|
+
* @param {number} dofIndex DOF 0..5 (selects mode / limits / motor / imp slot)
|
|
323
|
+
* @param {number} k axis index 0..2 (output-array slot for this DOF group)
|
|
324
|
+
* @param {number} keff raw effective-mass denominator `K` (0 ⇒ no row); the
|
|
325
|
+
* row's effective mass is `1/K`, or `1/(K+γ)` softened for a spring.
|
|
326
|
+
* @param {number} pos signed DOF position (`C·axis` linear / `e·axis` angular)
|
|
327
|
+
* @param {number} vel signed DOF velocity along the same axis (selects the
|
|
328
|
+
* approached bound for an in-range LIMITED DOF)
|
|
329
|
+
* @param {number} spook_locked Baumgarte gain `β / dt_sub` (LOCKED)
|
|
330
|
+
* @param {number} spook_spec speculative gain `1 / dt_sub` (LIMITED)
|
|
331
|
+
* @param {number} dt_sub substep size (MOTOR impulse cap = `maxForce·dt_sub`;
|
|
332
|
+
* SPRING compliance derives from it)
|
|
333
|
+
* @param {Uint8Array} active @param {Float64Array} effOut
|
|
334
|
+
* @param {Float64Array} biasOut @param {Float64Array} cloOut @param {Float64Array} chiOut
|
|
335
|
+
* @param {Float64Array} gammaOut per-row regularisation (0 for non-spring rows)
|
|
336
|
+
*/
|
|
337
|
+
function fill_row(joint, dofIndex, k, keff, pos, vel, spook_locked, spook_spec, dt_sub,
|
|
338
|
+
active, effOut, biasOut, cloOut, chiOut, gammaOut) {
|
|
339
|
+
const mode = joint.dofMode[dofIndex];
|
|
340
|
+
const imp = joint.dofImpulse;
|
|
341
|
+
|
|
342
|
+
if (mode === DofMode.LOCKED) {
|
|
343
|
+
if (keff === 0) { active[k] = 0; return; }
|
|
344
|
+
effOut[k] = 1 / keff;
|
|
345
|
+
biasOut[k] = clamp_bias(spook_locked * pos);
|
|
346
|
+
cloOut[k] = -Infinity;
|
|
347
|
+
chiOut[k] = Infinity;
|
|
348
|
+
gammaOut[k] = 0;
|
|
349
|
+
active[k] = 1;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (mode === DofMode.LIMITED) {
|
|
353
|
+
if (keff === 0) { imp[dofIndex] = 0; active[k] = 0; return; }
|
|
354
|
+
const lower = joint.dofLowerLimit[dofIndex];
|
|
355
|
+
const upper = joint.dofUpperLimit[dofIndex];
|
|
356
|
+
// Select the bound: the violated one, else the one being approached.
|
|
357
|
+
let bound, lower_side;
|
|
358
|
+
if (pos <= lower) { bound = lower; lower_side = true; }
|
|
359
|
+
else if (pos >= upper) { bound = upper; lower_side = false; }
|
|
360
|
+
else { lower_side = vel <= 0; bound = lower_side ? lower : upper; }
|
|
361
|
+
|
|
362
|
+
let b = spook_spec * (pos - bound);
|
|
363
|
+
if (lower_side) {
|
|
364
|
+
// Lower stop: impulse pushes the DOF up (+), clamp [0, +∞). The
|
|
365
|
+
// push-out side of the bias is the negative one — bound it.
|
|
366
|
+
cloOut[k] = 0; chiOut[k] = Infinity;
|
|
367
|
+
if (imp[dofIndex] < 0) imp[dofIndex] = 0; // drop stale upper-side impulse
|
|
368
|
+
if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
|
|
369
|
+
} else {
|
|
370
|
+
// Upper stop: impulse pushes the DOF down (−), clamp (−∞, 0]. The
|
|
371
|
+
// push-out side of the bias is the positive one — bound it.
|
|
372
|
+
cloOut[k] = -Infinity; chiOut[k] = 0;
|
|
373
|
+
if (imp[dofIndex] > 0) imp[dofIndex] = 0; // drop stale lower-side impulse
|
|
374
|
+
if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS;
|
|
375
|
+
}
|
|
376
|
+
effOut[k] = 1 / keff;
|
|
377
|
+
biasOut[k] = b;
|
|
378
|
+
gammaOut[k] = 0;
|
|
379
|
+
active[k] = 1;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (mode === DofMode.MOTOR) {
|
|
383
|
+
if (keff === 0) { imp[dofIndex] = 0; active[k] = 0; return; }
|
|
384
|
+
const maxImpulse = joint.dofMotorMaxForce[dofIndex] * dt_sub;
|
|
385
|
+
if (maxImpulse <= 0) { imp[dofIndex] = 0; active[k] = 0; return; }
|
|
386
|
+
effOut[k] = 1 / keff;
|
|
387
|
+
// Drive the relative velocity to the target: λ = −eff·(vrel − target).
|
|
388
|
+
biasOut[k] = -joint.dofMotorTargetVelocity[dofIndex];
|
|
389
|
+
cloOut[k] = -maxImpulse;
|
|
390
|
+
chiOut[k] = maxImpulse;
|
|
391
|
+
gammaOut[k] = 0;
|
|
392
|
+
active[k] = 1;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (mode === DofMode.SPRING) {
|
|
396
|
+
if (keff === 0) { imp[dofIndex] = 0; active[k] = 0; return; }
|
|
397
|
+
const ks = joint.dofStiffness[dofIndex];
|
|
398
|
+
const cs = joint.dofDamping[dofIndex];
|
|
399
|
+
const denom = cs + dt_sub * ks;
|
|
400
|
+
if (denom <= 0) { imp[dofIndex] = 0; active[k] = 0; return; } // inert (no k, no c)
|
|
401
|
+
const gamma = 1 / (dt_sub * denom);
|
|
402
|
+
effOut[k] = 1 / (keff + gamma);
|
|
403
|
+
biasOut[k] = (ks / denom) * pos; // restoring velocity toward pos = 0
|
|
404
|
+
cloOut[k] = -Infinity;
|
|
405
|
+
chiOut[k] = Infinity;
|
|
406
|
+
gammaOut[k] = gamma;
|
|
407
|
+
active[k] = 1;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// FREE: no row this substep.
|
|
411
|
+
imp[dofIndex] = 0;
|
|
412
|
+
active[k] = 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
154
415
|
/**
|
|
155
416
|
* Solve every joint for one substep: recompute geometry at the current poses,
|
|
156
|
-
*
|
|
157
|
-
*
|
|
417
|
+
* derive each DOF's row parameters from its mode, replay the per-substep
|
|
418
|
+
* warm-start, and run `iters` velocity iterations.
|
|
158
419
|
*
|
|
159
420
|
* Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
|
|
160
421
|
* solve so the two share the substep / warm-start cadence.
|
|
@@ -169,7 +430,8 @@ export function solve_joints(joints, system, dt_sub, iters) {
|
|
|
169
430
|
const n = joints.length;
|
|
170
431
|
if (n === 0 || dt_sub <= 0) return;
|
|
171
432
|
|
|
172
|
-
const
|
|
433
|
+
const spook_locked = JOINT_BAUMGARTE / dt_sub;
|
|
434
|
+
const spook_spec = 1 / dt_sub;
|
|
173
435
|
|
|
174
436
|
const storage = system.storage;
|
|
175
437
|
|
|
@@ -207,6 +469,11 @@ export function solve_joints(joints, system, dt_sub, iters) {
|
|
|
207
469
|
const invMB = to_world ? 0 : inv_mass_of(rbB);
|
|
208
470
|
if (invMA === 0 && invMB === 0) continue;
|
|
209
471
|
|
|
472
|
+
const mode = joint.dofMode;
|
|
473
|
+
const linConstrained = mode[0] !== DofMode.FREE || mode[1] !== DofMode.FREE || mode[2] !== DofMode.FREE;
|
|
474
|
+
const angConstrained = mode[3] !== DofMode.FREE || mode[4] !== DofMode.FREE || mode[5] !== DofMode.FREE;
|
|
475
|
+
if (!linConstrained && !angConstrained) continue;
|
|
476
|
+
|
|
210
477
|
// World anchor A and its lever arm rA = R_A · localAnchorA.
|
|
211
478
|
const la = joint.localAnchorA;
|
|
212
479
|
v3_quat3_apply(scratch_anchor, 0, la.x, la.y, la.z,
|
|
@@ -232,81 +499,59 @@ export function solve_joints(joints, system, dt_sub, iters) {
|
|
|
232
499
|
anchorBz = trB.position.z + rBz;
|
|
233
500
|
}
|
|
234
501
|
|
|
235
|
-
// Position error C = anchorA − anchorB (
|
|
502
|
+
// Position error C = anchorA − anchorB (its projection on a frame axis
|
|
503
|
+
// is the signed DOF offset; locked axes drive it to zero, limited axes
|
|
504
|
+
// bound it).
|
|
236
505
|
const Cx = anchorAx - anchorBx;
|
|
237
506
|
const Cy = anchorAy - anchorBy;
|
|
238
507
|
const Cz = anchorAz - anchorBz;
|
|
239
508
|
|
|
240
|
-
const mode = joint.dofMode;
|
|
241
509
|
const imp = joint.dofImpulse;
|
|
242
510
|
const lvA = rbA.linearVelocity, avA = rbA.angularVelocity;
|
|
243
511
|
const lvB = to_world ? null : rbB.linearVelocity;
|
|
244
512
|
const avB = to_world ? null : rbB.angularVelocity;
|
|
245
513
|
|
|
246
514
|
// === Frame A world axes — shared by the linear AND angular rows ===
|
|
247
|
-
// Every DOF is expressed in the joint's frame on body A. A
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// angular DOF pins relative rotation about a frame axis; a FREE one is
|
|
251
|
-
// a hinge axis. Frame A = body-A rotation ⊗ localBasisA; the rows of
|
|
252
|
-
// its matrix are the frame axes in world (see quat3_to_matrix3).
|
|
253
|
-
const linLockX = mode[0] === DofMode.LOCKED;
|
|
254
|
-
const linLockY = mode[1] === DofMode.LOCKED;
|
|
255
|
-
const linLockZ = mode[2] === DofMode.LOCKED;
|
|
256
|
-
const angLockX = mode[3] === DofMode.LOCKED;
|
|
257
|
-
const angLockY = mode[4] === DofMode.LOCKED;
|
|
258
|
-
const angLockZ = mode[5] === DofMode.LOCKED;
|
|
259
|
-
if (!(linLockX || linLockY || linLockZ || angLockX || angLockY || angLockZ)) continue;
|
|
260
|
-
|
|
515
|
+
// Every DOF is expressed in the joint's frame on body A. Frame A =
|
|
516
|
+
// body-A rotation ⊗ localBasisA; the rows of its matrix are the frame
|
|
517
|
+
// axes in world (see quat3_to_matrix3).
|
|
261
518
|
const lba = joint.localBasisA;
|
|
262
519
|
quat3_multiply(scratch_q, 0, trA.rotation[0], trA.rotation[1], trA.rotation[2], trA.rotation[3], lba[0], lba[1], lba[2], lba[3]);
|
|
263
520
|
const qAx = scratch_q[0], qAy = scratch_q[1], qAz = scratch_q[2], qAw = scratch_q[3];
|
|
264
521
|
quat3_to_matrix3(scratch_frame_a, 0, qAx, qAy, qAz, qAw);
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const k = axis_effective_mass(rbA, trA, invMA, rAx, rAy, rAz, aZx, aZy, aZz)
|
|
293
|
-
+ (to_world ? 0 : axis_effective_mass(rbB, trB, invMB, rBx, rBy, rBz, aZx, aZy, aZz));
|
|
294
|
-
m_eff_lz = k > 0 ? 1 / k : 0;
|
|
295
|
-
let b = spook_a * (Cx * aZx + Cy * aZy + Cz * aZz);
|
|
296
|
-
if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
|
|
297
|
-
biasLz = b;
|
|
522
|
+
const fa = scratch_frame_a;
|
|
523
|
+
|
|
524
|
+
// --- Linear DOF row setup (frame axes). Position along axis k is
|
|
525
|
+
// `C · axis_k`; convention is A−B (impulse +to A / −to B). ---
|
|
526
|
+
if (linConstrained) {
|
|
527
|
+
for (let k = 0; k < 3; k++) {
|
|
528
|
+
lin_active[k] = 0;
|
|
529
|
+
const m = mode[k];
|
|
530
|
+
if (m === DofMode.FREE) { imp[k] = 0; continue; }
|
|
531
|
+
const ax = fa[3 * k], ay = fa[3 * k + 1], az = fa[3 * k + 2];
|
|
532
|
+
const keff = axis_effective_mass(rbA, trA, invMA, rAx, rAy, rAz, ax, ay, az)
|
|
533
|
+
+ (to_world ? 0 : axis_effective_mass(rbB, trB, invMB, rBx, rBy, rBz, ax, ay, az));
|
|
534
|
+
const pos = Cx * ax + Cy * ay + Cz * az;
|
|
535
|
+
// Relative anchor velocity along the axis (A−B), for LIMITED
|
|
536
|
+
// bound selection. Cheap; only the projection is used.
|
|
537
|
+
let vel = 0;
|
|
538
|
+
if (m === DofMode.LIMITED) {
|
|
539
|
+
const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
|
|
540
|
+
const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
|
|
541
|
+
const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
|
|
542
|
+
vel = rvx * ax + rvy * ay + rvz * az;
|
|
543
|
+
}
|
|
544
|
+
fill_row(joint, k, k, keff, pos, vel, spook_locked, spook_spec, dt_sub,
|
|
545
|
+
lin_active, lin_eff, lin_bias, lin_clo, lin_chi, lin_gamma);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
lin_active[0] = lin_active[1] = lin_active[2] = 0;
|
|
298
549
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// locked frame axis. Convention is B−A (relative angular velocity
|
|
305
|
-
// ωB−ωA, impulse +to B / −to A). ---
|
|
306
|
-
let m_eff_ax = 0, m_eff_ay = 0, m_eff_az = 0;
|
|
307
|
-
let biasAx = 0, biasAy = 0, biasAz = 0;
|
|
308
|
-
let lockAX = false, lockAY = false, lockAZ = false;
|
|
309
|
-
if (angLockX || angLockY || angLockZ) {
|
|
550
|
+
|
|
551
|
+
// --- Angular DOF row setup (frame axes). Position along axis k is the
|
|
552
|
+
// k-th component of the frame-local small-angle error vector `e`;
|
|
553
|
+
// convention is B−A (impulse +to B / −to A). ---
|
|
554
|
+
if (angConstrained) {
|
|
310
555
|
// Frame B world rotation (identity for a world anchor → locks A's
|
|
311
556
|
// frame to world axes).
|
|
312
557
|
let qBx = 0, qBy = 0, qBz = 0, qBw = 1;
|
|
@@ -315,58 +560,68 @@ export function solve_joints(joints, system, dt_sub, iters) {
|
|
|
315
560
|
quat3_multiply(scratch_q, 0, trB.rotation[0], trB.rotation[1], trB.rotation[2], trB.rotation[3], lbb[0], lbb[1], lbb[2], lbb[3]);
|
|
316
561
|
qBx = scratch_q[0]; qBy = scratch_q[1]; qBz = scratch_q[2]; qBw = scratch_q[3];
|
|
317
562
|
}
|
|
318
|
-
// qD = conj(qA) ⊗ qB
|
|
319
|
-
//
|
|
320
|
-
//
|
|
563
|
+
// qD = conj(qA) ⊗ qB → the per-axis angular position in frame-A
|
|
564
|
+
// coords. Two measures: the cheap small-angle rotation vector
|
|
565
|
+
// (default — exact at the origin, fine for welds / hinges / modest
|
|
566
|
+
// limits) or, when the joint opts into `swingTwist`, the swing-twist
|
|
567
|
+
// decomposition (X = twist, Y/Z = swing) with *exact* angles, for
|
|
568
|
+
// wide cone-twist range-of-motion (ragdoll shoulders) where the
|
|
569
|
+
// small-angle measure under-reports.
|
|
321
570
|
quat3_multiply(scratch_q, 0, -qAx, -qAy, -qAz, qAw, qBx, qBy, qBz, qBw);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
+ (to_world ? 0 : angular_axis_effective_mass(rbB, trB, aXx, aXy, aXz));
|
|
330
|
-
m_eff_ax = k > 0 ? 1 / k : 0;
|
|
331
|
-
let b = spook_a * eX;
|
|
332
|
-
if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
|
|
333
|
-
biasAx = b;
|
|
334
|
-
lockAX = m_eff_ax !== 0;
|
|
571
|
+
if (joint.swingTwist) {
|
|
572
|
+
swing_twist_error(scratch_q[0], scratch_q[1], scratch_q[2], scratch_q[3], scratch_ang_err);
|
|
573
|
+
} else {
|
|
574
|
+
const es = scratch_q[3] < 0 ? -2 : 2;
|
|
575
|
+
scratch_ang_err[0] = es * scratch_q[0];
|
|
576
|
+
scratch_ang_err[1] = es * scratch_q[1];
|
|
577
|
+
scratch_ang_err[2] = es * scratch_q[2];
|
|
335
578
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
579
|
+
|
|
580
|
+
for (let k = 0; k < 3; k++) {
|
|
581
|
+
ang_active[k] = 0;
|
|
582
|
+
const m = mode[3 + k];
|
|
583
|
+
if (m === DofMode.FREE) { imp[3 + k] = 0; continue; }
|
|
584
|
+
const ax = fa[3 * k], ay = fa[3 * k + 1], az = fa[3 * k + 2];
|
|
585
|
+
const keff = angular_axis_effective_mass(rbA, trA, ax, ay, az)
|
|
586
|
+
+ (to_world ? 0 : angular_axis_effective_mass(rbB, trB, ax, ay, az));
|
|
587
|
+
const pos = scratch_ang_err[k];
|
|
588
|
+
// Relative angular velocity about the axis (B−A), for LIMITED
|
|
589
|
+
// bound selection.
|
|
590
|
+
let vel = 0;
|
|
591
|
+
if (m === DofMode.LIMITED) {
|
|
592
|
+
vel = -(avA[0] * ax + avA[1] * ay + avA[2] * az);
|
|
593
|
+
if (!to_world) vel += avB[0] * ax + avB[1] * ay + avB[2] * az;
|
|
594
|
+
}
|
|
595
|
+
fill_row(joint, 3 + k, k, keff, pos, vel, spook_locked, spook_spec, dt_sub,
|
|
596
|
+
ang_active, ang_eff, ang_bias, ang_clo, ang_chi, ang_gamma);
|
|
353
597
|
}
|
|
598
|
+
} else {
|
|
599
|
+
ang_active[0] = ang_active[1] = ang_active[2] = 0;
|
|
354
600
|
}
|
|
355
601
|
|
|
356
|
-
|
|
357
|
-
|
|
602
|
+
if (!lin_active[0] && !lin_active[1] && !lin_active[2]
|
|
603
|
+
&& !ang_active[0] && !ang_active[1] && !ang_active[2]) continue;
|
|
604
|
+
|
|
605
|
+
// --- Per-substep warm-start (frame axes): linear impulse `Σ imp[k]·aₖ`
|
|
606
|
+
// applied at the anchors, angular impulse `Σ imp[3+k]·aₖ` about the
|
|
358
607
|
// frame axes (+to B / −to A). ---
|
|
359
608
|
{
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
609
|
+
let Px = 0, Py = 0, Pz = 0;
|
|
610
|
+
for (let k = 0; k < 3; k++) {
|
|
611
|
+
if (!lin_active[k]) continue;
|
|
612
|
+
const w = imp[k];
|
|
613
|
+
Px += w * fa[3 * k]; Py += w * fa[3 * k + 1]; Pz += w * fa[3 * k + 2];
|
|
614
|
+
}
|
|
363
615
|
if (Px !== 0 || Py !== 0 || Pz !== 0) {
|
|
364
616
|
apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, Px, Py, Pz, +1);
|
|
365
617
|
if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, Px, Py, Pz, -1);
|
|
366
618
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
619
|
+
let Lx = 0, Ly = 0, Lz = 0;
|
|
620
|
+
for (let k = 0; k < 3; k++) {
|
|
621
|
+
if (!ang_active[k]) continue;
|
|
622
|
+
const w = imp[3 + k];
|
|
623
|
+
Lx += w * fa[3 * k]; Ly += w * fa[3 * k + 1]; Lz += w * fa[3 * k + 2];
|
|
624
|
+
}
|
|
370
625
|
if (Lx !== 0 || Ly !== 0 || Lz !== 0) {
|
|
371
626
|
if (!to_world) apply_angular_impulse(rbB, trB, Lx, Ly, Lz, 1, +1);
|
|
372
627
|
apply_angular_impulse(rbA, trA, Lx, Ly, Lz, 1, -1);
|
|
@@ -377,59 +632,41 @@ export function solve_joints(joints, system, dt_sub, iters) {
|
|
|
377
632
|
// Linear rows (frame axes). The relative anchor velocity is
|
|
378
633
|
// recomputed before each row so successive rows see the prior
|
|
379
634
|
// impulses (Gauss-Seidel coupling).
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
|
|
384
|
-
const lambda = -m_eff_lx * ((rvx * aXx + rvy * aXy + rvz * aXz) + biasLx);
|
|
385
|
-
imp[0] += lambda;
|
|
386
|
-
apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, lambda * aXx, lambda * aXy, lambda * aXz, +1);
|
|
387
|
-
if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, lambda * aXx, lambda * aXy, lambda * aXz, -1);
|
|
388
|
-
}
|
|
389
|
-
if (doLinY) {
|
|
390
|
-
const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
|
|
391
|
-
const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
|
|
392
|
-
const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
|
|
393
|
-
const lambda = -m_eff_ly * ((rvx * aYx + rvy * aYy + rvz * aYz) + biasLy);
|
|
394
|
-
imp[1] += lambda;
|
|
395
|
-
apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, lambda * aYx, lambda * aYy, lambda * aYz, +1);
|
|
396
|
-
if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, lambda * aYx, lambda * aYy, lambda * aYz, -1);
|
|
397
|
-
}
|
|
398
|
-
if (doLinZ) {
|
|
635
|
+
for (let k = 0; k < 3; k++) {
|
|
636
|
+
if (!lin_active[k]) continue;
|
|
637
|
+
const ax = fa[3 * k], ay = fa[3 * k + 1], az = fa[3 * k + 2];
|
|
399
638
|
const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
|
|
400
639
|
const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
|
|
401
640
|
const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
|
|
402
|
-
const
|
|
403
|
-
imp[
|
|
404
|
-
|
|
405
|
-
|
|
641
|
+
const vrel = rvx * ax + rvy * ay + rvz * az;
|
|
642
|
+
const old = imp[k];
|
|
643
|
+
// `+ γ·old` regularises a spring row (γ = 0 otherwise → exact).
|
|
644
|
+
let nv = old - lin_eff[k] * (vrel + lin_bias[k] + lin_gamma[k] * old);
|
|
645
|
+
if (nv < lin_clo[k]) nv = lin_clo[k]; else if (nv > lin_chi[k]) nv = lin_chi[k];
|
|
646
|
+
imp[k] = nv;
|
|
647
|
+
const d = nv - old;
|
|
648
|
+
if (d !== 0) {
|
|
649
|
+
apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, d * ax, d * ay, d * az, +1);
|
|
650
|
+
if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, d * ax, d * ay, d * az, -1);
|
|
651
|
+
}
|
|
406
652
|
}
|
|
407
653
|
|
|
408
|
-
// Angular rows (frame axes): drive ωB−ωA about each
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
apply_angular_impulse(rbA, trA, aYx, aYy, aYz, lambda, -1);
|
|
425
|
-
}
|
|
426
|
-
if (lockAZ) {
|
|
427
|
-
let wrel = -(avA[0] * aZx + avA[1] * aZy + avA[2] * aZz);
|
|
428
|
-
if (!to_world) wrel += avB[0] * aZx + avB[1] * aZy + avB[2] * aZz;
|
|
429
|
-
const lambda = -m_eff_az * (wrel + biasAz);
|
|
430
|
-
imp[5] += lambda;
|
|
431
|
-
if (!to_world) apply_angular_impulse(rbB, trB, aZx, aZy, aZz, lambda, +1);
|
|
432
|
-
apply_angular_impulse(rbA, trA, aZx, aZy, aZz, lambda, -1);
|
|
654
|
+
// Angular rows (frame axes): drive ωB−ωA about each axis toward the
|
|
655
|
+
// bias target, clamped per the row's bound.
|
|
656
|
+
for (let k = 0; k < 3; k++) {
|
|
657
|
+
if (!ang_active[k]) continue;
|
|
658
|
+
const ax = fa[3 * k], ay = fa[3 * k + 1], az = fa[3 * k + 2];
|
|
659
|
+
let wrel = -(avA[0] * ax + avA[1] * ay + avA[2] * az);
|
|
660
|
+
if (!to_world) wrel += avB[0] * ax + avB[1] * ay + avB[2] * az;
|
|
661
|
+
const old = imp[3 + k];
|
|
662
|
+
let nv = old - ang_eff[k] * (wrel + ang_bias[k] + ang_gamma[k] * old);
|
|
663
|
+
if (nv < ang_clo[k]) nv = ang_clo[k]; else if (nv > ang_chi[k]) nv = ang_chi[k];
|
|
664
|
+
imp[3 + k] = nv;
|
|
665
|
+
const d = nv - old;
|
|
666
|
+
if (d !== 0) {
|
|
667
|
+
if (!to_world) apply_angular_impulse(rbB, trB, ax, ay, az, d, +1);
|
|
668
|
+
apply_angular_impulse(rbA, trA, ax, ay, az, d, -1);
|
|
669
|
+
}
|
|
433
670
|
}
|
|
434
671
|
}
|
|
435
672
|
}
|