@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
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { createCollisionContactManifold3D, createCollisionTimeOfImpact3D } from '@flighthq/collision/contract';
|
|
2
|
+
import { DEG_TO_RAD } from '@flighthq/math/contract';
|
|
3
|
+
import { synchronizePhysics3DBroadphase, synchronizePhysics3DSweptBroadphase } from './broadphase';
|
|
4
|
+
import { collidePhysics3DColliderShapes, sweepPhysics3DColliderShapes } from './colliderCollision';
|
|
5
|
+
import { updatePhysics3DColliderWorldShape } from './colliderTransform';
|
|
6
|
+
import { createPhysics3DContactPoint } from './contacts';
|
|
7
|
+
import { integrateRigidBody3DPose, refreshRigidBody3DWorldInertia } from './integrate';
|
|
8
|
+
import { isPhysics3DPairJointSuppressed } from './jointCollisionSuppression';
|
|
9
|
+
import { isPhysics3DPairOrdered } from './jointRegistry';
|
|
10
|
+
import { mixPhysics3DFriction, mixPhysics3DRestitution } from './material';
|
|
11
|
+
import { steppingPhysics3DWorlds } from './ownership';
|
|
12
|
+
import { isPhysics3DContactValid } from './stepValidation';
|
|
13
|
+
import { applySymmetricTensor, TENSOR_XX, TENSOR_XY, TENSOR_XZ, TENSOR_YY, TENSOR_YZ, TENSOR_ZZ, } from './symmetricTensor';
|
|
14
|
+
import { writeRigidBody3DWorldCenter } from './world';
|
|
15
|
+
// Continuous collision: advancing poses in chronological order of impact rather than in one jump.
|
|
16
|
+
//
|
|
17
|
+
// The ordinary discrete path integrates every body across the whole interval and then asks what overlaps.
|
|
18
|
+
// A body fast enough to cross a wall within one interval overlaps NOTHING at either end, so it passes
|
|
19
|
+
// through — and no amount of solver iteration recovers it, because the contact was never generated. This
|
|
20
|
+
// path finds the earliest impact in the interval, advances everything only that far, resolves it, and
|
|
21
|
+
// repeats.
|
|
22
|
+
//
|
|
23
|
+
// Opt-in per world (`config.continuousCollision`) and per body (`body.bullet`), because it costs a swept
|
|
24
|
+
// query per candidate pair per sub-interval. The discrete path remains the default and is what almost
|
|
25
|
+
// every body wants.
|
|
26
|
+
// Whether any body in this world is currently asking for continuous treatment. Checked before the
|
|
27
|
+
// continuous path runs at all, so a world that enabled the config but has no bullets pays one scan
|
|
28
|
+
// rather than a swept broadphase.
|
|
29
|
+
export function hasActivePhysics3DBullet(world) {
|
|
30
|
+
for (const body of world.bodies) {
|
|
31
|
+
if (body.type === 'dynamic' && body.bullet && !body.sleeping)
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Advances every awake body across `dt`, stopping at each impact in chronological order.
|
|
37
|
+
//
|
|
38
|
+
// The substep budget is a HARD bound rather than a convergence target: an adversarial scene — a fast body
|
|
39
|
+
// in a narrow gap — can generate impacts without limit, and a loop that ran until none were left would
|
|
40
|
+
// make one frame's cost unbounded. When the budget runs out the remaining time is advanced discretely,
|
|
41
|
+
// which is the behaviour the caller would have had anyway. A custom loop entering here directly acquires
|
|
42
|
+
// the same mutation boundary as `stepPhysics3D`; the standard step already owns it. A second continuous
|
|
43
|
+
// entry on the same world is always recursion and is rejected before it can share this function's scratch
|
|
44
|
+
// or advance a half-resolved impact from inside a contact hook.
|
|
45
|
+
export function integratePhysics3DContinuous(world, dt) {
|
|
46
|
+
if (integratingPhysics3DWorlds.has(world)) {
|
|
47
|
+
throw new Error('Cannot integrate a physics world recursively while it is stepping');
|
|
48
|
+
}
|
|
49
|
+
const scratch = acquirePhysics3DContinuousScratch();
|
|
50
|
+
const ownsMutationBoundary = !steppingPhysics3DWorlds.has(world);
|
|
51
|
+
integratingPhysics3DWorlds.add(world);
|
|
52
|
+
if (ownsMutationBoundary)
|
|
53
|
+
steppingPhysics3DWorlds.add(world);
|
|
54
|
+
try {
|
|
55
|
+
let remaining = dt;
|
|
56
|
+
for (let substep = 0; substep < world.config.maxCcdSubsteps && remaining > 0; substep += 1) {
|
|
57
|
+
if (!findEarliestPhysics3DImpact(world, remaining, scratch))
|
|
58
|
+
break;
|
|
59
|
+
const advance = remaining * scratch.fraction;
|
|
60
|
+
advanceAwakePhysics3DBodies(world, advance);
|
|
61
|
+
remaining -= advance;
|
|
62
|
+
synchronizePhysics3DBroadphase(world);
|
|
63
|
+
resolvePhysics3DImpact(world, scratch);
|
|
64
|
+
// The impact landed at the very end of the interval, so there is no time left to advance into.
|
|
65
|
+
if (scratch.fraction >= 1)
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (remaining > 0)
|
|
69
|
+
advanceAwakePhysics3DBodies(world, remaining);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
releasePhysics3DContinuousScratch(scratch);
|
|
73
|
+
if (ownsMutationBoundary)
|
|
74
|
+
steppingPhysics3DWorlds.delete(world);
|
|
75
|
+
integratingPhysics3DWorlds.delete(world);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Writes the configured rotational CCD sampling guarantee without running a world. This is the authoring
|
|
79
|
+
// check for a fast blade or offset collider: feed it the maximum angular speed and the furthest collider
|
|
80
|
+
// point from the body origin, then compare the reported angular/arc gap with the thinnest interaction the
|
|
81
|
+
// game must not miss.
|
|
82
|
+
export function writePhysics3DRotationalCcdEnvelope(angularSpeed, maximumRadius, dt, maxSubsteps, out) {
|
|
83
|
+
if (!Number.isFinite(angularSpeed) ||
|
|
84
|
+
angularSpeed < 0 ||
|
|
85
|
+
!Number.isFinite(maximumRadius) ||
|
|
86
|
+
maximumRadius < 0 ||
|
|
87
|
+
!Number.isFinite(dt) ||
|
|
88
|
+
dt < 0 ||
|
|
89
|
+
!Number.isSafeInteger(maxSubsteps) ||
|
|
90
|
+
maxSubsteps < 0) {
|
|
91
|
+
clearPhysics3DRotationalCcdEnvelope(out);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const angularTravel = angularSpeed * dt;
|
|
95
|
+
if (!Number.isFinite(angularTravel)) {
|
|
96
|
+
clearPhysics3DRotationalCcdEnvelope(out);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const sampleCount = getPhysics3DRotationSampleCount(angularTravel, maxSubsteps);
|
|
100
|
+
const maxAngularIncrement = angularTravel === 0 ? 0 : sampleCount === 0 ? Number.POSITIVE_INFINITY : angularTravel / sampleCount;
|
|
101
|
+
out.angularTravel = angularTravel;
|
|
102
|
+
out.sampleCount = sampleCount;
|
|
103
|
+
out.maxAngularIncrement = maxAngularIncrement;
|
|
104
|
+
out.maxPointArcTravel = maximumRadius === 0 ? 0 : maximumRadius * maxAngularIncrement;
|
|
105
|
+
out.targetIncrementMet = angularTravel === 0 || maxAngularIncrement <= CCD_ROTATION_INCREMENT;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
function clearPhysics3DRotationalCcdEnvelope(out) {
|
|
109
|
+
out.angularTravel = 0;
|
|
110
|
+
out.sampleCount = 0;
|
|
111
|
+
out.maxAngularIncrement = 0;
|
|
112
|
+
out.maxPointArcTravel = 0;
|
|
113
|
+
out.targetIncrementMet = false;
|
|
114
|
+
}
|
|
115
|
+
function advanceAwakePhysics3DBodies(world, dt) {
|
|
116
|
+
for (const body of world.bodies) {
|
|
117
|
+
if (body.type === 'static' || body.sleeping)
|
|
118
|
+
continue;
|
|
119
|
+
integrateRigidBody3DPose(body, dt);
|
|
120
|
+
refreshRigidBody3DWorldInertia(body);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Finds the earliest impact among the pairs eligible for continuous treatment, writing it into `scratch`.
|
|
124
|
+
//
|
|
125
|
+
// The swept broadphase is widened, queried, and IMMEDIATELY restored, so nothing outside this function
|
|
126
|
+
// ever observes the inflated bounds — a query made between substeps would otherwise report bodies as
|
|
127
|
+
// candidates at positions they are not at.
|
|
128
|
+
function findEarliestPhysics3DImpact(world, dt, scratch) {
|
|
129
|
+
scratch.fraction = Number.POSITIVE_INFINITY;
|
|
130
|
+
scratch.bodyA = -1;
|
|
131
|
+
scratch.bodyB = -1;
|
|
132
|
+
synchronizePhysics3DSweptBroadphase(world, dt);
|
|
133
|
+
world.index.querySpatialPairs(scratch.pairs);
|
|
134
|
+
synchronizePhysics3DBroadphase(world);
|
|
135
|
+
for (const pair of scratch.pairs) {
|
|
136
|
+
const first = world.bodyByIndex.get(pair.a);
|
|
137
|
+
const second = world.bodyByIndex.get(pair.b);
|
|
138
|
+
if (first === undefined || second === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
const ordered = isPhysics3DPairOrdered(first.index, second.index);
|
|
141
|
+
const bodyA = ordered ? first : second;
|
|
142
|
+
const bodyB = ordered ? second : first;
|
|
143
|
+
if (!isPhysics3DCcdPairActive(bodyA, bodyB))
|
|
144
|
+
continue;
|
|
145
|
+
if (isPhysics3DPairJointSuppressed(world, bodyA.index, bodyB.index))
|
|
146
|
+
continue;
|
|
147
|
+
for (let i = 0; i < bodyA.colliders.length; i += 1) {
|
|
148
|
+
for (let j = 0; j < bodyB.colliders.length; j += 1) {
|
|
149
|
+
const colliderA = bodyA.colliders[i];
|
|
150
|
+
const colliderB = bodyB.colliders[j];
|
|
151
|
+
if (!isPhysics3DColliderPairEnabled(colliderA, colliderB))
|
|
152
|
+
continue;
|
|
153
|
+
if (colliderA.sensor || colliderB.sensor)
|
|
154
|
+
continue;
|
|
155
|
+
if (!findPhysics3DColliderImpact(world, bodyA, bodyB, colliderA, colliderB, dt, false, scratch))
|
|
156
|
+
continue;
|
|
157
|
+
const candidate = scratch.candidateRotational ? scratch.rotationalImpact : scratch.linearImpact;
|
|
158
|
+
if (candidate.fraction >= scratch.fraction)
|
|
159
|
+
continue;
|
|
160
|
+
scratch.fraction = candidate.fraction;
|
|
161
|
+
scratch.bodyA = bodyA.index;
|
|
162
|
+
scratch.bodyB = bodyB.index;
|
|
163
|
+
scratch.colliderA = i;
|
|
164
|
+
scratch.colliderB = j;
|
|
165
|
+
scratch.normalX = candidate.normalX;
|
|
166
|
+
scratch.normalY = candidate.normalY;
|
|
167
|
+
scratch.normalZ = candidate.normalZ;
|
|
168
|
+
scratch.friction = mixPhysics3DFriction(colliderA.material.friction, colliderB.material.friction);
|
|
169
|
+
scratch.restitution = mixPhysics3DRestitution(colliderA.material.restitution, colliderB.material.restitution);
|
|
170
|
+
scratch.rotational = scratch.candidateRotational;
|
|
171
|
+
scratch.pointCount = scratch.rotational ? scratch.rotationalManifold.pointCount : 1;
|
|
172
|
+
if (scratch.rotational) {
|
|
173
|
+
for (let point = 0; point < scratch.pointCount; point += 1) {
|
|
174
|
+
const source = scratch.rotationalManifold.points[point];
|
|
175
|
+
const offset = point * 3;
|
|
176
|
+
scratch.points[offset] = source.x;
|
|
177
|
+
scratch.points[offset + 1] = source.y;
|
|
178
|
+
scratch.points[offset + 2] = source.z;
|
|
179
|
+
scratch.depths[point] = source.depth;
|
|
180
|
+
scratch.featureIds[point] = source.featureId;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
scratch.points[0] = candidate.x;
|
|
185
|
+
scratch.points[1] = candidate.y;
|
|
186
|
+
scratch.points[2] = candidate.z;
|
|
187
|
+
scratch.depths[0] = 0;
|
|
188
|
+
scratch.featureIds[0] = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Sensor sweeps are evaluated only after the earliest solid TOI is known. Reporting them in the first
|
|
194
|
+
// pass can publish a trigger behind a nearer wall even though that wall stops the bullet before it ever
|
|
195
|
+
// reaches the trigger. Every sensor at or before the solid boundary is observable; later sensors are
|
|
196
|
+
// reconsidered from the post-impact state on the next bounded CCD sub-interval.
|
|
197
|
+
reportPhysics3DSensorImpactsBeforeFraction(world, dt, scratch.fraction, scratch);
|
|
198
|
+
return scratch.bodyA >= 0;
|
|
199
|
+
}
|
|
200
|
+
function reportPhysics3DSensorImpactsBeforeFraction(world, dt, maximumFraction, scratch) {
|
|
201
|
+
for (let pairIndex = 0; pairIndex < scratch.pairs.length; pairIndex += 1) {
|
|
202
|
+
const pair = scratch.pairs[pairIndex];
|
|
203
|
+
const first = world.bodyByIndex.get(pair.a);
|
|
204
|
+
const second = world.bodyByIndex.get(pair.b);
|
|
205
|
+
if (first === undefined || second === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
const ordered = isPhysics3DPairOrdered(first.index, second.index);
|
|
208
|
+
const bodyA = ordered ? first : second;
|
|
209
|
+
const bodyB = ordered ? second : first;
|
|
210
|
+
if (!isPhysics3DCcdPairActive(bodyA, bodyB))
|
|
211
|
+
continue;
|
|
212
|
+
if (isPhysics3DPairJointSuppressed(world, bodyA.index, bodyB.index))
|
|
213
|
+
continue;
|
|
214
|
+
for (let colliderAIndex = 0; colliderAIndex < bodyA.colliders.length; colliderAIndex += 1) {
|
|
215
|
+
for (let colliderBIndex = 0; colliderBIndex < bodyB.colliders.length; colliderBIndex += 1) {
|
|
216
|
+
const colliderA = bodyA.colliders[colliderAIndex];
|
|
217
|
+
const colliderB = bodyB.colliders[colliderBIndex];
|
|
218
|
+
if (!colliderA.sensor && !colliderB.sensor)
|
|
219
|
+
continue;
|
|
220
|
+
if (!isPhysics3DColliderPairEnabled(colliderA, colliderB))
|
|
221
|
+
continue;
|
|
222
|
+
if (hasPhysics3DContactTransition(world.events.began, bodyA.index, bodyB.index, colliderAIndex, colliderBIndex)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!findPhysics3DColliderImpact(world, bodyA, bodyB, colliderA, colliderB, dt, true, scratch))
|
|
226
|
+
continue;
|
|
227
|
+
const candidate = scratch.candidateRotational ? scratch.rotationalImpact : scratch.linearImpact;
|
|
228
|
+
if (candidate.fraction > maximumFraction)
|
|
229
|
+
continue;
|
|
230
|
+
reportPhysics3DSensorImpact(world, bodyA, bodyB, colliderAIndex, colliderBIndex, colliderA, colliderB, candidate, scratch.candidateRotational, dt, scratch);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Selects the earlier analytic-translation or sampled-rotation impact for one collider pair. Sensors
|
|
236
|
+
// accept any crossing because they generate no impulse; solids additionally require an approaching
|
|
237
|
+
// velocity so a grazing or separating touch cannot consume the chronological substep budget.
|
|
238
|
+
function findPhysics3DColliderImpact(world, bodyA, bodyB, colliderA, colliderB, dt, acceptNonApproaching, scratch) {
|
|
239
|
+
const movingA = isPhysics3DBodyMoving(bodyA);
|
|
240
|
+
const movingB = isPhysics3DBodyMoving(bodyB);
|
|
241
|
+
// Preserve the analytic translation sweep even while the body spins. Replacing it with angular
|
|
242
|
+
// samples would let a fast, slightly rotating bullet cross a thin wall between those samples.
|
|
243
|
+
const linearHit = sweepPhysics3DColliderShapes(colliderA.world, movingA ? bodyA.velocityX * dt : 0, movingA ? bodyA.velocityY * dt : 0, movingA ? bodyA.velocityZ * dt : 0, colliderB.world, movingB ? bodyB.velocityX * dt : 0, movingB ? bodyB.velocityY * dt : 0, movingB ? bodyB.velocityZ * dt : 0, scratch.linearImpact, 1) && scratch.linearImpact.fraction > 0;
|
|
244
|
+
const angularTravelA = movingA
|
|
245
|
+
? Math.hypot(bodyA.angularVelocityX, bodyA.angularVelocityY, bodyA.angularVelocityZ) * dt
|
|
246
|
+
: 0;
|
|
247
|
+
const angularTravelB = movingB
|
|
248
|
+
? Math.hypot(bodyB.angularVelocityX, bodyB.angularVelocityY, bodyB.angularVelocityZ) * dt
|
|
249
|
+
: 0;
|
|
250
|
+
const rotationalHit = (angularTravelA > 0 || angularTravelB > 0) &&
|
|
251
|
+
world.config.maxCcdRotationSubsteps > 0 &&
|
|
252
|
+
findPhysics3DRotationalImpact(bodyA, bodyB, colliderA, colliderB, dt, Math.max(angularTravelA, angularTravelB), world.config.maxCcdRotationSubsteps, scratch) &&
|
|
253
|
+
scratch.rotationalImpact.fraction > 0;
|
|
254
|
+
let found = false;
|
|
255
|
+
scratch.candidateRotational = false;
|
|
256
|
+
if (linearHit && (acceptNonApproaching || isPhysics3DImpactApproaching(bodyA, bodyB, scratch.linearImpact))) {
|
|
257
|
+
found = true;
|
|
258
|
+
}
|
|
259
|
+
if (rotationalHit &&
|
|
260
|
+
(!found || scratch.rotationalImpact.fraction < scratch.linearImpact.fraction) &&
|
|
261
|
+
(acceptNonApproaching || isPhysics3DRotationalImpactApproaching(bodyA, bodyB, scratch.rotationalManifold, scratch))) {
|
|
262
|
+
found = true;
|
|
263
|
+
scratch.candidateRotational = true;
|
|
264
|
+
}
|
|
265
|
+
return found;
|
|
266
|
+
}
|
|
267
|
+
// Rotation changes a convex shape rather than translating a fixed one, so collision's analytic linear
|
|
268
|
+
// sweep cannot answer it. Sample at a bounded angular increment, then bisect the first overlapping
|
|
269
|
+
// interval. The hard cap keeps multi-revolution bodies deterministic in cost; zero selects the linear
|
|
270
|
+
// path above.
|
|
271
|
+
function findPhysics3DRotationalImpact(bodyA, bodyB, colliderA, colliderB, dt, angularTravel, maxSubsteps, scratch) {
|
|
272
|
+
// A pair already touching is the discrete solver's work. Without this check bisection reports the
|
|
273
|
+
// first sample as a new impact infinitesimally after zero and consumes CCD budget without advancing.
|
|
274
|
+
if (testPhysics3DColliderOverlapAtFraction(bodyA, bodyB, colliderA, colliderB, dt, 0, scratch))
|
|
275
|
+
return false;
|
|
276
|
+
const substeps = getPhysics3DRotationSampleCount(angularTravel, maxSubsteps);
|
|
277
|
+
let lowerFraction = 0;
|
|
278
|
+
for (let sample = 1; sample <= substeps; sample += 1) {
|
|
279
|
+
const upperFraction = sample / substeps;
|
|
280
|
+
if (!testPhysics3DColliderOverlapAtFraction(bodyA, bodyB, colliderA, colliderB, dt, upperFraction, scratch)) {
|
|
281
|
+
lowerFraction = upperFraction;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
let lower = lowerFraction;
|
|
285
|
+
let upper = upperFraction;
|
|
286
|
+
for (let iteration = 0; iteration < CCD_ROTATION_BISECTION_ITERATIONS; iteration += 1) {
|
|
287
|
+
const middle = (lower + upper) * 0.5;
|
|
288
|
+
if (testPhysics3DColliderOverlapAtFraction(bodyA, bodyB, colliderA, colliderB, dt, middle, scratch)) {
|
|
289
|
+
upper = middle;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
lower = middle;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
testPhysics3DColliderOverlapAtFraction(bodyA, bodyB, colliderA, colliderB, dt, upper, scratch);
|
|
296
|
+
const manifold = scratch.rotationalManifold;
|
|
297
|
+
let pointX = 0;
|
|
298
|
+
let pointY = 0;
|
|
299
|
+
let pointZ = 0;
|
|
300
|
+
for (let point = 0; point < manifold.pointCount; point += 1) {
|
|
301
|
+
pointX += manifold.points[point].x;
|
|
302
|
+
pointY += manifold.points[point].y;
|
|
303
|
+
pointZ += manifold.points[point].z;
|
|
304
|
+
}
|
|
305
|
+
const inverseCount = manifold.pointCount > 0 ? 1 / manifold.pointCount : 0;
|
|
306
|
+
scratch.rotationalImpact.fraction = upper;
|
|
307
|
+
scratch.rotationalImpact.x = pointX * inverseCount;
|
|
308
|
+
scratch.rotationalImpact.y = pointY * inverseCount;
|
|
309
|
+
scratch.rotationalImpact.z = pointZ * inverseCount;
|
|
310
|
+
scratch.rotationalImpact.normalX = manifold.normalX;
|
|
311
|
+
scratch.rotationalImpact.normalY = manifold.normalY;
|
|
312
|
+
scratch.rotationalImpact.normalZ = manifold.normalZ;
|
|
313
|
+
return manifold.pointCount > 0;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
function getPhysics3DRotationSampleCount(angularTravel, maxSubsteps) {
|
|
318
|
+
if (!(angularTravel > 0) || maxSubsteps <= 0)
|
|
319
|
+
return 0;
|
|
320
|
+
return Math.min(maxSubsteps, Math.max(1, Math.ceil(angularTravel / CCD_ROTATION_INCREMENT)));
|
|
321
|
+
}
|
|
322
|
+
function testPhysics3DColliderOverlapAtFraction(bodyA, bodyB, colliderA, colliderB, dt, fraction, scratch) {
|
|
323
|
+
savePhysics3DBodyPose(bodyA, scratch.poseA);
|
|
324
|
+
savePhysics3DBodyPose(bodyB, scratch.poseB);
|
|
325
|
+
try {
|
|
326
|
+
integrateRigidBody3DPose(bodyA, dt * fraction);
|
|
327
|
+
integrateRigidBody3DPose(bodyB, dt * fraction);
|
|
328
|
+
updatePhysics3DColliderWorldShape(colliderA, bodyA);
|
|
329
|
+
updatePhysics3DColliderWorldShape(colliderB, bodyB);
|
|
330
|
+
const overlapping = collidePhysics3DColliderShapes(colliderA.world, colliderB.world, scratch.rotationalManifold);
|
|
331
|
+
if (overlapping) {
|
|
332
|
+
writeRigidBody3DWorldCenter(bodyA, scratch.candidateCenterA);
|
|
333
|
+
writeRigidBody3DWorldCenter(bodyB, scratch.candidateCenterB);
|
|
334
|
+
}
|
|
335
|
+
return overlapping;
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
restorePhysics3DBodyPose(bodyA, scratch.poseA);
|
|
339
|
+
restorePhysics3DBodyPose(bodyB, scratch.poseB);
|
|
340
|
+
updatePhysics3DColliderWorldShape(colliderA, bodyA);
|
|
341
|
+
updatePhysics3DColliderWorldShape(colliderB, bodyB);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function savePhysics3DBodyPose(body, out) {
|
|
345
|
+
out[0] = body.x;
|
|
346
|
+
out[1] = body.y;
|
|
347
|
+
out[2] = body.z;
|
|
348
|
+
out[3] = body.orientationX;
|
|
349
|
+
out[4] = body.orientationY;
|
|
350
|
+
out[5] = body.orientationZ;
|
|
351
|
+
out[6] = body.orientationW;
|
|
352
|
+
}
|
|
353
|
+
function restorePhysics3DBodyPose(body, pose) {
|
|
354
|
+
body.x = pose[0];
|
|
355
|
+
body.y = pose[1];
|
|
356
|
+
body.z = pose[2];
|
|
357
|
+
body.orientationX = pose[3];
|
|
358
|
+
body.orientationY = pose[4];
|
|
359
|
+
body.orientationZ = pose[5];
|
|
360
|
+
body.orientationW = pose[6];
|
|
361
|
+
}
|
|
362
|
+
function isPhysics3DBodyMoving(body) {
|
|
363
|
+
return body.type !== 'static' && !body.sleeping;
|
|
364
|
+
}
|
|
365
|
+
// Continuous treatment needs at least one body ASKING for it and at least one that can be moved by the
|
|
366
|
+
// resolution. Two immovable bodies produce an impact nothing can respond to.
|
|
367
|
+
function isPhysics3DCcdPairActive(bodyA, bodyB) {
|
|
368
|
+
const bulletA = bodyA.type === 'dynamic' && bodyA.bullet && !bodyA.sleeping;
|
|
369
|
+
const bulletB = bodyB.type === 'dynamic' && bodyB.bullet && !bodyB.sleeping;
|
|
370
|
+
if (!bulletA && !bulletB)
|
|
371
|
+
return false;
|
|
372
|
+
return bodyA.inverseMass > 0 || bodyB.inverseMass > 0;
|
|
373
|
+
}
|
|
374
|
+
function isPhysics3DColliderPairEnabled(colliderA, colliderB) {
|
|
375
|
+
const filterA = colliderA.filter;
|
|
376
|
+
const filterB = colliderB.filter;
|
|
377
|
+
if (filterA.groupIndex !== 0 && filterA.groupIndex === filterB.groupIndex)
|
|
378
|
+
return filterA.groupIndex > 0;
|
|
379
|
+
return (filterA.maskBits & filterB.categoryBits) !== 0 && (filterB.maskBits & filterA.categoryBits) !== 0;
|
|
380
|
+
}
|
|
381
|
+
// Whether the pair is actually closing, rather than merely arriving at a shared surface while
|
|
382
|
+
// separating. A grazing pass touches without approaching, and stopping the world for one would halt a
|
|
383
|
+
// body that was never going to collide.
|
|
384
|
+
function isPhysics3DImpactApproaching(bodyA, bodyB, impact) {
|
|
385
|
+
return getRelativeNormalVelocity(bodyA, bodyB, impact.normalX, impact.normalY, impact.normalZ) < -APPROACH_EPSILON;
|
|
386
|
+
}
|
|
387
|
+
function isPhysics3DRotationalImpactApproaching(bodyA, bodyB, manifold, scratch) {
|
|
388
|
+
for (let point = 0; point < manifold.pointCount; point += 1) {
|
|
389
|
+
const source = manifold.points[point];
|
|
390
|
+
if (getRelativePointNormalVelocity(bodyA, bodyB, source.x - scratch.candidateCenterA[0], source.y - scratch.candidateCenterA[1], source.z - scratch.candidateCenterA[2], source.x - scratch.candidateCenterB[0], source.y - scratch.candidateCenterB[1], source.z - scratch.candidateCenterB[2], manifold.normalX, manifold.normalY, manifold.normalZ) < -APPROACH_EPSILON) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
// The relative velocity of the two bodies along the normal, between their centres of mass. Negative means
|
|
397
|
+
// approaching, matching the sign convention the contact solver uses.
|
|
398
|
+
function getRelativeNormalVelocity(bodyA, bodyB, normalX, normalY, normalZ) {
|
|
399
|
+
return ((bodyA.velocityX - bodyB.velocityX) * normalX +
|
|
400
|
+
(bodyA.velocityY - bodyB.velocityY) * normalY +
|
|
401
|
+
(bodyA.velocityZ - bodyB.velocityZ) * normalZ);
|
|
402
|
+
}
|
|
403
|
+
// Publishes a solid TOI through the ordinary persistent-contact lane. Appending is deliberate: contact
|
|
404
|
+
// constraints prepared earlier in the interval store list indices, so inserting into canonical order
|
|
405
|
+
// here would retarget those constraints midway through their solve. The step restores canonical order
|
|
406
|
+
// after its position pass, once those indices have expired.
|
|
407
|
+
function writePhysics3DImpactContact(world, bodyA, bodyB, scratch) {
|
|
408
|
+
let contact = null;
|
|
409
|
+
for (let index = 0; index < world.contacts.length; index += 1) {
|
|
410
|
+
const existing = world.contacts[index];
|
|
411
|
+
if (existing.bodyA === scratch.bodyA &&
|
|
412
|
+
existing.bodyB === scratch.bodyB &&
|
|
413
|
+
existing.colliderA === scratch.colliderA &&
|
|
414
|
+
existing.colliderB === scratch.colliderB) {
|
|
415
|
+
contact = existing;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
scratch.contactCreated = contact === null;
|
|
420
|
+
if (contact === null) {
|
|
421
|
+
contact = {
|
|
422
|
+
bodyA: scratch.bodyA,
|
|
423
|
+
bodyB: scratch.bodyB,
|
|
424
|
+
colliderA: scratch.colliderA,
|
|
425
|
+
colliderB: scratch.colliderB,
|
|
426
|
+
normalX: scratch.normalX,
|
|
427
|
+
normalY: scratch.normalY,
|
|
428
|
+
normalZ: scratch.normalZ,
|
|
429
|
+
pointCount: 0,
|
|
430
|
+
points: [],
|
|
431
|
+
friction: scratch.friction,
|
|
432
|
+
restitution: scratch.restitution,
|
|
433
|
+
enabled: true,
|
|
434
|
+
sensor: false,
|
|
435
|
+
touching: true,
|
|
436
|
+
};
|
|
437
|
+
world.contacts.push(contact);
|
|
438
|
+
world.events.began.push(contact);
|
|
439
|
+
}
|
|
440
|
+
contact.normalX = scratch.normalX;
|
|
441
|
+
contact.normalY = scratch.normalY;
|
|
442
|
+
contact.normalZ = scratch.normalZ;
|
|
443
|
+
contact.touching = true;
|
|
444
|
+
while (contact.points.length < scratch.pointCount)
|
|
445
|
+
contact.points.push(createPhysics3DContactPoint());
|
|
446
|
+
writeRigidBody3DWorldCenter(bodyA, scratch.centerA);
|
|
447
|
+
writeRigidBody3DWorldCenter(bodyB, scratch.centerB);
|
|
448
|
+
for (let pointIndex = 0; pointIndex < scratch.pointCount; pointIndex += 1) {
|
|
449
|
+
const offset = pointIndex * 3;
|
|
450
|
+
const point = contact.points[pointIndex];
|
|
451
|
+
point.x = scratch.points[offset];
|
|
452
|
+
point.y = scratch.points[offset + 1];
|
|
453
|
+
point.z = scratch.points[offset + 2];
|
|
454
|
+
point.depth = scratch.depths[pointIndex];
|
|
455
|
+
point.featureId = scratch.featureIds[pointIndex];
|
|
456
|
+
point.rAX = point.x - scratch.centerA[0];
|
|
457
|
+
point.rAY = point.y - scratch.centerA[1];
|
|
458
|
+
point.rAZ = point.z - scratch.centerA[2];
|
|
459
|
+
point.rBX = point.x - scratch.centerB[0];
|
|
460
|
+
point.rBY = point.y - scratch.centerB[1];
|
|
461
|
+
point.rBZ = point.z - scratch.centerB[2];
|
|
462
|
+
}
|
|
463
|
+
contact.pointCount = scratch.pointCount;
|
|
464
|
+
return contact;
|
|
465
|
+
}
|
|
466
|
+
// A sensor impact enters the ordinary persistent-contact lane at TOI. It cannot stop the body, and only
|
|
467
|
+
// a later pose can prove whether the body crossed the whole volume or ended inside it, so the next
|
|
468
|
+
// contact-intake pass makes that decision — a later solver substep or the next public step. A pass-through
|
|
469
|
+
// emits `ended` there with the SAME record identity; a body that remains overlapping simply keeps the
|
|
470
|
+
// record. Sensors invoke neither contact hook and resolve no impulse, matching the discrete sensor
|
|
471
|
+
// contract.
|
|
472
|
+
function reportPhysics3DSensorImpact(world, bodyA, bodyB, colliderAIndex, colliderBIndex, colliderA, colliderB, impact, rotational, dt, scratch) {
|
|
473
|
+
if (hasPhysics3DContactTransition(world.events.began, bodyA.index, bodyB.index, colliderAIndex, colliderBIndex)) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
savePhysics3DBodyPose(bodyA, scratch.poseA);
|
|
477
|
+
savePhysics3DBodyPose(bodyB, scratch.poseB);
|
|
478
|
+
try {
|
|
479
|
+
integrateRigidBody3DPose(bodyA, dt * impact.fraction);
|
|
480
|
+
integrateRigidBody3DPose(bodyB, dt * impact.fraction);
|
|
481
|
+
writeRigidBody3DWorldCenter(bodyA, scratch.centerA);
|
|
482
|
+
writeRigidBody3DWorldCenter(bodyB, scratch.centerB);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
restorePhysics3DBodyPose(bodyA, scratch.poseA);
|
|
486
|
+
restorePhysics3DBodyPose(bodyB, scratch.poseB);
|
|
487
|
+
}
|
|
488
|
+
const pointCount = rotational ? scratch.rotationalManifold.pointCount : 1;
|
|
489
|
+
const contact = {
|
|
490
|
+
bodyA: bodyA.index,
|
|
491
|
+
bodyB: bodyB.index,
|
|
492
|
+
colliderA: colliderAIndex,
|
|
493
|
+
colliderB: colliderBIndex,
|
|
494
|
+
normalX: impact.normalX,
|
|
495
|
+
normalY: impact.normalY,
|
|
496
|
+
normalZ: impact.normalZ,
|
|
497
|
+
pointCount,
|
|
498
|
+
points: [],
|
|
499
|
+
friction: mixPhysics3DFriction(colliderA.material.friction, colliderB.material.friction),
|
|
500
|
+
restitution: mixPhysics3DRestitution(colliderA.material.restitution, colliderB.material.restitution),
|
|
501
|
+
enabled: true,
|
|
502
|
+
sensor: true,
|
|
503
|
+
touching: true,
|
|
504
|
+
};
|
|
505
|
+
for (let pointIndex = 0; pointIndex < pointCount; pointIndex += 1) {
|
|
506
|
+
const source = rotational ? scratch.rotationalManifold.points[pointIndex] : impact;
|
|
507
|
+
const point = createPhysics3DContactPoint();
|
|
508
|
+
point.x = source.x;
|
|
509
|
+
point.y = source.y;
|
|
510
|
+
point.z = source.z;
|
|
511
|
+
point.depth = rotational ? scratch.rotationalManifold.points[pointIndex].depth : 0;
|
|
512
|
+
point.featureId = rotational ? scratch.rotationalManifold.points[pointIndex].featureId : 0;
|
|
513
|
+
point.rAX = point.x - scratch.centerA[0];
|
|
514
|
+
point.rAY = point.y - scratch.centerA[1];
|
|
515
|
+
point.rAZ = point.z - scratch.centerA[2];
|
|
516
|
+
point.rBX = point.x - scratch.centerB[0];
|
|
517
|
+
point.rBY = point.y - scratch.centerB[1];
|
|
518
|
+
point.rBZ = point.z - scratch.centerB[2];
|
|
519
|
+
contact.points.push(point);
|
|
520
|
+
}
|
|
521
|
+
world.contacts.push(contact);
|
|
522
|
+
world.events.began.push(contact);
|
|
523
|
+
}
|
|
524
|
+
function hasPhysics3DContactTransition(contacts, bodyA, bodyB, colliderA, colliderB) {
|
|
525
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
526
|
+
const contact = contacts[index];
|
|
527
|
+
if (contact.bodyA === bodyA &&
|
|
528
|
+
contact.bodyB === bodyB &&
|
|
529
|
+
contact.colliderA === colliderA &&
|
|
530
|
+
contact.colliderB === colliderB) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
// The ordinary pre-solve hook has already run before pose integration. A contact discovered later by
|
|
537
|
+
// CCD gets exactly one equivalent invocation at TOI, before its impact impulse. Invalid writes and
|
|
538
|
+
// throws restore the four mutable override fields just like the ordinary lane. Because the step has
|
|
539
|
+
// necessarily advanced to discover this TOI, an exception leaves poses at the impact and retains the new
|
|
540
|
+
// contact/event rather than rolling the entire simulation back; that exception boundary is part of the
|
|
541
|
+
// CCD hook contract.
|
|
542
|
+
function runPhysics3DImpactPreSolveHook(world, contact) {
|
|
543
|
+
const hook = world.contactHooks.preSolve;
|
|
544
|
+
if (hook === null)
|
|
545
|
+
return;
|
|
546
|
+
const friction = contact.friction;
|
|
547
|
+
const restitution = contact.restitution;
|
|
548
|
+
const enabled = contact.enabled;
|
|
549
|
+
const sensor = contact.sensor;
|
|
550
|
+
try {
|
|
551
|
+
hook(world, contact);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
restorePhysics3DImpactHookFields(contact, friction, restitution, enabled, sensor);
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
if (!isPhysics3DContactValid(contact)) {
|
|
558
|
+
restorePhysics3DImpactHookFields(contact, friction, restitution, enabled, sensor);
|
|
559
|
+
throw new Error('Physics3D pre-solve hook produced invalid contact state');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function restorePhysics3DImpactHookFields(contact, friction, restitution, enabled, sensor) {
|
|
563
|
+
contact.friction = friction;
|
|
564
|
+
contact.restitution = restitution;
|
|
565
|
+
contact.enabled = enabled;
|
|
566
|
+
contact.sensor = sensor;
|
|
567
|
+
}
|
|
568
|
+
// Removes the approach velocity with a single LINEAR normal impulse through the centres of mass.
|
|
569
|
+
//
|
|
570
|
+
// NO LEVER ARM, and that omission is deliberate rather than a shortcut taken for speed. The impact
|
|
571
|
+
// carries a contact point, and it is a genuine one — the sweep reports the GJK witness, which is exact
|
|
572
|
+
// even where the closest feature is interior to an edge. The problem is not the point's accuracy, it is
|
|
573
|
+
// that one point cannot stand for a face-face contact. Where two flat faces meet, every point of the
|
|
574
|
+
// shared region is equally close and the witness settles on a CORNER of it; a box driven squarely into a
|
|
575
|
+
// wall would pick up `r x n` about that corner and come away spinning, from an impact that is symmetric
|
|
576
|
+
// and must produce no rotation at all. An earlier revision measured the other half of the same problem:
|
|
577
|
+
// a lever arm five units off-axis inflated the angular term by four orders of magnitude, the effective
|
|
578
|
+
// mass collapsed, and the impulse meant to stop a 600-unit-per-second bullet changed its velocity by
|
|
579
|
+
// 0.05. It tunnelled anyway, through a path that ran and reported success.
|
|
580
|
+
//
|
|
581
|
+
// The normal response therefore stays linear. Impact-time friction is linear too: it removes centre-of-
|
|
582
|
+
// mass tangential motion under the same Coulomb cone the ordinary solver uses, but invents no torque from
|
|
583
|
+
// the single witness. A rotational impact has a real manifold from its overlap sample and resolves both
|
|
584
|
+
// normal and friction impulses at those points, where the lever arms are supported by actual geometry.
|
|
585
|
+
// In both cases a persistent contact is published at TOI before resolution, so begin events and contact
|
|
586
|
+
// hooks observe the impact in the step where it happened rather than one frame later.
|
|
587
|
+
function resolvePhysics3DImpact(world, scratch) {
|
|
588
|
+
const bodyA = world.bodyByIndex.get(scratch.bodyA);
|
|
589
|
+
const bodyB = world.bodyByIndex.get(scratch.bodyB);
|
|
590
|
+
if (bodyA === undefined || bodyB === undefined)
|
|
591
|
+
return;
|
|
592
|
+
wakePhysics3DImpactBody(bodyA);
|
|
593
|
+
wakePhysics3DImpactBody(bodyB);
|
|
594
|
+
const contact = writePhysics3DImpactContact(world, bodyA, bodyB, scratch);
|
|
595
|
+
if (scratch.contactCreated)
|
|
596
|
+
runPhysics3DImpactPreSolveHook(world, contact);
|
|
597
|
+
if (!contact.enabled || contact.sensor)
|
|
598
|
+
return;
|
|
599
|
+
if (scratch.rotational && scratch.pointCount > 0) {
|
|
600
|
+
resolvePhysics3DRotationalImpact(world, bodyA, bodyB, contact, scratch);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const approach = getRelativeNormalVelocity(bodyA, bodyB, scratch.normalX, scratch.normalY, scratch.normalZ);
|
|
604
|
+
if (approach >= 0)
|
|
605
|
+
return;
|
|
606
|
+
const totalInverseMass = bodyA.inverseMass + bodyB.inverseMass;
|
|
607
|
+
if (!(totalInverseMass > 0))
|
|
608
|
+
return;
|
|
609
|
+
const restitution = approach < -world.config.sequentialImpulse.restitutionThreshold ? contact.restitution : 0;
|
|
610
|
+
const magnitude = (-(1 + restitution) * approach) / totalInverseMass;
|
|
611
|
+
bodyA.velocityX += scratch.normalX * magnitude * bodyA.inverseMass;
|
|
612
|
+
bodyA.velocityY += scratch.normalY * magnitude * bodyA.inverseMass;
|
|
613
|
+
bodyA.velocityZ += scratch.normalZ * magnitude * bodyA.inverseMass;
|
|
614
|
+
bodyB.velocityX -= scratch.normalX * magnitude * bodyB.inverseMass;
|
|
615
|
+
bodyB.velocityY -= scratch.normalY * magnitude * bodyB.inverseMass;
|
|
616
|
+
bodyB.velocityZ -= scratch.normalZ * magnitude * bodyB.inverseMass;
|
|
617
|
+
const relativeX = bodyA.velocityX - bodyB.velocityX;
|
|
618
|
+
const relativeY = bodyA.velocityY - bodyB.velocityY;
|
|
619
|
+
const relativeZ = bodyA.velocityZ - bodyB.velocityZ;
|
|
620
|
+
const normalVelocity = relativeX * scratch.normalX + relativeY * scratch.normalY + relativeZ * scratch.normalZ;
|
|
621
|
+
const tangentVelocityX = relativeX - scratch.normalX * normalVelocity;
|
|
622
|
+
const tangentVelocityY = relativeY - scratch.normalY * normalVelocity;
|
|
623
|
+
const tangentVelocityZ = relativeZ - scratch.normalZ * normalVelocity;
|
|
624
|
+
const tangentSpeed = Math.sqrt(tangentVelocityX * tangentVelocityX + tangentVelocityY * tangentVelocityY + tangentVelocityZ * tangentVelocityZ);
|
|
625
|
+
if (tangentSpeed <= 0 || contact.friction <= 0)
|
|
626
|
+
return;
|
|
627
|
+
const desiredMagnitude = tangentSpeed / totalInverseMass;
|
|
628
|
+
const frictionMagnitude = Math.min(desiredMagnitude, contact.friction * magnitude);
|
|
629
|
+
const scale = -frictionMagnitude / tangentSpeed;
|
|
630
|
+
const impulseX = tangentVelocityX * scale;
|
|
631
|
+
const impulseY = tangentVelocityY * scale;
|
|
632
|
+
const impulseZ = tangentVelocityZ * scale;
|
|
633
|
+
bodyA.velocityX += impulseX * bodyA.inverseMass;
|
|
634
|
+
bodyA.velocityY += impulseY * bodyA.inverseMass;
|
|
635
|
+
bodyA.velocityZ += impulseZ * bodyA.inverseMass;
|
|
636
|
+
bodyB.velocityX -= impulseX * bodyB.inverseMass;
|
|
637
|
+
bodyB.velocityY -= impulseY * bodyB.inverseMass;
|
|
638
|
+
bodyB.velocityZ -= impulseZ * bodyB.inverseMass;
|
|
639
|
+
}
|
|
640
|
+
function resolvePhysics3DRotationalImpact(world, bodyA, bodyB, contact, scratch) {
|
|
641
|
+
writeRigidBody3DWorldCenter(bodyA, scratch.centerA);
|
|
642
|
+
writeRigidBody3DWorldCenter(bodyB, scratch.centerB);
|
|
643
|
+
writePhysics3DImpactFrictionBasis(scratch.normalX, scratch.normalY, scratch.normalZ, scratch);
|
|
644
|
+
for (let point = 0; point < scratch.pointCount; point += 1) {
|
|
645
|
+
scratch.normalImpulses[point] = 0;
|
|
646
|
+
scratch.tangentImpulses0[point] = 0;
|
|
647
|
+
scratch.tangentImpulses1[point] = 0;
|
|
648
|
+
const offset = point * 3;
|
|
649
|
+
const approach = getRelativePointNormalVelocity(bodyA, bodyB, scratch.points[offset] - scratch.centerA[0], scratch.points[offset + 1] - scratch.centerA[1], scratch.points[offset + 2] - scratch.centerA[2], scratch.points[offset] - scratch.centerB[0], scratch.points[offset + 1] - scratch.centerB[1], scratch.points[offset + 2] - scratch.centerB[2], scratch.normalX, scratch.normalY, scratch.normalZ);
|
|
650
|
+
scratch.velocityBiases[point] =
|
|
651
|
+
approach < -world.config.sequentialImpulse.restitutionThreshold ? -contact.restitution * approach : 0;
|
|
652
|
+
}
|
|
653
|
+
for (let iteration = 0; iteration < ROTATIONAL_IMPACT_ITERATIONS; iteration += 1) {
|
|
654
|
+
for (let point = 0; point < scratch.pointCount; point += 1) {
|
|
655
|
+
const offset = point * 3;
|
|
656
|
+
const rAX = scratch.points[offset] - scratch.centerA[0];
|
|
657
|
+
const rAY = scratch.points[offset + 1] - scratch.centerA[1];
|
|
658
|
+
const rAZ = scratch.points[offset + 2] - scratch.centerA[2];
|
|
659
|
+
const rBX = scratch.points[offset] - scratch.centerB[0];
|
|
660
|
+
const rBY = scratch.points[offset + 1] - scratch.centerB[1];
|
|
661
|
+
const rBZ = scratch.points[offset + 2] - scratch.centerB[2];
|
|
662
|
+
const velocity = getRelativePointNormalVelocity(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.normalX, scratch.normalY, scratch.normalZ);
|
|
663
|
+
const mass = getPhysics3DImpactEffectiveMass(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.normalX, scratch.normalY, scratch.normalZ);
|
|
664
|
+
const impulse = Math.max(scratch.normalImpulses[point] + (scratch.velocityBiases[point] - velocity) * mass, 0);
|
|
665
|
+
const delta = impulse - scratch.normalImpulses[point];
|
|
666
|
+
scratch.normalImpulses[point] = impulse;
|
|
667
|
+
applyPhysics3DImpactImpulse(bodyA, rAX, rAY, rAZ, scratch.normalX * delta, scratch.normalY * delta, scratch.normalZ * delta);
|
|
668
|
+
applyPhysics3DImpactImpulse(bodyB, rBX, rBY, rBZ, -scratch.normalX * delta, -scratch.normalY * delta, -scratch.normalZ * delta);
|
|
669
|
+
const maxFriction = contact.friction * impulse;
|
|
670
|
+
const tangentVelocity0 = getRelativePointNormalVelocity(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.tangent0X, scratch.tangent0Y, scratch.tangent0Z);
|
|
671
|
+
const tangentMass0 = getPhysics3DImpactEffectiveMass(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.tangent0X, scratch.tangent0Y, scratch.tangent0Z);
|
|
672
|
+
let tangentImpulse0 = scratch.tangentImpulses0[point] - tangentVelocity0 * tangentMass0;
|
|
673
|
+
const tangentVelocity1 = getRelativePointNormalVelocity(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.tangent1X, scratch.tangent1Y, scratch.tangent1Z);
|
|
674
|
+
const tangentMass1 = getPhysics3DImpactEffectiveMass(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, scratch.tangent1X, scratch.tangent1Y, scratch.tangent1Z);
|
|
675
|
+
let tangentImpulse1 = scratch.tangentImpulses1[point] - tangentVelocity1 * tangentMass1;
|
|
676
|
+
const tangentMagnitude = Math.sqrt(tangentImpulse0 * tangentImpulse0 + tangentImpulse1 * tangentImpulse1);
|
|
677
|
+
if (tangentMagnitude > maxFriction) {
|
|
678
|
+
const frictionScale = maxFriction / tangentMagnitude;
|
|
679
|
+
tangentImpulse0 *= frictionScale;
|
|
680
|
+
tangentImpulse1 *= frictionScale;
|
|
681
|
+
}
|
|
682
|
+
const deltaTangent0 = tangentImpulse0 - scratch.tangentImpulses0[point];
|
|
683
|
+
const deltaTangent1 = tangentImpulse1 - scratch.tangentImpulses1[point];
|
|
684
|
+
scratch.tangentImpulses0[point] = tangentImpulse0;
|
|
685
|
+
scratch.tangentImpulses1[point] = tangentImpulse1;
|
|
686
|
+
const frictionX = scratch.tangent0X * deltaTangent0 + scratch.tangent1X * deltaTangent1;
|
|
687
|
+
const frictionY = scratch.tangent0Y * deltaTangent0 + scratch.tangent1Y * deltaTangent1;
|
|
688
|
+
const frictionZ = scratch.tangent0Z * deltaTangent0 + scratch.tangent1Z * deltaTangent1;
|
|
689
|
+
applyPhysics3DImpactImpulse(bodyA, rAX, rAY, rAZ, frictionX, frictionY, frictionZ);
|
|
690
|
+
applyPhysics3DImpactImpulse(bodyB, rBX, rBY, rBZ, -frictionX, -frictionY, -frictionZ);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function writePhysics3DImpactFrictionBasis(normalX, normalY, normalZ, out) {
|
|
695
|
+
let seedX = 0;
|
|
696
|
+
let seedY = 0;
|
|
697
|
+
let seedZ = 0;
|
|
698
|
+
if (Math.abs(normalX) < IMPACT_AXIS_SELECTION_THRESHOLD)
|
|
699
|
+
seedX = 1;
|
|
700
|
+
else if (Math.abs(normalY) < IMPACT_AXIS_SELECTION_THRESHOLD)
|
|
701
|
+
seedY = 1;
|
|
702
|
+
else
|
|
703
|
+
seedZ = 1;
|
|
704
|
+
let tangent0X = normalY * seedZ - normalZ * seedY;
|
|
705
|
+
let tangent0Y = normalZ * seedX - normalX * seedZ;
|
|
706
|
+
let tangent0Z = normalX * seedY - normalY * seedX;
|
|
707
|
+
const length = Math.sqrt(tangent0X * tangent0X + tangent0Y * tangent0Y + tangent0Z * tangent0Z);
|
|
708
|
+
if (length > 0) {
|
|
709
|
+
const inverseLength = 1 / length;
|
|
710
|
+
tangent0X *= inverseLength;
|
|
711
|
+
tangent0Y *= inverseLength;
|
|
712
|
+
tangent0Z *= inverseLength;
|
|
713
|
+
}
|
|
714
|
+
out.tangent0X = tangent0X;
|
|
715
|
+
out.tangent0Y = tangent0Y;
|
|
716
|
+
out.tangent0Z = tangent0Z;
|
|
717
|
+
out.tangent1X = normalY * tangent0Z - normalZ * tangent0Y;
|
|
718
|
+
out.tangent1Y = normalZ * tangent0X - normalX * tangent0Z;
|
|
719
|
+
out.tangent1Z = normalX * tangent0Y - normalY * tangent0X;
|
|
720
|
+
}
|
|
721
|
+
function getRelativePointNormalVelocity(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, normalX, normalY, normalZ) {
|
|
722
|
+
const pointAX = bodyA.velocityX + bodyA.angularVelocityY * rAZ - bodyA.angularVelocityZ * rAY;
|
|
723
|
+
const pointAY = bodyA.velocityY + bodyA.angularVelocityZ * rAX - bodyA.angularVelocityX * rAZ;
|
|
724
|
+
const pointAZ = bodyA.velocityZ + bodyA.angularVelocityX * rAY - bodyA.angularVelocityY * rAX;
|
|
725
|
+
const pointBX = bodyB.velocityX + bodyB.angularVelocityY * rBZ - bodyB.angularVelocityZ * rBY;
|
|
726
|
+
const pointBY = bodyB.velocityY + bodyB.angularVelocityZ * rBX - bodyB.angularVelocityX * rBZ;
|
|
727
|
+
const pointBZ = bodyB.velocityZ + bodyB.angularVelocityX * rBY - bodyB.angularVelocityY * rBX;
|
|
728
|
+
return (pointAX - pointBX) * normalX + (pointAY - pointBY) * normalY + (pointAZ - pointBZ) * normalZ;
|
|
729
|
+
}
|
|
730
|
+
function getPhysics3DImpactEffectiveMass(bodyA, bodyB, rAX, rAY, rAZ, rBX, rBY, rBZ, normalX, normalY, normalZ) {
|
|
731
|
+
let denominator = bodyA.inverseMass + bodyB.inverseMass;
|
|
732
|
+
denominator += getPhysics3DAngularMassTerm(bodyA, rAX, rAY, rAZ, normalX, normalY, normalZ);
|
|
733
|
+
denominator += getPhysics3DAngularMassTerm(bodyB, rBX, rBY, rBZ, normalX, normalY, normalZ);
|
|
734
|
+
return denominator > 0 ? 1 / denominator : 0;
|
|
735
|
+
}
|
|
736
|
+
function getPhysics3DAngularMassTerm(body, rX, rY, rZ, normalX, normalY, normalZ) {
|
|
737
|
+
const crossX = rY * normalZ - rZ * normalY;
|
|
738
|
+
const crossY = rZ * normalX - rX * normalZ;
|
|
739
|
+
const crossZ = rX * normalY - rY * normalX;
|
|
740
|
+
readPhysics3DWorldInverseInertia(body, impactTensor);
|
|
741
|
+
applySymmetricTensor(impactTensor, crossX, crossY, crossZ, impactVector);
|
|
742
|
+
return crossX * impactVector[0] + crossY * impactVector[1] + crossZ * impactVector[2];
|
|
743
|
+
}
|
|
744
|
+
function applyPhysics3DImpactImpulse(body, rX, rY, rZ, impulseX, impulseY, impulseZ) {
|
|
745
|
+
body.velocityX += impulseX * body.inverseMass;
|
|
746
|
+
body.velocityY += impulseY * body.inverseMass;
|
|
747
|
+
body.velocityZ += impulseZ * body.inverseMass;
|
|
748
|
+
const torqueX = rY * impulseZ - rZ * impulseY;
|
|
749
|
+
const torqueY = rZ * impulseX - rX * impulseZ;
|
|
750
|
+
const torqueZ = rX * impulseY - rY * impulseX;
|
|
751
|
+
readPhysics3DWorldInverseInertia(body, impactTensor);
|
|
752
|
+
applySymmetricTensor(impactTensor, torqueX, torqueY, torqueZ, impactVector);
|
|
753
|
+
body.angularVelocityX += impactVector[0];
|
|
754
|
+
body.angularVelocityY += impactVector[1];
|
|
755
|
+
body.angularVelocityZ += impactVector[2];
|
|
756
|
+
}
|
|
757
|
+
function readPhysics3DWorldInverseInertia(body, out) {
|
|
758
|
+
out[TENSOR_XX] = body.inverseInertiaWorldXX;
|
|
759
|
+
out[TENSOR_YY] = body.inverseInertiaWorldYY;
|
|
760
|
+
out[TENSOR_ZZ] = body.inverseInertiaWorldZZ;
|
|
761
|
+
out[TENSOR_XY] = body.inverseInertiaWorldXY;
|
|
762
|
+
out[TENSOR_XZ] = body.inverseInertiaWorldXZ;
|
|
763
|
+
out[TENSOR_YZ] = body.inverseInertiaWorldYZ;
|
|
764
|
+
}
|
|
765
|
+
function wakePhysics3DImpactBody(body) {
|
|
766
|
+
if (body.type === 'static')
|
|
767
|
+
return;
|
|
768
|
+
body.sleeping = false;
|
|
769
|
+
body.sleepTimer = 0;
|
|
770
|
+
}
|
|
771
|
+
// Below this the pair is grazing rather than approaching, and stopping the world for it would halt a
|
|
772
|
+
// body that was never going to collide.
|
|
773
|
+
const APPROACH_EPSILON = 1e-9;
|
|
774
|
+
const CCD_ROTATION_INCREMENT = DEG_TO_RAD;
|
|
775
|
+
const CCD_ROTATION_BISECTION_ITERATIONS = 12;
|
|
776
|
+
const IMPACT_AXIS_SELECTION_THRESHOLD = 0.5773502691896258;
|
|
777
|
+
const ROTATIONAL_IMPACT_ITERATIONS = 4;
|
|
778
|
+
const impactTensor = [0, 0, 0, 0, 0, 0];
|
|
779
|
+
const impactVector = [0, 0, 0];
|
|
780
|
+
function acquirePhysics3DContinuousScratch() {
|
|
781
|
+
return physics3DContinuousScratchPool.pop() ?? createPhysics3DContinuousScratch();
|
|
782
|
+
}
|
|
783
|
+
function createPhysics3DContinuousScratch() {
|
|
784
|
+
return {
|
|
785
|
+
pairs: [],
|
|
786
|
+
linearImpact: createCollisionTimeOfImpact3D(),
|
|
787
|
+
rotationalImpact: createCollisionTimeOfImpact3D(),
|
|
788
|
+
fraction: 0,
|
|
789
|
+
bodyA: -1,
|
|
790
|
+
bodyB: -1,
|
|
791
|
+
colliderA: -1,
|
|
792
|
+
colliderB: -1,
|
|
793
|
+
normalX: 0,
|
|
794
|
+
normalY: 0,
|
|
795
|
+
normalZ: 0,
|
|
796
|
+
friction: 0,
|
|
797
|
+
restitution: 0,
|
|
798
|
+
rotational: false,
|
|
799
|
+
candidateRotational: false,
|
|
800
|
+
contactCreated: false,
|
|
801
|
+
rotationalManifold: createCollisionContactManifold3D(),
|
|
802
|
+
pointCount: 0,
|
|
803
|
+
points: new Array(12).fill(0),
|
|
804
|
+
depths: new Array(4).fill(0),
|
|
805
|
+
featureIds: new Array(4).fill(0),
|
|
806
|
+
normalImpulses: new Array(4).fill(0),
|
|
807
|
+
tangentImpulses0: new Array(4).fill(0),
|
|
808
|
+
tangentImpulses1: new Array(4).fill(0),
|
|
809
|
+
velocityBiases: new Array(4).fill(0),
|
|
810
|
+
tangent0X: 0,
|
|
811
|
+
tangent0Y: 0,
|
|
812
|
+
tangent0Z: 0,
|
|
813
|
+
tangent1X: 0,
|
|
814
|
+
tangent1Y: 0,
|
|
815
|
+
tangent1Z: 0,
|
|
816
|
+
centerA: [0, 0, 0],
|
|
817
|
+
centerB: [0, 0, 0],
|
|
818
|
+
candidateCenterA: [0, 0, 0],
|
|
819
|
+
candidateCenterB: [0, 0, 0],
|
|
820
|
+
poseA: new Array(7).fill(0),
|
|
821
|
+
poseB: new Array(7).fill(0),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function releasePhysics3DContinuousScratch(scratch) {
|
|
825
|
+
physics3DContinuousScratchPool.push(scratch);
|
|
826
|
+
}
|
|
827
|
+
const physics3DContinuousScratchPool = [createPhysics3DContinuousScratch()];
|
|
828
|
+
const integratingPhysics3DWorlds = new WeakSet();
|
|
829
|
+
//# sourceMappingURL=continuous.js.map
|