@flighthq/physics3d 0.4.0-next.1694.598ef6f
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/dist/contacts.d.ts +4 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +55 -0
- package/dist/contacts.js.map +1 -0
- package/dist/contract.d.ts +17 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +17 -0
- package/dist/contract.js.map +1 -0
- package/dist/enablePhysics3DGuards.d.ts +4 -0
- package/dist/enablePhysics3DGuards.d.ts.map +1 -0
- package/dist/enablePhysics3DGuards.js +68 -0
- package/dist/enablePhysics3DGuards.js.map +1 -0
- package/dist/explainPhysics3DJoints.d.ts +3 -0
- package/dist/explainPhysics3DJoints.d.ts.map +1 -0
- package/dist/explainPhysics3DJoints.js +32 -0
- package/dist/explainPhysics3DJoints.js.map +1 -0
- package/dist/explainPhysics3DStep.d.ts +3 -0
- package/dist/explainPhysics3DStep.d.ts.map +1 -0
- package/dist/explainPhysics3DStep.js +48 -0
- package/dist/explainPhysics3DStep.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/integrate.d.ts +6 -0
- package/dist/integrate.d.ts.map +1 -0
- package/dist/integrate.js +154 -0
- package/dist/integrate.js.map +1 -0
- package/dist/islands.d.ts +5 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +316 -0
- package/dist/islands.js.map +1 -0
- package/dist/jointCollisionSuppression.d.ts +4 -0
- package/dist/jointCollisionSuppression.d.ts.map +1 -0
- package/dist/jointCollisionSuppression.js +40 -0
- package/dist/jointCollisionSuppression.js.map +1 -0
- package/dist/jointFactories.d.ts +8 -0
- package/dist/jointFactories.d.ts.map +1 -0
- package/dist/jointFactories.js +126 -0
- package/dist/jointFactories.js.map +1 -0
- package/dist/jointMath.d.ts +17 -0
- package/dist/jointMath.d.ts.map +1 -0
- package/dist/jointMath.js +361 -0
- package/dist/jointMath.js.map +1 -0
- package/dist/jointRegistry.d.ts +8 -0
- package/dist/jointRegistry.d.ts.map +1 -0
- package/dist/jointRegistry.js +144 -0
- package/dist/jointRegistry.js.map +1 -0
- package/dist/jointRows.d.ts +32 -0
- package/dist/jointRows.d.ts.map +1 -0
- package/dist/jointRows.js +308 -0
- package/dist/jointRows.js.map +1 -0
- package/dist/joints.d.ts +14 -0
- package/dist/joints.d.ts.map +1 -0
- package/dist/joints.js +758 -0
- package/dist/joints.js.map +1 -0
- package/dist/massProperties.d.ts +8 -0
- package/dist/massProperties.d.ts.map +1 -0
- package/dist/massProperties.js +169 -0
- package/dist/massProperties.js.map +1 -0
- package/dist/ownership.d.ts +5 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/ownership.js +16 -0
- package/dist/ownership.js.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.js +14 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.js.map +1 -0
- package/dist/solver.d.ts +8 -0
- package/dist/solver.d.ts.map +1 -0
- package/dist/solver.js +383 -0
- package/dist/solver.js.map +1 -0
- package/dist/step.d.ts +5 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +250 -0
- package/dist/step.js.map +1 -0
- package/dist/stepValidation.d.ts +12 -0
- package/dist/stepValidation.d.ts.map +1 -0
- package/dist/stepValidation.js +201 -0
- package/dist/stepValidation.js.map +1 -0
- package/dist/symmetricTensor.d.ts +11 -0
- package/dist/symmetricTensor.d.ts.map +1 -0
- package/dist/symmetricTensor.js +150 -0
- package/dist/symmetricTensor.js.map +1 -0
- package/dist/world.d.ts +19 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +370 -0
- package/dist/world.js.map +1 -0
- package/package.json +49 -0
- package/src/contacts.test.ts +64 -0
- package/src/enablePhysics3DGuards.test.ts +105 -0
- package/src/explainPhysics3DJoints.test.ts +101 -0
- package/src/explainPhysics3DStep.test.ts +123 -0
- package/src/integrate.test.ts +249 -0
- package/src/islands.test.ts +357 -0
- package/src/jointCollisionSuppression.test.ts +94 -0
- package/src/jointFactories.test.ts +195 -0
- package/src/jointMath.test.ts +643 -0
- package/src/jointRegistry.test.ts +229 -0
- package/src/jointRows.test.ts +484 -0
- package/src/joints.test.ts +677 -0
- package/src/massProperties.test.ts +305 -0
- package/src/ownership.test.ts +36 -0
- package/src/registerBuiltInPhysics3DJointSolvers.test.ts +48 -0
- package/src/solver.test.ts +481 -0
- package/src/step.test.ts +390 -0
- package/src/stepValidation.test.ts +244 -0
- package/src/symmetricTensor.test.ts +166 -0
- package/src/world.test.ts +512 -0
package/dist/joints.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { swapPhysics3DJointFrames, writePhysics3DJointAnchors, writePhysics3DJointRelativeRotation, writePhysics3DJointRotationError, writePhysics3DJointSeparation, } from './jointMath';
|
|
2
|
+
import { applyRow, beginJointSolve, clearJointSolve, frameABasis, frameARotation, frameBBasis, frameBRotation, getJointSolveState, getRowMass, POINT_LENGTH, prepareAngularBlock, preparePointBlock, readFrameBases, readFrameRotations, ROW_LENGTH, solveAngularBlock, solveEqualityRow, solveLowerLimitRow, solveMotorRow, solvePointBlock, solveUpperLimitRow, readJointImpulse, warmStartAngularBlock, warmStartPointBlock, writeAngularRow, writeRow, } from './jointRows';
|
|
3
|
+
import { findPhysics3DBody } from './world';
|
|
4
|
+
// The built-in joint kinds. Bare names are reserved for these; a user's own joint takes a vendor prefix, and
|
|
5
|
+
// that convention rather than a registration guard is what keeps the two from colliding.
|
|
6
|
+
export const Physics3DBallAndSocketJointKind = 'BallAndSocket';
|
|
7
|
+
export const Physics3DConeTwistJointKind = 'ConeTwist';
|
|
8
|
+
export const Physics3DFixedJointKind = 'Fixed';
|
|
9
|
+
export const Physics3DGeneric6DofJointKind = 'Generic6Dof';
|
|
10
|
+
export const Physics3DHingeJointKind = 'Hinge';
|
|
11
|
+
export const Physics3DSliderJointKind = 'Slider';
|
|
12
|
+
// Pins two anchors together, leaving rotation free. Three coupled rows and nothing else — the simplest kind,
|
|
13
|
+
// and the one every other kind's point half is the same block of.
|
|
14
|
+
export const physics3DBallAndSocketJointSolver = {
|
|
15
|
+
clearAccumulatedImpulses(joint) {
|
|
16
|
+
joint.impulse0 = 0;
|
|
17
|
+
joint.impulse1 = 0;
|
|
18
|
+
joint.impulse2 = 0;
|
|
19
|
+
},
|
|
20
|
+
prepare(world, joint, dt) {
|
|
21
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
22
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
23
|
+
if (bodyA === null || bodyB === null) {
|
|
24
|
+
clearJointSolve(joint);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
preparePointBlock(bodyA, bodyB, joint, beginJointSolve(joint, POINT_LENGTH), dt);
|
|
28
|
+
},
|
|
29
|
+
solve(world, joint) {
|
|
30
|
+
const state = getJointSolveState(joint);
|
|
31
|
+
if (state === undefined)
|
|
32
|
+
return;
|
|
33
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
34
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
35
|
+
if (bodyA === null || bodyB === null)
|
|
36
|
+
return;
|
|
37
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
38
|
+
},
|
|
39
|
+
warmStart(world, joint) {
|
|
40
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
41
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
42
|
+
if (bodyA === null || bodyB === null)
|
|
43
|
+
return;
|
|
44
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
// Pins two anchors together and locks the relative orientation. The point block plus the angular block, with
|
|
48
|
+
// no parameters of its own: a fixed joint is defined entirely by the pose it was authored in.
|
|
49
|
+
export const physics3DFixedJointSolver = {
|
|
50
|
+
clearAccumulatedImpulses(joint) {
|
|
51
|
+
joint.impulse0 = 0;
|
|
52
|
+
joint.impulse1 = 0;
|
|
53
|
+
joint.impulse2 = 0;
|
|
54
|
+
joint.impulse3 = 0;
|
|
55
|
+
joint.impulse4 = 0;
|
|
56
|
+
joint.impulse5 = 0;
|
|
57
|
+
},
|
|
58
|
+
// The frames travel with their bodies and nothing about them reverses: "these two frames coincide" reads
|
|
59
|
+
// the same from either end. The generic swap moves the bodies and anchors; this moves the frames with them.
|
|
60
|
+
swapEnds(joint) {
|
|
61
|
+
swapPhysics3DJointFrames(joint);
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
prepare(world, joint, dt) {
|
|
65
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
66
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
67
|
+
if (bodyA === null || bodyB === null) {
|
|
68
|
+
clearJointSolve(joint);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const state = beginJointSolve(joint, FIXED_LENGTH);
|
|
72
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
73
|
+
readFrameRotations(bodyA, bodyB, joint);
|
|
74
|
+
prepareAngularBlock(bodyA, bodyB, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS, dt);
|
|
75
|
+
},
|
|
76
|
+
solve(world, joint) {
|
|
77
|
+
const state = getJointSolveState(joint);
|
|
78
|
+
if (state === undefined)
|
|
79
|
+
return;
|
|
80
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
81
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
82
|
+
if (bodyA === null || bodyB === null)
|
|
83
|
+
return;
|
|
84
|
+
solveAngularBlock(bodyA, bodyB, joint, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS);
|
|
85
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
86
|
+
},
|
|
87
|
+
warmStart(world, joint) {
|
|
88
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
89
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
90
|
+
if (bodyA === null || bodyB === null)
|
|
91
|
+
return;
|
|
92
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
93
|
+
warmStartAngularBlock(bodyA, bodyB, joint);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
// Pins two anchors together and confines rotation to the frame's X axis, optionally motorized and limited.
|
|
97
|
+
//
|
|
98
|
+
// The point block, then two angular rows holding frame B's X onto frame A's, then a motor and a pair of
|
|
99
|
+
// limits sharing the axis row. The two lock rows are solved sequentially rather than as a coupled 2x2: they
|
|
100
|
+
// turn about perpendicular axes, so they couple only through an anisotropic inertia tensor, and the residual
|
|
101
|
+
// is what the remaining velocity iterations absorb.
|
|
102
|
+
export const physics3DHingeJointSolver = {
|
|
103
|
+
clearAccumulatedImpulses(joint) {
|
|
104
|
+
joint.impulse0 = 0;
|
|
105
|
+
joint.impulse1 = 0;
|
|
106
|
+
joint.impulse2 = 0;
|
|
107
|
+
joint.impulse3 = 0;
|
|
108
|
+
joint.impulse4 = 0;
|
|
109
|
+
const hinge = joint;
|
|
110
|
+
hinge.motorImpulse = 0;
|
|
111
|
+
hinge.lowerLimitImpulse = 0;
|
|
112
|
+
hinge.upperLimitImpulse = 0;
|
|
113
|
+
},
|
|
114
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
115
|
+
const hinge = joint;
|
|
116
|
+
hinge.motorImpulse = (hinge.motorImpulse ?? 0) * timestepRatio;
|
|
117
|
+
hinge.lowerLimitImpulse = (hinge.lowerLimitImpulse ?? 0) * timestepRatio;
|
|
118
|
+
hinge.upperLimitImpulse = (hinge.upperLimitImpulse ?? 0) * timestepRatio;
|
|
119
|
+
},
|
|
120
|
+
// The angle is measured from frame A's Y axis to frame B's about the shared X axis, so exchanging the ends
|
|
121
|
+
// reverses it. The limit interval negates AND its ends exchange — the old lower bound becomes the new
|
|
122
|
+
// upper — and the motor's target relative velocity reverses with it.
|
|
123
|
+
//
|
|
124
|
+
// Deriving the interval: the constraint is lower <= angle <= upper. After the swap the same physical
|
|
125
|
+
// interval requires lower' = -upper and upper' = -lower.
|
|
126
|
+
swapEnds(joint) {
|
|
127
|
+
const hinge = joint;
|
|
128
|
+
swapPhysics3DJointFrames(hinge);
|
|
129
|
+
const lower = hinge.lowerAngle;
|
|
130
|
+
hinge.lowerAngle = -hinge.upperAngle;
|
|
131
|
+
hinge.upperAngle = -lower;
|
|
132
|
+
hinge.motorSpeed = -hinge.motorSpeed;
|
|
133
|
+
// Defaulted BEFORE the negation. This runs when the joint is added, ahead of the first prepare, so the
|
|
134
|
+
// field may still be absent — and `-undefined` is NaN, which is not nullish, so a later `??` would accept
|
|
135
|
+
// the poison rather than replace it.
|
|
136
|
+
hinge.motorImpulse = -(hinge.motorImpulse ?? 0);
|
|
137
|
+
// Exchanged, NOT negated. Each is a non-negative magnitude whose direction is carried by the row that
|
|
138
|
+
// owns it, so the push that used to hold the lower bound is the one that now holds the upper.
|
|
139
|
+
const lowerImpulse = hinge.lowerLimitImpulse ?? 0;
|
|
140
|
+
hinge.lowerLimitImpulse = hinge.upperLimitImpulse ?? 0;
|
|
141
|
+
hinge.upperLimitImpulse = lowerImpulse;
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
prepare(world, joint, dt) {
|
|
145
|
+
const hinge = joint;
|
|
146
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
147
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
148
|
+
if (bodyA === null || bodyB === null) {
|
|
149
|
+
clearJointSolve(joint);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const state = beginJointSolve(joint, HINGE_LENGTH);
|
|
153
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
154
|
+
readFrameRotations(bodyA, bodyB, hinge);
|
|
155
|
+
readFrameBases(bodyA, bodyB, hinge);
|
|
156
|
+
writeAngularRow(state, HINGE_AXIS_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
|
|
157
|
+
writeAngularRow(state, HINGE_LOCK_ROW0, frameABasis[3], frameABasis[4], frameABasis[5]);
|
|
158
|
+
writeAngularRow(state, HINGE_LOCK_ROW1, frameABasis[6], frameABasis[7], frameABasis[8]);
|
|
159
|
+
state[HINGE_AXIS_MASS] = getRowMass(bodyA, bodyB, state, HINGE_AXIS_ROW);
|
|
160
|
+
state[HINGE_LOCK_MASS0] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW0);
|
|
161
|
+
state[HINGE_LOCK_MASS1] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW1);
|
|
162
|
+
// The two lock rows' positional error is the misalignment rotation projected onto the axes they turn
|
|
163
|
+
// about. Its third component, about the hinge axis itself, is the one rotation the joint permits and is
|
|
164
|
+
// deliberately not read here — the limits read the exact angle instead.
|
|
165
|
+
writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
|
|
166
|
+
state[HINGE_LOCK_ERROR0] = dot(rotationError, 0, frameABasis, 3);
|
|
167
|
+
state[HINGE_LOCK_ERROR1] = dot(rotationError, 0, frameABasis, 6);
|
|
168
|
+
state[HINGE_ANGLE] = getSignedAxisAngle(frameABasis, frameBBasis);
|
|
169
|
+
state[HINGE_LOCK_BIAS] = BAUMGARTE / dt;
|
|
170
|
+
state[HINGE_LIMIT_BIAS] = 1 / dt;
|
|
171
|
+
state[HINGE_MAX_MOTOR] = Math.max(0, dt * hinge.maxMotorTorque);
|
|
172
|
+
// Seeded from the carried accumulators rather than zeroed, which is what makes the limit rows warm
|
|
173
|
+
// start. Gated on the CURRENT `enableLimit` for the same reason the motor is: a cached impulse is only
|
|
174
|
+
// valid while the row that produced it is still being solved.
|
|
175
|
+
hinge.lowerLimitImpulse ??= 0;
|
|
176
|
+
hinge.upperLimitImpulse ??= 0;
|
|
177
|
+
if (!hinge.enableLimit) {
|
|
178
|
+
hinge.lowerLimitImpulse = 0;
|
|
179
|
+
hinge.upperLimitImpulse = 0;
|
|
180
|
+
}
|
|
181
|
+
state[HINGE_LOWER_IMPULSE] = hinge.lowerLimitImpulse;
|
|
182
|
+
state[HINGE_UPPER_IMPULSE] = hinge.upperLimitImpulse;
|
|
183
|
+
hinge.motorImpulse ??= 0;
|
|
184
|
+
if (hinge.enableMotor) {
|
|
185
|
+
hinge.motorImpulse = clamp(hinge.motorImpulse, -state[HINGE_MAX_MOTOR], state[HINGE_MAX_MOTOR]);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
hinge.motorImpulse = 0;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
solve(world, joint) {
|
|
192
|
+
const hinge = joint;
|
|
193
|
+
const state = getJointSolveState(joint);
|
|
194
|
+
if (state === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
197
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
198
|
+
if (bodyA === null || bodyB === null)
|
|
199
|
+
return;
|
|
200
|
+
// Motor first: it is a soft target the limits are allowed to override, so solving it before them lets a
|
|
201
|
+
// limit's impulse win within the same iteration when the two disagree.
|
|
202
|
+
if (hinge.enableMotor && state[HINGE_AXIS_MASS] > 0) {
|
|
203
|
+
hinge.motorImpulse = solveMotorRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], hinge.motorSpeed, state[HINGE_MAX_MOTOR], hinge.motorImpulse);
|
|
204
|
+
}
|
|
205
|
+
if (hinge.enableLimit && state[HINGE_AXIS_MASS] > 0) {
|
|
206
|
+
const angle = state[HINGE_ANGLE];
|
|
207
|
+
solveLowerLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], angle - hinge.lowerAngle, state[HINGE_LIMIT_BIAS], HINGE_LOWER_IMPULSE);
|
|
208
|
+
solveUpperLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], hinge.upperAngle - angle, state[HINGE_LIMIT_BIAS], HINGE_UPPER_IMPULSE);
|
|
209
|
+
// Written back every iteration, not once at the end of the step: the solve state is per-sub-interval
|
|
210
|
+
// scratch that `beginJointSolve` reallocates, so the joint field is the only place a value survives to
|
|
211
|
+
// the next one.
|
|
212
|
+
hinge.lowerLimitImpulse = state[HINGE_LOWER_IMPULSE];
|
|
213
|
+
hinge.upperLimitImpulse = state[HINGE_UPPER_IMPULSE];
|
|
214
|
+
}
|
|
215
|
+
solveEqualityRow(bodyA, bodyB, joint, 3, state, HINGE_LOCK_ROW0, state[HINGE_LOCK_MASS0], state[HINGE_LOCK_ERROR0], state[HINGE_LOCK_BIAS]);
|
|
216
|
+
solveEqualityRow(bodyA, bodyB, joint, 4, state, HINGE_LOCK_ROW1, state[HINGE_LOCK_MASS1], state[HINGE_LOCK_ERROR1], state[HINGE_LOCK_BIAS]);
|
|
217
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
218
|
+
},
|
|
219
|
+
warmStart(world, joint) {
|
|
220
|
+
const hinge = joint;
|
|
221
|
+
const state = getJointSolveState(joint);
|
|
222
|
+
if (state === undefined)
|
|
223
|
+
return;
|
|
224
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
225
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
226
|
+
if (bodyA === null || bodyB === null)
|
|
227
|
+
return;
|
|
228
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
229
|
+
applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW0, joint.impulse3);
|
|
230
|
+
applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW1, joint.impulse4);
|
|
231
|
+
// Gated on the CURRENT `enableMotor`, not on the value being non-zero. A cached impulse is valid only
|
|
232
|
+
// while the thing that produced it is still running: `solve` skips a disabled motor, so reapplying its
|
|
233
|
+
// last impulse here would exert a torque no solver ever asks for and nothing ever cancels — a disabled
|
|
234
|
+
// motor that keeps turning the hinge forever.
|
|
235
|
+
if (hinge.enableMotor)
|
|
236
|
+
applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.motorImpulse);
|
|
237
|
+
// The lower row pushes along the axis and the upper row against it, which is the sign each carries in
|
|
238
|
+
// `solveLowerLimitRow`/`solveUpperLimitRow`. Reapplying them with the wrong signs would drive the joint
|
|
239
|
+
// toward the stop it is resting against instead of holding it off.
|
|
240
|
+
if (hinge.enableLimit) {
|
|
241
|
+
applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.lowerLimitImpulse - hinge.upperLimitImpulse);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
// Pins two anchors together and bounds relative rotation as a cone plus a twist — the ragdoll joint.
|
|
246
|
+
//
|
|
247
|
+
// The point block, one swing limit, and a pair of twist limits. The swing limit is a single one-sided row
|
|
248
|
+
// about the axis the tilt is happening in, not two per-axis bounds: a cone is not a box, and a shoulder
|
|
249
|
+
// bounded by three independent intervals reaches poses along the diagonal that the anatomy does not have.
|
|
250
|
+
export const physics3DConeTwistJointSolver = {
|
|
251
|
+
clearAccumulatedImpulses(joint) {
|
|
252
|
+
joint.impulse0 = 0;
|
|
253
|
+
joint.impulse1 = 0;
|
|
254
|
+
joint.impulse2 = 0;
|
|
255
|
+
const cone = joint;
|
|
256
|
+
cone.swingLimitImpulse = 0;
|
|
257
|
+
cone.lowerTwistImpulse = 0;
|
|
258
|
+
cone.upperTwistImpulse = 0;
|
|
259
|
+
},
|
|
260
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
261
|
+
const cone = joint;
|
|
262
|
+
cone.swingLimitImpulse = (cone.swingLimitImpulse ?? 0) * timestepRatio;
|
|
263
|
+
cone.lowerTwistImpulse = (cone.lowerTwistImpulse ?? 0) * timestepRatio;
|
|
264
|
+
cone.upperTwistImpulse = (cone.upperTwistImpulse ?? 0) * timestepRatio;
|
|
265
|
+
},
|
|
266
|
+
// Vetoed, and the reason is exact rather than cautious. A kind may exchange its ends only when its own
|
|
267
|
+
// constraint keeps the two frames aligned on the axes its parameters are measured against — a hinge and a
|
|
268
|
+
// slider both lock frame B's X onto frame A's, so their axis survives the exchange. A cone-twist
|
|
269
|
+
// deliberately leaves that alignment FREE within the cone, so after the exchange the cone would be
|
|
270
|
+
// measured against the other body's frame: a different constraint wearing the same numbers.
|
|
271
|
+
swapEnds() {
|
|
272
|
+
return false;
|
|
273
|
+
},
|
|
274
|
+
prepare(world, joint, dt) {
|
|
275
|
+
const cone = joint;
|
|
276
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
277
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
278
|
+
if (bodyA === null || bodyB === null) {
|
|
279
|
+
clearJointSolve(joint);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const state = beginJointSolve(joint, CONE_LENGTH);
|
|
283
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
284
|
+
readFrameRotations(bodyA, bodyB, cone);
|
|
285
|
+
readFrameBases(bodyA, bodyB, cone);
|
|
286
|
+
// The swing axis is perpendicular to both twist axes, so turning B about it is exactly what opens and
|
|
287
|
+
// closes the cone. It vanishes when the two are parallel, which is the joint sitting at the centre of its
|
|
288
|
+
// cone — no direction to limit, and nothing to limit it against.
|
|
289
|
+
const swingX = frameABasis[1] * frameBBasis[2] - frameABasis[2] * frameBBasis[1];
|
|
290
|
+
const swingY = frameABasis[2] * frameBBasis[0] - frameABasis[0] * frameBBasis[2];
|
|
291
|
+
const swingZ = frameABasis[0] * frameBBasis[1] - frameABasis[1] * frameBBasis[0];
|
|
292
|
+
const swingLength = Math.sqrt(swingX * swingX + swingY * swingY + swingZ * swingZ);
|
|
293
|
+
const active = swingLength > AXIS_EPSILON;
|
|
294
|
+
state[CONE_SWING_ACTIVE] = active ? 1 : 0;
|
|
295
|
+
if (active) {
|
|
296
|
+
writeAngularRow(state, CONE_SWING_ROW, swingX / swingLength, swingY / swingLength, swingZ / swingLength);
|
|
297
|
+
state[CONE_SWING_MASS] = getRowMass(bodyA, bodyB, state, CONE_SWING_ROW);
|
|
298
|
+
const swing = Math.acos(clamp(dot(frameABasis, 0, frameBBasis, 0), -1, 1));
|
|
299
|
+
state[CONE_SWING_ERROR] = getConeSwingLimit(cone) - swing;
|
|
300
|
+
}
|
|
301
|
+
writeAngularRow(state, CONE_TWIST_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
|
|
302
|
+
state[CONE_TWIST_MASS] = getRowMass(bodyA, bodyB, state, CONE_TWIST_ROW);
|
|
303
|
+
// Twist is the X component of the relative rotation taken in A's own frame — the rotation left over once
|
|
304
|
+
// the swing is removed. Reading it here rather than from the world-space error vector is what keeps it
|
|
305
|
+
// independent of how far the joint has swung.
|
|
306
|
+
writePhysics3DJointRelativeRotation(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], relativeRotation);
|
|
307
|
+
state[CONE_TWIST_ANGLE] = 2 * Math.atan2(relativeRotation[0], relativeRotation[3]);
|
|
308
|
+
state[CONE_LIMIT_BIAS] = 1 / dt;
|
|
309
|
+
// The swing row's AXIS moves with the tilt direction, so its carried impulse is reapplied along a
|
|
310
|
+
// slightly different axis than the one that earned it — the same approximation contact warm starting
|
|
311
|
+
// makes when a normal rotates, and it converges for the same reason: one iteration corrects it.
|
|
312
|
+
cone.swingLimitImpulse ??= 0;
|
|
313
|
+
cone.lowerTwistImpulse ??= 0;
|
|
314
|
+
cone.upperTwistImpulse ??= 0;
|
|
315
|
+
if (!cone.enableSwingLimit)
|
|
316
|
+
cone.swingLimitImpulse = 0;
|
|
317
|
+
if (!cone.enableTwistLimit) {
|
|
318
|
+
cone.lowerTwistImpulse = 0;
|
|
319
|
+
cone.upperTwistImpulse = 0;
|
|
320
|
+
}
|
|
321
|
+
state[CONE_SWING_IMPULSE] = cone.swingLimitImpulse;
|
|
322
|
+
state[CONE_LOWER_TWIST_IMPULSE] = cone.lowerTwistImpulse;
|
|
323
|
+
state[CONE_UPPER_TWIST_IMPULSE] = cone.upperTwistImpulse;
|
|
324
|
+
},
|
|
325
|
+
solve(world, joint) {
|
|
326
|
+
const cone = joint;
|
|
327
|
+
const state = getJointSolveState(joint);
|
|
328
|
+
if (state === undefined)
|
|
329
|
+
return;
|
|
330
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
331
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
332
|
+
if (bodyA === null || bodyB === null)
|
|
333
|
+
return;
|
|
334
|
+
if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1 && state[CONE_SWING_MASS] > 0) {
|
|
335
|
+
solveUpperLimitRow(bodyA, bodyB, state, CONE_SWING_ROW, state[CONE_SWING_MASS], state[CONE_SWING_ERROR], state[CONE_LIMIT_BIAS], CONE_SWING_IMPULSE);
|
|
336
|
+
cone.swingLimitImpulse = state[CONE_SWING_IMPULSE];
|
|
337
|
+
}
|
|
338
|
+
if (cone.enableTwistLimit && state[CONE_TWIST_MASS] > 0) {
|
|
339
|
+
const twist = state[CONE_TWIST_ANGLE];
|
|
340
|
+
solveLowerLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_MASS], twist - cone.lowerTwistAngle, state[CONE_LIMIT_BIAS], CONE_LOWER_TWIST_IMPULSE);
|
|
341
|
+
solveUpperLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_MASS], cone.upperTwistAngle - twist, state[CONE_LIMIT_BIAS], CONE_UPPER_TWIST_IMPULSE);
|
|
342
|
+
cone.lowerTwistImpulse = state[CONE_LOWER_TWIST_IMPULSE];
|
|
343
|
+
cone.upperTwistImpulse = state[CONE_UPPER_TWIST_IMPULSE];
|
|
344
|
+
}
|
|
345
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
346
|
+
},
|
|
347
|
+
warmStart(world, joint) {
|
|
348
|
+
const cone = joint;
|
|
349
|
+
const state = getJointSolveState(joint);
|
|
350
|
+
if (state === undefined)
|
|
351
|
+
return;
|
|
352
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
353
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
354
|
+
if (bodyA === null || bodyB === null)
|
|
355
|
+
return;
|
|
356
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
357
|
+
// The swing row is reapplied only while it is the ACTIVE side of the cone. Outside that, the row it was
|
|
358
|
+
// measured against does not exist this sub-interval and its axis is stale.
|
|
359
|
+
if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1) {
|
|
360
|
+
applyRow(bodyA, bodyB, state, CONE_SWING_ROW, -cone.swingLimitImpulse);
|
|
361
|
+
}
|
|
362
|
+
if (cone.enableTwistLimit) {
|
|
363
|
+
applyRow(bodyA, bodyB, state, CONE_TWIST_ROW, cone.lowerTwistImpulse - cone.upperTwistImpulse);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
// Bounds each of the six degrees of freedom independently — the configurable joint every other kind is a
|
|
368
|
+
// preset of.
|
|
369
|
+
//
|
|
370
|
+
// Each axis reads its state from its bounds rather than from a mode flag: `lower > upper` is free,
|
|
371
|
+
// `lower === upper` is locked, and anything else is limited. A separate flag would be a second source that
|
|
372
|
+
// can disagree with the numbers it describes.
|
|
373
|
+
export const physics3DGeneric6DofJointSolver = {
|
|
374
|
+
clearAccumulatedImpulses(joint) {
|
|
375
|
+
const dof = joint;
|
|
376
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
377
|
+
dof.lowerLimitImpulses[axis] = 0;
|
|
378
|
+
dof.upperLimitImpulses[axis] = 0;
|
|
379
|
+
}
|
|
380
|
+
joint.impulse0 = 0;
|
|
381
|
+
joint.impulse1 = 0;
|
|
382
|
+
joint.impulse2 = 0;
|
|
383
|
+
joint.impulse3 = 0;
|
|
384
|
+
joint.impulse4 = 0;
|
|
385
|
+
joint.impulse5 = 0;
|
|
386
|
+
},
|
|
387
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
388
|
+
const dof = joint;
|
|
389
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
390
|
+
dof.lowerLimitImpulses[axis] *= timestepRatio;
|
|
391
|
+
dof.upperLimitImpulses[axis] *= timestepRatio;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
// Vetoed for the same reason as a cone-twist, and for a kind whose entire purpose is per-axis freedom the
|
|
395
|
+
// reason is unavoidable: any axis left free lets the two frames diverge about it, so the bounds — which are
|
|
396
|
+
// measured against frame A — would after the exchange be measured against a frame the joint never held
|
|
397
|
+
// still. An all-locked 6-DOF could be exchanged safely, but a kind cannot answer this per instance without
|
|
398
|
+
// its ends already meaning two different things depending on how it was configured.
|
|
399
|
+
swapEnds() {
|
|
400
|
+
return false;
|
|
401
|
+
},
|
|
402
|
+
prepare(world, joint, dt) {
|
|
403
|
+
const dof = joint;
|
|
404
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
405
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
406
|
+
if (bodyA === null || bodyB === null) {
|
|
407
|
+
clearJointSolve(joint);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const state = beginJointSolve(joint, DOF_LENGTH);
|
|
411
|
+
writePhysics3DJointAnchors(bodyA, bodyB, joint);
|
|
412
|
+
writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
|
|
413
|
+
readFrameRotations(bodyA, bodyB, dof);
|
|
414
|
+
readFrameBases(bodyA, bodyB, dof);
|
|
415
|
+
writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
|
|
416
|
+
readDofBounds(dof);
|
|
417
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
418
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
419
|
+
const basisOffset = (axis % 3) * 3;
|
|
420
|
+
const lower = dofLower[axis];
|
|
421
|
+
const upper = dofUpper[axis];
|
|
422
|
+
state[DOF_MODE + axis] = lower > upper ? DOF_FREE : lower === upper ? DOF_LOCKED : DOF_LIMITED;
|
|
423
|
+
// Carried only while the axis is still LIMITED. An axis whose bounds changed it to free or locked has
|
|
424
|
+
// an accumulator describing a constraint that no longer exists, and reapplying it would push against a
|
|
425
|
+
// bound the joint may no longer be anywhere near.
|
|
426
|
+
if (state[DOF_MODE + axis] !== DOF_LIMITED) {
|
|
427
|
+
dof.lowerLimitImpulses[axis] = 0;
|
|
428
|
+
dof.upperLimitImpulses[axis] = 0;
|
|
429
|
+
}
|
|
430
|
+
state[DOF_LOWER_IMPULSE + axis] = dof.lowerLimitImpulses[axis];
|
|
431
|
+
state[DOF_UPPER_IMPULSE + axis] = dof.upperLimitImpulses[axis];
|
|
432
|
+
if (state[DOF_MODE + axis] === DOF_FREE)
|
|
433
|
+
continue;
|
|
434
|
+
if (axis < 3) {
|
|
435
|
+
writeLinearAxisRow(state, rowOffset, joint, basisOffset);
|
|
436
|
+
state[DOF_VALUE + axis] = dot(separation, 0, frameABasis, basisOffset);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
writeAngularRow(state, rowOffset, frameABasis[basisOffset], frameABasis[basisOffset + 1], frameABasis[basisOffset + 2]);
|
|
440
|
+
state[DOF_VALUE + axis] = dot(rotationError, 0, frameABasis, basisOffset);
|
|
441
|
+
}
|
|
442
|
+
state[DOF_MASS + axis] = getRowMass(bodyA, bodyB, state, rowOffset);
|
|
443
|
+
}
|
|
444
|
+
state[DOF_LOCK_BIAS] = BAUMGARTE / dt;
|
|
445
|
+
state[DOF_LIMIT_BIAS] = 1 / dt;
|
|
446
|
+
},
|
|
447
|
+
solve(world, joint) {
|
|
448
|
+
const dof = joint;
|
|
449
|
+
const state = getJointSolveState(joint);
|
|
450
|
+
if (state === undefined)
|
|
451
|
+
return;
|
|
452
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
453
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
454
|
+
if (bodyA === null || bodyB === null)
|
|
455
|
+
return;
|
|
456
|
+
readDofBounds(dof);
|
|
457
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
458
|
+
const mode = state[DOF_MODE + axis];
|
|
459
|
+
if (mode === DOF_FREE || state[DOF_MASS + axis] <= 0)
|
|
460
|
+
continue;
|
|
461
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
462
|
+
const value = state[DOF_VALUE + axis];
|
|
463
|
+
if (mode === DOF_LOCKED) {
|
|
464
|
+
solveEqualityRow(bodyA, bodyB, joint, axis, state, rowOffset, state[DOF_MASS + axis], value - dofLower[axis], state[DOF_LOCK_BIAS]);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
solveLowerLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_MASS + axis], value - dofLower[axis], state[DOF_LIMIT_BIAS], DOF_LOWER_IMPULSE + axis);
|
|
468
|
+
solveUpperLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_MASS + axis], dofUpper[axis] - value, state[DOF_LIMIT_BIAS], DOF_UPPER_IMPULSE + axis);
|
|
469
|
+
dof.lowerLimitImpulses[axis] = state[DOF_LOWER_IMPULSE + axis];
|
|
470
|
+
dof.upperLimitImpulses[axis] = state[DOF_UPPER_IMPULSE + axis];
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
// Locked axes reapply the equality block's slot; limited axes reapply their own pair of one-sided
|
|
474
|
+
// accumulators. The two come from different places because a locked axis is one signed row while a limited
|
|
475
|
+
// axis is two rows that may only push, and neither can be read out of the other's storage.
|
|
476
|
+
warmStart(world, joint) {
|
|
477
|
+
const dof = joint;
|
|
478
|
+
const state = getJointSolveState(joint);
|
|
479
|
+
if (state === undefined)
|
|
480
|
+
return;
|
|
481
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
482
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
483
|
+
if (bodyA === null || bodyB === null)
|
|
484
|
+
return;
|
|
485
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
486
|
+
const mode = state[DOF_MODE + axis];
|
|
487
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
488
|
+
if (mode === DOF_LOCKED) {
|
|
489
|
+
applyRow(bodyA, bodyB, state, rowOffset, readJointImpulse(joint, axis));
|
|
490
|
+
}
|
|
491
|
+
else if (mode === DOF_LIMITED) {
|
|
492
|
+
applyRow(bodyA, bodyB, state, rowOffset, dof.lowerLimitImpulses[axis] - dof.upperLimitImpulses[axis]);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
// Confines relative motion to translation along the frame's X axis, optionally motorized and travel-limited.
|
|
498
|
+
//
|
|
499
|
+
// Two perpendicular linear rows hold the anchors on the rail, three angular rows lock the orientation, and
|
|
500
|
+
// the axis row carries the motor and the limits. The two perpendicular rows are solved sequentially rather
|
|
501
|
+
// than as a coupled 2x2: their linear directions are orthogonal, so they couple only through an anisotropic
|
|
502
|
+
// inertia tensor, and the residual is what the remaining velocity iterations absorb.
|
|
503
|
+
export const physics3DSliderJointSolver = {
|
|
504
|
+
clearAccumulatedImpulses(joint) {
|
|
505
|
+
joint.impulse0 = 0;
|
|
506
|
+
joint.impulse1 = 0;
|
|
507
|
+
joint.impulse3 = 0;
|
|
508
|
+
joint.impulse4 = 0;
|
|
509
|
+
joint.impulse5 = 0;
|
|
510
|
+
const slider = joint;
|
|
511
|
+
slider.motorImpulse = 0;
|
|
512
|
+
slider.lowerLimitImpulse = 0;
|
|
513
|
+
slider.upperLimitImpulse = 0;
|
|
514
|
+
},
|
|
515
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
516
|
+
const slider = joint;
|
|
517
|
+
slider.motorImpulse = (slider.motorImpulse ?? 0) * timestepRatio;
|
|
518
|
+
slider.lowerLimitImpulse = (slider.lowerLimitImpulse ?? 0) * timestepRatio;
|
|
519
|
+
slider.upperLimitImpulse = (slider.upperLimitImpulse ?? 0) * timestepRatio;
|
|
520
|
+
},
|
|
521
|
+
// The translation is measured from A's anchor to B's along the shared axis, so exchanging the ends reverses
|
|
522
|
+
// it: the interval negates AND its ends exchange, and the motor's target velocity reverses with them. The
|
|
523
|
+
// exchange is exact because the slider's own angular lock holds frame B's X onto frame A's, so the axis the
|
|
524
|
+
// bounds are measured against is the same line from either end.
|
|
525
|
+
swapEnds(joint) {
|
|
526
|
+
const slider = joint;
|
|
527
|
+
swapPhysics3DJointFrames(slider);
|
|
528
|
+
const lower = slider.lowerTranslation;
|
|
529
|
+
slider.lowerTranslation = -slider.upperTranslation;
|
|
530
|
+
slider.upperTranslation = -lower;
|
|
531
|
+
slider.motorSpeed = -slider.motorSpeed;
|
|
532
|
+
slider.motorImpulse = -(slider.motorImpulse ?? 0);
|
|
533
|
+
// Exchanged, not negated — see the hinge's swapEnds for why a magnitude behaves differently here from
|
|
534
|
+
// the bound it holds.
|
|
535
|
+
const lowerImpulse = slider.lowerLimitImpulse ?? 0;
|
|
536
|
+
slider.lowerLimitImpulse = slider.upperLimitImpulse ?? 0;
|
|
537
|
+
slider.upperLimitImpulse = lowerImpulse;
|
|
538
|
+
return true;
|
|
539
|
+
},
|
|
540
|
+
prepare(world, joint, dt) {
|
|
541
|
+
const slider = joint;
|
|
542
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
543
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
544
|
+
if (bodyA === null || bodyB === null) {
|
|
545
|
+
clearJointSolve(joint);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const state = beginJointSolve(joint, SLIDER_LENGTH);
|
|
549
|
+
writePhysics3DJointAnchors(bodyA, bodyB, joint);
|
|
550
|
+
writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
|
|
551
|
+
readFrameRotations(bodyA, bodyB, slider);
|
|
552
|
+
readFrameBases(bodyA, bodyB, slider);
|
|
553
|
+
writeLinearAxisRow(state, SLIDER_AXIS_ROW, joint, 0);
|
|
554
|
+
writeLinearAxisRow(state, SLIDER_PERP_ROW0, joint, 3);
|
|
555
|
+
writeLinearAxisRow(state, SLIDER_PERP_ROW1, joint, 6);
|
|
556
|
+
state[SLIDER_AXIS_MASS] = getRowMass(bodyA, bodyB, state, SLIDER_AXIS_ROW);
|
|
557
|
+
state[SLIDER_PERP_MASS0] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW0);
|
|
558
|
+
state[SLIDER_PERP_MASS1] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW1);
|
|
559
|
+
state[SLIDER_TRANSLATION] = dot(separation, 0, frameABasis, 0);
|
|
560
|
+
state[SLIDER_PERP_ERROR0] = dot(separation, 0, frameABasis, 3);
|
|
561
|
+
state[SLIDER_PERP_ERROR1] = dot(separation, 0, frameABasis, 6);
|
|
562
|
+
prepareAngularBlock(bodyA, bodyB, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS, dt);
|
|
563
|
+
state[SLIDER_LOCK_BIAS] = BAUMGARTE / dt;
|
|
564
|
+
state[SLIDER_LIMIT_BIAS] = 1 / dt;
|
|
565
|
+
state[SLIDER_MAX_MOTOR] = Math.max(0, dt * slider.maxMotorForce);
|
|
566
|
+
slider.lowerLimitImpulse ??= 0;
|
|
567
|
+
slider.upperLimitImpulse ??= 0;
|
|
568
|
+
if (!slider.enableLimit) {
|
|
569
|
+
slider.lowerLimitImpulse = 0;
|
|
570
|
+
slider.upperLimitImpulse = 0;
|
|
571
|
+
}
|
|
572
|
+
state[SLIDER_LOWER_IMPULSE] = slider.lowerLimitImpulse;
|
|
573
|
+
state[SLIDER_UPPER_IMPULSE] = slider.upperLimitImpulse;
|
|
574
|
+
slider.motorImpulse ??= 0;
|
|
575
|
+
if (slider.enableMotor) {
|
|
576
|
+
slider.motorImpulse = clamp(slider.motorImpulse, -state[SLIDER_MAX_MOTOR], state[SLIDER_MAX_MOTOR]);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
slider.motorImpulse = 0;
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
solve(world, joint) {
|
|
583
|
+
const slider = joint;
|
|
584
|
+
const state = getJointSolveState(joint);
|
|
585
|
+
if (state === undefined)
|
|
586
|
+
return;
|
|
587
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
588
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
589
|
+
if (bodyA === null || bodyB === null)
|
|
590
|
+
return;
|
|
591
|
+
if (slider.enableMotor && state[SLIDER_AXIS_MASS] > 0) {
|
|
592
|
+
slider.motorImpulse = solveMotorRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], slider.motorSpeed, state[SLIDER_MAX_MOTOR], slider.motorImpulse);
|
|
593
|
+
}
|
|
594
|
+
if (slider.enableLimit && state[SLIDER_AXIS_MASS] > 0) {
|
|
595
|
+
const translation = state[SLIDER_TRANSLATION];
|
|
596
|
+
solveLowerLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], translation - slider.lowerTranslation, state[SLIDER_LIMIT_BIAS], SLIDER_LOWER_IMPULSE);
|
|
597
|
+
solveUpperLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], slider.upperTranslation - translation, state[SLIDER_LIMIT_BIAS], SLIDER_UPPER_IMPULSE);
|
|
598
|
+
slider.lowerLimitImpulse = state[SLIDER_LOWER_IMPULSE];
|
|
599
|
+
slider.upperLimitImpulse = state[SLIDER_UPPER_IMPULSE];
|
|
600
|
+
}
|
|
601
|
+
solveAngularBlock(bodyA, bodyB, joint, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS);
|
|
602
|
+
solveEqualityRow(bodyA, bodyB, joint, 0, state, SLIDER_PERP_ROW0, state[SLIDER_PERP_MASS0], state[SLIDER_PERP_ERROR0], state[SLIDER_LOCK_BIAS]);
|
|
603
|
+
solveEqualityRow(bodyA, bodyB, joint, 1, state, SLIDER_PERP_ROW1, state[SLIDER_PERP_MASS1], state[SLIDER_PERP_ERROR1], state[SLIDER_LOCK_BIAS]);
|
|
604
|
+
},
|
|
605
|
+
warmStart(world, joint) {
|
|
606
|
+
const slider = joint;
|
|
607
|
+
const state = getJointSolveState(joint);
|
|
608
|
+
if (state === undefined)
|
|
609
|
+
return;
|
|
610
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
611
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
612
|
+
if (bodyA === null || bodyB === null)
|
|
613
|
+
return;
|
|
614
|
+
warmStartAngularBlock(bodyA, bodyB, joint);
|
|
615
|
+
applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW0, joint.impulse0);
|
|
616
|
+
applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW1, joint.impulse1);
|
|
617
|
+
if (slider.enableMotor)
|
|
618
|
+
applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.motorImpulse);
|
|
619
|
+
if (slider.enableLimit) {
|
|
620
|
+
applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.lowerLimitImpulse - slider.upperLimitImpulse);
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
function clamp(value, low, high) {
|
|
625
|
+
return Math.min(Math.max(value, low), high);
|
|
626
|
+
}
|
|
627
|
+
function dot(a, aOffset, b, bOffset) {
|
|
628
|
+
return a[aOffset] * b[bOffset] + a[aOffset + 1] * b[bOffset + 1] + a[aOffset + 2] * b[bOffset + 2];
|
|
629
|
+
}
|
|
630
|
+
// The signed angle from frame A's Y axis to frame B's, measured about frame A's X axis. Exact for any pose,
|
|
631
|
+
// which is why the limits read this rather than the axis component of the rotation error: that component is
|
|
632
|
+
// the true angle only while the two lock rows are converged, so a hinge under load would find its stops
|
|
633
|
+
// drifting with how hard it was being pushed.
|
|
634
|
+
function getSignedAxisAngle(basisA, basisB) {
|
|
635
|
+
const crossX = basisA[4] * basisB[5] - basisA[5] * basisB[4];
|
|
636
|
+
const crossY = basisA[5] * basisB[3] - basisA[3] * basisB[5];
|
|
637
|
+
const crossZ = basisA[3] * basisB[4] - basisA[4] * basisB[3];
|
|
638
|
+
const sine = crossX * basisA[0] + crossY * basisA[1] + crossZ * basisA[2];
|
|
639
|
+
return Math.atan2(sine, dot(basisA, 3, basisB, 3));
|
|
640
|
+
}
|
|
641
|
+
const BAUMGARTE = 0.2;
|
|
642
|
+
const FIXED_ANGULAR_MASS = POINT_LENGTH;
|
|
643
|
+
const FIXED_ANGULAR_BIAS = FIXED_ANGULAR_MASS + 6;
|
|
644
|
+
const FIXED_LENGTH = FIXED_ANGULAR_BIAS + 3;
|
|
645
|
+
const HINGE_AXIS_ROW = POINT_LENGTH;
|
|
646
|
+
const HINGE_LOCK_ROW0 = HINGE_AXIS_ROW + ROW_LENGTH;
|
|
647
|
+
const HINGE_LOCK_ROW1 = HINGE_LOCK_ROW0 + ROW_LENGTH;
|
|
648
|
+
const HINGE_AXIS_MASS = HINGE_LOCK_ROW1 + ROW_LENGTH;
|
|
649
|
+
const HINGE_LOCK_MASS0 = HINGE_AXIS_MASS + 1;
|
|
650
|
+
const HINGE_LOCK_MASS1 = HINGE_LOCK_MASS0 + 1;
|
|
651
|
+
const HINGE_LOCK_ERROR0 = HINGE_LOCK_MASS1 + 1;
|
|
652
|
+
const HINGE_LOCK_ERROR1 = HINGE_LOCK_ERROR0 + 1;
|
|
653
|
+
const HINGE_ANGLE = HINGE_LOCK_ERROR1 + 1;
|
|
654
|
+
const HINGE_LOCK_BIAS = HINGE_ANGLE + 1;
|
|
655
|
+
const HINGE_LIMIT_BIAS = HINGE_LOCK_BIAS + 1;
|
|
656
|
+
const HINGE_MAX_MOTOR = HINGE_LIMIT_BIAS + 1;
|
|
657
|
+
const HINGE_LOWER_IMPULSE = HINGE_MAX_MOTOR + 1;
|
|
658
|
+
const HINGE_UPPER_IMPULSE = HINGE_LOWER_IMPULSE + 1;
|
|
659
|
+
const HINGE_LENGTH = HINGE_UPPER_IMPULSE + 1;
|
|
660
|
+
// The effective cone half-angle in the direction the twist axis has actually tilted. Equal limits give a
|
|
661
|
+
// circular cone; unequal ones give an ellipse, and the blend below is the ellipse in polar form — it returns
|
|
662
|
+
// `swingLimitY` for a tilt straight toward frame A's Y and `swingLimitZ` for one straight toward Z.
|
|
663
|
+
//
|
|
664
|
+
// A non-positive limit is a cone with no opening, and the smaller of the two is the honest reading of it: an
|
|
665
|
+
// ellipse with a zero semi-axis is a line segment, not an ellipse.
|
|
666
|
+
function getConeSwingLimit(cone) {
|
|
667
|
+
if (cone.swingLimitY <= 0 || cone.swingLimitZ <= 0)
|
|
668
|
+
return Math.min(cone.swingLimitY, cone.swingLimitZ);
|
|
669
|
+
const towardY = dot(frameBBasis, 0, frameABasis, 3);
|
|
670
|
+
const towardZ = dot(frameBBasis, 0, frameABasis, 6);
|
|
671
|
+
const tilt = Math.sqrt(towardY * towardY + towardZ * towardZ);
|
|
672
|
+
if (tilt <= AXIS_EPSILON)
|
|
673
|
+
return Math.min(cone.swingLimitY, cone.swingLimitZ);
|
|
674
|
+
const unitY = towardY / tilt / cone.swingLimitY;
|
|
675
|
+
const unitZ = towardZ / tilt / cone.swingLimitZ;
|
|
676
|
+
return 1 / Math.sqrt(unitY * unitY + unitZ * unitZ);
|
|
677
|
+
}
|
|
678
|
+
// Copies a 6-DOF joint's twelve bounds into the two scratch arrays, ordered linear X, Y, Z then angular X, Y,
|
|
679
|
+
// Z — the axis numbering every loop over the six degrees of freedom uses.
|
|
680
|
+
function readDofBounds(dof) {
|
|
681
|
+
dofLower[0] = dof.lowerLinearX;
|
|
682
|
+
dofLower[1] = dof.lowerLinearY;
|
|
683
|
+
dofLower[2] = dof.lowerLinearZ;
|
|
684
|
+
dofLower[3] = dof.lowerAngularX;
|
|
685
|
+
dofLower[4] = dof.lowerAngularY;
|
|
686
|
+
dofLower[5] = dof.lowerAngularZ;
|
|
687
|
+
dofUpper[0] = dof.upperLinearX;
|
|
688
|
+
dofUpper[1] = dof.upperLinearY;
|
|
689
|
+
dofUpper[2] = dof.upperLinearZ;
|
|
690
|
+
dofUpper[3] = dof.upperAngularX;
|
|
691
|
+
dofUpper[4] = dof.upperAngularY;
|
|
692
|
+
dofUpper[5] = dof.upperAngularZ;
|
|
693
|
+
}
|
|
694
|
+
// Writes the row for a linear coordinate measured along one of frame A's axes, reading the separation left by
|
|
695
|
+
// `writePhysics3DJointSeparation`.
|
|
696
|
+
//
|
|
697
|
+
// A's arm is `(rA + separation) x direction`, NOT `rA x direction`. The direction is carried by A and rotates
|
|
698
|
+
// with it, so the coordinate changes when A turns even if neither anchor moves; an arm taken from A's bare
|
|
699
|
+
// lever arm drops that term and leaks motion sideways whenever the rail body rotates.
|
|
700
|
+
function writeLinearAxisRow(state, offset, joint, basisOffset) {
|
|
701
|
+
const directionX = frameABasis[basisOffset];
|
|
702
|
+
const directionY = frameABasis[basisOffset + 1];
|
|
703
|
+
const directionZ = frameABasis[basisOffset + 2];
|
|
704
|
+
const armAX = joint.rAX + separation[0];
|
|
705
|
+
const armAY = joint.rAY + separation[1];
|
|
706
|
+
const armAZ = joint.rAZ + separation[2];
|
|
707
|
+
writeRow(state, offset, directionX, directionY, directionZ, armAY * directionZ - armAZ * directionY, armAZ * directionX - armAX * directionZ, armAX * directionY - armAY * directionX, joint.rBY * directionZ - joint.rBZ * directionY, joint.rBZ * directionX - joint.rBX * directionZ, joint.rBX * directionY - joint.rBY * directionX);
|
|
708
|
+
}
|
|
709
|
+
// Below this the swing axis is degenerate: the two twist axes are parallel, the joint sits at the centre of
|
|
710
|
+
// its cone, and there is no direction for a limit to act along.
|
|
711
|
+
const AXIS_EPSILON = 1e-9;
|
|
712
|
+
const CONE_SWING_ROW = POINT_LENGTH;
|
|
713
|
+
const CONE_TWIST_ROW = CONE_SWING_ROW + ROW_LENGTH;
|
|
714
|
+
const CONE_SWING_MASS = CONE_TWIST_ROW + ROW_LENGTH;
|
|
715
|
+
const CONE_SWING_ERROR = CONE_SWING_MASS + 1;
|
|
716
|
+
const CONE_SWING_ACTIVE = CONE_SWING_ERROR + 1;
|
|
717
|
+
const CONE_TWIST_MASS = CONE_SWING_ACTIVE + 1;
|
|
718
|
+
const CONE_TWIST_ANGLE = CONE_TWIST_MASS + 1;
|
|
719
|
+
const CONE_LIMIT_BIAS = CONE_TWIST_ANGLE + 1;
|
|
720
|
+
const CONE_SWING_IMPULSE = CONE_LIMIT_BIAS + 1;
|
|
721
|
+
const CONE_LOWER_TWIST_IMPULSE = CONE_SWING_IMPULSE + 1;
|
|
722
|
+
const CONE_UPPER_TWIST_IMPULSE = CONE_LOWER_TWIST_IMPULSE + 1;
|
|
723
|
+
const CONE_LENGTH = CONE_UPPER_TWIST_IMPULSE + 1;
|
|
724
|
+
const DOF_FREE = 0;
|
|
725
|
+
const DOF_LOCKED = 1;
|
|
726
|
+
const DOF_LIMITED = 2;
|
|
727
|
+
const DOF_ROWS = 0;
|
|
728
|
+
const DOF_MASS = DOF_ROWS + 6 * ROW_LENGTH;
|
|
729
|
+
const DOF_VALUE = DOF_MASS + 6;
|
|
730
|
+
const DOF_MODE = DOF_VALUE + 6;
|
|
731
|
+
const DOF_LOWER_IMPULSE = DOF_MODE + 6;
|
|
732
|
+
const DOF_UPPER_IMPULSE = DOF_LOWER_IMPULSE + 6;
|
|
733
|
+
const DOF_LOCK_BIAS = DOF_UPPER_IMPULSE + 6;
|
|
734
|
+
const DOF_LIMIT_BIAS = DOF_LOCK_BIAS + 1;
|
|
735
|
+
const DOF_LENGTH = DOF_LIMIT_BIAS + 1;
|
|
736
|
+
const SLIDER_PERP_ROW0 = 0;
|
|
737
|
+
const SLIDER_PERP_ROW1 = SLIDER_PERP_ROW0 + ROW_LENGTH;
|
|
738
|
+
const SLIDER_AXIS_ROW = SLIDER_PERP_ROW1 + ROW_LENGTH;
|
|
739
|
+
const SLIDER_ANGULAR_MASS = SLIDER_AXIS_ROW + ROW_LENGTH;
|
|
740
|
+
const SLIDER_ANGULAR_BIAS = SLIDER_ANGULAR_MASS + 6;
|
|
741
|
+
const SLIDER_PERP_MASS0 = SLIDER_ANGULAR_BIAS + 3;
|
|
742
|
+
const SLIDER_PERP_MASS1 = SLIDER_PERP_MASS0 + 1;
|
|
743
|
+
const SLIDER_PERP_ERROR0 = SLIDER_PERP_MASS1 + 1;
|
|
744
|
+
const SLIDER_PERP_ERROR1 = SLIDER_PERP_ERROR0 + 1;
|
|
745
|
+
const SLIDER_AXIS_MASS = SLIDER_PERP_ERROR1 + 1;
|
|
746
|
+
const SLIDER_TRANSLATION = SLIDER_AXIS_MASS + 1;
|
|
747
|
+
const SLIDER_LOCK_BIAS = SLIDER_TRANSLATION + 1;
|
|
748
|
+
const SLIDER_LIMIT_BIAS = SLIDER_LOCK_BIAS + 1;
|
|
749
|
+
const SLIDER_MAX_MOTOR = SLIDER_LIMIT_BIAS + 1;
|
|
750
|
+
const SLIDER_LOWER_IMPULSE = SLIDER_MAX_MOTOR + 1;
|
|
751
|
+
const SLIDER_UPPER_IMPULSE = SLIDER_LOWER_IMPULSE + 1;
|
|
752
|
+
const SLIDER_LENGTH = SLIDER_UPPER_IMPULSE + 1;
|
|
753
|
+
const dofLower = [0, 0, 0, 0, 0, 0];
|
|
754
|
+
const dofUpper = [0, 0, 0, 0, 0, 0];
|
|
755
|
+
const relativeRotation = [0, 0, 0, 1];
|
|
756
|
+
const rotationError = [0, 0, 0];
|
|
757
|
+
const separation = [0, 0, 0];
|
|
758
|
+
//# sourceMappingURL=joints.js.map
|