@flighthq/physics3d 0.4.0-edge.1908.751021c
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/broadphase.d.ts +4 -0
- package/dist/broadphase.d.ts.map +1 -0
- package/dist/broadphase.js +158 -0
- package/dist/broadphase.js.map +1 -0
- package/dist/colliderCollision.d.ts +5 -0
- package/dist/colliderCollision.d.ts.map +1 -0
- package/dist/colliderCollision.js +55 -0
- package/dist/colliderCollision.js.map +1 -0
- package/dist/colliderTransform.d.ts +5 -0
- package/dist/colliderTransform.d.ts.map +1 -0
- package/dist/colliderTransform.js +424 -0
- package/dist/colliderTransform.js.map +1 -0
- package/dist/contactIntake.d.ts +5 -0
- package/dist/contactIntake.d.ts.map +1 -0
- package/dist/contactIntake.js +237 -0
- package/dist/contactIntake.js.map +1 -0
- package/dist/contacts.d.ts +4 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +64 -0
- package/dist/contacts.js.map +1 -0
- package/dist/continuous.d.ts +5 -0
- package/dist/continuous.d.ts.map +1 -0
- package/dist/continuous.js +829 -0
- package/dist/continuous.js.map +1 -0
- package/dist/contract.d.ts +28 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +28 -0
- package/dist/contract.js.map +1 -0
- package/dist/debugGeometry.d.ts +4 -0
- package/dist/debugGeometry.d.ts.map +1 -0
- package/dist/debugGeometry.js +333 -0
- package/dist/debugGeometry.js.map +1 -0
- package/dist/enablePhysics3DGuards.d.ts +4 -0
- package/dist/enablePhysics3DGuards.d.ts.map +1 -0
- package/dist/enablePhysics3DGuards.js +155 -0
- package/dist/enablePhysics3DGuards.js.map +1 -0
- package/dist/explainPhysics3DCollision.d.ts +3 -0
- package/dist/explainPhysics3DCollision.d.ts.map +1 -0
- package/dist/explainPhysics3DCollision.js +30 -0
- package/dist/explainPhysics3DCollision.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 +51 -0
- package/dist/explainPhysics3DStep.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -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 +184 -0
- package/dist/integrate.js.map +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +333 -0
- package/dist/islands.js.map +1 -0
- package/dist/jointBreakage.d.ts +5 -0
- package/dist/jointBreakage.d.ts.map +1 -0
- package/dist/jointBreakage.js +44 -0
- package/dist/jointBreakage.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 +9 -0
- package/dist/jointFactories.d.ts.map +1 -0
- package/dist/jointFactories.js +159 -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/jointReaction.d.ts +8 -0
- package/dist/jointReaction.d.ts.map +1 -0
- package/dist/jointReaction.js +65 -0
- package/dist/jointReaction.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 +33 -0
- package/dist/jointRows.d.ts.map +1 -0
- package/dist/jointRows.js +352 -0
- package/dist/jointRows.js.map +1 -0
- package/dist/joints.d.ts +16 -0
- package/dist/joints.d.ts.map +1 -0
- package/dist/joints.js +1060 -0
- package/dist/joints.js.map +1 -0
- package/dist/massProperties.d.ts +13 -0
- package/dist/massProperties.d.ts.map +1 -0
- package/dist/massProperties.js +462 -0
- package/dist/massProperties.js.map +1 -0
- package/dist/material.d.ts +3 -0
- package/dist/material.d.ts.map +1 -0
- package/dist/material.js +13 -0
- package/dist/material.js.map +1 -0
- package/dist/nodeSync.d.ts +3 -0
- package/dist/nodeSync.d.ts.map +1 -0
- package/dist/nodeSync.js +16 -0
- package/dist/nodeSync.js.map +1 -0
- package/dist/ownership.d.ts +8 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/ownership.js +26 -0
- package/dist/ownership.js.map +1 -0
- package/dist/physics3DBroadphasePublication.d.ts +5 -0
- package/dist/physics3DBroadphasePublication.d.ts.map +1 -0
- package/dist/physics3DBroadphasePublication.js +49 -0
- package/dist/physics3DBroadphasePublication.js.map +1 -0
- package/dist/physics3DSpatialIndexingGuards.d.ts +4 -0
- package/dist/physics3DSpatialIndexingGuards.d.ts.map +1 -0
- package/dist/physics3DSpatialIndexingGuards.js +11 -0
- package/dist/physics3DSpatialIndexingGuards.js.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.js +15 -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 +460 -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 +302 -0
- package/dist/step.js.map +1 -0
- package/dist/stepValidation.d.ts +13 -0
- package/dist/stepValidation.d.ts.map +1 -0
- package/dist/stepValidation.js +284 -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 +27 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +762 -0
- package/dist/world.js.map +1 -0
- package/dist/worldQueries.d.ts +11 -0
- package/dist/worldQueries.d.ts.map +1 -0
- package/dist/worldQueries.js +409 -0
- package/dist/worldQueries.js.map +1 -0
- package/package.json +55 -0
- package/src/broadphase.test.ts +265 -0
- package/src/colliderCollision.test.ts +57 -0
- package/src/colliderTransform.test.ts +221 -0
- package/src/contactIntake.test.ts +370 -0
- package/src/contacts.test.ts +64 -0
- package/src/continuous.test.ts +679 -0
- package/src/debugGeometry.test.ts +288 -0
- package/src/enablePhysics3DGuards.test.ts +206 -0
- package/src/explainPhysics3DCollision.test.ts +78 -0
- package/src/explainPhysics3DJoints.test.ts +101 -0
- package/src/explainPhysics3DStep.test.ts +142 -0
- package/src/integrate.test.ts +279 -0
- package/src/islands.test.ts +384 -0
- package/src/jointBreakage.test.ts +143 -0
- package/src/jointCollisionSuppression.test.ts +94 -0
- package/src/jointFactories.test.ts +244 -0
- package/src/jointMath.test.ts +646 -0
- package/src/jointReaction.test.ts +241 -0
- package/src/jointRegistry.test.ts +229 -0
- package/src/jointRows.test.ts +548 -0
- package/src/joints.test.ts +1066 -0
- package/src/massProperties.test.ts +611 -0
- package/src/material.test.ts +40 -0
- package/src/nodeSync.test.ts +50 -0
- package/src/ownership.test.ts +61 -0
- package/src/physics3DBroadphasePublication.test.ts +63 -0
- package/src/physics3DSpatialIndexingGuards.test.ts +43 -0
- package/src/registerBuiltInPhysics3DJointSolvers.test.ts +51 -0
- package/src/solver.test.ts +567 -0
- package/src/step.test.ts +490 -0
- package/src/stepValidation.test.ts +346 -0
- package/src/stress.test.ts +526 -0
- package/src/symmetricTensor.test.ts +166 -0
- package/src/world.test.ts +1127 -0
- package/src/worldQueries.test.ts +565 -0
package/dist/joints.js
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import { swapPhysics3DJointFrames, writePhysics3DJointAnchors, writePhysics3DJointRelativeRotation, writePhysics3DJointRotationError, writePhysics3DJointSeparation, } from './jointMath';
|
|
2
|
+
import { accumulatePhysics3DJointRowReaction, clearPhysics3DJointReaction } from './jointReaction';
|
|
3
|
+
import { applyRow, beginJointSolve, clearJointSolve, frameABasis, frameARotation, frameBBasis, frameBRotation, getJointSolveState, getRowMass, getRowVelocity, POINT_LENGTH, prepareAngularBlock, preparePointBlock, readFrameBases, readFrameRotations, ROW_LENGTH, solveAngularBlock, solveEqualityRow, solveLowerLimitRow, solveMotorRow, solvePointBlock, solveUpperLimitRow, writePhysics3DSoftRowParameters, readJointImpulse, warmStartAngularBlock, warmStartPointBlock, writeAngularRow, writeRow, } from './jointRows';
|
|
4
|
+
import { findPhysics3DBody } from './world';
|
|
5
|
+
// The built-in joint kinds. Bare names are reserved for these; a user's own joint takes a vendor prefix, and
|
|
6
|
+
// that convention rather than a registration guard is what keeps the two from colliding.
|
|
7
|
+
export const Physics3DBallAndSocketJointKind = 'BallAndSocket';
|
|
8
|
+
export const Physics3DConeTwistJointKind = 'ConeTwist';
|
|
9
|
+
export const Physics3DDistanceJointKind = 'Distance';
|
|
10
|
+
export const Physics3DFixedJointKind = 'Fixed';
|
|
11
|
+
export const Physics3DGeneric6DofJointKind = 'Generic6Dof';
|
|
12
|
+
export const Physics3DHingeJointKind = 'Hinge';
|
|
13
|
+
export const Physics3DSliderJointKind = 'Slider';
|
|
14
|
+
// Pins two anchors together, leaving rotation free. Three coupled rows and nothing else — the simplest kind,
|
|
15
|
+
// and the one every other kind's point half is the same block of.
|
|
16
|
+
export const physics3DBallAndSocketJointSolver = {
|
|
17
|
+
clearAccumulatedImpulses(joint) {
|
|
18
|
+
joint.impulse0 = 0;
|
|
19
|
+
joint.impulse1 = 0;
|
|
20
|
+
joint.impulse2 = 0;
|
|
21
|
+
},
|
|
22
|
+
prepare(world, joint, dt) {
|
|
23
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
24
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
25
|
+
if (bodyA === null || bodyB === null) {
|
|
26
|
+
clearJointSolve(joint);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
preparePointBlock(bodyA, bodyB, joint, beginJointSolve(joint, POINT_LENGTH), dt);
|
|
30
|
+
},
|
|
31
|
+
// A point block's three rows ARE the world axes acting at the anchor, so its accumulators are already
|
|
32
|
+
// the world-space linear impulse and no row arithmetic is needed. It reports no torque because it
|
|
33
|
+
// constrains no rotation — a zero here is a measurement, not a gap.
|
|
34
|
+
writeReaction(joint, inverseDt, out) {
|
|
35
|
+
if (getJointSolveState(joint) === undefined)
|
|
36
|
+
return false;
|
|
37
|
+
clearPhysics3DJointReaction(out);
|
|
38
|
+
writePointBlockForce(joint, inverseDt, out);
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
solve(world, joint) {
|
|
42
|
+
const state = getJointSolveState(joint);
|
|
43
|
+
if (state === undefined)
|
|
44
|
+
return;
|
|
45
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
46
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
47
|
+
if (bodyA === null || bodyB === null)
|
|
48
|
+
return;
|
|
49
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
50
|
+
},
|
|
51
|
+
warmStart(world, joint) {
|
|
52
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
53
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
54
|
+
if (bodyA === null || bodyB === null)
|
|
55
|
+
return;
|
|
56
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
// Holds two anchors apart along the line joining them. ONE row rather than the point block's three: the
|
|
60
|
+
// separation is a single scalar coordinate, and constraining the other two axes is what makes a joint a
|
|
61
|
+
// ball-and-socket instead.
|
|
62
|
+
//
|
|
63
|
+
// Up to three rows may be live at once — a rest-length row and the two one-sided limits — and they read
|
|
64
|
+
// DIFFERENT effective masses on purpose. The rest row's mass is softened by the spring's compliance so the
|
|
65
|
+
// spring can yield; a limit is a hard stop and reads the unsoftened mass, or a stiff spring would let the
|
|
66
|
+
// cable stretch straight through the bound it exists to enforce.
|
|
67
|
+
export const physics3DDistanceJointSolver = {
|
|
68
|
+
clearAccumulatedImpulses(joint) {
|
|
69
|
+
joint.impulse0 = 0;
|
|
70
|
+
const distance = joint;
|
|
71
|
+
distance.lowerLimitImpulse = 0;
|
|
72
|
+
distance.upperLimitImpulse = 0;
|
|
73
|
+
},
|
|
74
|
+
prepare(world, joint, dt) {
|
|
75
|
+
const distance = joint;
|
|
76
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
77
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
78
|
+
if (bodyA === null || bodyB === null) {
|
|
79
|
+
clearJointSolve(joint);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const state = beginJointSolve(joint, DISTANCE_LENGTH);
|
|
83
|
+
writePhysics3DJointAnchors(bodyA, bodyB, joint);
|
|
84
|
+
writePhysics3DJointSeparation(bodyA, bodyB, joint, distanceAxis);
|
|
85
|
+
const separation = Math.sqrt(distanceAxis[0] * distanceAxis[0] + distanceAxis[1] * distanceAxis[1] + distanceAxis[2] * distanceAxis[2]);
|
|
86
|
+
// Coincident anchors leave the axis undefined — every direction is equally the line between them. Any
|
|
87
|
+
// fixed unit vector is a defensible reading and none is more correct; picking one keeps the row finite,
|
|
88
|
+
// where normalizing by zero would put NaN into both bodies and never leave.
|
|
89
|
+
const axisX = separation > AXIS_EPSILON ? distanceAxis[0] / separation : 1;
|
|
90
|
+
const axisY = separation > AXIS_EPSILON ? distanceAxis[1] / separation : 0;
|
|
91
|
+
const axisZ = separation > AXIS_EPSILON ? distanceAxis[2] / separation : 0;
|
|
92
|
+
// The angular arms are the CROSS products of each lever arm with the axis, not the lever arms
|
|
93
|
+
// themselves: a row's arm is what the inverse inertia tensor is applied to, and the torque an axial
|
|
94
|
+
// impulse exerts about the centre of mass is `r x axis`.
|
|
95
|
+
writeRow(state, DISTANCE_ROW, axisX, axisY, axisZ, joint.rAY * axisZ - joint.rAZ * axisY, joint.rAZ * axisX - joint.rAX * axisZ, joint.rAX * axisY - joint.rAY * axisX, joint.rBY * axisZ - joint.rBZ * axisY, joint.rBZ * axisX - joint.rBX * axisZ, joint.rBX * axisY - joint.rBY * axisX);
|
|
96
|
+
const mass = getRowMass(bodyA, bodyB, state, DISTANCE_ROW);
|
|
97
|
+
state[DISTANCE_SEPARATION] = separation;
|
|
98
|
+
state[DISTANCE_LIMIT_MASS] = mass;
|
|
99
|
+
state[DISTANCE_LIMIT_BIAS] = 1 / dt;
|
|
100
|
+
// The rest-length row yields to the limits when the spring is off, because a fixed length and a slack
|
|
101
|
+
// interval cannot both hold on one axis. With the spring on, both are live and the interval bounds it.
|
|
102
|
+
const restActive = distance.enableSpring || !distance.enableLimit;
|
|
103
|
+
state[DISTANCE_REST_ACTIVE] = restActive ? 1 : 0;
|
|
104
|
+
const error = separation - distance.length;
|
|
105
|
+
if (restActive && distance.enableSpring) {
|
|
106
|
+
// The rest row's hard fallback is `BAUMGARTE / dt`, not the `1 / dt` its LIMIT rows use: this row is
|
|
107
|
+
// two-sided and corrects gently, where a one-sided stop corrects fully.
|
|
108
|
+
writePhysics3DSoftRowParameters(mass, distance.frequencyHz, distance.dampingRatio, dt, BAUMGARTE / dt, softRowScratch);
|
|
109
|
+
state[DISTANCE_MASS] = softRowScratch[0];
|
|
110
|
+
state[DISTANCE_BIAS] = error * softRowScratch[1];
|
|
111
|
+
state[DISTANCE_GAMMA] = softRowScratch[2];
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
state[DISTANCE_GAMMA] = 0;
|
|
115
|
+
state[DISTANCE_BIAS] = error * (BAUMGARTE / dt);
|
|
116
|
+
state[DISTANCE_MASS] = mass;
|
|
117
|
+
}
|
|
118
|
+
distance.lowerLimitImpulse ??= 0;
|
|
119
|
+
distance.upperLimitImpulse ??= 0;
|
|
120
|
+
if (!distance.enableLimit) {
|
|
121
|
+
distance.lowerLimitImpulse = 0;
|
|
122
|
+
distance.upperLimitImpulse = 0;
|
|
123
|
+
}
|
|
124
|
+
state[DISTANCE_LOWER_IMPULSE] = distance.lowerLimitImpulse;
|
|
125
|
+
state[DISTANCE_UPPER_IMPULSE] = distance.upperLimitImpulse;
|
|
126
|
+
},
|
|
127
|
+
solve(world, joint) {
|
|
128
|
+
const distance = joint;
|
|
129
|
+
const state = getJointSolveState(joint);
|
|
130
|
+
if (state === undefined)
|
|
131
|
+
return;
|
|
132
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
133
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
134
|
+
if (bodyA === null || bodyB === null)
|
|
135
|
+
return;
|
|
136
|
+
if (state[DISTANCE_REST_ACTIVE] === 1) {
|
|
137
|
+
// The gamma term is the whole of the softness, and it multiplies the ACCUMULATED impulse rather than
|
|
138
|
+
// this iteration's. A soft constraint is one that gives way in proportion to how hard it has already
|
|
139
|
+
// been pushing, which is a statement about the total, not the increment.
|
|
140
|
+
const velocity = getRowVelocity(bodyA, bodyB, state, DISTANCE_ROW);
|
|
141
|
+
const impulse = -state[DISTANCE_MASS] * (velocity + state[DISTANCE_BIAS] + state[DISTANCE_GAMMA] * joint.impulse0);
|
|
142
|
+
joint.impulse0 += impulse;
|
|
143
|
+
applyRow(bodyA, bodyB, state, DISTANCE_ROW, impulse);
|
|
144
|
+
}
|
|
145
|
+
if (distance.enableLimit) {
|
|
146
|
+
const separation = state[DISTANCE_SEPARATION];
|
|
147
|
+
solveLowerLimitRow(bodyA, bodyB, state, DISTANCE_ROW, state[DISTANCE_LIMIT_MASS], separation - distance.minLength, state[DISTANCE_LIMIT_BIAS], DISTANCE_LOWER_IMPULSE);
|
|
148
|
+
solveUpperLimitRow(bodyA, bodyB, state, DISTANCE_ROW, state[DISTANCE_LIMIT_MASS], distance.maxLength - separation, state[DISTANCE_LIMIT_BIAS], DISTANCE_UPPER_IMPULSE);
|
|
149
|
+
distance.lowerLimitImpulse = state[DISTANCE_LOWER_IMPULSE];
|
|
150
|
+
distance.upperLimitImpulse = state[DISTANCE_UPPER_IMPULSE];
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
// One axial row carrying up to three accumulators, which sum because they act along the SAME direction:
|
|
154
|
+
// the rest-length impulse and the two one-sided limits are all pushes or pulls on the one axis.
|
|
155
|
+
writeReaction(joint, inverseDt, out) {
|
|
156
|
+
const distance = joint;
|
|
157
|
+
const state = getJointSolveState(joint);
|
|
158
|
+
if (state === undefined)
|
|
159
|
+
return false;
|
|
160
|
+
clearPhysics3DJointReaction(out);
|
|
161
|
+
let axial = state[DISTANCE_REST_ACTIVE] === 1 ? joint.impulse0 : 0;
|
|
162
|
+
if (distance.enableLimit)
|
|
163
|
+
axial += (distance.lowerLimitImpulse ?? 0) - (distance.upperLimitImpulse ?? 0);
|
|
164
|
+
accumulatePhysics3DJointRowReaction(joint, state, DISTANCE_ROW, axial * inverseDt, out);
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
// Nothing to reverse. Every quantity this kind carries is an unsigned scalar along a line, and a line has
|
|
168
|
+
// the same length read from either end — unlike a hinge's angles, which are positions on an axis that
|
|
169
|
+
// reverses with the ends. The two limit accumulators are magnitudes of "push apart" and "pull together",
|
|
170
|
+
// and those keep their meaning too, because the row's direction flips with them.
|
|
171
|
+
swapEnds() {
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
174
|
+
warmStart(world, joint) {
|
|
175
|
+
const distance = joint;
|
|
176
|
+
const state = getJointSolveState(joint);
|
|
177
|
+
if (state === undefined)
|
|
178
|
+
return;
|
|
179
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
180
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
181
|
+
if (bodyA === null || bodyB === null)
|
|
182
|
+
return;
|
|
183
|
+
if (state[DISTANCE_REST_ACTIVE] === 1)
|
|
184
|
+
applyRow(bodyA, bodyB, state, DISTANCE_ROW, joint.impulse0);
|
|
185
|
+
if (distance.enableLimit) {
|
|
186
|
+
applyRow(bodyA, bodyB, state, DISTANCE_ROW, distance.lowerLimitImpulse - distance.upperLimitImpulse);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
// Pins two anchors together and locks the relative orientation. The point block plus the angular block, with
|
|
191
|
+
// no parameters of its own: a fixed joint is defined entirely by the pose it was authored in.
|
|
192
|
+
export const physics3DFixedJointSolver = {
|
|
193
|
+
clearAccumulatedImpulses(joint) {
|
|
194
|
+
joint.impulse0 = 0;
|
|
195
|
+
joint.impulse1 = 0;
|
|
196
|
+
joint.impulse2 = 0;
|
|
197
|
+
joint.impulse3 = 0;
|
|
198
|
+
joint.impulse4 = 0;
|
|
199
|
+
joint.impulse5 = 0;
|
|
200
|
+
},
|
|
201
|
+
// The frames travel with their bodies and nothing about them reverses: "these two frames coincide" reads
|
|
202
|
+
// the same from either end. The generic swap moves the bodies and anchors; this moves the frames with them.
|
|
203
|
+
swapEnds(joint) {
|
|
204
|
+
swapPhysics3DJointFrames(joint);
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
207
|
+
prepare(world, joint, dt) {
|
|
208
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
209
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
210
|
+
if (bodyA === null || bodyB === null) {
|
|
211
|
+
clearJointSolve(joint);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const state = beginJointSolve(joint, FIXED_LENGTH);
|
|
215
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
216
|
+
readFrameRotations(bodyA, bodyB, joint);
|
|
217
|
+
prepareAngularBlock(bodyA, bodyB, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS, dt);
|
|
218
|
+
},
|
|
219
|
+
solve(world, joint) {
|
|
220
|
+
const state = getJointSolveState(joint);
|
|
221
|
+
if (state === undefined)
|
|
222
|
+
return;
|
|
223
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
224
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
225
|
+
if (bodyA === null || bodyB === null)
|
|
226
|
+
return;
|
|
227
|
+
solveAngularBlock(bodyA, bodyB, joint, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS);
|
|
228
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
229
|
+
},
|
|
230
|
+
writeReaction(joint, inverseDt, out) {
|
|
231
|
+
if (getJointSolveState(joint) === undefined)
|
|
232
|
+
return false;
|
|
233
|
+
clearPhysics3DJointReaction(out);
|
|
234
|
+
writePointBlockForce(joint, inverseDt, out);
|
|
235
|
+
writeAngularBlockTorque(joint, inverseDt, out);
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
warmStart(world, joint) {
|
|
239
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
240
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
241
|
+
if (bodyA === null || bodyB === null)
|
|
242
|
+
return;
|
|
243
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
244
|
+
warmStartAngularBlock(bodyA, bodyB, joint);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
// Pins two anchors together and confines rotation to the frame's X axis, optionally motorized and limited.
|
|
248
|
+
//
|
|
249
|
+
// The point block, then two angular rows holding frame B's X onto frame A's, then a motor and a pair of
|
|
250
|
+
// limits sharing the axis row. The two lock rows are solved sequentially rather than as a coupled 2x2: they
|
|
251
|
+
// turn about perpendicular axes, so they couple only through an anisotropic inertia tensor, and the residual
|
|
252
|
+
// is what the remaining velocity iterations absorb.
|
|
253
|
+
export const physics3DHingeJointSolver = {
|
|
254
|
+
clearAccumulatedImpulses(joint) {
|
|
255
|
+
joint.impulse0 = 0;
|
|
256
|
+
joint.impulse1 = 0;
|
|
257
|
+
joint.impulse2 = 0;
|
|
258
|
+
joint.impulse3 = 0;
|
|
259
|
+
joint.impulse4 = 0;
|
|
260
|
+
const hinge = joint;
|
|
261
|
+
hinge.motorImpulse = 0;
|
|
262
|
+
hinge.lowerLimitImpulse = 0;
|
|
263
|
+
hinge.upperLimitImpulse = 0;
|
|
264
|
+
},
|
|
265
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
266
|
+
const hinge = joint;
|
|
267
|
+
hinge.motorImpulse = (hinge.motorImpulse ?? 0) * timestepRatio;
|
|
268
|
+
hinge.lowerLimitImpulse = (hinge.lowerLimitImpulse ?? 0) * timestepRatio;
|
|
269
|
+
hinge.upperLimitImpulse = (hinge.upperLimitImpulse ?? 0) * timestepRatio;
|
|
270
|
+
},
|
|
271
|
+
// The angle is measured from frame A's Y axis to frame B's about the shared X axis, so exchanging the ends
|
|
272
|
+
// reverses it. The limit interval negates AND its ends exchange — the old lower bound becomes the new
|
|
273
|
+
// upper — and the motor's target relative velocity reverses with it.
|
|
274
|
+
//
|
|
275
|
+
// Deriving the interval: the constraint is lower <= angle <= upper. After the swap the same physical
|
|
276
|
+
// interval requires lower' = -upper and upper' = -lower.
|
|
277
|
+
swapEnds(joint) {
|
|
278
|
+
const hinge = joint;
|
|
279
|
+
swapPhysics3DJointFrames(hinge);
|
|
280
|
+
const lower = hinge.lowerAngle;
|
|
281
|
+
hinge.lowerAngle = -hinge.upperAngle;
|
|
282
|
+
hinge.upperAngle = -lower;
|
|
283
|
+
hinge.motorSpeed = -hinge.motorSpeed;
|
|
284
|
+
// Defaulted BEFORE the negation. This runs when the joint is added, ahead of the first prepare, so the
|
|
285
|
+
// field may still be absent — and `-undefined` is NaN, which is not nullish, so a later `??` would accept
|
|
286
|
+
// the poison rather than replace it.
|
|
287
|
+
hinge.motorImpulse = -(hinge.motorImpulse ?? 0);
|
|
288
|
+
// Exchanged, NOT negated. Each is a non-negative magnitude whose direction is carried by the row that
|
|
289
|
+
// owns it, so the push that used to hold the lower bound is the one that now holds the upper.
|
|
290
|
+
const lowerImpulse = hinge.lowerLimitImpulse ?? 0;
|
|
291
|
+
hinge.lowerLimitImpulse = hinge.upperLimitImpulse ?? 0;
|
|
292
|
+
hinge.upperLimitImpulse = lowerImpulse;
|
|
293
|
+
return true;
|
|
294
|
+
},
|
|
295
|
+
prepare(world, joint, dt) {
|
|
296
|
+
const hinge = joint;
|
|
297
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
298
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
299
|
+
if (bodyA === null || bodyB === null) {
|
|
300
|
+
clearJointSolve(joint);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const state = beginJointSolve(joint, HINGE_LENGTH);
|
|
304
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
305
|
+
readFrameRotations(bodyA, bodyB, hinge);
|
|
306
|
+
readFrameBases(bodyA, bodyB, hinge);
|
|
307
|
+
writeAngularRow(state, HINGE_AXIS_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
|
|
308
|
+
writeAngularRow(state, HINGE_LOCK_ROW0, frameABasis[3], frameABasis[4], frameABasis[5]);
|
|
309
|
+
writeAngularRow(state, HINGE_LOCK_ROW1, frameABasis[6], frameABasis[7], frameABasis[8]);
|
|
310
|
+
state[HINGE_AXIS_MASS] = getRowMass(bodyA, bodyB, state, HINGE_AXIS_ROW);
|
|
311
|
+
state[HINGE_LOCK_MASS0] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW0);
|
|
312
|
+
state[HINGE_LOCK_MASS1] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW1);
|
|
313
|
+
// The two lock rows' positional error is the misalignment rotation projected onto the axes they turn
|
|
314
|
+
// about. Its third component, about the hinge axis itself, is the one rotation the joint permits and is
|
|
315
|
+
// deliberately not read here — the limits read the exact angle instead.
|
|
316
|
+
writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
|
|
317
|
+
state[HINGE_LOCK_ERROR0] = dot(rotationError, 0, frameABasis, 3);
|
|
318
|
+
state[HINGE_LOCK_ERROR1] = dot(rotationError, 0, frameABasis, 6);
|
|
319
|
+
state[HINGE_ANGLE] = getSignedAxisAngle(frameABasis, frameBBasis);
|
|
320
|
+
state[HINGE_LOCK_BIAS] = BAUMGARTE / dt;
|
|
321
|
+
writeLimitRowParameters(state, HINGE_LIMIT_MASS, HINGE_LIMIT_GAMMA, HINGE_LIMIT_BIAS, state[HINGE_AXIS_MASS], hinge.enableLimitSpring, hinge.limitFrequencyHz, hinge.limitDampingRatio, dt);
|
|
322
|
+
state[HINGE_MAX_MOTOR] = Math.max(0, dt * hinge.maxMotorTorque);
|
|
323
|
+
// Seeded from the carried accumulators rather than zeroed, which is what makes the limit rows warm
|
|
324
|
+
// start. Gated on the CURRENT `enableLimit` for the same reason the motor is: a cached impulse is only
|
|
325
|
+
// valid while the row that produced it is still being solved.
|
|
326
|
+
hinge.lowerLimitImpulse ??= 0;
|
|
327
|
+
hinge.upperLimitImpulse ??= 0;
|
|
328
|
+
if (!hinge.enableLimit) {
|
|
329
|
+
hinge.lowerLimitImpulse = 0;
|
|
330
|
+
hinge.upperLimitImpulse = 0;
|
|
331
|
+
}
|
|
332
|
+
state[HINGE_LOWER_IMPULSE] = hinge.lowerLimitImpulse;
|
|
333
|
+
state[HINGE_UPPER_IMPULSE] = hinge.upperLimitImpulse;
|
|
334
|
+
hinge.motorImpulse ??= 0;
|
|
335
|
+
if (hinge.enableMotor) {
|
|
336
|
+
hinge.motorImpulse = clamp(hinge.motorImpulse, -state[HINGE_MAX_MOTOR], state[HINGE_MAX_MOTOR]);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
hinge.motorImpulse = 0;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
solve(world, joint) {
|
|
343
|
+
const hinge = joint;
|
|
344
|
+
const state = getJointSolveState(joint);
|
|
345
|
+
if (state === undefined)
|
|
346
|
+
return;
|
|
347
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
348
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
349
|
+
if (bodyA === null || bodyB === null)
|
|
350
|
+
return;
|
|
351
|
+
// Motor first: it is a soft target the limits are allowed to override, so solving it before them lets a
|
|
352
|
+
// limit's impulse win within the same iteration when the two disagree.
|
|
353
|
+
if (hinge.enableMotor && state[HINGE_AXIS_MASS] > 0) {
|
|
354
|
+
hinge.motorImpulse = solveMotorRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], hinge.motorSpeed, state[HINGE_MAX_MOTOR], hinge.motorImpulse);
|
|
355
|
+
}
|
|
356
|
+
if (hinge.enableLimit && state[HINGE_AXIS_MASS] > 0) {
|
|
357
|
+
const angle = state[HINGE_ANGLE];
|
|
358
|
+
solveLowerLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_LIMIT_MASS], angle - hinge.lowerAngle, state[HINGE_LIMIT_BIAS], HINGE_LOWER_IMPULSE, state[HINGE_LIMIT_GAMMA]);
|
|
359
|
+
solveUpperLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_LIMIT_MASS], hinge.upperAngle - angle, state[HINGE_LIMIT_BIAS], HINGE_UPPER_IMPULSE, state[HINGE_LIMIT_GAMMA]);
|
|
360
|
+
// Written back every iteration, not once at the end of the step: the solve state is per-sub-interval
|
|
361
|
+
// scratch that `beginJointSolve` reallocates, so the joint field is the only place a value survives to
|
|
362
|
+
// the next one.
|
|
363
|
+
hinge.lowerLimitImpulse = state[HINGE_LOWER_IMPULSE];
|
|
364
|
+
hinge.upperLimitImpulse = state[HINGE_UPPER_IMPULSE];
|
|
365
|
+
}
|
|
366
|
+
solveEqualityRow(bodyA, bodyB, joint, 3, state, HINGE_LOCK_ROW0, state[HINGE_LOCK_MASS0], state[HINGE_LOCK_ERROR0], state[HINGE_LOCK_BIAS]);
|
|
367
|
+
solveEqualityRow(bodyA, bodyB, joint, 4, state, HINGE_LOCK_ROW1, state[HINGE_LOCK_MASS1], state[HINGE_LOCK_ERROR1], state[HINGE_LOCK_BIAS]);
|
|
368
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
369
|
+
},
|
|
370
|
+
// Two lock rows plus the axis row, each read against the accumulator its own `warmStart` reapplies.
|
|
371
|
+
// The motor and the limits share the axis row and therefore add: both are torques about the hinge.
|
|
372
|
+
writeReaction(joint, inverseDt, out) {
|
|
373
|
+
const hinge = joint;
|
|
374
|
+
const state = getJointSolveState(joint);
|
|
375
|
+
if (state === undefined)
|
|
376
|
+
return false;
|
|
377
|
+
clearPhysics3DJointReaction(out);
|
|
378
|
+
writePointBlockForce(joint, inverseDt, out);
|
|
379
|
+
accumulatePhysics3DJointRowReaction(joint, state, HINGE_LOCK_ROW0, joint.impulse3 * inverseDt, out);
|
|
380
|
+
accumulatePhysics3DJointRowReaction(joint, state, HINGE_LOCK_ROW1, joint.impulse4 * inverseDt, out);
|
|
381
|
+
let axial = hinge.enableMotor ? (hinge.motorImpulse ?? 0) : 0;
|
|
382
|
+
if (hinge.enableLimit)
|
|
383
|
+
axial += (hinge.lowerLimitImpulse ?? 0) - (hinge.upperLimitImpulse ?? 0);
|
|
384
|
+
accumulatePhysics3DJointRowReaction(joint, state, HINGE_AXIS_ROW, axial * inverseDt, out);
|
|
385
|
+
return true;
|
|
386
|
+
},
|
|
387
|
+
warmStart(world, joint) {
|
|
388
|
+
const hinge = joint;
|
|
389
|
+
const state = getJointSolveState(joint);
|
|
390
|
+
if (state === undefined)
|
|
391
|
+
return;
|
|
392
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
393
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
394
|
+
if (bodyA === null || bodyB === null)
|
|
395
|
+
return;
|
|
396
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
397
|
+
applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW0, joint.impulse3);
|
|
398
|
+
applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW1, joint.impulse4);
|
|
399
|
+
// Gated on the CURRENT `enableMotor`, not on the value being non-zero. A cached impulse is valid only
|
|
400
|
+
// while the thing that produced it is still running: `solve` skips a disabled motor, so reapplying its
|
|
401
|
+
// last impulse here would exert a torque no solver ever asks for and nothing ever cancels — a disabled
|
|
402
|
+
// motor that keeps turning the hinge forever.
|
|
403
|
+
if (hinge.enableMotor)
|
|
404
|
+
applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.motorImpulse);
|
|
405
|
+
// The lower row pushes along the axis and the upper row against it, which is the sign each carries in
|
|
406
|
+
// `solveLowerLimitRow`/`solveUpperLimitRow`. Reapplying them with the wrong signs would drive the joint
|
|
407
|
+
// toward the stop it is resting against instead of holding it off.
|
|
408
|
+
if (hinge.enableLimit) {
|
|
409
|
+
applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.lowerLimitImpulse - hinge.upperLimitImpulse);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
// Pins two anchors together and bounds relative rotation as a cone plus a twist — the ragdoll joint.
|
|
414
|
+
//
|
|
415
|
+
// The point block, one swing limit, and a pair of twist limits. The swing limit is a single one-sided row
|
|
416
|
+
// about the axis the tilt is happening in, not two per-axis bounds: a cone is not a box, and a shoulder
|
|
417
|
+
// bounded by three independent intervals reaches poses along the diagonal that the anatomy does not have.
|
|
418
|
+
export const physics3DConeTwistJointSolver = {
|
|
419
|
+
clearAccumulatedImpulses(joint) {
|
|
420
|
+
joint.impulse0 = 0;
|
|
421
|
+
joint.impulse1 = 0;
|
|
422
|
+
joint.impulse2 = 0;
|
|
423
|
+
const cone = joint;
|
|
424
|
+
cone.swingLimitImpulse = 0;
|
|
425
|
+
cone.lowerTwistImpulse = 0;
|
|
426
|
+
cone.upperTwistImpulse = 0;
|
|
427
|
+
},
|
|
428
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
429
|
+
const cone = joint;
|
|
430
|
+
cone.swingLimitImpulse = (cone.swingLimitImpulse ?? 0) * timestepRatio;
|
|
431
|
+
cone.lowerTwistImpulse = (cone.lowerTwistImpulse ?? 0) * timestepRatio;
|
|
432
|
+
cone.upperTwistImpulse = (cone.upperTwistImpulse ?? 0) * timestepRatio;
|
|
433
|
+
},
|
|
434
|
+
// Vetoed, and the reason is exact rather than cautious. A kind may exchange its ends only when its own
|
|
435
|
+
// constraint keeps the two frames aligned on the axes its parameters are measured against — a hinge and a
|
|
436
|
+
// slider both lock frame B's X onto frame A's, so their axis survives the exchange. A cone-twist
|
|
437
|
+
// deliberately leaves that alignment FREE within the cone, so after the exchange the cone would be
|
|
438
|
+
// measured against the other body's frame: a different constraint wearing the same numbers.
|
|
439
|
+
swapEnds() {
|
|
440
|
+
return false;
|
|
441
|
+
},
|
|
442
|
+
prepare(world, joint, dt) {
|
|
443
|
+
const cone = joint;
|
|
444
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
445
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
446
|
+
if (bodyA === null || bodyB === null) {
|
|
447
|
+
clearJointSolve(joint);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const state = beginJointSolve(joint, CONE_LENGTH);
|
|
451
|
+
preparePointBlock(bodyA, bodyB, joint, state, dt);
|
|
452
|
+
readFrameRotations(bodyA, bodyB, cone);
|
|
453
|
+
readFrameBases(bodyA, bodyB, cone);
|
|
454
|
+
// The swing axis is perpendicular to both twist axes, so turning B about it is exactly what opens and
|
|
455
|
+
// closes the cone. It vanishes when the two are parallel, which is the joint sitting at the centre of its
|
|
456
|
+
// cone — no direction to limit, and nothing to limit it against.
|
|
457
|
+
const swingX = frameABasis[1] * frameBBasis[2] - frameABasis[2] * frameBBasis[1];
|
|
458
|
+
const swingY = frameABasis[2] * frameBBasis[0] - frameABasis[0] * frameBBasis[2];
|
|
459
|
+
const swingZ = frameABasis[0] * frameBBasis[1] - frameABasis[1] * frameBBasis[0];
|
|
460
|
+
const swingLength = Math.sqrt(swingX * swingX + swingY * swingY + swingZ * swingZ);
|
|
461
|
+
const active = swingLength > AXIS_EPSILON;
|
|
462
|
+
state[CONE_SWING_ACTIVE] = active ? 1 : 0;
|
|
463
|
+
if (active) {
|
|
464
|
+
writeAngularRow(state, CONE_SWING_ROW, swingX / swingLength, swingY / swingLength, swingZ / swingLength);
|
|
465
|
+
state[CONE_SWING_MASS] = getRowMass(bodyA, bodyB, state, CONE_SWING_ROW);
|
|
466
|
+
const swing = Math.acos(clamp(dot(frameABasis, 0, frameBBasis, 0), -1, 1));
|
|
467
|
+
state[CONE_SWING_ERROR] = getConeSwingLimit(cone) - swing;
|
|
468
|
+
}
|
|
469
|
+
writeAngularRow(state, CONE_TWIST_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
|
|
470
|
+
state[CONE_TWIST_MASS] = getRowMass(bodyA, bodyB, state, CONE_TWIST_ROW);
|
|
471
|
+
// Twist is the X component of the relative rotation taken in A's own frame — the rotation left over once
|
|
472
|
+
// the swing is removed. Reading it here rather than from the world-space error vector is what keeps it
|
|
473
|
+
// independent of how far the joint has swung.
|
|
474
|
+
writePhysics3DJointRelativeRotation(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], relativeRotation);
|
|
475
|
+
state[CONE_TWIST_ANGLE] = 2 * Math.atan2(relativeRotation[0], relativeRotation[3]);
|
|
476
|
+
writeLimitRowParameters(state, CONE_SWING_SOFT_MASS, CONE_SWING_GAMMA, CONE_LIMIT_BIAS, state[CONE_SWING_MASS], cone.enableLimitSpring, cone.limitFrequencyHz, cone.limitDampingRatio, dt);
|
|
477
|
+
// The twist group writes the SAME bias slot, which is exact rather than a last-write-wins accident:
|
|
478
|
+
// the soft bias factor is mass-independent, so swing and twist agree on it even though their row
|
|
479
|
+
// masses differ. Only the softened mass and gamma need a slot each.
|
|
480
|
+
writeLimitRowParameters(state, CONE_TWIST_SOFT_MASS, CONE_TWIST_GAMMA, CONE_LIMIT_BIAS, state[CONE_TWIST_MASS], cone.enableLimitSpring, cone.limitFrequencyHz, cone.limitDampingRatio, dt);
|
|
481
|
+
// The swing row's AXIS moves with the tilt direction, so its carried impulse is reapplied along a
|
|
482
|
+
// slightly different axis than the one that earned it — the same approximation contact warm starting
|
|
483
|
+
// makes when a normal rotates, and it converges for the same reason: one iteration corrects it.
|
|
484
|
+
cone.swingLimitImpulse ??= 0;
|
|
485
|
+
cone.lowerTwistImpulse ??= 0;
|
|
486
|
+
cone.upperTwistImpulse ??= 0;
|
|
487
|
+
if (!cone.enableSwingLimit)
|
|
488
|
+
cone.swingLimitImpulse = 0;
|
|
489
|
+
if (!cone.enableTwistLimit) {
|
|
490
|
+
cone.lowerTwistImpulse = 0;
|
|
491
|
+
cone.upperTwistImpulse = 0;
|
|
492
|
+
}
|
|
493
|
+
state[CONE_SWING_IMPULSE] = cone.swingLimitImpulse;
|
|
494
|
+
state[CONE_LOWER_TWIST_IMPULSE] = cone.lowerTwistImpulse;
|
|
495
|
+
state[CONE_UPPER_TWIST_IMPULSE] = cone.upperTwistImpulse;
|
|
496
|
+
},
|
|
497
|
+
solve(world, joint) {
|
|
498
|
+
const cone = joint;
|
|
499
|
+
const state = getJointSolveState(joint);
|
|
500
|
+
if (state === undefined)
|
|
501
|
+
return;
|
|
502
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
503
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
504
|
+
if (bodyA === null || bodyB === null)
|
|
505
|
+
return;
|
|
506
|
+
if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1 && state[CONE_SWING_MASS] > 0) {
|
|
507
|
+
solveUpperLimitRow(bodyA, bodyB, state, CONE_SWING_ROW, state[CONE_SWING_SOFT_MASS], state[CONE_SWING_ERROR], state[CONE_LIMIT_BIAS], CONE_SWING_IMPULSE, state[CONE_SWING_GAMMA]);
|
|
508
|
+
cone.swingLimitImpulse = state[CONE_SWING_IMPULSE];
|
|
509
|
+
}
|
|
510
|
+
if (cone.enableTwistLimit && state[CONE_TWIST_MASS] > 0) {
|
|
511
|
+
const twist = state[CONE_TWIST_ANGLE];
|
|
512
|
+
solveLowerLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_SOFT_MASS], twist - cone.lowerTwistAngle, state[CONE_LIMIT_BIAS], CONE_LOWER_TWIST_IMPULSE, state[CONE_TWIST_GAMMA]);
|
|
513
|
+
solveUpperLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_SOFT_MASS], cone.upperTwistAngle - twist, state[CONE_LIMIT_BIAS], CONE_UPPER_TWIST_IMPULSE, state[CONE_TWIST_GAMMA]);
|
|
514
|
+
cone.lowerTwistImpulse = state[CONE_LOWER_TWIST_IMPULSE];
|
|
515
|
+
cone.upperTwistImpulse = state[CONE_UPPER_TWIST_IMPULSE];
|
|
516
|
+
}
|
|
517
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
518
|
+
},
|
|
519
|
+
// The swing row is read only while it is the ACTIVE side of the cone, matching `warmStart`: outside
|
|
520
|
+
// that the row was never built this sub-interval and its axis is stale, so reading it would attribute a
|
|
521
|
+
// real load to an arbitrary direction.
|
|
522
|
+
writeReaction(joint, inverseDt, out) {
|
|
523
|
+
const cone = joint;
|
|
524
|
+
const state = getJointSolveState(joint);
|
|
525
|
+
if (state === undefined)
|
|
526
|
+
return false;
|
|
527
|
+
clearPhysics3DJointReaction(out);
|
|
528
|
+
writePointBlockForce(joint, inverseDt, out);
|
|
529
|
+
if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1) {
|
|
530
|
+
accumulatePhysics3DJointRowReaction(joint, state, CONE_SWING_ROW, -(cone.swingLimitImpulse ?? 0) * inverseDt, out);
|
|
531
|
+
}
|
|
532
|
+
if (cone.enableTwistLimit) {
|
|
533
|
+
const twist = (cone.lowerTwistImpulse ?? 0) - (cone.upperTwistImpulse ?? 0);
|
|
534
|
+
accumulatePhysics3DJointRowReaction(joint, state, CONE_TWIST_ROW, twist * inverseDt, out);
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
},
|
|
538
|
+
warmStart(world, joint) {
|
|
539
|
+
const cone = joint;
|
|
540
|
+
const state = getJointSolveState(joint);
|
|
541
|
+
if (state === undefined)
|
|
542
|
+
return;
|
|
543
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
544
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
545
|
+
if (bodyA === null || bodyB === null)
|
|
546
|
+
return;
|
|
547
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
548
|
+
// The swing row is reapplied only while it is the ACTIVE side of the cone. Outside that, the row it was
|
|
549
|
+
// measured against does not exist this sub-interval and its axis is stale.
|
|
550
|
+
if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1) {
|
|
551
|
+
applyRow(bodyA, bodyB, state, CONE_SWING_ROW, -cone.swingLimitImpulse);
|
|
552
|
+
}
|
|
553
|
+
if (cone.enableTwistLimit) {
|
|
554
|
+
applyRow(bodyA, bodyB, state, CONE_TWIST_ROW, cone.lowerTwistImpulse - cone.upperTwistImpulse);
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
// Bounds each of the six degrees of freedom independently — the configurable joint every other kind is a
|
|
559
|
+
// preset of.
|
|
560
|
+
//
|
|
561
|
+
// Each axis reads its state from its bounds rather than from a mode flag: `lower > upper` is free,
|
|
562
|
+
// `lower === upper` is locked, and anything else is limited. A separate flag would be a second source that
|
|
563
|
+
// can disagree with the numbers it describes.
|
|
564
|
+
export const physics3DGeneric6DofJointSolver = {
|
|
565
|
+
clearAccumulatedImpulses(joint) {
|
|
566
|
+
const dof = joint;
|
|
567
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
568
|
+
dof.lowerLimitImpulses[axis] = 0;
|
|
569
|
+
dof.upperLimitImpulses[axis] = 0;
|
|
570
|
+
}
|
|
571
|
+
joint.impulse0 = 0;
|
|
572
|
+
joint.impulse1 = 0;
|
|
573
|
+
joint.impulse2 = 0;
|
|
574
|
+
joint.impulse3 = 0;
|
|
575
|
+
joint.impulse4 = 0;
|
|
576
|
+
joint.impulse5 = 0;
|
|
577
|
+
},
|
|
578
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
579
|
+
const dof = joint;
|
|
580
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
581
|
+
dof.lowerLimitImpulses[axis] *= timestepRatio;
|
|
582
|
+
dof.upperLimitImpulses[axis] *= timestepRatio;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
// Vetoed for the same reason as a cone-twist, and for a kind whose entire purpose is per-axis freedom the
|
|
586
|
+
// reason is unavoidable: any axis left free lets the two frames diverge about it, so the bounds — which are
|
|
587
|
+
// measured against frame A — would after the exchange be measured against a frame the joint never held
|
|
588
|
+
// still. An all-locked 6-DOF could be exchanged safely, but a kind cannot answer this per instance without
|
|
589
|
+
// its ends already meaning two different things depending on how it was configured.
|
|
590
|
+
swapEnds() {
|
|
591
|
+
return false;
|
|
592
|
+
},
|
|
593
|
+
prepare(world, joint, dt) {
|
|
594
|
+
const dof = joint;
|
|
595
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
596
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
597
|
+
if (bodyA === null || bodyB === null) {
|
|
598
|
+
clearJointSolve(joint);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const state = beginJointSolve(joint, DOF_LENGTH);
|
|
602
|
+
writePhysics3DJointAnchors(bodyA, bodyB, joint);
|
|
603
|
+
writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
|
|
604
|
+
readFrameRotations(bodyA, bodyB, dof);
|
|
605
|
+
readFrameBases(bodyA, bodyB, dof);
|
|
606
|
+
writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
|
|
607
|
+
readDofBounds(dof);
|
|
608
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
609
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
610
|
+
const basisOffset = (axis % 3) * 3;
|
|
611
|
+
const lower = dofLower[axis];
|
|
612
|
+
const upper = dofUpper[axis];
|
|
613
|
+
state[DOF_MODE + axis] = lower > upper ? DOF_FREE : lower === upper ? DOF_LOCKED : DOF_LIMITED;
|
|
614
|
+
// Carried only while the axis is still LIMITED. An axis whose bounds changed it to free or locked has
|
|
615
|
+
// an accumulator describing a constraint that no longer exists, and reapplying it would push against a
|
|
616
|
+
// bound the joint may no longer be anywhere near.
|
|
617
|
+
if (state[DOF_MODE + axis] !== DOF_LIMITED) {
|
|
618
|
+
dof.lowerLimitImpulses[axis] = 0;
|
|
619
|
+
dof.upperLimitImpulses[axis] = 0;
|
|
620
|
+
}
|
|
621
|
+
state[DOF_LOWER_IMPULSE + axis] = dof.lowerLimitImpulses[axis];
|
|
622
|
+
state[DOF_UPPER_IMPULSE + axis] = dof.upperLimitImpulses[axis];
|
|
623
|
+
if (state[DOF_MODE + axis] === DOF_FREE)
|
|
624
|
+
continue;
|
|
625
|
+
if (axis < 3) {
|
|
626
|
+
writeLinearAxisRow(state, rowOffset, joint, basisOffset);
|
|
627
|
+
state[DOF_VALUE + axis] = dot(separation, 0, frameABasis, basisOffset);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
writeAngularRow(state, rowOffset, frameABasis[basisOffset], frameABasis[basisOffset + 1], frameABasis[basisOffset + 2]);
|
|
631
|
+
state[DOF_VALUE + axis] = dot(rotationError, 0, frameABasis, basisOffset);
|
|
632
|
+
}
|
|
633
|
+
state[DOF_MASS + axis] = getRowMass(bodyA, bodyB, state, rowOffset);
|
|
634
|
+
}
|
|
635
|
+
state[DOF_LOCK_BIAS] = BAUMGARTE / dt;
|
|
636
|
+
// Per-axis mass and gamma, one shared bias. The six rows have six different masses, but the soft bias
|
|
637
|
+
// factor is mass-independent, so every LIMITED axis writes the same value into the one slot.
|
|
638
|
+
//
|
|
639
|
+
// Only limited axes, and that is load-bearing rather than an optimization. A free axis never had its
|
|
640
|
+
// row mass written — the loop above skips it — so its slot holds whatever was there before, and on a
|
|
641
|
+
// freshly allocated state array that is `undefined`. Feeding it to the spring produces NaN, and
|
|
642
|
+
// because the BIAS slot is shared, one free axis poisons the stop on every other axis. A locked axis
|
|
643
|
+
// is excluded for a different reason: softening a lock would make it a spring, which its mode says it
|
|
644
|
+
// is not.
|
|
645
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
646
|
+
if (state[DOF_MODE + axis] !== DOF_LIMITED)
|
|
647
|
+
continue;
|
|
648
|
+
writeLimitRowParameters(state, DOF_LIMIT_MASS + axis, DOF_LIMIT_GAMMA + axis, DOF_LIMIT_BIAS, state[DOF_MASS + axis], dof.enableLimitSpring, dof.limitFrequencyHz, dof.limitDampingRatio, dt);
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
solve(world, joint) {
|
|
652
|
+
const dof = joint;
|
|
653
|
+
const state = getJointSolveState(joint);
|
|
654
|
+
if (state === undefined)
|
|
655
|
+
return;
|
|
656
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
657
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
658
|
+
if (bodyA === null || bodyB === null)
|
|
659
|
+
return;
|
|
660
|
+
readDofBounds(dof);
|
|
661
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
662
|
+
const mode = state[DOF_MODE + axis];
|
|
663
|
+
if (mode === DOF_FREE || state[DOF_MASS + axis] <= 0)
|
|
664
|
+
continue;
|
|
665
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
666
|
+
const value = state[DOF_VALUE + axis];
|
|
667
|
+
if (mode === DOF_LOCKED) {
|
|
668
|
+
solveEqualityRow(bodyA, bodyB, joint, axis, state, rowOffset, state[DOF_MASS + axis], value - dofLower[axis], state[DOF_LOCK_BIAS]);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
solveLowerLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_LIMIT_MASS + axis], value - dofLower[axis], state[DOF_LIMIT_BIAS], DOF_LOWER_IMPULSE + axis, state[DOF_LIMIT_GAMMA + axis]);
|
|
672
|
+
solveUpperLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_LIMIT_MASS + axis], dofUpper[axis] - value, state[DOF_LIMIT_BIAS], DOF_UPPER_IMPULSE + axis, state[DOF_LIMIT_GAMMA + axis]);
|
|
673
|
+
dof.lowerLimitImpulses[axis] = state[DOF_LOWER_IMPULSE + axis];
|
|
674
|
+
dof.upperLimitImpulses[axis] = state[DOF_UPPER_IMPULSE + axis];
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
// Locked axes reapply the equality block's slot; limited axes reapply their own pair of one-sided
|
|
678
|
+
// accumulators. The two come from different places because a locked axis is one signed row while a limited
|
|
679
|
+
// axis is two rows that may only push, and neither can be read out of the other's storage.
|
|
680
|
+
// Six rows, each read against the accumulator its MODE selects — a free axis carries nothing, a locked
|
|
681
|
+
// one its equality impulse, a limited one the difference of its two stops. The first three rows are
|
|
682
|
+
// linear and the last three angular, and the row arithmetic sorts that out without a special case.
|
|
683
|
+
writeReaction(joint, inverseDt, out) {
|
|
684
|
+
const dof = joint;
|
|
685
|
+
const state = getJointSolveState(joint);
|
|
686
|
+
if (state === undefined)
|
|
687
|
+
return false;
|
|
688
|
+
clearPhysics3DJointReaction(out);
|
|
689
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
690
|
+
const mode = state[DOF_MODE + axis];
|
|
691
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
692
|
+
if (mode === DOF_LOCKED) {
|
|
693
|
+
accumulatePhysics3DJointRowReaction(joint, state, rowOffset, readJointImpulse(joint, axis) * inverseDt, out);
|
|
694
|
+
}
|
|
695
|
+
else if (mode === DOF_LIMITED) {
|
|
696
|
+
const limited = dof.lowerLimitImpulses[axis] - dof.upperLimitImpulses[axis];
|
|
697
|
+
accumulatePhysics3DJointRowReaction(joint, state, rowOffset, limited * inverseDt, out);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
},
|
|
702
|
+
warmStart(world, joint) {
|
|
703
|
+
const dof = joint;
|
|
704
|
+
const state = getJointSolveState(joint);
|
|
705
|
+
if (state === undefined)
|
|
706
|
+
return;
|
|
707
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
708
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
709
|
+
if (bodyA === null || bodyB === null)
|
|
710
|
+
return;
|
|
711
|
+
for (let axis = 0; axis < 6; axis += 1) {
|
|
712
|
+
const mode = state[DOF_MODE + axis];
|
|
713
|
+
const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
|
|
714
|
+
if (mode === DOF_LOCKED) {
|
|
715
|
+
applyRow(bodyA, bodyB, state, rowOffset, readJointImpulse(joint, axis));
|
|
716
|
+
}
|
|
717
|
+
else if (mode === DOF_LIMITED) {
|
|
718
|
+
applyRow(bodyA, bodyB, state, rowOffset, dof.lowerLimitImpulses[axis] - dof.upperLimitImpulses[axis]);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
// Confines relative motion to translation along the frame's X axis, optionally motorized and travel-limited.
|
|
724
|
+
//
|
|
725
|
+
// Two perpendicular linear rows hold the anchors on the rail, three angular rows lock the orientation, and
|
|
726
|
+
// the axis row carries the motor and the limits. The two perpendicular rows are solved sequentially rather
|
|
727
|
+
// than as a coupled 2x2: their linear directions are orthogonal, so they couple only through an anisotropic
|
|
728
|
+
// inertia tensor, and the residual is what the remaining velocity iterations absorb.
|
|
729
|
+
export const physics3DSliderJointSolver = {
|
|
730
|
+
clearAccumulatedImpulses(joint) {
|
|
731
|
+
joint.impulse0 = 0;
|
|
732
|
+
joint.impulse1 = 0;
|
|
733
|
+
joint.impulse3 = 0;
|
|
734
|
+
joint.impulse4 = 0;
|
|
735
|
+
joint.impulse5 = 0;
|
|
736
|
+
const slider = joint;
|
|
737
|
+
slider.motorImpulse = 0;
|
|
738
|
+
slider.lowerLimitImpulse = 0;
|
|
739
|
+
slider.upperLimitImpulse = 0;
|
|
740
|
+
},
|
|
741
|
+
scaleAccumulatedImpulses(joint, timestepRatio) {
|
|
742
|
+
const slider = joint;
|
|
743
|
+
slider.motorImpulse = (slider.motorImpulse ?? 0) * timestepRatio;
|
|
744
|
+
slider.lowerLimitImpulse = (slider.lowerLimitImpulse ?? 0) * timestepRatio;
|
|
745
|
+
slider.upperLimitImpulse = (slider.upperLimitImpulse ?? 0) * timestepRatio;
|
|
746
|
+
},
|
|
747
|
+
// The translation is measured from A's anchor to B's along the shared axis, so exchanging the ends reverses
|
|
748
|
+
// it: the interval negates AND its ends exchange, and the motor's target velocity reverses with them. The
|
|
749
|
+
// exchange is exact because the slider's own angular lock holds frame B's X onto frame A's, so the axis the
|
|
750
|
+
// bounds are measured against is the same line from either end.
|
|
751
|
+
swapEnds(joint) {
|
|
752
|
+
const slider = joint;
|
|
753
|
+
swapPhysics3DJointFrames(slider);
|
|
754
|
+
const lower = slider.lowerTranslation;
|
|
755
|
+
slider.lowerTranslation = -slider.upperTranslation;
|
|
756
|
+
slider.upperTranslation = -lower;
|
|
757
|
+
slider.motorSpeed = -slider.motorSpeed;
|
|
758
|
+
slider.motorImpulse = -(slider.motorImpulse ?? 0);
|
|
759
|
+
// Exchanged, not negated — see the hinge's swapEnds for why a magnitude behaves differently here from
|
|
760
|
+
// the bound it holds.
|
|
761
|
+
const lowerImpulse = slider.lowerLimitImpulse ?? 0;
|
|
762
|
+
slider.lowerLimitImpulse = slider.upperLimitImpulse ?? 0;
|
|
763
|
+
slider.upperLimitImpulse = lowerImpulse;
|
|
764
|
+
return true;
|
|
765
|
+
},
|
|
766
|
+
prepare(world, joint, dt) {
|
|
767
|
+
const slider = joint;
|
|
768
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
769
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
770
|
+
if (bodyA === null || bodyB === null) {
|
|
771
|
+
clearJointSolve(joint);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const state = beginJointSolve(joint, SLIDER_LENGTH);
|
|
775
|
+
writePhysics3DJointAnchors(bodyA, bodyB, joint);
|
|
776
|
+
writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
|
|
777
|
+
readFrameRotations(bodyA, bodyB, slider);
|
|
778
|
+
readFrameBases(bodyA, bodyB, slider);
|
|
779
|
+
writeLinearAxisRow(state, SLIDER_AXIS_ROW, joint, 0);
|
|
780
|
+
writeLinearAxisRow(state, SLIDER_PERP_ROW0, joint, 3);
|
|
781
|
+
writeLinearAxisRow(state, SLIDER_PERP_ROW1, joint, 6);
|
|
782
|
+
state[SLIDER_AXIS_MASS] = getRowMass(bodyA, bodyB, state, SLIDER_AXIS_ROW);
|
|
783
|
+
state[SLIDER_PERP_MASS0] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW0);
|
|
784
|
+
state[SLIDER_PERP_MASS1] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW1);
|
|
785
|
+
state[SLIDER_TRANSLATION] = dot(separation, 0, frameABasis, 0);
|
|
786
|
+
state[SLIDER_PERP_ERROR0] = dot(separation, 0, frameABasis, 3);
|
|
787
|
+
state[SLIDER_PERP_ERROR1] = dot(separation, 0, frameABasis, 6);
|
|
788
|
+
prepareAngularBlock(bodyA, bodyB, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS, dt);
|
|
789
|
+
state[SLIDER_LOCK_BIAS] = BAUMGARTE / dt;
|
|
790
|
+
writeLimitRowParameters(state, SLIDER_LIMIT_MASS, SLIDER_LIMIT_GAMMA, SLIDER_LIMIT_BIAS, state[SLIDER_AXIS_MASS], slider.enableLimitSpring, slider.limitFrequencyHz, slider.limitDampingRatio, dt);
|
|
791
|
+
state[SLIDER_MAX_MOTOR] = Math.max(0, dt * slider.maxMotorForce);
|
|
792
|
+
slider.lowerLimitImpulse ??= 0;
|
|
793
|
+
slider.upperLimitImpulse ??= 0;
|
|
794
|
+
if (!slider.enableLimit) {
|
|
795
|
+
slider.lowerLimitImpulse = 0;
|
|
796
|
+
slider.upperLimitImpulse = 0;
|
|
797
|
+
}
|
|
798
|
+
state[SLIDER_LOWER_IMPULSE] = slider.lowerLimitImpulse;
|
|
799
|
+
state[SLIDER_UPPER_IMPULSE] = slider.upperLimitImpulse;
|
|
800
|
+
slider.motorImpulse ??= 0;
|
|
801
|
+
if (slider.enableMotor) {
|
|
802
|
+
slider.motorImpulse = clamp(slider.motorImpulse, -state[SLIDER_MAX_MOTOR], state[SLIDER_MAX_MOTOR]);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
slider.motorImpulse = 0;
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
solve(world, joint) {
|
|
809
|
+
const slider = joint;
|
|
810
|
+
const state = getJointSolveState(joint);
|
|
811
|
+
if (state === undefined)
|
|
812
|
+
return;
|
|
813
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
814
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
815
|
+
if (bodyA === null || bodyB === null)
|
|
816
|
+
return;
|
|
817
|
+
if (slider.enableMotor && state[SLIDER_AXIS_MASS] > 0) {
|
|
818
|
+
slider.motorImpulse = solveMotorRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], slider.motorSpeed, state[SLIDER_MAX_MOTOR], slider.motorImpulse);
|
|
819
|
+
}
|
|
820
|
+
if (slider.enableLimit && state[SLIDER_AXIS_MASS] > 0) {
|
|
821
|
+
const translation = state[SLIDER_TRANSLATION];
|
|
822
|
+
solveLowerLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_LIMIT_MASS], translation - slider.lowerTranslation, state[SLIDER_LIMIT_BIAS], SLIDER_LOWER_IMPULSE, state[SLIDER_LIMIT_GAMMA]);
|
|
823
|
+
solveUpperLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_LIMIT_MASS], slider.upperTranslation - translation, state[SLIDER_LIMIT_BIAS], SLIDER_UPPER_IMPULSE, state[SLIDER_LIMIT_GAMMA]);
|
|
824
|
+
slider.lowerLimitImpulse = state[SLIDER_LOWER_IMPULSE];
|
|
825
|
+
slider.upperLimitImpulse = state[SLIDER_UPPER_IMPULSE];
|
|
826
|
+
}
|
|
827
|
+
solveAngularBlock(bodyA, bodyB, joint, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS);
|
|
828
|
+
solveEqualityRow(bodyA, bodyB, joint, 0, state, SLIDER_PERP_ROW0, state[SLIDER_PERP_MASS0], state[SLIDER_PERP_ERROR0], state[SLIDER_LOCK_BIAS]);
|
|
829
|
+
solveEqualityRow(bodyA, bodyB, joint, 1, state, SLIDER_PERP_ROW1, state[SLIDER_PERP_MASS1], state[SLIDER_PERP_ERROR1], state[SLIDER_LOCK_BIAS]);
|
|
830
|
+
},
|
|
831
|
+
// Two perpendicular LINEAR rows carry the load across the rail, and the axis row carries the motor and
|
|
832
|
+
// the stops ALONG it. The angular block is the rotation lock, read as a world-axis triple.
|
|
833
|
+
writeReaction(joint, inverseDt, out) {
|
|
834
|
+
const slider = joint;
|
|
835
|
+
const state = getJointSolveState(joint);
|
|
836
|
+
if (state === undefined)
|
|
837
|
+
return false;
|
|
838
|
+
clearPhysics3DJointReaction(out);
|
|
839
|
+
accumulatePhysics3DJointRowReaction(joint, state, SLIDER_PERP_ROW0, joint.impulse0 * inverseDt, out);
|
|
840
|
+
accumulatePhysics3DJointRowReaction(joint, state, SLIDER_PERP_ROW1, joint.impulse1 * inverseDt, out);
|
|
841
|
+
let axial = slider.enableMotor ? (slider.motorImpulse ?? 0) : 0;
|
|
842
|
+
if (slider.enableLimit)
|
|
843
|
+
axial += (slider.lowerLimitImpulse ?? 0) - (slider.upperLimitImpulse ?? 0);
|
|
844
|
+
accumulatePhysics3DJointRowReaction(joint, state, SLIDER_AXIS_ROW, axial * inverseDt, out);
|
|
845
|
+
writeAngularBlockTorque(joint, inverseDt, out);
|
|
846
|
+
return true;
|
|
847
|
+
},
|
|
848
|
+
warmStart(world, joint) {
|
|
849
|
+
const slider = joint;
|
|
850
|
+
const state = getJointSolveState(joint);
|
|
851
|
+
if (state === undefined)
|
|
852
|
+
return;
|
|
853
|
+
const bodyA = findPhysics3DBody(world, joint.bodyA);
|
|
854
|
+
const bodyB = findPhysics3DBody(world, joint.bodyB);
|
|
855
|
+
if (bodyA === null || bodyB === null)
|
|
856
|
+
return;
|
|
857
|
+
warmStartAngularBlock(bodyA, bodyB, joint);
|
|
858
|
+
applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW0, joint.impulse0);
|
|
859
|
+
applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW1, joint.impulse1);
|
|
860
|
+
if (slider.enableMotor)
|
|
861
|
+
applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.motorImpulse);
|
|
862
|
+
if (slider.enableLimit) {
|
|
863
|
+
applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.lowerLimitImpulse - slider.upperLimitImpulse);
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
function clamp(value, low, high) {
|
|
868
|
+
return Math.min(Math.max(value, low), high);
|
|
869
|
+
}
|
|
870
|
+
function dot(a, aOffset, b, bOffset) {
|
|
871
|
+
return a[aOffset] * b[bOffset] + a[aOffset + 1] * b[bOffset + 1] + a[aOffset + 2] * b[bOffset + 2];
|
|
872
|
+
}
|
|
873
|
+
// The signed angle from frame A's Y axis to frame B's, measured about frame A's X axis. Exact for any pose,
|
|
874
|
+
// which is why the limits read this rather than the axis component of the rotation error: that component is
|
|
875
|
+
// the true angle only while the two lock rows are converged, so a hinge under load would find its stops
|
|
876
|
+
// drifting with how hard it was being pushed.
|
|
877
|
+
function getSignedAxisAngle(basisA, basisB) {
|
|
878
|
+
const crossX = basisA[4] * basisB[5] - basisA[5] * basisB[4];
|
|
879
|
+
const crossY = basisA[5] * basisB[3] - basisA[3] * basisB[5];
|
|
880
|
+
const crossZ = basisA[3] * basisB[4] - basisA[4] * basisB[3];
|
|
881
|
+
const sine = crossX * basisA[0] + crossY * basisA[1] + crossZ * basisA[2];
|
|
882
|
+
return Math.atan2(sine, dot(basisA, 3, basisB, 3));
|
|
883
|
+
}
|
|
884
|
+
// The linear force a point block carried. Its accumulators are world-axis impulses, so this is a scale
|
|
885
|
+
// rather than a projection.
|
|
886
|
+
function writePointBlockForce(joint, inverseDt, out) {
|
|
887
|
+
out.forceX += joint.impulse0 * inverseDt;
|
|
888
|
+
out.forceY += joint.impulse1 * inverseDt;
|
|
889
|
+
out.forceZ += joint.impulse2 * inverseDt;
|
|
890
|
+
}
|
|
891
|
+
// The torque an angular block carried. Like the point block, its three accumulators are already world-axis
|
|
892
|
+
// impulses — the block is built from the summed inverse inertia tensors rather than from named rows.
|
|
893
|
+
function writeAngularBlockTorque(joint, inverseDt, out) {
|
|
894
|
+
out.torqueX += joint.impulse3 * inverseDt;
|
|
895
|
+
out.torqueY += joint.impulse4 * inverseDt;
|
|
896
|
+
out.torqueZ += joint.impulse5 * inverseDt;
|
|
897
|
+
}
|
|
898
|
+
// Fills a limit row group's mass, bias, and gamma slots, choosing a hard stop or a compliant one.
|
|
899
|
+
//
|
|
900
|
+
// One BIAS slot serves every row of a joint even when the rows have different masses, and that is exact
|
|
901
|
+
// rather than an approximation: the soft bias factor works out to `w^2 / (2*z*w + dt*w^2)`, in which the
|
|
902
|
+
// mass cancels completely. Only the softened mass and gamma carry it, which is why those get a slot per
|
|
903
|
+
// row and the bias does not.
|
|
904
|
+
//
|
|
905
|
+
// The hard fallback is `1 / dt`, the limit rows' own full-correction factor, NOT the `BAUMGARTE / dt` a
|
|
906
|
+
// two-sided row uses. Passing the wrong one would leave hard stops behaving differently than before the
|
|
907
|
+
// spring existed.
|
|
908
|
+
function writeLimitRowParameters(state, massSlot, gammaSlot, biasSlot, rowMass, soft, frequencyHz, dampingRatio, dt) {
|
|
909
|
+
if (soft && frequencyHz > 0) {
|
|
910
|
+
writePhysics3DSoftRowParameters(rowMass, frequencyHz, dampingRatio, dt, 1 / dt, softRowScratch);
|
|
911
|
+
state[massSlot] = softRowScratch[0];
|
|
912
|
+
state[biasSlot] = softRowScratch[1];
|
|
913
|
+
state[gammaSlot] = softRowScratch[2];
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
state[massSlot] = rowMass;
|
|
917
|
+
state[biasSlot] = 1 / dt;
|
|
918
|
+
state[gammaSlot] = 0;
|
|
919
|
+
}
|
|
920
|
+
const BAUMGARTE = 0.2;
|
|
921
|
+
const softRowScratch = [0, 0, 0];
|
|
922
|
+
const DISTANCE_ROW = 0;
|
|
923
|
+
const DISTANCE_MASS = DISTANCE_ROW + ROW_LENGTH;
|
|
924
|
+
const DISTANCE_BIAS = DISTANCE_MASS + 1;
|
|
925
|
+
const DISTANCE_GAMMA = DISTANCE_BIAS + 1;
|
|
926
|
+
const DISTANCE_LIMIT_MASS = DISTANCE_GAMMA + 1;
|
|
927
|
+
const DISTANCE_LIMIT_BIAS = DISTANCE_LIMIT_MASS + 1;
|
|
928
|
+
const DISTANCE_SEPARATION = DISTANCE_LIMIT_BIAS + 1;
|
|
929
|
+
const DISTANCE_REST_ACTIVE = DISTANCE_SEPARATION + 1;
|
|
930
|
+
const DISTANCE_LOWER_IMPULSE = DISTANCE_REST_ACTIVE + 1;
|
|
931
|
+
const DISTANCE_UPPER_IMPULSE = DISTANCE_LOWER_IMPULSE + 1;
|
|
932
|
+
const DISTANCE_LENGTH = DISTANCE_UPPER_IMPULSE + 1;
|
|
933
|
+
const distanceAxis = [0, 0, 0];
|
|
934
|
+
const FIXED_ANGULAR_MASS = POINT_LENGTH;
|
|
935
|
+
const FIXED_ANGULAR_BIAS = FIXED_ANGULAR_MASS + 6;
|
|
936
|
+
const FIXED_LENGTH = FIXED_ANGULAR_BIAS + 3;
|
|
937
|
+
const HINGE_AXIS_ROW = POINT_LENGTH;
|
|
938
|
+
const HINGE_LOCK_ROW0 = HINGE_AXIS_ROW + ROW_LENGTH;
|
|
939
|
+
const HINGE_LOCK_ROW1 = HINGE_LOCK_ROW0 + ROW_LENGTH;
|
|
940
|
+
const HINGE_AXIS_MASS = HINGE_LOCK_ROW1 + ROW_LENGTH;
|
|
941
|
+
const HINGE_LOCK_MASS0 = HINGE_AXIS_MASS + 1;
|
|
942
|
+
const HINGE_LOCK_MASS1 = HINGE_LOCK_MASS0 + 1;
|
|
943
|
+
const HINGE_LOCK_ERROR0 = HINGE_LOCK_MASS1 + 1;
|
|
944
|
+
const HINGE_LOCK_ERROR1 = HINGE_LOCK_ERROR0 + 1;
|
|
945
|
+
const HINGE_ANGLE = HINGE_LOCK_ERROR1 + 1;
|
|
946
|
+
const HINGE_LOCK_BIAS = HINGE_ANGLE + 1;
|
|
947
|
+
const HINGE_LIMIT_BIAS = HINGE_LOCK_BIAS + 1;
|
|
948
|
+
const HINGE_MAX_MOTOR = HINGE_LIMIT_BIAS + 1;
|
|
949
|
+
const HINGE_LOWER_IMPULSE = HINGE_MAX_MOTOR + 1;
|
|
950
|
+
const HINGE_UPPER_IMPULSE = HINGE_LOWER_IMPULSE + 1;
|
|
951
|
+
const HINGE_LIMIT_MASS = HINGE_UPPER_IMPULSE + 1;
|
|
952
|
+
const HINGE_LIMIT_GAMMA = HINGE_LIMIT_MASS + 1;
|
|
953
|
+
const HINGE_LENGTH = HINGE_LIMIT_GAMMA + 1;
|
|
954
|
+
// The effective cone half-angle in the direction the twist axis has actually tilted. Equal limits give a
|
|
955
|
+
// circular cone; unequal ones give an ellipse, and the blend below is the ellipse in polar form — it returns
|
|
956
|
+
// `swingLimitY` for a tilt straight toward frame A's Y and `swingLimitZ` for one straight toward Z.
|
|
957
|
+
//
|
|
958
|
+
// A non-positive limit is a cone with no opening, and the smaller of the two is the honest reading of it: an
|
|
959
|
+
// ellipse with a zero semi-axis is a line segment, not an ellipse.
|
|
960
|
+
function getConeSwingLimit(cone) {
|
|
961
|
+
if (cone.swingLimitY <= 0 || cone.swingLimitZ <= 0)
|
|
962
|
+
return Math.min(cone.swingLimitY, cone.swingLimitZ);
|
|
963
|
+
const towardY = dot(frameBBasis, 0, frameABasis, 3);
|
|
964
|
+
const towardZ = dot(frameBBasis, 0, frameABasis, 6);
|
|
965
|
+
const tilt = Math.sqrt(towardY * towardY + towardZ * towardZ);
|
|
966
|
+
if (tilt <= AXIS_EPSILON)
|
|
967
|
+
return Math.min(cone.swingLimitY, cone.swingLimitZ);
|
|
968
|
+
const unitY = towardY / tilt / cone.swingLimitY;
|
|
969
|
+
const unitZ = towardZ / tilt / cone.swingLimitZ;
|
|
970
|
+
return 1 / Math.sqrt(unitY * unitY + unitZ * unitZ);
|
|
971
|
+
}
|
|
972
|
+
// Copies a 6-DOF joint's twelve bounds into the two scratch arrays, ordered linear X, Y, Z then angular X, Y,
|
|
973
|
+
// Z — the axis numbering every loop over the six degrees of freedom uses.
|
|
974
|
+
function readDofBounds(dof) {
|
|
975
|
+
dofLower[0] = dof.lowerLinearX;
|
|
976
|
+
dofLower[1] = dof.lowerLinearY;
|
|
977
|
+
dofLower[2] = dof.lowerLinearZ;
|
|
978
|
+
dofLower[3] = dof.lowerAngularX;
|
|
979
|
+
dofLower[4] = dof.lowerAngularY;
|
|
980
|
+
dofLower[5] = dof.lowerAngularZ;
|
|
981
|
+
dofUpper[0] = dof.upperLinearX;
|
|
982
|
+
dofUpper[1] = dof.upperLinearY;
|
|
983
|
+
dofUpper[2] = dof.upperLinearZ;
|
|
984
|
+
dofUpper[3] = dof.upperAngularX;
|
|
985
|
+
dofUpper[4] = dof.upperAngularY;
|
|
986
|
+
dofUpper[5] = dof.upperAngularZ;
|
|
987
|
+
}
|
|
988
|
+
// Writes the row for a linear coordinate measured along one of frame A's axes, reading the separation left by
|
|
989
|
+
// `writePhysics3DJointSeparation`.
|
|
990
|
+
//
|
|
991
|
+
// A's arm is `(rA + separation) x direction`, NOT `rA x direction`. The direction is carried by A and rotates
|
|
992
|
+
// with it, so the coordinate changes when A turns even if neither anchor moves; an arm taken from A's bare
|
|
993
|
+
// lever arm drops that term and leaks motion sideways whenever the rail body rotates.
|
|
994
|
+
function writeLinearAxisRow(state, offset, joint, basisOffset) {
|
|
995
|
+
const directionX = frameABasis[basisOffset];
|
|
996
|
+
const directionY = frameABasis[basisOffset + 1];
|
|
997
|
+
const directionZ = frameABasis[basisOffset + 2];
|
|
998
|
+
const armAX = joint.rAX + separation[0];
|
|
999
|
+
const armAY = joint.rAY + separation[1];
|
|
1000
|
+
const armAZ = joint.rAZ + separation[2];
|
|
1001
|
+
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);
|
|
1002
|
+
}
|
|
1003
|
+
// Below this the swing axis is degenerate: the two twist axes are parallel, the joint sits at the centre of
|
|
1004
|
+
// its cone, and there is no direction for a limit to act along.
|
|
1005
|
+
const AXIS_EPSILON = 1e-9;
|
|
1006
|
+
const CONE_SWING_ROW = POINT_LENGTH;
|
|
1007
|
+
const CONE_TWIST_ROW = CONE_SWING_ROW + ROW_LENGTH;
|
|
1008
|
+
const CONE_SWING_MASS = CONE_TWIST_ROW + ROW_LENGTH;
|
|
1009
|
+
const CONE_SWING_ERROR = CONE_SWING_MASS + 1;
|
|
1010
|
+
const CONE_SWING_ACTIVE = CONE_SWING_ERROR + 1;
|
|
1011
|
+
const CONE_TWIST_MASS = CONE_SWING_ACTIVE + 1;
|
|
1012
|
+
const CONE_TWIST_ANGLE = CONE_TWIST_MASS + 1;
|
|
1013
|
+
const CONE_LIMIT_BIAS = CONE_TWIST_ANGLE + 1;
|
|
1014
|
+
const CONE_SWING_IMPULSE = CONE_LIMIT_BIAS + 1;
|
|
1015
|
+
const CONE_LOWER_TWIST_IMPULSE = CONE_SWING_IMPULSE + 1;
|
|
1016
|
+
const CONE_UPPER_TWIST_IMPULSE = CONE_LOWER_TWIST_IMPULSE + 1;
|
|
1017
|
+
const CONE_SWING_SOFT_MASS = CONE_UPPER_TWIST_IMPULSE + 1;
|
|
1018
|
+
const CONE_SWING_GAMMA = CONE_SWING_SOFT_MASS + 1;
|
|
1019
|
+
const CONE_TWIST_SOFT_MASS = CONE_SWING_GAMMA + 1;
|
|
1020
|
+
const CONE_TWIST_GAMMA = CONE_TWIST_SOFT_MASS + 1;
|
|
1021
|
+
const CONE_LENGTH = CONE_TWIST_GAMMA + 1;
|
|
1022
|
+
const DOF_FREE = 0;
|
|
1023
|
+
const DOF_LOCKED = 1;
|
|
1024
|
+
const DOF_LIMITED = 2;
|
|
1025
|
+
const DOF_ROWS = 0;
|
|
1026
|
+
const DOF_MASS = DOF_ROWS + 6 * ROW_LENGTH;
|
|
1027
|
+
const DOF_VALUE = DOF_MASS + 6;
|
|
1028
|
+
const DOF_MODE = DOF_VALUE + 6;
|
|
1029
|
+
const DOF_LOWER_IMPULSE = DOF_MODE + 6;
|
|
1030
|
+
const DOF_UPPER_IMPULSE = DOF_LOWER_IMPULSE + 6;
|
|
1031
|
+
const DOF_LOCK_BIAS = DOF_UPPER_IMPULSE + 6;
|
|
1032
|
+
const DOF_LIMIT_BIAS = DOF_LOCK_BIAS + 1;
|
|
1033
|
+
const DOF_LIMIT_MASS = DOF_LIMIT_BIAS + 1;
|
|
1034
|
+
const DOF_LIMIT_GAMMA = DOF_LIMIT_MASS + 6;
|
|
1035
|
+
const DOF_LENGTH = DOF_LIMIT_GAMMA + 6;
|
|
1036
|
+
const SLIDER_PERP_ROW0 = 0;
|
|
1037
|
+
const SLIDER_PERP_ROW1 = SLIDER_PERP_ROW0 + ROW_LENGTH;
|
|
1038
|
+
const SLIDER_AXIS_ROW = SLIDER_PERP_ROW1 + ROW_LENGTH;
|
|
1039
|
+
const SLIDER_ANGULAR_MASS = SLIDER_AXIS_ROW + ROW_LENGTH;
|
|
1040
|
+
const SLIDER_ANGULAR_BIAS = SLIDER_ANGULAR_MASS + 6;
|
|
1041
|
+
const SLIDER_PERP_MASS0 = SLIDER_ANGULAR_BIAS + 3;
|
|
1042
|
+
const SLIDER_PERP_MASS1 = SLIDER_PERP_MASS0 + 1;
|
|
1043
|
+
const SLIDER_PERP_ERROR0 = SLIDER_PERP_MASS1 + 1;
|
|
1044
|
+
const SLIDER_PERP_ERROR1 = SLIDER_PERP_ERROR0 + 1;
|
|
1045
|
+
const SLIDER_AXIS_MASS = SLIDER_PERP_ERROR1 + 1;
|
|
1046
|
+
const SLIDER_TRANSLATION = SLIDER_AXIS_MASS + 1;
|
|
1047
|
+
const SLIDER_LOCK_BIAS = SLIDER_TRANSLATION + 1;
|
|
1048
|
+
const SLIDER_LIMIT_BIAS = SLIDER_LOCK_BIAS + 1;
|
|
1049
|
+
const SLIDER_MAX_MOTOR = SLIDER_LIMIT_BIAS + 1;
|
|
1050
|
+
const SLIDER_LOWER_IMPULSE = SLIDER_MAX_MOTOR + 1;
|
|
1051
|
+
const SLIDER_UPPER_IMPULSE = SLIDER_LOWER_IMPULSE + 1;
|
|
1052
|
+
const SLIDER_LIMIT_MASS = SLIDER_UPPER_IMPULSE + 1;
|
|
1053
|
+
const SLIDER_LIMIT_GAMMA = SLIDER_LIMIT_MASS + 1;
|
|
1054
|
+
const SLIDER_LENGTH = SLIDER_LIMIT_GAMMA + 1;
|
|
1055
|
+
const dofLower = [0, 0, 0, 0, 0, 0];
|
|
1056
|
+
const dofUpper = [0, 0, 0, 0, 0, 0];
|
|
1057
|
+
const relativeRotation = [0, 0, 0, 1];
|
|
1058
|
+
const rotationError = [0, 0, 0];
|
|
1059
|
+
const separation = [0, 0, 0];
|
|
1060
|
+
//# sourceMappingURL=joints.js.map
|