babylon-box3d 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -29
- package/dist/box3dPlugin.d.ts +445 -0
- package/dist/box3dPlugin.d.ts.map +1 -0
- package/dist/box3dPlugin.js +2432 -0
- package/dist/box3dPlugin.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/{box3d.d.ts → lib/esm/box3d.d.ts} +40 -1
- package/lib/esm/box3d.js +2 -2
- package/lib/esm/box3d.wasm +0 -0
- package/lib/node/box3d.d.ts +221 -0
- package/lib/node/box3d.mjs +2 -2
- package/lib/node/box3d.wasm +0 -0
- package/lib/umd/box3d.umd.js +2 -2
- package/lib/umd/box3d.umd.wasm +0 -0
- package/package.json +58 -16
- package/umd/babylon.box3d.js +2602 -0
- package/umd/babylon.box3d.js.map +7 -0
- package/umd/babylon.box3d.min.js +3 -0
- package/umd/babylon.box3d.min.js.map +7 -0
- package/{src → wasm}/box3d_shim.c +444 -96
|
@@ -0,0 +1,2432 @@
|
|
|
1
|
+
import { Matrix, Quaternion, TmpVectors, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
|
|
2
|
+
import { PhysicsShapeType, PhysicsConstraintType, PhysicsMotionType, PhysicsConstraintAxis, PhysicsConstraintAxisLimitMode, PhysicsConstraintMotorType, PhysicsEventType, PhysicsPrestepType, PhysicsActivationControl, } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin.js";
|
|
3
|
+
import { PhysicsRaycastResult } from "@babylonjs/core/Physics/physicsRaycastResult.js";
|
|
4
|
+
import { Logger } from "@babylonjs/core/Misc/logger.js";
|
|
5
|
+
import { BoundingBox } from "@babylonjs/core/Culling/boundingBox.js";
|
|
6
|
+
import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
|
|
7
|
+
import { InstancedMesh } from "@babylonjs/core/Meshes/instancedMesh.js";
|
|
8
|
+
import { VertexBuffer } from "@babylonjs/core/Buffers/buffer.js";
|
|
9
|
+
import { Observable } from "@babylonjs/core/Misc/observable.js";
|
|
10
|
+
/** The cell material that cuts a hole in a height field: nothing collides with the cell and rays pass through it. */
|
|
11
|
+
export const BOX3D_HEIGHT_FIELD_HOLE = 255;
|
|
12
|
+
/**
|
|
13
|
+
* Per body plugin data. One instance per Box3D body (thin instances get one each).
|
|
14
|
+
*/
|
|
15
|
+
class Box3DBodyData {
|
|
16
|
+
constructor(
|
|
17
|
+
/** slot handle inside the WASM shim */
|
|
18
|
+
slot) {
|
|
19
|
+
this.slot = slot;
|
|
20
|
+
this.userMassProps = {};
|
|
21
|
+
/** Box3D angular motion locks currently set on the body, bits x, y, z */
|
|
22
|
+
this.angularLocks = 0;
|
|
23
|
+
/** Havok's EventType bits: 1 collision started, 2 collision continued, 4 collision finished */
|
|
24
|
+
this.eventMask = 0;
|
|
25
|
+
this.motionType = PhysicsMotionType.STATIC;
|
|
26
|
+
/** Babylon activation control; ALWAYS_INACTIVE parks the body as a static Box3D body, see setActivationControl */
|
|
27
|
+
this.activation = PhysicsActivationControl.SIMULATION_CONTROLLED;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Per shape plugin data. A Babylon shape is a description that gets instantiated on each body it is set on.
|
|
32
|
+
*/
|
|
33
|
+
class Box3DShapeData {
|
|
34
|
+
constructor(slot, type) {
|
|
35
|
+
this.slot = slot;
|
|
36
|
+
this.type = type;
|
|
37
|
+
this.material = {};
|
|
38
|
+
/** bodies currently instantiating this shape, refreshed when the description changes */
|
|
39
|
+
this.users = new Set();
|
|
40
|
+
/** for containers: child shapes, so changes propagate to users */
|
|
41
|
+
this.children = [];
|
|
42
|
+
this.parents = new Set();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
class Box3DConstraintData {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.joints = [];
|
|
48
|
+
this.pairs = [];
|
|
49
|
+
this.type = PhysicsConstraintType.LOCK;
|
|
50
|
+
this.jointType = 0;
|
|
51
|
+
this.enabled = true;
|
|
52
|
+
this.collisions = false;
|
|
53
|
+
this.frames = new Float32Array(14);
|
|
54
|
+
this.length = 0;
|
|
55
|
+
this.axes = new Map();
|
|
56
|
+
/** SIX_DOF only */
|
|
57
|
+
this.plan = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// shim joint types
|
|
61
|
+
var Box3DJointType;
|
|
62
|
+
(function (Box3DJointType) {
|
|
63
|
+
Box3DJointType[Box3DJointType["WELD"] = 0] = "WELD";
|
|
64
|
+
Box3DJointType[Box3DJointType["SPHERICAL"] = 1] = "SPHERICAL";
|
|
65
|
+
Box3DJointType[Box3DJointType["REVOLUTE"] = 2] = "REVOLUTE";
|
|
66
|
+
Box3DJointType[Box3DJointType["PRISMATIC"] = 3] = "PRISMATIC";
|
|
67
|
+
Box3DJointType[Box3DJointType["DISTANCE"] = 4] = "DISTANCE";
|
|
68
|
+
Box3DJointType[Box3DJointType["FILTER"] = 5] = "FILTER";
|
|
69
|
+
Box3DJointType[Box3DJointType["WHEEL"] = 6] = "WHEEL";
|
|
70
|
+
Box3DJointType[Box3DJointType["PARALLEL"] = 7] = "PARALLEL";
|
|
71
|
+
})(Box3DJointType || (Box3DJointType = {}));
|
|
72
|
+
/**
|
|
73
|
+
* Handle to a Box3D wheel joint created with Box3DPlugin.createWheelJoint.
|
|
74
|
+
*/
|
|
75
|
+
export class Box3DWheelJoint {
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
constructor(_plugin,
|
|
80
|
+
/** @internal */
|
|
81
|
+
_slot) {
|
|
82
|
+
this._plugin = _plugin;
|
|
83
|
+
this._slot = _slot;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sets the target angular speed of the spin motor.
|
|
87
|
+
* @param speed radians per second
|
|
88
|
+
*/
|
|
89
|
+
setSpinSpeed(speed) {
|
|
90
|
+
this._plugin._wheelCall("_bx_WheelJoint_SetSpinMotorSpeed", this._slot, speed);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sets the maximum torque the spin motor can apply.
|
|
94
|
+
* @param torque newton meters
|
|
95
|
+
*/
|
|
96
|
+
setMaxSpinTorque(torque) {
|
|
97
|
+
this._plugin._wheelCall("_bx_WheelJoint_SetMaxSpinTorque", this._slot, torque);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Enables or disables the spin motor (disabled wheels roll freely).
|
|
101
|
+
* @param enabled true to enable
|
|
102
|
+
*/
|
|
103
|
+
enableSpinMotor(enabled) {
|
|
104
|
+
this._plugin._wheelCall("_bx_WheelJoint_EnableSpinMotor", this._slot, enabled ? 1 : 0);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sets the target steering angle.
|
|
108
|
+
* @param radians steering angle
|
|
109
|
+
*/
|
|
110
|
+
setSteeringAngle(radians) {
|
|
111
|
+
this._plugin._wheelCall("_bx_WheelJoint_SetTargetSteeringAngle", this._slot, radians);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Tunes the suspension spring.
|
|
115
|
+
* @param hertz stiffness
|
|
116
|
+
* @param dampingRatio damping ratio
|
|
117
|
+
*/
|
|
118
|
+
setSuspension(hertz, dampingRatio) {
|
|
119
|
+
this._plugin._wheelCall("_bx_WheelJoint_SetSuspension", this._slot, hertz, dampingRatio);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Removes the joint.
|
|
123
|
+
*/
|
|
124
|
+
dispose() {
|
|
125
|
+
this._plugin._destroyExtraJoint(this._slot);
|
|
126
|
+
this._slot = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Body event mask bits, the same values Havok's plugin uses so masks can be copied over from Havok code.
|
|
131
|
+
* Box3D reports begin and end touch events together (they are one shape flag) and hit events separately.
|
|
132
|
+
*/
|
|
133
|
+
var Box3DEventBits;
|
|
134
|
+
(function (Box3DEventBits) {
|
|
135
|
+
Box3DEventBits[Box3DEventBits["COLLISION_STARTED"] = 1] = "COLLISION_STARTED";
|
|
136
|
+
Box3DEventBits[Box3DEventBits["COLLISION_CONTINUED"] = 2] = "COLLISION_CONTINUED";
|
|
137
|
+
Box3DEventBits[Box3DEventBits["COLLISION_FINISHED"] = 4] = "COLLISION_FINISHED";
|
|
138
|
+
Box3DEventBits[Box3DEventBits["ALL"] = 7] = "ALL";
|
|
139
|
+
})(Box3DEventBits || (Box3DEventBits = {}));
|
|
140
|
+
/**
|
|
141
|
+
* How much heavier a locked rotation axis is made than the heaviest free one. Babylon locks an axis with a zero inertia
|
|
142
|
+
* component, Box3D needs an invertible tensor; 1e5 leaves a body a hundred thousand times harder to turn about that axis
|
|
143
|
+
* while the tensor stays well conditioned in float32.
|
|
144
|
+
*/
|
|
145
|
+
const LockedInertiaRatio = 1e5;
|
|
146
|
+
/**
|
|
147
|
+
* Diagonalizes a symmetric inertia tensor [ixx, iyy, izz, ixy, ixz, iyz] into its principal moments and the rotation
|
|
148
|
+
* that turns principal axes into body axes (Babylon's inertiaOrientation). Jacobi rotations, a few sweeps are plenty for
|
|
149
|
+
* a 3x3, and the common diagonal case exits immediately.
|
|
150
|
+
*/
|
|
151
|
+
function DiagonalizeInertia(tensor, moments, orientation) {
|
|
152
|
+
const a = [
|
|
153
|
+
[tensor[0], tensor[3], tensor[4]],
|
|
154
|
+
[tensor[3], tensor[1], tensor[5]],
|
|
155
|
+
[tensor[4], tensor[5], tensor[2]],
|
|
156
|
+
];
|
|
157
|
+
orientation.set(0, 0, 0, 1);
|
|
158
|
+
moments.set(tensor[0], tensor[1], tensor[2]);
|
|
159
|
+
const q = [0, 0, 0, 1];
|
|
160
|
+
for (let sweep = 0; sweep < 24; sweep++) {
|
|
161
|
+
// rotation matrix of q, columns are the current principal axes
|
|
162
|
+
const [x, y, z, w] = q;
|
|
163
|
+
const r = [
|
|
164
|
+
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
|
|
165
|
+
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
|
|
166
|
+
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
|
|
167
|
+
];
|
|
168
|
+
// d = transpose(r) * a * r
|
|
169
|
+
const d = [0, 1, 2].map((i) => [0, 1, 2].map((j) => [0, 1, 2].reduce((sum, k) => sum + r[k][i] * [0, 1, 2].reduce((s2, l) => s2 + a[k][l] * r[l][j], 0), 0)));
|
|
170
|
+
moments.set(d[0][0], d[1][1], d[2][2]);
|
|
171
|
+
orientation.set(q[0], q[1], q[2], q[3]);
|
|
172
|
+
const offDiagonal = [Math.abs(d[1][2]), Math.abs(d[0][2]), Math.abs(d[0][1])];
|
|
173
|
+
const k = offDiagonal.indexOf(Math.max(...offDiagonal));
|
|
174
|
+
const k1 = (k + 1) % 3;
|
|
175
|
+
const k2 = (k + 2) % 3;
|
|
176
|
+
const off = d[k1][k2];
|
|
177
|
+
const scale = Math.abs(d[0][0]) + Math.abs(d[1][1]) + Math.abs(d[2][2]) + 1e-30;
|
|
178
|
+
if (Math.abs(off) <= 1e-9 * scale) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let theta = (d[k2][k2] - d[k1][k1]) / (2 * off);
|
|
182
|
+
const sign = theta > 0 ? 1 : -1;
|
|
183
|
+
theta *= sign;
|
|
184
|
+
const t = sign / (theta + (theta < 1e6 ? Math.sqrt(theta * theta + 1) : theta));
|
|
185
|
+
const c = 1 / Math.sqrt(t * t + 1);
|
|
186
|
+
if (c === 1) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const jacobi = [0, 0, 0, 0];
|
|
190
|
+
// d is transpose(r) * a * r and the step right multiplies q, so d becomes transpose(jacobi) * d * jacobi: the
|
|
191
|
+
// quaternion has to turn by -theta where the matrix form of the Jacobi rotation turns by +theta. With the other
|
|
192
|
+
// sign every sweep puts the off diagonal term straight back and the loop never converges.
|
|
193
|
+
jacobi[k] = -sign * Math.sqrt((1 - c) / 2);
|
|
194
|
+
jacobi[3] = Math.sqrt(1 - jacobi[k] * jacobi[k]);
|
|
195
|
+
// q = q * jacobi
|
|
196
|
+
const [qx, qy, qz, qw] = q;
|
|
197
|
+
const [jx, jy, jz, jw] = jacobi;
|
|
198
|
+
q[0] = qw * jx + qx * jw + qy * jz - qz * jy;
|
|
199
|
+
q[1] = qw * jy - qx * jz + qy * jw + qz * jx;
|
|
200
|
+
q[2] = qw * jz + qx * jy - qy * jx + qz * jw;
|
|
201
|
+
q[3] = qw * jw - qx * jx - qy * jy - qz * jz;
|
|
202
|
+
const length = Math.hypot(q[0], q[1], q[2], q[3]) || 1;
|
|
203
|
+
for (let i = 0; i < 4; i++) {
|
|
204
|
+
q[i] /= length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Builds the symmetric tensor [ixx, iyy, izz, ixy, ixz, iyz] of principal moments rotated into body space. */
|
|
209
|
+
function ComposeInertia(moments, orientation) {
|
|
210
|
+
const { x, y, z, w } = orientation;
|
|
211
|
+
const r = [
|
|
212
|
+
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
|
|
213
|
+
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
|
|
214
|
+
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
|
|
215
|
+
];
|
|
216
|
+
const at = (i, j) => r[i][0] * moments[0] * r[j][0] + r[i][1] * moments[1] * r[j][1] + r[i][2] * moments[2] * r[j][2];
|
|
217
|
+
return [at(0, 0), at(1, 1), at(2, 2), at(0, 1), at(0, 2), at(1, 2)];
|
|
218
|
+
}
|
|
219
|
+
/** Shape description properties that Box3D can change on live shapes, see bx_Body_SyncShapeDesc. */
|
|
220
|
+
var Box3DShapeSync;
|
|
221
|
+
(function (Box3DShapeSync) {
|
|
222
|
+
Box3DShapeSync[Box3DShapeSync["FILTER"] = 1] = "FILTER";
|
|
223
|
+
Box3DShapeSync[Box3DShapeSync["MATERIAL"] = 2] = "MATERIAL";
|
|
224
|
+
Box3DShapeSync[Box3DShapeSync["DENSITY"] = 4] = "DENSITY";
|
|
225
|
+
})(Box3DShapeSync || (Box3DShapeSync = {}));
|
|
226
|
+
const MOVE_STRIDE = 9;
|
|
227
|
+
const CONTACT_STRIDE = 12;
|
|
228
|
+
const SENSOR_STRIDE = 5;
|
|
229
|
+
const RAY_STRIDE = 11;
|
|
230
|
+
/**
|
|
231
|
+
* Collects mesh geometry in the physics body's local space (scaling baked in).
|
|
232
|
+
*/
|
|
233
|
+
class MeshGeometryAccumulator {
|
|
234
|
+
constructor(_collectIndices, _flipWinding) {
|
|
235
|
+
this._collectIndices = _collectIndices;
|
|
236
|
+
this._flipWinding = _flipWinding;
|
|
237
|
+
this._vertices = [];
|
|
238
|
+
this._indices = [];
|
|
239
|
+
}
|
|
240
|
+
addNodeMeshes(mesh, includeChildren) {
|
|
241
|
+
mesh.computeWorldMatrix(true);
|
|
242
|
+
const rootScaled = TmpVectors.Matrix[0];
|
|
243
|
+
Matrix.ScalingToRef(mesh.absoluteScaling.x, mesh.absoluteScaling.y, mesh.absoluteScaling.z, rootScaled);
|
|
244
|
+
if (mesh instanceof Mesh) {
|
|
245
|
+
this._addMesh(mesh, rootScaled);
|
|
246
|
+
}
|
|
247
|
+
else if (mesh instanceof InstancedMesh) {
|
|
248
|
+
this._addMesh(mesh.sourceMesh, rootScaled);
|
|
249
|
+
}
|
|
250
|
+
if (includeChildren) {
|
|
251
|
+
const worldToRoot = TmpVectors.Matrix[1];
|
|
252
|
+
mesh.computeWorldMatrix().invertToRef(worldToRoot);
|
|
253
|
+
const worldToRootScaled = TmpVectors.Matrix[2];
|
|
254
|
+
worldToRoot.multiplyToRef(rootScaled, worldToRootScaled);
|
|
255
|
+
const children = mesh.getChildMeshes(false).filter((m) => !m.physicsBody);
|
|
256
|
+
for (const m of children) {
|
|
257
|
+
const childToWorld = m.computeWorldMatrix();
|
|
258
|
+
const childToRootScaled = TmpVectors.Matrix[3];
|
|
259
|
+
childToWorld.multiplyToRef(worldToRootScaled, childToRootScaled);
|
|
260
|
+
if (m instanceof Mesh) {
|
|
261
|
+
this._addMesh(m, childToRootScaled);
|
|
262
|
+
}
|
|
263
|
+
else if (m instanceof InstancedMesh) {
|
|
264
|
+
this._addMesh(m.sourceMesh, childToRootScaled);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
_addMesh(mesh, meshToRoot) {
|
|
270
|
+
const vertexData = mesh.getVerticesData(VertexBuffer.PositionKind) || [];
|
|
271
|
+
const numVerts = vertexData.length / 3;
|
|
272
|
+
const indexOffset = this._vertices.length / 3;
|
|
273
|
+
const p = TmpVectors.Vector3[0];
|
|
274
|
+
for (let v = 0; v < numVerts; v++) {
|
|
275
|
+
p.set(vertexData[v * 3 + 0], vertexData[v * 3 + 1], vertexData[v * 3 + 2]);
|
|
276
|
+
Vector3.TransformCoordinatesToRef(p, meshToRoot, p);
|
|
277
|
+
this._vertices.push(p.x, p.y, p.z);
|
|
278
|
+
}
|
|
279
|
+
if (this._collectIndices) {
|
|
280
|
+
const meshIndices = mesh.getIndices();
|
|
281
|
+
if (meshIndices) {
|
|
282
|
+
for (let i = 0; i < meshIndices.length; i += 3) {
|
|
283
|
+
if (this._flipWinding) {
|
|
284
|
+
this._indices.push(meshIndices[i + 2] + indexOffset, meshIndices[i + 1] + indexOffset, meshIndices[i + 0] + indexOffset);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
this._indices.push(meshIndices[i + 0] + indexOffset, meshIndices[i + 1] + indexOffset, meshIndices[i + 2] + indexOffset);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
get vertexCount() {
|
|
294
|
+
return this._vertices.length / 3;
|
|
295
|
+
}
|
|
296
|
+
get triangleCount() {
|
|
297
|
+
return this._indices.length / 3;
|
|
298
|
+
}
|
|
299
|
+
get vertices() {
|
|
300
|
+
return this._vertices;
|
|
301
|
+
}
|
|
302
|
+
get indices() {
|
|
303
|
+
return this._indices;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Every native entry point the plugin calls. The constructor checks that the module has all of them, because the two ways this goes wrong
|
|
308
|
+
* in practice - a box3d.js loader left behind by an older install, or a box3d.wasm served from a stale copy - otherwise turn into a physics
|
|
309
|
+
* world where nothing ever happens. A test keeps the list in step with the code.
|
|
310
|
+
*/
|
|
311
|
+
const RequiredNativeExports = [
|
|
312
|
+
"_bx_Body_AllowFastRotation", "_bx_Body_ApplyAngularImpulse", "_bx_Body_ApplyForce", "_bx_Body_ApplyLinearImpulse",
|
|
313
|
+
"_bx_Body_ApplyMassFromShapes", "_bx_Body_ApplyTorque", "_bx_Body_ComputeShapeMassData", "_bx_Body_EnableSleep", "_bx_Body_GetAABB",
|
|
314
|
+
"_bx_Body_GetAngularDamping", "_bx_Body_GetAngularVelocity", "_bx_Body_GetGravityScale", "_bx_Body_GetLinearDamping",
|
|
315
|
+
"_bx_Body_GetLinearVelocity", "_bx_Body_GetMassData", "_bx_Body_GetTransform", "_bx_Body_SetAngularDamping", "_bx_Body_SetAngularVelocity",
|
|
316
|
+
"_bx_Body_SetAwake", "_bx_Body_SetEventFlags", "_bx_Body_SetGravityScale", "_bx_Body_SetLinearDamping", "_bx_Body_SetLinearVelocity",
|
|
317
|
+
"_bx_Body_SetMassDataFull", "_bx_Body_SetMotionLocks", "_bx_Body_SetShape", "_bx_Body_SetTargetTransform", "_bx_Body_SetTransform",
|
|
318
|
+
"_bx_Body_SetType", "_bx_Body_SyncShapeDesc", "_bx_ComputeAlignedFrameB", "_bx_ContactEventsPtr", "_bx_CreateBody", "_bx_CreateJoint",
|
|
319
|
+
"_bx_CreateWorld", "_bx_DebugIndexCount", "_bx_DebugIndicesPtr", "_bx_DebugPositionsPtr", "_bx_DestroyBody", "_bx_DestroyJoint",
|
|
320
|
+
"_bx_DestroyWorld", "_bx_GetRejectedSlotCount", "_bx_Joint_EnableLimit", "_bx_Joint_EnableMotor", "_bx_Joint_EnableSpring", "_bx_Joint_IsValid",
|
|
321
|
+
"_bx_Joint_SetCollideConnected", "_bx_Joint_SetConstraintTuning", "_bx_Joint_SetLimits", "_bx_Joint_SetMaxMotorForce",
|
|
322
|
+
"_bx_Joint_SetMotorSpeed", "_bx_Joint_SetSphericalMotor", "_bx_Joint_SetSphericalTarget", "_bx_Joint_SetSpring", "_bx_Joint_SetTarget",
|
|
323
|
+
"_bx_Joint_SetTwistLimits", "_bx_Joint_UpdateMotorFrame", "_bx_Joint_WakeBodies", "_bx_MoveEventsPtr", "_bx_RayHitsPtr", "_bx_Scratch",
|
|
324
|
+
"_bx_SensorEventsPtr", "_bx_ShapeDesc_AddChild", "_bx_ShapeDesc_BuildDebugGeometry", "_bx_ShapeDesc_CreateBox", "_bx_ShapeDesc_CreateCapsule",
|
|
325
|
+
"_bx_ShapeDesc_CreateContainer", "_bx_ShapeDesc_CreateCylinder", "_bx_ShapeDesc_CreateHeightField", "_bx_ShapeDesc_CreateHull",
|
|
326
|
+
"_bx_ShapeDesc_CreateMesh", "_bx_ShapeDesc_CreateSphere", "_bx_ShapeDesc_Destroy", "_bx_ShapeDesc_GetAABB", "_bx_ShapeDesc_GetCategoryBits",
|
|
327
|
+
"_bx_ShapeDesc_GetChildCount", "_bx_ShapeDesc_GetDensity", "_bx_ShapeDesc_GetFriction", "_bx_ShapeDesc_GetMaskBits",
|
|
328
|
+
"_bx_ShapeDesc_GetRestitution", "_bx_ShapeDesc_RemoveChild", "_bx_ShapeDesc_SetDensity", "_bx_ShapeDesc_SetFilter", "_bx_ShapeDesc_SetGroup",
|
|
329
|
+
"_bx_ShapeDesc_SetMaterial", "_bx_ShapeDesc_SetRollingResistance", "_bx_ShapeDesc_SetSensor", "_bx_World_CastRay", "_bx_World_EnableContinuous",
|
|
330
|
+
"_bx_World_EnableSleeping", "_bx_World_Explode", "_bx_World_GetContactEvents", "_bx_World_GetMaximumLinearSpeed", "_bx_World_GetMoveEvents",
|
|
331
|
+
"_bx_World_GetSensorEvents", "_bx_World_GetStats", "_bx_World_SetGravity", "_bx_World_SetMaximumLinearSpeed", "_bx_World_Step"
|
|
332
|
+
];
|
|
333
|
+
/**
|
|
334
|
+
* Box3D physics plugin for Babylon.js physics v2.
|
|
335
|
+
* Box3D is Erin Catto's 3D rigid body engine (https://github.com/erincatto/box3d). This plugin drives the
|
|
336
|
+
* WebAssembly build that ships in this package (a flat, handle based C shim over box3d).
|
|
337
|
+
* Usage mirrors the Havok plugin:
|
|
338
|
+
* ```ts
|
|
339
|
+
* import { Box3D, Box3DPlugin } from "babylon-box3d";
|
|
340
|
+
* const box3d = await Box3D();
|
|
341
|
+
* scene.enablePhysics(new Vector3(0, -9.81, 0), new Box3DPlugin(true, box3d));
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
export class Box3DPlugin {
|
|
345
|
+
/**
|
|
346
|
+
* Creates a Box3D plugin.
|
|
347
|
+
* @param _useDeltaForWorldStep step the world with the frame delta (true) or a fixed time step (false)
|
|
348
|
+
* @param box3dModule the resolved Box3D WASM module (await Box3DModule())
|
|
349
|
+
*/
|
|
350
|
+
constructor(_useDeltaForWorldStep = true, box3dModule) {
|
|
351
|
+
this._useDeltaForWorldStep = _useDeltaForWorldStep;
|
|
352
|
+
/** Name of the plugin */
|
|
353
|
+
this.name = "Box3DPlugin";
|
|
354
|
+
/** Number of solver sub steps per world step. Box3D recommends 4. */
|
|
355
|
+
this.subStepCount = 4;
|
|
356
|
+
/** Wall clock time of the last world step in milliseconds (JavaScript side, includes the event sync). */
|
|
357
|
+
this.lastStepTimeMs = 0;
|
|
358
|
+
this._worldSlot = 0;
|
|
359
|
+
this._fixedTimeStep = 1 / 60;
|
|
360
|
+
this._timeStep = 1 / 60;
|
|
361
|
+
this._maxAngularVelocity = 4 * Math.PI * 10;
|
|
362
|
+
this._bodies = [];
|
|
363
|
+
this._shapes = [];
|
|
364
|
+
this._bodyCollisionObservable = new Map();
|
|
365
|
+
this._bodyCollisionEndedObservable = new Map();
|
|
366
|
+
this._touchedInstanceMeshes = new Set();
|
|
367
|
+
/** spherical joints whose velocity motor target has to be re-expressed in world space every step */
|
|
368
|
+
this._motorJoints = new Set();
|
|
369
|
+
this._tmpVec3 = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
|
|
370
|
+
this._tmpQuat = [new Quaternion(), new Quaternion(), new Quaternion()];
|
|
371
|
+
this._warned = new Set();
|
|
372
|
+
/** the shim's rejected slot counter as of the last step, see executeStep */
|
|
373
|
+
this._rejectedSlots = 0;
|
|
374
|
+
/** Observable for collision started and continued events */
|
|
375
|
+
this.onCollisionObservable = new Observable();
|
|
376
|
+
/** Observable for collision ended events */
|
|
377
|
+
this.onCollisionEndedObservable = new Observable();
|
|
378
|
+
/** Observable for trigger entered and exited events */
|
|
379
|
+
this.onTriggerCollisionObservable = new Observable();
|
|
380
|
+
if (!box3dModule) {
|
|
381
|
+
throw new Error("Box3D module is required: pass the awaited result of the Box3D module factory.");
|
|
382
|
+
}
|
|
383
|
+
const missing = RequiredNativeExports.filter((name) => typeof box3dModule[name] !== "function");
|
|
384
|
+
if (missing.length > 0) {
|
|
385
|
+
throw new Error(`Box3DPlugin: the Box3D module is missing ${missing.length} entry point${missing.length === 1 ? "" : "s"} this build needs` +
|
|
386
|
+
` (${missing.slice(0, 3).join(", ")}${missing.length > 3 ? ", ..." : ""}). box3d.js and box3d.wasm have to come from the` +
|
|
387
|
+
" same build of this package: check what locateFile points at and clear any copied or cached box3d.wasm.");
|
|
388
|
+
}
|
|
389
|
+
this._b3 = box3dModule;
|
|
390
|
+
this.world = box3dModule;
|
|
391
|
+
this._worldSlot = this._b3._bx_CreateWorld(0, -9.81, 0);
|
|
392
|
+
if (!this._worldSlot) {
|
|
393
|
+
throw new Error("Box3D: could not create a world (max worlds reached?)");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// ----------------------------------------------------------------------------------------
|
|
397
|
+
// helpers
|
|
398
|
+
// ----------------------------------------------------------------------------------------
|
|
399
|
+
_scratch() {
|
|
400
|
+
return new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_Scratch(), 64);
|
|
401
|
+
}
|
|
402
|
+
_warnOnce(key, message) {
|
|
403
|
+
if (!this._warned.has(key)) {
|
|
404
|
+
this._warned.add(key);
|
|
405
|
+
Logger.Warn(`Box3DPlugin: ${message}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
_getPluginReference(body, instanceIndex) {
|
|
409
|
+
return body._pluginDataInstances?.length ? body._pluginDataInstances[instanceIndex ?? 0] : body._pluginData;
|
|
410
|
+
}
|
|
411
|
+
_applyToBodyOrInstances(body, fn, instanceIndex) {
|
|
412
|
+
if (body._pluginDataInstances?.length > 0 && instanceIndex === undefined) {
|
|
413
|
+
for (const data of body._pluginDataInstances) {
|
|
414
|
+
fn(data);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
fn(this._getPluginReference(body, instanceIndex));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
_motionTypeToNative(motionType) {
|
|
422
|
+
switch (motionType) {
|
|
423
|
+
case PhysicsMotionType.STATIC:
|
|
424
|
+
return 0;
|
|
425
|
+
case PhysicsMotionType.ANIMATED:
|
|
426
|
+
return 1;
|
|
427
|
+
default:
|
|
428
|
+
return 2;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
_nodeTransformToRef(node, position, orientation) {
|
|
432
|
+
if (node.parent) {
|
|
433
|
+
node.computeWorldMatrix(true);
|
|
434
|
+
position.copyFrom(node.absolutePosition);
|
|
435
|
+
orientation.copyFrom(node.absoluteRotationQuaternion);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
position.copyFrom(node.position);
|
|
439
|
+
if (node.rotationQuaternion) {
|
|
440
|
+
orientation.copyFrom(node.rotationQuaternion);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
Quaternion.FromEulerAnglesToRef(node.rotation.x, node.rotation.y, node.rotation.z, orientation);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ----------------------------------------------------------------------------------------
|
|
447
|
+
// world
|
|
448
|
+
// ----------------------------------------------------------------------------------------
|
|
449
|
+
setGravity(gravity) {
|
|
450
|
+
this._b3._bx_World_SetGravity(this._worldSlot, gravity.x, gravity.y, gravity.z);
|
|
451
|
+
}
|
|
452
|
+
setTimeStep(timeStep) {
|
|
453
|
+
this._fixedTimeStep = timeStep;
|
|
454
|
+
}
|
|
455
|
+
getTimeStep() {
|
|
456
|
+
return this._fixedTimeStep;
|
|
457
|
+
}
|
|
458
|
+
executeStep(delta, physicsBodies) {
|
|
459
|
+
for (const physicsBody of physicsBodies) {
|
|
460
|
+
if (physicsBody.disablePreStep) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
this.setPhysicsBodyTransformation(physicsBody, physicsBody.transformNode);
|
|
464
|
+
}
|
|
465
|
+
for (const joint of this._motorJoints) {
|
|
466
|
+
// a joint whose bodies were destroyed is gone with them, drop it instead of holding the slot forever
|
|
467
|
+
if (this._b3._bx_Joint_IsValid(joint)) {
|
|
468
|
+
this._b3._bx_Joint_UpdateMotorFrame(joint);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
this._motorJoints.delete(joint);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const deltaTime = this._useDeltaForWorldStep ? delta : this._fixedTimeStep;
|
|
475
|
+
this._timeStep = deltaTime;
|
|
476
|
+
const start = performance.now();
|
|
477
|
+
this._b3._bx_World_Step(this._worldSlot, deltaTime, this.subStepCount);
|
|
478
|
+
// Only bodies that moved report an event, so sync those instead of iterating every body.
|
|
479
|
+
const moveCount = this._b3._bx_World_GetMoveEvents(this._worldSlot);
|
|
480
|
+
if (moveCount > 0) {
|
|
481
|
+
const movePointer = this._b3._bx_MoveEventsPtr();
|
|
482
|
+
let buffer = new Float32Array(this._b3.HEAPF32.buffer, movePointer, moveCount * MOVE_STRIDE);
|
|
483
|
+
for (let i = 0; i < moveCount; i++) {
|
|
484
|
+
if (buffer.buffer !== this._b3.HEAPF32.buffer) {
|
|
485
|
+
// a world matrix observer grew the wasm memory; the records are still in place
|
|
486
|
+
buffer = new Float32Array(this._b3.HEAPF32.buffer, movePointer, moveCount * MOVE_STRIDE);
|
|
487
|
+
}
|
|
488
|
+
const offset = i * MOVE_STRIDE;
|
|
489
|
+
const ref = this._bodies[buffer[offset]];
|
|
490
|
+
if (!ref || ref.body.disableSync) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
this._syncBodyFromBuffer(ref.body, ref.index, buffer, offset + 1);
|
|
494
|
+
}
|
|
495
|
+
for (const mesh of this._touchedInstanceMeshes) {
|
|
496
|
+
mesh.thinInstanceBufferUpdated("matrix");
|
|
497
|
+
}
|
|
498
|
+
this._touchedInstanceMeshes.clear();
|
|
499
|
+
}
|
|
500
|
+
this._notifyCollisions();
|
|
501
|
+
this._notifyTriggers();
|
|
502
|
+
this.lastStepTimeMs = performance.now() - start;
|
|
503
|
+
// Every native entry point returns quietly when it does not recognize a handle. That is what teardown needs and
|
|
504
|
+
// it hides everything else, so say it once instead of simulating a world where half the calls do nothing.
|
|
505
|
+
const rejected = this._b3._bx_GetRejectedSlotCount();
|
|
506
|
+
if (rejected > this._rejectedSlots) {
|
|
507
|
+
this._rejectedSlots = rejected;
|
|
508
|
+
this._warnOnce("rejected-slots", `the native module did not recognize ${rejected} of the handles it was given, and those calls did nothing. Something is using a ` +
|
|
509
|
+
"body, shape or joint after it was destroyed, or box3d.js and box3d.wasm are from different builds.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
getPluginVersion() {
|
|
513
|
+
return 2;
|
|
514
|
+
}
|
|
515
|
+
setVelocityLimits(maxLinearVelocity, maxAngularVelocity) {
|
|
516
|
+
this._b3._bx_World_SetMaximumLinearSpeed(this._worldSlot, maxLinearVelocity);
|
|
517
|
+
this._maxAngularVelocity = maxAngularVelocity;
|
|
518
|
+
}
|
|
519
|
+
getMaxLinearVelocity() {
|
|
520
|
+
return this._b3._bx_World_GetMaximumLinearSpeed(this._worldSlot);
|
|
521
|
+
}
|
|
522
|
+
getMaxAngularVelocity() {
|
|
523
|
+
return this._maxAngularVelocity;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Box3D counters and timings for the current world.
|
|
527
|
+
* @returns body, shape, contact, joint and island counts plus the last step time in milliseconds
|
|
528
|
+
*/
|
|
529
|
+
getStats() {
|
|
530
|
+
this._b3._bx_World_GetStats(this._worldSlot);
|
|
531
|
+
const s = this._scratch();
|
|
532
|
+
return { bodies: s[0], shapes: s[1], contacts: s[2], joints: s[3], islands: s[4], stepMs: s[5], collideMs: s[6], solveMs: s[7] };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Enables or disables island sleeping.
|
|
536
|
+
* @param enabled true to let resting bodies sleep
|
|
537
|
+
*/
|
|
538
|
+
setSleepingEnabled(enabled) {
|
|
539
|
+
this._b3._bx_World_EnableSleeping(this._worldSlot, enabled ? 1 : 0);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Enables or disables continuous collision against static geometry.
|
|
543
|
+
* @param enabled true to enable continuous collision
|
|
544
|
+
*/
|
|
545
|
+
setContinuousEnabled(enabled) {
|
|
546
|
+
this._b3._bx_World_EnableContinuous(this._worldSlot, enabled ? 1 : 0);
|
|
547
|
+
}
|
|
548
|
+
// ----------------------------------------------------------------------------------------
|
|
549
|
+
// bodies
|
|
550
|
+
// ----------------------------------------------------------------------------------------
|
|
551
|
+
_createNativeBody(motionType, position, orientation, startAsleep) {
|
|
552
|
+
const slot = this._b3._bx_CreateBody(this._worldSlot, this._motionTypeToNative(motionType), position.x, position.y, position.z, orientation.x, orientation.y, orientation.z, orientation.w, startAsleep ? 0 : 1);
|
|
553
|
+
const data = new Box3DBodyData(slot);
|
|
554
|
+
data.motionType = motionType;
|
|
555
|
+
return data;
|
|
556
|
+
}
|
|
557
|
+
initBody(body, motionType, position, orientation) {
|
|
558
|
+
const data = this._createNativeBody(motionType, position, orientation, body.startAsleep);
|
|
559
|
+
body._pluginData = data;
|
|
560
|
+
this._bodies[data.slot] = { body, index: 0, data };
|
|
561
|
+
}
|
|
562
|
+
initBodyInstances(body, motionType, mesh) {
|
|
563
|
+
const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
|
|
564
|
+
const matrixData = mesh._thinInstanceDataStorage?.matrixData;
|
|
565
|
+
if (!matrixData) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const shape = body.shape?._pluginData;
|
|
569
|
+
if (instancesCount > 8 && shape && this._bakesMeshCopies(shape)) {
|
|
570
|
+
this._warnOnce("instanced-mesh-child", "a mesh inside a container has no local transform in Box3D, so every instance gets its own baked copy of the mesh data; " +
|
|
571
|
+
"put the mesh on the body directly, or bake the offset into the mesh, to share it.");
|
|
572
|
+
}
|
|
573
|
+
this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, false);
|
|
574
|
+
}
|
|
575
|
+
_createOrUpdateBodyInstances(body, motionType, matrixData, startIndex, endIndex, update) {
|
|
576
|
+
const rotation = TmpVectors.Quaternion[0];
|
|
577
|
+
const rotationMatrix = TmpVectors.Matrix[0];
|
|
578
|
+
const position = TmpVectors.Vector3[0];
|
|
579
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
580
|
+
position.set(matrixData[i * 16 + 12], matrixData[i * 16 + 13], matrixData[i * 16 + 14]);
|
|
581
|
+
Matrix.FromArrayToRef(matrixData, i * 16, rotationMatrix);
|
|
582
|
+
rotationMatrix.decompose(undefined, rotation, undefined);
|
|
583
|
+
if (update) {
|
|
584
|
+
const data = body._pluginDataInstances[i];
|
|
585
|
+
this._b3._bx_Body_SetTransform(data.slot, position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
const data = this._createNativeBody(motionType, position, rotation, body.startAsleep);
|
|
589
|
+
const sibling = body._pluginDataInstances[0];
|
|
590
|
+
if (sibling) {
|
|
591
|
+
// a new instance behaves like the ones already there: same mass properties, events and activation
|
|
592
|
+
data.userMassProps = sibling.userMassProps;
|
|
593
|
+
data.eventMask = sibling.eventMask;
|
|
594
|
+
this._applyEventFlags(data);
|
|
595
|
+
if (sibling.activation !== PhysicsActivationControl.SIMULATION_CONTROLLED) {
|
|
596
|
+
data.activation = sibling.activation;
|
|
597
|
+
this._applyNativeMotionType(data);
|
|
598
|
+
this._b3._bx_Body_EnableSleep(data.slot, sibling.activation === PhysicsActivationControl.ALWAYS_ACTIVE ? 0 : 1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
body._pluginDataInstances.push(data);
|
|
602
|
+
this._bodies[data.slot] = { body, index: i, data };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
updateBodyInstances(body, mesh) {
|
|
607
|
+
const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
|
|
608
|
+
const matrixData = mesh._thinInstanceDataStorage?.matrixData;
|
|
609
|
+
if (!matrixData) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const pluginInstancesCount = body._pluginDataInstances.length;
|
|
613
|
+
const motionType = this.getMotionType(body);
|
|
614
|
+
if (instancesCount > pluginInstancesCount) {
|
|
615
|
+
this._createOrUpdateBodyInstances(body, motionType, matrixData, pluginInstancesCount, instancesCount, false);
|
|
616
|
+
const shape = body.shape;
|
|
617
|
+
if (shape && shape._pluginData) {
|
|
618
|
+
for (let i = pluginInstancesCount; i < instancesCount; i++) {
|
|
619
|
+
this._setBodyShape(body._pluginDataInstances[i], shape._pluginData);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else if (instancesCount < pluginInstancesCount) {
|
|
624
|
+
const instancesToRemove = pluginInstancesCount - instancesCount;
|
|
625
|
+
for (let i = 0; i < instancesToRemove; i++) {
|
|
626
|
+
const data = body._pluginDataInstances.pop();
|
|
627
|
+
this._destroyBodyData(data);
|
|
628
|
+
}
|
|
629
|
+
this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, true);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
_destroyBodyData(data) {
|
|
633
|
+
if (!data) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
for (const shape of this._shapes) {
|
|
637
|
+
shape?.users.delete(data);
|
|
638
|
+
}
|
|
639
|
+
this._bodyCollisionObservable.delete(data.slot);
|
|
640
|
+
this._bodyCollisionEndedObservable.delete(data.slot);
|
|
641
|
+
this._bodies[data.slot] = undefined;
|
|
642
|
+
this._b3._bx_DestroyBody(data.slot);
|
|
643
|
+
data.slot = 0;
|
|
644
|
+
}
|
|
645
|
+
removeBody(body) {
|
|
646
|
+
// Box3D has no add/remove without destroying. The body is destroyed and re-created by initBody if needed.
|
|
647
|
+
if (body._pluginDataInstances?.length) {
|
|
648
|
+
for (const data of body._pluginDataInstances) {
|
|
649
|
+
this._destroyBodyData(data);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (body._pluginData) {
|
|
653
|
+
this._destroyBodyData(body._pluginData);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
sync(body) {
|
|
657
|
+
this.syncTransform(body, body.transformNode);
|
|
658
|
+
}
|
|
659
|
+
_syncBodyFromBuffer(body, index, buffer, offset) {
|
|
660
|
+
const node = body.transformNode;
|
|
661
|
+
if (body._pluginDataInstances.length) {
|
|
662
|
+
const m = node;
|
|
663
|
+
const matrixData = m._thinInstanceDataStorage?.matrixData;
|
|
664
|
+
if (!matrixData) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const scale = this._tmpVec3[0];
|
|
668
|
+
const base = index * 16;
|
|
669
|
+
scale.set(Math.hypot(matrixData[base], matrixData[base + 1], matrixData[base + 2]), Math.hypot(matrixData[base + 4], matrixData[base + 5], matrixData[base + 6]), Math.hypot(matrixData[base + 8], matrixData[base + 9], matrixData[base + 10]));
|
|
670
|
+
const quat = this._tmpQuat[0];
|
|
671
|
+
quat.set(buffer[offset + 3], buffer[offset + 4], buffer[offset + 5], buffer[offset + 6]);
|
|
672
|
+
const position = this._tmpVec3[1];
|
|
673
|
+
position.set(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
|
|
674
|
+
const matrix = TmpVectors.Matrix[0];
|
|
675
|
+
Matrix.ComposeToRef(scale, quat, position, matrix);
|
|
676
|
+
matrix.copyToArray(matrixData, base);
|
|
677
|
+
this._touchedInstanceMeshes.add(m);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this._applyTransformToNode(node, buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3], buffer[offset + 4], buffer[offset + 5], buffer[offset + 6]);
|
|
681
|
+
}
|
|
682
|
+
_applyTransformToNode(node, px, py, pz, qx, qy, qz, qw) {
|
|
683
|
+
const quat = this._tmpQuat[0];
|
|
684
|
+
quat.set(qx, qy, qz, qw);
|
|
685
|
+
const parent = node.parent;
|
|
686
|
+
if (parent && !parent.getWorldMatrix().isIdentity()) {
|
|
687
|
+
parent.computeWorldMatrix(true);
|
|
688
|
+
const savedScaling = this._tmpVec3[2];
|
|
689
|
+
savedScaling.copyFrom(node.scaling);
|
|
690
|
+
const finalTransform = TmpVectors.Matrix[0];
|
|
691
|
+
const finalTranslation = this._tmpVec3[3];
|
|
692
|
+
finalTranslation.set(px, py, pz);
|
|
693
|
+
Matrix.ComposeToRef(node.absoluteScaling, quat, finalTranslation, finalTransform);
|
|
694
|
+
const parentInverse = TmpVectors.Matrix[1];
|
|
695
|
+
parent.getWorldMatrix().invertToRef(parentInverse);
|
|
696
|
+
const localTransform = TmpVectors.Matrix[2];
|
|
697
|
+
finalTransform.multiplyToRef(parentInverse, localTransform);
|
|
698
|
+
localTransform.decomposeToTransformNode(node);
|
|
699
|
+
node.rotationQuaternion?.normalize();
|
|
700
|
+
node.scaling.copyFrom(savedScaling);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
node.position.set(px, py, pz);
|
|
704
|
+
if (node.rotationQuaternion) {
|
|
705
|
+
node.rotationQuaternion.copyFrom(quat);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
quat.toEulerAnglesToRef(node.rotation);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
syncTransform(body, transformNode) {
|
|
713
|
+
if (body._pluginDataInstances.length) {
|
|
714
|
+
const m = transformNode;
|
|
715
|
+
const matrixData = m._thinInstanceDataStorage?.matrixData;
|
|
716
|
+
if (!matrixData) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
for (let i = 0; i < body._pluginDataInstances.length; i++) {
|
|
720
|
+
const data = body._pluginDataInstances[i];
|
|
721
|
+
this._b3._bx_Body_GetTransform(data.slot);
|
|
722
|
+
this._syncBodyFromBuffer(body, i, this._scratch(), 0);
|
|
723
|
+
}
|
|
724
|
+
m.thinInstanceBufferUpdated("matrix");
|
|
725
|
+
this._touchedInstanceMeshes.delete(m);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const data = body._pluginData;
|
|
729
|
+
if (!data || !data.slot) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
this._b3._bx_Body_GetTransform(data.slot);
|
|
733
|
+
const s = this._scratch();
|
|
734
|
+
this._applyTransformToNode(transformNode, s[0], s[1], s[2], s[3], s[4], s[5], s[6]);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Pushes the transform node's pose into the physics body (called during the pre step).
|
|
738
|
+
* @param body the physics body
|
|
739
|
+
* @param node the transform node to read from
|
|
740
|
+
*/
|
|
741
|
+
setPhysicsBodyTransformation(body, node) {
|
|
742
|
+
const prestep = body.getPrestepType();
|
|
743
|
+
if (prestep == PhysicsPrestepType.TELEPORT) {
|
|
744
|
+
if (body._pluginDataInstances.length > 0) {
|
|
745
|
+
const m = node;
|
|
746
|
+
const matrixData = m._thinInstanceDataStorage?.matrixData;
|
|
747
|
+
if (!matrixData) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
this._createOrUpdateBodyInstances(body, body.getMotionType(), matrixData, 0, body._pluginDataInstances.length, true);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const data = body._pluginData;
|
|
754
|
+
const position = this._tmpVec3[0];
|
|
755
|
+
const orientation = this._tmpQuat[0];
|
|
756
|
+
this._nodeTransformToRef(node, position, orientation);
|
|
757
|
+
this._b3._bx_Body_SetTransform(data.slot, position.x, position.y, position.z, orientation.x, orientation.y, orientation.z, orientation.w);
|
|
758
|
+
}
|
|
759
|
+
else if (prestep == PhysicsPrestepType.ACTION) {
|
|
760
|
+
this.setTargetTransform(body, node.absolutePosition, node.absoluteRotationQuaternion);
|
|
761
|
+
}
|
|
762
|
+
else if (prestep == PhysicsPrestepType.DISABLED) {
|
|
763
|
+
Logger.Warn("Prestep type is set to DISABLED. Unable to set physics body transformation.");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
setTargetTransform(body, position, rotation, instanceIndex) {
|
|
767
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
768
|
+
this._b3._bx_Body_SetTargetTransform(data.slot, position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, this._timeStep);
|
|
769
|
+
}, instanceIndex);
|
|
770
|
+
}
|
|
771
|
+
/** True when the shape puts a mesh inside a container, which Box3D can only do by baking a copy per body. */
|
|
772
|
+
_bakesMeshCopies(data, visited = new Set()) {
|
|
773
|
+
if (visited.has(data)) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
visited.add(data);
|
|
777
|
+
return data.children.some((child) => child.type === PhysicsShapeType.MESH || this._bakesMeshCopies(child, visited));
|
|
778
|
+
}
|
|
779
|
+
_setBodyShape(data, shape) {
|
|
780
|
+
for (const existing of this._shapes) {
|
|
781
|
+
existing?.users.delete(data);
|
|
782
|
+
}
|
|
783
|
+
this._b3._bx_Body_SetShape(data.slot, shape ? shape.slot : 0);
|
|
784
|
+
if (shape) {
|
|
785
|
+
shape.users.add(data);
|
|
786
|
+
}
|
|
787
|
+
this._applyEventFlags(data);
|
|
788
|
+
this._internalUpdateMassProperties(data);
|
|
789
|
+
}
|
|
790
|
+
setShape(body, shape) {
|
|
791
|
+
const shapeData = shape && shape._pluginData ? shape._pluginData : null;
|
|
792
|
+
this._applyToBodyOrInstances(body, (data) => this._setBodyShape(data, shapeData));
|
|
793
|
+
}
|
|
794
|
+
getShape(body) {
|
|
795
|
+
return body.shape ?? null;
|
|
796
|
+
}
|
|
797
|
+
getShapeType(shape) {
|
|
798
|
+
return shape.type ?? shape._pluginData?.type ?? PhysicsShapeType.CONTAINER;
|
|
799
|
+
}
|
|
800
|
+
/** Turns the body's event mask into Box3D's two shape flags, so a body only pays for the events it asked for. */
|
|
801
|
+
_applyEventFlags(data) {
|
|
802
|
+
const touch = data.eventMask & (Box3DEventBits.COLLISION_STARTED | Box3DEventBits.COLLISION_FINISHED) ? 1 : 0;
|
|
803
|
+
const hit = data.eventMask & Box3DEventBits.COLLISION_CONTINUED ? 1 : 0;
|
|
804
|
+
this._b3._bx_Body_SetEventFlags(data.slot, touch, hit);
|
|
805
|
+
}
|
|
806
|
+
setEventMask(body, eventMask, instanceIndex) {
|
|
807
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
808
|
+
data.eventMask = eventMask;
|
|
809
|
+
this._applyEventFlags(data);
|
|
810
|
+
}, instanceIndex);
|
|
811
|
+
}
|
|
812
|
+
getEventMask(body, instanceIndex) {
|
|
813
|
+
return this._getPluginReference(body, instanceIndex).eventMask;
|
|
814
|
+
}
|
|
815
|
+
setMotionType(body, motionType, instanceIndex) {
|
|
816
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
817
|
+
data.motionType = motionType;
|
|
818
|
+
this._applyNativeMotionType(data);
|
|
819
|
+
this._internalUpdateMassProperties(data);
|
|
820
|
+
}, instanceIndex);
|
|
821
|
+
}
|
|
822
|
+
/** The Box3D body type: the Babylon motion type, unless the body is parked by ALWAYS_INACTIVE. */
|
|
823
|
+
_applyNativeMotionType(data) {
|
|
824
|
+
const native = data.activation === PhysicsActivationControl.ALWAYS_INACTIVE ? 0 : this._motionTypeToNative(data.motionType);
|
|
825
|
+
this._b3._bx_Body_SetType(data.slot, native);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Babylon's activation control, matched to Havok's behaviour with Box3D sleeping.
|
|
829
|
+
* ALWAYS_ACTIVE turns sleeping off for the body, SIMULATION_CONTROLLED turns it back on. ALWAYS_INACTIVE parks the
|
|
830
|
+
* body: in Havok such a body ignores impulses and velocity changes, is not woken by anything that touches it and
|
|
831
|
+
* still blocks other bodies, which Box3D sleeping alone cannot do, so it becomes a static Box3D body until the
|
|
832
|
+
* control changes. Coming back it is left asleep, like Havok, until something wakes it.
|
|
833
|
+
* @param body the physics body
|
|
834
|
+
* @param controlMode how the body may be activated and deactivated
|
|
835
|
+
* @param instanceIndex optional thin instance index
|
|
836
|
+
*/
|
|
837
|
+
setActivationControl(body, controlMode, instanceIndex) {
|
|
838
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
839
|
+
if (data.activation === controlMode) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const wasInactive = data.activation === PhysicsActivationControl.ALWAYS_INACTIVE;
|
|
843
|
+
data.activation = controlMode;
|
|
844
|
+
if (controlMode === PhysicsActivationControl.ALWAYS_INACTIVE) {
|
|
845
|
+
this._b3._bx_Body_SetLinearVelocity(data.slot, 0, 0, 0);
|
|
846
|
+
this._b3._bx_Body_SetAngularVelocity(data.slot, 0, 0, 0);
|
|
847
|
+
this._applyNativeMotionType(data);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
this._applyNativeMotionType(data);
|
|
851
|
+
this._internalUpdateMassProperties(data);
|
|
852
|
+
this._b3._bx_Body_EnableSleep(data.slot, controlMode === PhysicsActivationControl.ALWAYS_ACTIVE ? 0 : 1);
|
|
853
|
+
if (wasInactive && controlMode === PhysicsActivationControl.SIMULATION_CONTROLLED) {
|
|
854
|
+
this._b3._bx_Body_SetAwake(data.slot, 0);
|
|
855
|
+
}
|
|
856
|
+
}, instanceIndex);
|
|
857
|
+
}
|
|
858
|
+
getMotionType(body, instanceIndex) {
|
|
859
|
+
return this._getPluginReference(body, instanceIndex).motionType;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Reads Box3D's mass data and converts it to Babylon's form: the inertia is the principal moments of the tensor
|
|
863
|
+
* divided by the mass (Babylon's inertia is per unit mass) and the orientation rotates the principal axes into
|
|
864
|
+
* body space.
|
|
865
|
+
*/
|
|
866
|
+
_readMassData(data) {
|
|
867
|
+
// Box3D keeps mass 0 on static and kinematic bodies; Havok reports what the shapes weigh whatever the type
|
|
868
|
+
if (data.motionType === PhysicsMotionType.DYNAMIC && data.activation !== PhysicsActivationControl.ALWAYS_INACTIVE) {
|
|
869
|
+
this._b3._bx_Body_GetMassData(data.slot);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
this._b3._bx_Body_ComputeShapeMassData(data.slot);
|
|
873
|
+
}
|
|
874
|
+
const s = this._scratch();
|
|
875
|
+
const mass = s[0];
|
|
876
|
+
const principal = new Vector3();
|
|
877
|
+
const orientation = new Quaternion();
|
|
878
|
+
DiagonalizeInertia([s[4], s[5], s[6], s[7], s[8], s[9]], principal, orientation);
|
|
879
|
+
if (mass > 0) {
|
|
880
|
+
principal.scaleInPlace(1 / mass);
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
mass,
|
|
884
|
+
centerOfMass: new Vector3(s[1], s[2], s[3]),
|
|
885
|
+
inertia: principal,
|
|
886
|
+
inertiaOrientation: orientation,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/** The mass properties of the body: what the user asked for, filled in from the shapes where they said nothing. */
|
|
890
|
+
_resolveMassProperties(data) {
|
|
891
|
+
const computed = this._readMassData(data);
|
|
892
|
+
const user = data.userMassProps ?? {};
|
|
893
|
+
// no invented mass here: a body with no shapes really does weigh nothing, only the tensor written to Box3D
|
|
894
|
+
// needs a positive mass (see _internalUpdateMassProperties)
|
|
895
|
+
const mass = user.mass !== undefined && user.mass > 0 ? user.mass : computed.mass;
|
|
896
|
+
return {
|
|
897
|
+
mass,
|
|
898
|
+
centerOfMass: user.centerOfMass ?? computed.centerOfMass,
|
|
899
|
+
inertia: user.inertia ?? computed.inertia,
|
|
900
|
+
inertiaOrientation: user.inertiaOrientation ?? (user.inertia ? Quaternion.Identity() : computed.inertiaOrientation),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
_internalUpdateMassProperties(data) {
|
|
904
|
+
if (data.motionType !== PhysicsMotionType.DYNAMIC || data.activation === PhysicsActivationControl.ALWAYS_INACTIVE) {
|
|
905
|
+
// motion locks are world space velocity locks, they would fight a kinematic body's target transform
|
|
906
|
+
this._applyMotionLocks(data, false, false, false);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
this._b3._bx_Body_ApplyMassFromShapes(data.slot);
|
|
910
|
+
const user = data.userMassProps;
|
|
911
|
+
const hasUser = !!user && (user.mass !== undefined || !!user.centerOfMass || !!user.inertia || !!user.inertiaOrientation);
|
|
912
|
+
if (!hasUser) {
|
|
913
|
+
this._applyMotionLocks(data, false, false, false);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const props = this._resolveMassProperties(data);
|
|
917
|
+
// a dynamic body with no mass at all would ignore gravity and every impulse
|
|
918
|
+
const mass = props.mass > 0 ? props.mass : 1;
|
|
919
|
+
const inertia = props.inertia;
|
|
920
|
+
const orientation = props.inertiaOrientation;
|
|
921
|
+
// Babylon's inertia is per unit mass and a zero component means infinite inertia about that principal axis.
|
|
922
|
+
// Box3D needs an invertible tensor, so a locked axis gets a very large moment; where the free axes stay aligned
|
|
923
|
+
// with the world, Box3D's motion locks are added on top and make it exact.
|
|
924
|
+
const locked = [inertia.x <= 0, inertia.y <= 0, inertia.z <= 0];
|
|
925
|
+
const largest = Math.max(inertia.x, inertia.y, inertia.z, 1e-4) * LockedInertiaRatio;
|
|
926
|
+
const moments = [locked[0] ? largest : inertia.x, locked[1] ? largest : inertia.y, locked[2] ? largest : inertia.z].map((value) => value * mass);
|
|
927
|
+
const tensor = ComposeInertia(moments, orientation);
|
|
928
|
+
this._b3._bx_Body_SetMassDataFull(data.slot, mass, props.centerOfMass.x, props.centerOfMass.y, props.centerOfMass.z, tensor[0], tensor[1], tensor[2], tensor[3], tensor[4], tensor[5]);
|
|
929
|
+
this._applyLockedAxes(data, locked, orientation);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Box3D locks are world space, Babylon's zero inertia components are body local. Locking two of them leaves a single
|
|
933
|
+
* free axis, and if that axis is world aligned the body can only ever spin about it, so the world locks are exact
|
|
934
|
+
* and stay exact. Everything else relies on the large moment above.
|
|
935
|
+
*/
|
|
936
|
+
_applyLockedAxes(data, locked, orientation) {
|
|
937
|
+
const count = locked.filter(Boolean).length;
|
|
938
|
+
if (count === 3) {
|
|
939
|
+
this._applyMotionLocks(data, true, true, true);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (count !== 2) {
|
|
943
|
+
this._applyMotionLocks(data, false, false, false);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
this._b3._bx_Body_GetTransform(data.slot);
|
|
947
|
+
const s = this._scratch();
|
|
948
|
+
const bodyRotation = this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
|
|
949
|
+
const free = locked.indexOf(false);
|
|
950
|
+
const axis = this._tmpVec3[0].set(free === 0 ? 1 : 0, free === 1 ? 1 : 0, free === 2 ? 1 : 0);
|
|
951
|
+
axis.applyRotationQuaternionInPlace(orientation).applyRotationQuaternionInPlace(bodyRotation);
|
|
952
|
+
const components = [Math.abs(axis.x), Math.abs(axis.y), Math.abs(axis.z)];
|
|
953
|
+
const dominant = components.indexOf(Math.max(...components));
|
|
954
|
+
if (components[dominant] > 0.9999) {
|
|
955
|
+
this._applyMotionLocks(data, dominant !== 0, dominant !== 1, dominant !== 2);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
this._applyMotionLocks(data, false, false, false);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
_applyMotionLocks(data, x, y, z) {
|
|
962
|
+
const locks = (x ? 1 : 0) | (y ? 2 : 0) | (z ? 4 : 0);
|
|
963
|
+
if (locks === data.angularLocks) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
data.angularLocks = locks;
|
|
967
|
+
this._b3._bx_Body_SetMotionLocks(data.slot, 0, 0, 0, x ? 1 : 0, y ? 1 : 0, z ? 1 : 0);
|
|
968
|
+
}
|
|
969
|
+
computeMassProperties(body, instanceIndex) {
|
|
970
|
+
const data = this._getPluginReference(body, instanceIndex);
|
|
971
|
+
this._b3._bx_Body_ApplyMassFromShapes(data.slot);
|
|
972
|
+
const computed = this._readMassData(data);
|
|
973
|
+
this._internalUpdateMassProperties(data);
|
|
974
|
+
return computed;
|
|
975
|
+
}
|
|
976
|
+
setMassProperties(body, massProps, instanceIndex) {
|
|
977
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
978
|
+
data.userMassProps = massProps;
|
|
979
|
+
this._internalUpdateMassProperties(data);
|
|
980
|
+
}, instanceIndex);
|
|
981
|
+
}
|
|
982
|
+
getMassProperties(body, instanceIndex) {
|
|
983
|
+
return this._resolveMassProperties(this._getPluginReference(body, instanceIndex));
|
|
984
|
+
}
|
|
985
|
+
setLinearDamping(body, damping, instanceIndex) {
|
|
986
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetLinearDamping(data.slot, damping), instanceIndex);
|
|
987
|
+
}
|
|
988
|
+
getLinearDamping(body, instanceIndex) {
|
|
989
|
+
return this._b3._bx_Body_GetLinearDamping(this._getPluginReference(body, instanceIndex).slot);
|
|
990
|
+
}
|
|
991
|
+
setAngularDamping(body, damping, instanceIndex) {
|
|
992
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetAngularDamping(data.slot, damping), instanceIndex);
|
|
993
|
+
}
|
|
994
|
+
getAngularDamping(body, instanceIndex) {
|
|
995
|
+
return this._b3._bx_Body_GetAngularDamping(this._getPluginReference(body, instanceIndex).slot);
|
|
996
|
+
}
|
|
997
|
+
setLinearVelocity(body, linVel, instanceIndex) {
|
|
998
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetLinearVelocity(data.slot, linVel.x, linVel.y, linVel.z), instanceIndex);
|
|
999
|
+
}
|
|
1000
|
+
getLinearVelocityToRef(body, linVel, instanceIndex) {
|
|
1001
|
+
this._b3._bx_Body_GetLinearVelocity(this._getPluginReference(body, instanceIndex).slot);
|
|
1002
|
+
const s = this._scratch();
|
|
1003
|
+
linVel.set(s[0], s[1], s[2]);
|
|
1004
|
+
}
|
|
1005
|
+
applyImpulse(body, impulse, location, instanceIndex) {
|
|
1006
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyLinearImpulse(data.slot, impulse.x, impulse.y, impulse.z, location.x, location.y, location.z), instanceIndex);
|
|
1007
|
+
}
|
|
1008
|
+
applyAngularImpulse(body, angularImpulse, instanceIndex) {
|
|
1009
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyAngularImpulse(data.slot, angularImpulse.x, angularImpulse.y, angularImpulse.z), instanceIndex);
|
|
1010
|
+
}
|
|
1011
|
+
applyForce(body, force, location, instanceIndex) {
|
|
1012
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyForce(data.slot, force.x, force.y, force.z, location.x, location.y, location.z), instanceIndex);
|
|
1013
|
+
}
|
|
1014
|
+
applyTorque(body, torque, instanceIndex) {
|
|
1015
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyTorque(data.slot, torque.x, torque.y, torque.z), instanceIndex);
|
|
1016
|
+
}
|
|
1017
|
+
setAngularVelocity(body, angVel, instanceIndex) {
|
|
1018
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetAngularVelocity(data.slot, angVel.x, angVel.y, angVel.z), instanceIndex);
|
|
1019
|
+
}
|
|
1020
|
+
getAngularVelocityToRef(body, angVel, instanceIndex) {
|
|
1021
|
+
this._b3._bx_Body_GetAngularVelocity(this._getPluginReference(body, instanceIndex).slot);
|
|
1022
|
+
const s = this._scratch();
|
|
1023
|
+
angVel.set(s[0], s[1], s[2]);
|
|
1024
|
+
}
|
|
1025
|
+
getBodyGeometry(body) {
|
|
1026
|
+
const shape = body.shape?._pluginData;
|
|
1027
|
+
if (!shape) {
|
|
1028
|
+
return { positions: [], indices: [] };
|
|
1029
|
+
}
|
|
1030
|
+
const vertexCount = this._b3._bx_ShapeDesc_BuildDebugGeometry(shape.slot);
|
|
1031
|
+
const indexCount = this._b3._bx_DebugIndexCount();
|
|
1032
|
+
if (!vertexCount || !indexCount) {
|
|
1033
|
+
return { positions: [], indices: [] };
|
|
1034
|
+
}
|
|
1035
|
+
const positions = new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_DebugPositionsPtr(), vertexCount * 3).slice();
|
|
1036
|
+
const indices = new Uint32Array(this._b3.HEAPU32.buffer, this._b3._bx_DebugIndicesPtr(), indexCount).slice();
|
|
1037
|
+
return { positions, indices };
|
|
1038
|
+
}
|
|
1039
|
+
disposeBody(body) {
|
|
1040
|
+
this.removeBody(body);
|
|
1041
|
+
body._pluginData = undefined;
|
|
1042
|
+
body._pluginDataInstances.length = 0;
|
|
1043
|
+
}
|
|
1044
|
+
setCollisionCallbackEnabled(body, enabled, instanceIndex) {
|
|
1045
|
+
// Havok replaces the mask with all three collision events, or clears it
|
|
1046
|
+
this.setEventMask(body, enabled ? Box3DEventBits.ALL : 0, instanceIndex);
|
|
1047
|
+
}
|
|
1048
|
+
setCollisionEndedCallbackEnabled(body, enabled, instanceIndex) {
|
|
1049
|
+
this._applyToBodyOrInstances(body, (data) => {
|
|
1050
|
+
data.eventMask = enabled ? data.eventMask | Box3DEventBits.COLLISION_FINISHED : data.eventMask & ~Box3DEventBits.COLLISION_FINISHED;
|
|
1051
|
+
this._applyEventFlags(data);
|
|
1052
|
+
}, instanceIndex);
|
|
1053
|
+
}
|
|
1054
|
+
getCollisionObservable(body, instanceIndex) {
|
|
1055
|
+
const slot = this._getPluginReference(body, instanceIndex).slot;
|
|
1056
|
+
let observable = this._bodyCollisionObservable.get(slot);
|
|
1057
|
+
if (!observable) {
|
|
1058
|
+
observable = new Observable();
|
|
1059
|
+
this._bodyCollisionObservable.set(slot, observable);
|
|
1060
|
+
}
|
|
1061
|
+
return observable;
|
|
1062
|
+
}
|
|
1063
|
+
getCollisionEndedObservable(body, instanceIndex) {
|
|
1064
|
+
const slot = this._getPluginReference(body, instanceIndex).slot;
|
|
1065
|
+
let observable = this._bodyCollisionEndedObservable.get(slot);
|
|
1066
|
+
if (!observable) {
|
|
1067
|
+
observable = new Observable();
|
|
1068
|
+
this._bodyCollisionEndedObservable.set(slot, observable);
|
|
1069
|
+
}
|
|
1070
|
+
return observable;
|
|
1071
|
+
}
|
|
1072
|
+
setGravityFactor(body, factor, instanceIndex) {
|
|
1073
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetGravityScale(data.slot, factor), instanceIndex);
|
|
1074
|
+
}
|
|
1075
|
+
getGravityFactor(body, instanceIndex) {
|
|
1076
|
+
return this._b3._bx_Body_GetGravityScale(this._getPluginReference(body, instanceIndex).slot);
|
|
1077
|
+
}
|
|
1078
|
+
// ----------------------------------------------------------------------------------------
|
|
1079
|
+
// shapes
|
|
1080
|
+
// ----------------------------------------------------------------------------------------
|
|
1081
|
+
_registerShape(shape, slot, type) {
|
|
1082
|
+
if (!slot) {
|
|
1083
|
+
throw new Error("Box3DPlugin: shape creation failed (degenerate geometry?)");
|
|
1084
|
+
}
|
|
1085
|
+
const data = new Box3DShapeData(slot, type);
|
|
1086
|
+
shape._pluginData = data;
|
|
1087
|
+
this._shapes[slot] = data;
|
|
1088
|
+
return data;
|
|
1089
|
+
}
|
|
1090
|
+
_withFloatBuffer(values, fn) {
|
|
1091
|
+
const ptr = this._b3._malloc(values.length * 4);
|
|
1092
|
+
new Float32Array(this._b3.HEAPF32.buffer, ptr, values.length).set(values);
|
|
1093
|
+
try {
|
|
1094
|
+
return fn(ptr);
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
this._b3._free(ptr);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/** Copies bytes into the module for the length of `fn`; with no bytes, `fn` gets a null pointer. */
|
|
1101
|
+
_withByteBuffer(values, fn) {
|
|
1102
|
+
if (!values || values.length === 0) {
|
|
1103
|
+
return fn(0);
|
|
1104
|
+
}
|
|
1105
|
+
const ptr = this._b3._malloc(values.length);
|
|
1106
|
+
new Uint8Array(this._b3.HEAPU8.buffer, ptr, values.length).set(values);
|
|
1107
|
+
try {
|
|
1108
|
+
return fn(ptr);
|
|
1109
|
+
}
|
|
1110
|
+
finally {
|
|
1111
|
+
this._b3._free(ptr);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
_withIntBuffer(values, fn) {
|
|
1115
|
+
const ptr = this._b3._malloc(values.length * 4);
|
|
1116
|
+
new Int32Array(this._b3.HEAP32.buffer, ptr, values.length).set(values);
|
|
1117
|
+
try {
|
|
1118
|
+
return fn(ptr);
|
|
1119
|
+
}
|
|
1120
|
+
finally {
|
|
1121
|
+
this._b3._free(ptr);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
_createOptionsFromGroundMesh(options) {
|
|
1125
|
+
const mesh = options.groundMesh;
|
|
1126
|
+
if (!mesh) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const pos = mesh.getVerticesData(VertexBuffer.PositionKind);
|
|
1130
|
+
const transform = mesh.computeWorldMatrix(true);
|
|
1131
|
+
const transformed = [];
|
|
1132
|
+
const p = TmpVectors.Vector3[0];
|
|
1133
|
+
for (let i = 0; i < pos.length; i += 3) {
|
|
1134
|
+
Vector3.FromArrayToRef(pos, i, p);
|
|
1135
|
+
Vector3.TransformCoordinatesToRef(p, transform, p);
|
|
1136
|
+
p.toArray(transformed, i);
|
|
1137
|
+
}
|
|
1138
|
+
const arraySize = ~~(Math.sqrt(transformed.length / 3) - 1);
|
|
1139
|
+
const boundingInfo = mesh.getBoundingInfo();
|
|
1140
|
+
const dim = Math.min(boundingInfo.boundingBox.extendSizeWorld.x, boundingInfo.boundingBox.extendSizeWorld.z);
|
|
1141
|
+
const minX = boundingInfo.boundingBox.minimumWorld.x;
|
|
1142
|
+
const minY = boundingInfo.boundingBox.minimumWorld.y;
|
|
1143
|
+
const minZ = boundingInfo.boundingBox.minimumWorld.z;
|
|
1144
|
+
const matrix = new Float32Array((arraySize + 1) * (arraySize + 1));
|
|
1145
|
+
const elementSize = (dim * 2) / arraySize;
|
|
1146
|
+
matrix.fill(minY);
|
|
1147
|
+
for (let i = 0; i < transformed.length; i += 3) {
|
|
1148
|
+
const x = Math.round((transformed[i + 0] - minX) / elementSize);
|
|
1149
|
+
const z = arraySize - Math.round((transformed[i + 2] - minZ) / elementSize);
|
|
1150
|
+
matrix[z * (arraySize + 1) + x] = transformed[i + 1] - minY;
|
|
1151
|
+
}
|
|
1152
|
+
options.numHeightFieldSamplesX = arraySize + 1;
|
|
1153
|
+
options.numHeightFieldSamplesZ = arraySize + 1;
|
|
1154
|
+
options.heightFieldSizeX = boundingInfo.boundingBox.extendSizeWorld.x * 2;
|
|
1155
|
+
options.heightFieldSizeZ = boundingInfo.boundingBox.extendSizeWorld.z * 2;
|
|
1156
|
+
options.heightFieldData = matrix;
|
|
1157
|
+
}
|
|
1158
|
+
initShape(shape, type, options) {
|
|
1159
|
+
const b3 = this._b3;
|
|
1160
|
+
const center = options.center ?? Vector3.ZeroReadOnly;
|
|
1161
|
+
const rotation = options.rotation ?? Quaternion.Identity();
|
|
1162
|
+
switch (type) {
|
|
1163
|
+
case PhysicsShapeType.SPHERE: {
|
|
1164
|
+
const radius = options.radius ?? 1;
|
|
1165
|
+
this._registerShape(shape, b3._bx_ShapeDesc_CreateSphere(center.x, center.y, center.z, radius), type);
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case PhysicsShapeType.CAPSULE: {
|
|
1169
|
+
const a = options.pointA ?? Vector3.ZeroReadOnly;
|
|
1170
|
+
const b = options.pointB ?? Vector3.UpReadOnly;
|
|
1171
|
+
this._registerShape(shape, b3._bx_ShapeDesc_CreateCapsule(a.x, a.y, a.z, b.x, b.y, b.z, options.radius ?? 0), type);
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
case PhysicsShapeType.BOX: {
|
|
1175
|
+
const extents = options.extents ?? Vector3.OneReadOnly;
|
|
1176
|
+
this._registerShape(shape, b3._bx_ShapeDesc_CreateBox(extents.x * 0.5, extents.y * 0.5, extents.z * 0.5, center.x, center.y, center.z, rotation.x, rotation.y, rotation.z, rotation.w), type);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
case PhysicsShapeType.CYLINDER: {
|
|
1180
|
+
const a = options.pointA ?? Vector3.ZeroReadOnly;
|
|
1181
|
+
const b = options.pointB ?? Vector3.UpReadOnly;
|
|
1182
|
+
const axis = b.subtract(a);
|
|
1183
|
+
const height = axis.length();
|
|
1184
|
+
const mid = a.add(b).scaleInPlace(0.5);
|
|
1185
|
+
const q = this._tmpQuat[0];
|
|
1186
|
+
if (height > 1e-6) {
|
|
1187
|
+
axis.scaleInPlace(1 / height);
|
|
1188
|
+
Quaternion.FromUnitVectorsToRef(Vector3.UpReadOnly, axis, q);
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
q.set(0, 0, 0, 1);
|
|
1192
|
+
}
|
|
1193
|
+
this._registerShape(shape, b3._bx_ShapeDesc_CreateCylinder(height, options.radius ?? 0, 16, mid.x, mid.y, mid.z, q.x, q.y, q.z, q.w), type);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
case PhysicsShapeType.CONVEX_HULL:
|
|
1197
|
+
case PhysicsShapeType.MESH: {
|
|
1198
|
+
const mesh = options.mesh;
|
|
1199
|
+
if (!mesh) {
|
|
1200
|
+
throw new Error("No mesh provided to create physics shape.");
|
|
1201
|
+
}
|
|
1202
|
+
const needIndices = type === PhysicsShapeType.MESH;
|
|
1203
|
+
const flipWinding = !mesh.getScene().useRightHandedSystem;
|
|
1204
|
+
const accum = new MeshGeometryAccumulator(needIndices, flipWinding);
|
|
1205
|
+
accum.addNodeMeshes(mesh, !!options.includeChildMeshes);
|
|
1206
|
+
if (type === PhysicsShapeType.CONVEX_HULL) {
|
|
1207
|
+
const slot = this._withFloatBuffer(accum.vertices, (ptr) => b3._bx_ShapeDesc_CreateHull(ptr, accum.vertexCount));
|
|
1208
|
+
this._registerShape(shape, slot, type);
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
const slot = this._withFloatBuffer(accum.vertices, (vptr) => this._withIntBuffer(accum.indices, (iptr) => b3._bx_ShapeDesc_CreateMesh(vptr, accum.vertexCount, iptr, accum.triangleCount, 1, 1, 1, 0, 0)));
|
|
1212
|
+
this._registerShape(shape, slot, type);
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
case PhysicsShapeType.HEIGHTFIELD: {
|
|
1217
|
+
if (options.groundMesh) {
|
|
1218
|
+
this._createOptionsFromGroundMesh(options);
|
|
1219
|
+
}
|
|
1220
|
+
const nx = options.numHeightFieldSamplesX;
|
|
1221
|
+
const nz = options.numHeightFieldSamplesZ;
|
|
1222
|
+
if (!nx || !nz || !options.heightFieldSizeX || !options.heightFieldSizeZ || !options.heightFieldData) {
|
|
1223
|
+
throw new Error("Missing required heightfield parameters");
|
|
1224
|
+
}
|
|
1225
|
+
const heights = new Float32Array(nx * nz);
|
|
1226
|
+
for (let x = 0; x < nx; x++) {
|
|
1227
|
+
for (let z = 0; z < nz; z++) {
|
|
1228
|
+
// box3d rows run along z (index z * countX + x); Babylon data rows are flipped along z
|
|
1229
|
+
heights[z * nx + x] = options.heightFieldData[(nz - 1 - z) * nx + x];
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const scaleX = options.heightFieldSizeX / (nx - 1);
|
|
1233
|
+
const scaleZ = options.heightFieldSizeZ / (nz - 1);
|
|
1234
|
+
const cellsX = nx - 1;
|
|
1235
|
+
const cellsZ = nz - 1;
|
|
1236
|
+
const source = options.heightFieldMaterials;
|
|
1237
|
+
if (source && source.length !== cellsX * cellsZ) {
|
|
1238
|
+
throw new Error(`heightFieldMaterials needs ${cellsX * cellsZ} cells (${cellsX} × ${cellsZ}), got ${source.length}`);
|
|
1239
|
+
}
|
|
1240
|
+
// Cells flip along z the same way the heights do.
|
|
1241
|
+
const materials = source ? new Uint8Array(cellsX * cellsZ) : null;
|
|
1242
|
+
if (source && materials) {
|
|
1243
|
+
for (let x = 0; x < cellsX; x++) {
|
|
1244
|
+
for (let z = 0; z < cellsZ; z++) {
|
|
1245
|
+
materials[z * cellsX + x] = source[(cellsZ - 1 - z) * cellsX + x];
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const slot = this._withFloatBuffer(heights, (heightsPtr) => this._withByteBuffer(materials, (materialsPtr) => b3._bx_ShapeDesc_CreateHeightField(heightsPtr, materialsPtr, nx, nz, scaleX, 1, scaleZ, 0, -options.heightFieldSizeX * 0.5, 0, -options.heightFieldSizeZ * 0.5)));
|
|
1250
|
+
this._registerShape(shape, slot, type);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
case PhysicsShapeType.CONTAINER: {
|
|
1254
|
+
this._registerShape(shape, b3._bx_ShapeDesc_CreateContainer(), type);
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
default:
|
|
1258
|
+
throw new Error("Unsupported Shape Type.");
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// Re-instantiates the shape on every body that uses it (or uses a container holding it). Only needed for geometry
|
|
1262
|
+
// and child changes; filters, materials and density are applied in place, see _syncShapeUsers.
|
|
1263
|
+
_refreshShapeUsers(data, visited = new Set()) {
|
|
1264
|
+
if (visited.has(data)) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
visited.add(data);
|
|
1268
|
+
for (const body of data.users) {
|
|
1269
|
+
this._b3._bx_Body_SetShape(body.slot, data.slot);
|
|
1270
|
+
this._applyEventFlags(body);
|
|
1271
|
+
this._internalUpdateMassProperties(body);
|
|
1272
|
+
}
|
|
1273
|
+
for (const parent of data.parents) {
|
|
1274
|
+
this._refreshShapeUsers(parent, visited);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Pushes changed description properties into the Box3D shapes that are already on the bodies using this shape,
|
|
1279
|
+
* directly or through a container. Rebuilding them instead would drop their contacts and re-apply the body mass.
|
|
1280
|
+
*/
|
|
1281
|
+
_syncShapeUsers(data, what, visited = new Set(), changed = data) {
|
|
1282
|
+
if (visited.has(data)) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
visited.add(data);
|
|
1286
|
+
for (const body of data.users) {
|
|
1287
|
+
this._b3._bx_Body_SyncShapeDesc(body.slot, changed.slot, what);
|
|
1288
|
+
if (what & Box3DShapeSync.DENSITY) {
|
|
1289
|
+
this._internalUpdateMassProperties(body);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
for (const parent of data.parents) {
|
|
1293
|
+
this._syncShapeUsers(parent, what, visited, changed);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
setShapeFilterMembershipMask(shape, membershipMask) {
|
|
1297
|
+
const data = shape._pluginData;
|
|
1298
|
+
const collide = this._b3._bx_ShapeDesc_GetMaskBits(data.slot);
|
|
1299
|
+
this._b3._bx_ShapeDesc_SetFilter(data.slot, membershipMask >>> 0, collide >>> 0);
|
|
1300
|
+
this._syncShapeUsers(data, Box3DShapeSync.FILTER);
|
|
1301
|
+
}
|
|
1302
|
+
getShapeFilterMembershipMask(shape) {
|
|
1303
|
+
return this._b3._bx_ShapeDesc_GetCategoryBits(shape._pluginData.slot);
|
|
1304
|
+
}
|
|
1305
|
+
setShapeFilterCollideMask(shape, collideMask) {
|
|
1306
|
+
const data = shape._pluginData;
|
|
1307
|
+
const membership = this._b3._bx_ShapeDesc_GetCategoryBits(data.slot);
|
|
1308
|
+
this._b3._bx_ShapeDesc_SetFilter(data.slot, membership >>> 0, collideMask >>> 0);
|
|
1309
|
+
this._syncShapeUsers(data, Box3DShapeSync.FILTER);
|
|
1310
|
+
}
|
|
1311
|
+
getShapeFilterCollideMask(shape) {
|
|
1312
|
+
return this._b3._bx_ShapeDesc_GetMaskBits(shape._pluginData.slot);
|
|
1313
|
+
}
|
|
1314
|
+
setMaterial(shape, material) {
|
|
1315
|
+
const data = shape._pluginData;
|
|
1316
|
+
data.material = material;
|
|
1317
|
+
this._b3._bx_ShapeDesc_SetMaterial(data.slot, material.friction ?? 0.5, material.restitution ?? 0);
|
|
1318
|
+
this._syncShapeUsers(data, Box3DShapeSync.MATERIAL);
|
|
1319
|
+
}
|
|
1320
|
+
getMaterial(shape) {
|
|
1321
|
+
const data = shape._pluginData;
|
|
1322
|
+
return {
|
|
1323
|
+
friction: this._b3._bx_ShapeDesc_GetFriction(data.slot),
|
|
1324
|
+
staticFriction: data.material.staticFriction,
|
|
1325
|
+
restitution: this._b3._bx_ShapeDesc_GetRestitution(data.slot),
|
|
1326
|
+
frictionCombine: data.material.frictionCombine,
|
|
1327
|
+
restitutionCombine: data.material.restitutionCombine,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
setDensity(shape, density) {
|
|
1331
|
+
const data = shape._pluginData;
|
|
1332
|
+
this._b3._bx_ShapeDesc_SetDensity(data.slot, density);
|
|
1333
|
+
this._syncShapeUsers(data, Box3DShapeSync.DENSITY);
|
|
1334
|
+
}
|
|
1335
|
+
getDensity(shape) {
|
|
1336
|
+
return this._b3._bx_ShapeDesc_GetDensity(shape._pluginData.slot);
|
|
1337
|
+
}
|
|
1338
|
+
addChild(shape, newChild, translation, rotation, scale) {
|
|
1339
|
+
const parent = shape._pluginData;
|
|
1340
|
+
const child = newChild._pluginData;
|
|
1341
|
+
const t = translation ?? Vector3.ZeroReadOnly;
|
|
1342
|
+
const r = rotation ?? Quaternion.Identity();
|
|
1343
|
+
const s = scale ?? Vector3.OneReadOnly;
|
|
1344
|
+
this._b3._bx_ShapeDesc_AddChild(parent.slot, child.slot, t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z);
|
|
1345
|
+
parent.children.push(child);
|
|
1346
|
+
child.parents.add(parent);
|
|
1347
|
+
this._refreshShapeUsers(parent);
|
|
1348
|
+
}
|
|
1349
|
+
removeChild(shape, childIndex) {
|
|
1350
|
+
const parent = shape._pluginData;
|
|
1351
|
+
this._b3._bx_ShapeDesc_RemoveChild(parent.slot, childIndex);
|
|
1352
|
+
const [child] = parent.children.splice(childIndex, 1);
|
|
1353
|
+
if (child && !parent.children.includes(child)) {
|
|
1354
|
+
child.parents.delete(parent);
|
|
1355
|
+
}
|
|
1356
|
+
this._refreshShapeUsers(parent);
|
|
1357
|
+
}
|
|
1358
|
+
getNumChildren(shape) {
|
|
1359
|
+
return this._b3._bx_ShapeDesc_GetChildCount(shape._pluginData.slot);
|
|
1360
|
+
}
|
|
1361
|
+
getBoundingBox(shape) {
|
|
1362
|
+
this._b3._bx_ShapeDesc_GetAABB(shape._pluginData.slot);
|
|
1363
|
+
const s = this._scratch();
|
|
1364
|
+
TmpVectors.Vector3[0].set(s[0], s[1], s[2]);
|
|
1365
|
+
TmpVectors.Vector3[1].set(s[3], s[4], s[5]);
|
|
1366
|
+
return new BoundingBox(TmpVectors.Vector3[0], TmpVectors.Vector3[1], Matrix.IdentityReadOnly);
|
|
1367
|
+
}
|
|
1368
|
+
getBodyBoundingBox(body) {
|
|
1369
|
+
const data = this._getPluginReference(body);
|
|
1370
|
+
this._b3._bx_Body_GetAABB(data.slot);
|
|
1371
|
+
const s = this._scratch();
|
|
1372
|
+
TmpVectors.Vector3[0].set(s[0], s[1], s[2]);
|
|
1373
|
+
TmpVectors.Vector3[1].set(s[3], s[4], s[5]);
|
|
1374
|
+
return new BoundingBox(TmpVectors.Vector3[0], TmpVectors.Vector3[1], Matrix.IdentityReadOnly);
|
|
1375
|
+
}
|
|
1376
|
+
disposeShape(shape) {
|
|
1377
|
+
const data = shape._pluginData;
|
|
1378
|
+
if (!data) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
for (const parent of data.parents) {
|
|
1382
|
+
const index = parent.children.indexOf(data);
|
|
1383
|
+
if (index >= 0) {
|
|
1384
|
+
parent.children.splice(index, 1);
|
|
1385
|
+
this._b3._bx_ShapeDesc_RemoveChild(parent.slot, index);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const child of data.children) {
|
|
1389
|
+
child.parents.delete(data);
|
|
1390
|
+
}
|
|
1391
|
+
this._shapes[data.slot] = undefined;
|
|
1392
|
+
this._b3._bx_ShapeDesc_Destroy(data.slot);
|
|
1393
|
+
shape._pluginData = undefined;
|
|
1394
|
+
}
|
|
1395
|
+
setTrigger(shape, isTrigger) {
|
|
1396
|
+
const data = shape._pluginData;
|
|
1397
|
+
this._b3._bx_ShapeDesc_SetSensor(data.slot, isTrigger ? 1 : 0);
|
|
1398
|
+
// Box3D decides at creation whether a shape is a sensor, so this one does need a rebuild
|
|
1399
|
+
this._refreshShapeUsers(data);
|
|
1400
|
+
}
|
|
1401
|
+
// ----------------------------------------------------------------------------------------
|
|
1402
|
+
// constraints
|
|
1403
|
+
// ----------------------------------------------------------------------------------------
|
|
1404
|
+
_frameToBuffer(buffer, offset, pivot, xAxis, yAxis, zAxis) {
|
|
1405
|
+
const m = TmpVectors.Matrix[0];
|
|
1406
|
+
Matrix.FromValuesToRef(xAxis.x, xAxis.y, xAxis.z, 0, yAxis.x, yAxis.y, yAxis.z, 0, zAxis.x, zAxis.y, zAxis.z, 0, 0, 0, 0, 1, m);
|
|
1407
|
+
const q = this._tmpQuat[2];
|
|
1408
|
+
Quaternion.FromRotationMatrixToRef(m, q);
|
|
1409
|
+
q.normalize();
|
|
1410
|
+
buffer[offset] = pivot.x;
|
|
1411
|
+
buffer[offset + 1] = pivot.y;
|
|
1412
|
+
buffer[offset + 2] = pivot.z;
|
|
1413
|
+
buffer[offset + 3] = q.x;
|
|
1414
|
+
buffer[offset + 4] = q.y;
|
|
1415
|
+
buffer[offset + 5] = q.z;
|
|
1416
|
+
buffer[offset + 6] = q.w;
|
|
1417
|
+
}
|
|
1418
|
+
_buildFrames(cdata, constraint, bodyA, bodyB) {
|
|
1419
|
+
const options = constraint.options;
|
|
1420
|
+
const pivotA = options.pivotA ?? Vector3.ZeroReadOnly;
|
|
1421
|
+
const pivotB = options.pivotB ?? Vector3.ZeroReadOnly;
|
|
1422
|
+
const axisA = (options.axisA ?? Vector3.RightReadOnly).normalizeToNew();
|
|
1423
|
+
const axisB = (options.axisB ?? Vector3.RightReadOnly).normalizeToNew();
|
|
1424
|
+
const perpA = options.perpAxisA ? options.perpAxisA.normalizeToNew() : axisA.getNormalToRef(new Vector3());
|
|
1425
|
+
const perpB = options.perpAxisB ? options.perpAxisB.normalizeToNew() : axisB.getNormalToRef(new Vector3());
|
|
1426
|
+
// Gram-Schmidt in case the user's perp axis is not exactly perpendicular
|
|
1427
|
+
perpA.subtractInPlace(axisA.scale(Vector3.Dot(axisA, perpA))).normalize();
|
|
1428
|
+
perpB.subtractInPlace(axisB.scale(Vector3.Dot(axisB, perpB))).normalize();
|
|
1429
|
+
const thirdA = Vector3.Cross(axisA, perpA);
|
|
1430
|
+
const thirdB = Vector3.Cross(axisB, perpB);
|
|
1431
|
+
if (!constraint._initOptions) {
|
|
1432
|
+
constraint._initOptions = {
|
|
1433
|
+
axisA: axisA.clone(),
|
|
1434
|
+
axisB: axisB.clone(),
|
|
1435
|
+
perpAxisA: perpA.clone(),
|
|
1436
|
+
perpAxisB: perpB.clone(),
|
|
1437
|
+
pivotA: pivotA.clone(),
|
|
1438
|
+
pivotB: pivotB.clone(),
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const frames = cdata.frames;
|
|
1442
|
+
if (cdata.plan) {
|
|
1443
|
+
// SIX_DOF: the constraint basis is Havok's [axis, perpAxis, axis x perpAxis]; the plan picks which of them
|
|
1444
|
+
// becomes Box3D's frame x, y and z (cyclic permutations, so the frames stay right handed)
|
|
1445
|
+
const basis = cdata.plan.basis;
|
|
1446
|
+
if (basis) {
|
|
1447
|
+
const basisA = [axisA, perpA, thirdA];
|
|
1448
|
+
const basisB = [axisB, perpB, thirdB];
|
|
1449
|
+
this._frameToBuffer(frames, 0, pivotA, basisA[basis[0]], basisA[basis[1]], basisA[basis[2]]);
|
|
1450
|
+
this._frameToBuffer(frames, 7, pivotB, basisB[basis[0]], basisB[basis[1]], basisB[basis[2]]);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, 0, 0, 0, 1]);
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
switch (cdata.type) {
|
|
1458
|
+
case PhysicsConstraintType.HINGE:
|
|
1459
|
+
case PhysicsConstraintType.BALL_AND_SOCKET:
|
|
1460
|
+
// box3d: revolute rotates about the frame z axis, spherical cone is centered on frame A z
|
|
1461
|
+
this._frameToBuffer(frames, 0, pivotA, perpA, Vector3.Cross(axisA, perpA), axisA);
|
|
1462
|
+
this._frameToBuffer(frames, 7, pivotB, perpB, Vector3.Cross(axisB, perpB), axisB);
|
|
1463
|
+
break;
|
|
1464
|
+
case PhysicsConstraintType.PRISMATIC:
|
|
1465
|
+
case PhysicsConstraintType.SLIDER:
|
|
1466
|
+
// box3d: prismatic slides along the frame A x axis
|
|
1467
|
+
this._frameToBuffer(frames, 0, pivotA, axisA, perpA, thirdA);
|
|
1468
|
+
this._frameToBuffer(frames, 7, pivotB, axisB, perpB, thirdB);
|
|
1469
|
+
break;
|
|
1470
|
+
case PhysicsConstraintType.LOCK: {
|
|
1471
|
+
// weld the bodies in their current relative pose
|
|
1472
|
+
this._b3._bx_Body_GetTransform(bodyA.slot);
|
|
1473
|
+
let s = this._scratch();
|
|
1474
|
+
const qA = this._tmpQuat[0];
|
|
1475
|
+
qA.set(s[3], s[4], s[5], s[6]);
|
|
1476
|
+
this._b3._bx_Body_GetTransform(bodyB.slot);
|
|
1477
|
+
s = this._scratch();
|
|
1478
|
+
const qB = this._tmpQuat[1];
|
|
1479
|
+
qB.set(s[3], s[4], s[5], s[6]);
|
|
1480
|
+
// frameB rotation = inv(qB) * qA so both frames coincide in world space now
|
|
1481
|
+
const rel = qB.invert().multiply(qA).normalize();
|
|
1482
|
+
frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, rel.x, rel.y, rel.z, rel.w]);
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
default:
|
|
1486
|
+
frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, 0, 0, 0, 1]);
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
_createJoint(cdata, bodyA, bodyB) {
|
|
1491
|
+
this._scratch().set(cdata.frames);
|
|
1492
|
+
const joint = this._b3._bx_CreateJoint(this._worldSlot, cdata.jointType, bodyA.slot, bodyB.slot, cdata.collisions ? 1 : 0, cdata.length);
|
|
1493
|
+
if (!joint) {
|
|
1494
|
+
Logger.Warn("Box3DPlugin: joint creation failed.");
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
// A slider allows rotation about the axis in Havok. Box3D's prismatic locks it.
|
|
1498
|
+
this._applyAxisState(cdata, joint, bodyA, bodyB);
|
|
1499
|
+
return joint;
|
|
1500
|
+
}
|
|
1501
|
+
/** Reads Physics6DoFConstraint.limits into the axis table with Havok's rules. */
|
|
1502
|
+
_readSixDofLimits(cdata, limits) {
|
|
1503
|
+
for (const limit of limits ?? []) {
|
|
1504
|
+
const state = this._axisState(cdata, limit.axis);
|
|
1505
|
+
if (limit.minLimit === 0 && limit.maxLimit === 0) {
|
|
1506
|
+
state.mode = PhysicsConstraintAxisLimitMode.LOCKED;
|
|
1507
|
+
state.min = 0;
|
|
1508
|
+
state.max = 0;
|
|
1509
|
+
}
|
|
1510
|
+
else if (limit.minLimit !== undefined || limit.maxLimit !== undefined) {
|
|
1511
|
+
// a missing side is unbounded
|
|
1512
|
+
state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
|
|
1513
|
+
state.min = limit.minLimit ?? -Infinity;
|
|
1514
|
+
state.max = limit.maxLimit ?? Infinity;
|
|
1515
|
+
}
|
|
1516
|
+
state.stiffness = limit.stiffness ?? 0;
|
|
1517
|
+
state.damping = limit.damping ?? 0;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Maps the six axes of a SIX_DOF constraint onto one Box3D joint, following Havok's semantics: an axis that is not
|
|
1522
|
+
* listed is free, 0/0 is locked, anything else is limited. The constraint frame is [axis, perpAxis, axis x perpAxis];
|
|
1523
|
+
* ANGULAR_X rotates about axis, ANGULAR_Y about perpAxis and ANGULAR_Z about axis x perpAxis, all measured as the
|
|
1524
|
+
* rotation of the child frame relative to the parent frame (right handed, like Havok).
|
|
1525
|
+
* - all linear locked, all angular locked: weld
|
|
1526
|
+
* - all linear locked, one angular axis open: revolute about it, with its limits (one sided ranges work)
|
|
1527
|
+
* - all linear locked, two or three angular axes open: spherical, twist limits from ANGULAR_X and one symmetric
|
|
1528
|
+
* cone from ANGULAR_Y/Z (Box3D has no elliptical cone, the larger swing range is used)
|
|
1529
|
+
* - two linear locked, angular locked: prismatic along the open linear axis
|
|
1530
|
+
* - only LINEAR_DISTANCE constrained: distance joint (a spring when min == max and a stiffness is set)
|
|
1531
|
+
* - nothing constrained: filter joint (collision filtering only)
|
|
1532
|
+
* Anything else warns once and uses the closest of these.
|
|
1533
|
+
*/
|
|
1534
|
+
_planSixDof(cdata) {
|
|
1535
|
+
const Axis = PhysicsConstraintAxis;
|
|
1536
|
+
const Mode = PhysicsConstraintAxisLimitMode;
|
|
1537
|
+
const mode = (axis) => cdata.axes.get(axis)?.mode ?? Mode.FREE;
|
|
1538
|
+
const range = (axis) => {
|
|
1539
|
+
const state = cdata.axes.get(axis);
|
|
1540
|
+
return state.mode === Mode.LOCKED ? [0, 0] : [Math.min(state.min, state.max), Math.max(state.min, state.max)];
|
|
1541
|
+
};
|
|
1542
|
+
const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
|
|
1543
|
+
const maxAngle = 0.99 * Math.PI;
|
|
1544
|
+
// Box3D's B3_HUGE, keeps an unbounded side of a linear range out of the solver as a real number
|
|
1545
|
+
const maxLength = 100000;
|
|
1546
|
+
const plan = {
|
|
1547
|
+
jointType: Box3DJointType.WELD,
|
|
1548
|
+
basis: [0, 1, 2],
|
|
1549
|
+
primaryAxis: null,
|
|
1550
|
+
coneAngle: -1,
|
|
1551
|
+
twist: null,
|
|
1552
|
+
limit: null,
|
|
1553
|
+
spring: false,
|
|
1554
|
+
stiffness: 0,
|
|
1555
|
+
damping: 0,
|
|
1556
|
+
angularStiffness: false,
|
|
1557
|
+
};
|
|
1558
|
+
for (const [axis, state] of cdata.axes) {
|
|
1559
|
+
if ((state.stiffness > 0 || state.damping > 0) && state.stiffness >= plan.stiffness) {
|
|
1560
|
+
plan.stiffness = state.stiffness;
|
|
1561
|
+
plan.damping = state.damping;
|
|
1562
|
+
plan.angularStiffness = axis === Axis.ANGULAR_X || axis === Axis.ANGULAR_Y || axis === Axis.ANGULAR_Z;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const linear = [Axis.LINEAR_X, Axis.LINEAR_Y, Axis.LINEAR_Z];
|
|
1566
|
+
const angular = [Axis.ANGULAR_X, Axis.ANGULAR_Y, Axis.ANGULAR_Z];
|
|
1567
|
+
const linearLocked = linear.filter((a) => mode(a) === Mode.LOCKED).length;
|
|
1568
|
+
const linearFree = linear.filter((a) => mode(a) === Mode.FREE).length;
|
|
1569
|
+
const angularOpen = angular.filter((a) => mode(a) !== Mode.LOCKED);
|
|
1570
|
+
const angularFree = angular.filter((a) => mode(a) === Mode.FREE).length;
|
|
1571
|
+
const distanceMode = mode(Axis.LINEAR_DISTANCE);
|
|
1572
|
+
if (distanceMode !== Mode.FREE) {
|
|
1573
|
+
if (linearFree === 3 && angularFree === 3) {
|
|
1574
|
+
const [lower, upper] = range(Axis.LINEAR_DISTANCE);
|
|
1575
|
+
plan.jointType = Box3DJointType.DISTANCE;
|
|
1576
|
+
plan.basis = null;
|
|
1577
|
+
plan.primaryAxis = Axis.LINEAR_DISTANCE;
|
|
1578
|
+
plan.limit = [Math.min(Math.max(0, lower), maxLength), Math.min(Math.max(0, upper), maxLength)];
|
|
1579
|
+
plan.spring = plan.stiffness > 0 && Math.abs(upper - lower) < 1e-6 && isFinite(lower);
|
|
1580
|
+
return plan;
|
|
1581
|
+
}
|
|
1582
|
+
this._warnOnce("sixdof-distance", "SIX_DOF: LINEAR_DISTANCE combined with other constrained axes is not supported with Box3D, the distance limit is ignored.");
|
|
1583
|
+
}
|
|
1584
|
+
// one open linear axis (free or limited) with the other two locked slides; see the prismatic case below
|
|
1585
|
+
let pointJoint = linearLocked === 3;
|
|
1586
|
+
if (!pointJoint && linearLocked !== 2 && linearFree === 0) {
|
|
1587
|
+
this._warnOnce("sixdof-linear-limited", "SIX_DOF: two or more limited linear axes are treated as locked with Box3D.");
|
|
1588
|
+
pointJoint = true;
|
|
1589
|
+
}
|
|
1590
|
+
if (pointJoint) {
|
|
1591
|
+
if (angularOpen.length === 0) {
|
|
1592
|
+
return plan;
|
|
1593
|
+
}
|
|
1594
|
+
if (angularOpen.length === 1) {
|
|
1595
|
+
// Box3D revolutes rotate about frame z
|
|
1596
|
+
const axis = angularOpen[0];
|
|
1597
|
+
plan.jointType = Box3DJointType.REVOLUTE;
|
|
1598
|
+
plan.basis = [
|
|
1599
|
+
[1, 2, 0],
|
|
1600
|
+
[2, 0, 1],
|
|
1601
|
+
[0, 1, 2],
|
|
1602
|
+
][angular.indexOf(axis)];
|
|
1603
|
+
plan.primaryAxis = axis;
|
|
1604
|
+
if (mode(axis) === Mode.LIMITED) {
|
|
1605
|
+
const [lower, upper] = range(axis);
|
|
1606
|
+
plan.limit = [clamp(lower, maxAngle), clamp(upper, maxAngle)];
|
|
1607
|
+
}
|
|
1608
|
+
return plan;
|
|
1609
|
+
}
|
|
1610
|
+
// spherical: cone centered on frame z (axis), twist about frame z
|
|
1611
|
+
plan.jointType = Box3DJointType.SPHERICAL;
|
|
1612
|
+
plan.basis = [1, 2, 0];
|
|
1613
|
+
if (mode(Axis.ANGULAR_X) !== Mode.FREE) {
|
|
1614
|
+
const [lower, upper] = range(Axis.ANGULAR_X);
|
|
1615
|
+
plan.twist = [clamp(lower, maxAngle), clamp(upper, maxAngle)];
|
|
1616
|
+
}
|
|
1617
|
+
const swingModes = [mode(Axis.ANGULAR_Y), mode(Axis.ANGULAR_Z)];
|
|
1618
|
+
if (swingModes.some((m) => m !== Mode.FREE)) {
|
|
1619
|
+
const extents = [];
|
|
1620
|
+
let asymmetric = false;
|
|
1621
|
+
for (const axis of [Axis.ANGULAR_Y, Axis.ANGULAR_Z]) {
|
|
1622
|
+
if (mode(axis) === Mode.FREE) {
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const [lower, upper] = range(axis);
|
|
1626
|
+
extents.push(Math.max(Math.abs(lower), Math.abs(upper)));
|
|
1627
|
+
asymmetric || (asymmetric = Math.abs(lower + upper) > 1e-4);
|
|
1628
|
+
}
|
|
1629
|
+
const cone = Math.max(...extents);
|
|
1630
|
+
if (isFinite(cone)) {
|
|
1631
|
+
if (swingModes.includes(Mode.FREE) || Math.abs(extents[0] - extents[1]) > 1e-4 || asymmetric) {
|
|
1632
|
+
this._warnOnce("sixdof-cone", "SIX_DOF: Box3D has one symmetric swing cone, ANGULAR_Y and ANGULAR_Z use the larger of their ranges.");
|
|
1633
|
+
}
|
|
1634
|
+
if (cone > Math.PI / 2) {
|
|
1635
|
+
this._warnOnce("sixdof-cone-90", "SIX_DOF: Box3D cone limits are at most 90 degrees, larger swing ranges are clamped.");
|
|
1636
|
+
}
|
|
1637
|
+
plan.coneAngle = Math.min(cone, Math.PI / 2);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return plan;
|
|
1641
|
+
}
|
|
1642
|
+
if (linearLocked === 2) {
|
|
1643
|
+
// Box3D prismatics slide along frame x
|
|
1644
|
+
const slide = linear.find((a) => mode(a) !== Mode.LOCKED);
|
|
1645
|
+
plan.jointType = Box3DJointType.PRISMATIC;
|
|
1646
|
+
plan.basis = [
|
|
1647
|
+
[0, 1, 2],
|
|
1648
|
+
[1, 2, 0],
|
|
1649
|
+
[2, 0, 1],
|
|
1650
|
+
][linear.indexOf(slide)];
|
|
1651
|
+
plan.primaryAxis = slide;
|
|
1652
|
+
if (mode(slide) === Mode.LIMITED) {
|
|
1653
|
+
const [lower, upper] = range(slide);
|
|
1654
|
+
plan.limit = [clamp(lower, maxLength), clamp(upper, maxLength)];
|
|
1655
|
+
}
|
|
1656
|
+
if (angularOpen.length) {
|
|
1657
|
+
this._warnOnce("sixdof-prismatic", "SIX_DOF: a sliding axis with rotation is not supported with Box3D, rotation is locked.");
|
|
1658
|
+
}
|
|
1659
|
+
return plan;
|
|
1660
|
+
}
|
|
1661
|
+
plan.jointType = Box3DJointType.FILTER;
|
|
1662
|
+
plan.basis = null;
|
|
1663
|
+
if (linearFree !== 3 || angularFree !== 3) {
|
|
1664
|
+
this._warnOnce("sixdof-unsupported", "SIX_DOF: this combination of free and limited axes is not supported with Box3D, the bodies are left unconstrained.");
|
|
1665
|
+
}
|
|
1666
|
+
return plan;
|
|
1667
|
+
}
|
|
1668
|
+
/** Box3D spring settings for a Havok spring constant and damping on a body pair. */
|
|
1669
|
+
_springToHertz(stiffness, damping, angular, bodyA, bodyB) {
|
|
1670
|
+
// effective mass (or rotational inertia) of the pair, a static or animated body counts as infinitely heavy
|
|
1671
|
+
const inverse = (data) => {
|
|
1672
|
+
if (data.motionType !== PhysicsMotionType.DYNAMIC) {
|
|
1673
|
+
return 0;
|
|
1674
|
+
}
|
|
1675
|
+
this._b3._bx_Body_GetMassData(data.slot);
|
|
1676
|
+
const s = this._scratch();
|
|
1677
|
+
const value = angular ? (s[4] + s[5] + s[6]) / 3 : s[0];
|
|
1678
|
+
return value > 0 ? 1 / value : 0;
|
|
1679
|
+
};
|
|
1680
|
+
const inverseSum = inverse(bodyA) + inverse(bodyB);
|
|
1681
|
+
const mass = inverseSum > 0 ? 1 / inverseSum : 1;
|
|
1682
|
+
const k = Math.max(stiffness, 0);
|
|
1683
|
+
return {
|
|
1684
|
+
hertz: Math.sqrt(k / mass) / (2 * Math.PI),
|
|
1685
|
+
dampingRatio: k > 0 ? damping / (2 * Math.sqrt(k * mass)) : 1,
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
initConstraint(constraint, body, childBody, instanceIndex, childInstanceIndex) {
|
|
1689
|
+
const type = constraint.type;
|
|
1690
|
+
const options = constraint.options;
|
|
1691
|
+
if (!type || !options) {
|
|
1692
|
+
Logger.Warn("No constraint type or options. Constraint is invalid.");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if ((body._pluginDataInstances.length > 0 && instanceIndex === undefined) || (childBody._pluginDataInstances.length > 0 && childInstanceIndex === undefined)) {
|
|
1696
|
+
Logger.Warn("Body is instanced but no instance index was specified. Constraint will not be applied.");
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const firstPair = !constraint._pluginData;
|
|
1700
|
+
const cdata = constraint._pluginData ?? new Box3DConstraintData();
|
|
1701
|
+
constraint._pluginData = cdata;
|
|
1702
|
+
cdata.type = type;
|
|
1703
|
+
if (firstPair) {
|
|
1704
|
+
cdata.collisions = !!options.collision;
|
|
1705
|
+
}
|
|
1706
|
+
switch (type) {
|
|
1707
|
+
case PhysicsConstraintType.BALL_AND_SOCKET:
|
|
1708
|
+
cdata.jointType = Box3DJointType.SPHERICAL;
|
|
1709
|
+
break;
|
|
1710
|
+
case PhysicsConstraintType.HINGE:
|
|
1711
|
+
cdata.jointType = Box3DJointType.REVOLUTE;
|
|
1712
|
+
break;
|
|
1713
|
+
case PhysicsConstraintType.PRISMATIC:
|
|
1714
|
+
case PhysicsConstraintType.SLIDER:
|
|
1715
|
+
cdata.jointType = Box3DJointType.PRISMATIC;
|
|
1716
|
+
break;
|
|
1717
|
+
case PhysicsConstraintType.LOCK:
|
|
1718
|
+
cdata.jointType = Box3DJointType.WELD;
|
|
1719
|
+
break;
|
|
1720
|
+
case PhysicsConstraintType.DISTANCE:
|
|
1721
|
+
cdata.jointType = Box3DJointType.DISTANCE;
|
|
1722
|
+
break;
|
|
1723
|
+
case PhysicsConstraintType.SIX_DOF:
|
|
1724
|
+
// later pairs of the same constraint share the axis table, including changes made since the first pair
|
|
1725
|
+
if (firstPair) {
|
|
1726
|
+
this._readSixDofLimits(cdata, constraint.limits);
|
|
1727
|
+
}
|
|
1728
|
+
cdata.plan = this._planSixDof(cdata);
|
|
1729
|
+
cdata.jointType = cdata.plan.jointType;
|
|
1730
|
+
break;
|
|
1731
|
+
default:
|
|
1732
|
+
Logger.Warn("Box3DPlugin: unknown constraint type " + type);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const bodyA = this._getPluginReference(body, instanceIndex);
|
|
1736
|
+
const bodyB = this._getPluginReference(childBody, childInstanceIndex);
|
|
1737
|
+
this._buildFrames(cdata, constraint, bodyA, bodyB);
|
|
1738
|
+
this._updateJointLength(cdata, options.maxDistance, bodyA, bodyB);
|
|
1739
|
+
// joints[i] belongs to pairs[i], 0 while the constraint is disabled or when creation failed
|
|
1740
|
+
const joint = cdata.enabled ? this._createJoint(cdata, bodyA, bodyB) : 0;
|
|
1741
|
+
cdata.joints.push(joint);
|
|
1742
|
+
cdata.pairs.push({ parent: body, parentIndex: instanceIndex ?? 0, child: childBody, childIndex: childInstanceIndex ?? 0, parentData: bodyA, childData: bodyB });
|
|
1743
|
+
}
|
|
1744
|
+
/** Length parameter passed to Box3D's distance joint at creation (the rest length of its spring). */
|
|
1745
|
+
_updateJointLength(cdata, maxDistance, bodyA, bodyB) {
|
|
1746
|
+
if (cdata.type === PhysicsConstraintType.DISTANCE) {
|
|
1747
|
+
cdata.length = maxDistance ?? 0;
|
|
1748
|
+
}
|
|
1749
|
+
else if (cdata.plan?.jointType === Box3DJointType.DISTANCE && cdata.plan.limit) {
|
|
1750
|
+
// a spring rests at its (single) limit; for a rope the rest length is unused, its limits do the work
|
|
1751
|
+
cdata.length = cdata.plan.limit[0];
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (cdata.length <= 0) {
|
|
1757
|
+
// use the current distance between the world space pivots
|
|
1758
|
+
cdata.length = Math.max(0.01, this._currentPivotDistance(cdata, bodyA, bodyB));
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
_currentPivotDistance(cdata, bodyA, bodyB) {
|
|
1762
|
+
const f = cdata.frames;
|
|
1763
|
+
const worldA = this._tmpVec3[0];
|
|
1764
|
+
const worldB = this._tmpVec3[1];
|
|
1765
|
+
this._b3._bx_Body_GetTransform(bodyA.slot);
|
|
1766
|
+
let s = this._scratch();
|
|
1767
|
+
this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
|
|
1768
|
+
worldA.set(f[0], f[1], f[2]).applyRotationQuaternionInPlace(this._tmpQuat[0]).addInPlaceFromFloats(s[0], s[1], s[2]);
|
|
1769
|
+
this._b3._bx_Body_GetTransform(bodyB.slot);
|
|
1770
|
+
s = this._scratch();
|
|
1771
|
+
this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
|
|
1772
|
+
worldB.set(f[7], f[8], f[9]).applyRotationQuaternionInPlace(this._tmpQuat[0]).addInPlaceFromFloats(s[0], s[1], s[2]);
|
|
1773
|
+
return Vector3.Distance(worldA, worldB);
|
|
1774
|
+
}
|
|
1775
|
+
setEnabled(constraint, isEnabled) {
|
|
1776
|
+
const cdata = constraint._pluginData;
|
|
1777
|
+
if (!cdata || cdata.enabled === isEnabled) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
cdata.enabled = isEnabled;
|
|
1781
|
+
// Box3D has no enable flag on joints: destroy them, or re-create them from the saved frames, plan and axis state.
|
|
1782
|
+
this._recreateJoints(cdata);
|
|
1783
|
+
}
|
|
1784
|
+
_destroyJoint(joint) {
|
|
1785
|
+
this._motorJoints.delete(joint);
|
|
1786
|
+
this._b3._bx_DestroyJoint(joint);
|
|
1787
|
+
}
|
|
1788
|
+
_recreateJoints(cdata) {
|
|
1789
|
+
for (let i = 0; i < cdata.pairs.length; i++) {
|
|
1790
|
+
if (cdata.joints[i]) {
|
|
1791
|
+
this._destroyJoint(cdata.joints[i]);
|
|
1792
|
+
}
|
|
1793
|
+
const pair = cdata.pairs[i];
|
|
1794
|
+
cdata.joints[i] = cdata.enabled && pair.parentData.slot && pair.childData.slot ? this._createJoint(cdata, pair.parentData, pair.childData) : 0;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
getEnabled(constraint) {
|
|
1798
|
+
return constraint._pluginData?.enabled ?? false;
|
|
1799
|
+
}
|
|
1800
|
+
setCollisionsEnabled(constraint, isEnabled) {
|
|
1801
|
+
const cdata = constraint._pluginData;
|
|
1802
|
+
cdata.collisions = isEnabled;
|
|
1803
|
+
for (const joint of cdata.joints) {
|
|
1804
|
+
if (joint) {
|
|
1805
|
+
this._b3._bx_Joint_SetCollideConnected(joint, isEnabled ? 1 : 0);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
getCollisionsEnabled(constraint) {
|
|
1810
|
+
return constraint._pluginData?.collisions ?? false;
|
|
1811
|
+
}
|
|
1812
|
+
// The one axis box3d exposes for this constraint type.
|
|
1813
|
+
_primaryAxis(cdata) {
|
|
1814
|
+
switch (cdata.type) {
|
|
1815
|
+
case PhysicsConstraintType.HINGE:
|
|
1816
|
+
return PhysicsConstraintAxis.ANGULAR_X;
|
|
1817
|
+
case PhysicsConstraintType.PRISMATIC:
|
|
1818
|
+
case PhysicsConstraintType.SLIDER:
|
|
1819
|
+
return PhysicsConstraintAxis.LINEAR_X;
|
|
1820
|
+
case PhysicsConstraintType.DISTANCE:
|
|
1821
|
+
return PhysicsConstraintAxis.LINEAR_DISTANCE;
|
|
1822
|
+
case PhysicsConstraintType.BALL_AND_SOCKET:
|
|
1823
|
+
return PhysicsConstraintAxis.ANGULAR_Y;
|
|
1824
|
+
case PhysicsConstraintType.SIX_DOF:
|
|
1825
|
+
return cdata.plan?.primaryAxis ?? null;
|
|
1826
|
+
default:
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
_isAngularAxis(axis) {
|
|
1831
|
+
return axis === PhysicsConstraintAxis.ANGULAR_X || axis === PhysicsConstraintAxis.ANGULAR_Y || axis === PhysicsConstraintAxis.ANGULAR_Z;
|
|
1832
|
+
}
|
|
1833
|
+
_isPrimaryAxis(cdata, axis) {
|
|
1834
|
+
if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET || cdata.plan?.jointType === Box3DJointType.SPHERICAL) {
|
|
1835
|
+
return this._isAngularAxis(axis);
|
|
1836
|
+
}
|
|
1837
|
+
return axis === this._primaryAxis(cdata);
|
|
1838
|
+
}
|
|
1839
|
+
_axisState(cdata, axis) {
|
|
1840
|
+
let state = cdata.axes.get(axis);
|
|
1841
|
+
if (!state) {
|
|
1842
|
+
state = {
|
|
1843
|
+
mode: PhysicsConstraintAxisLimitMode.FREE,
|
|
1844
|
+
min: 0,
|
|
1845
|
+
max: 0,
|
|
1846
|
+
motor: PhysicsConstraintMotorType.NONE,
|
|
1847
|
+
target: 0,
|
|
1848
|
+
maxForce: 0,
|
|
1849
|
+
friction: 0,
|
|
1850
|
+
stiffness: 0,
|
|
1851
|
+
damping: 0,
|
|
1852
|
+
};
|
|
1853
|
+
cdata.axes.set(axis, state);
|
|
1854
|
+
}
|
|
1855
|
+
return state;
|
|
1856
|
+
}
|
|
1857
|
+
_applyAxisState(cdata, joint, bodyA, bodyB) {
|
|
1858
|
+
const b3 = this._b3;
|
|
1859
|
+
if (cdata.plan) {
|
|
1860
|
+
this._applySixDofPlan(cdata, joint, bodyA, bodyB);
|
|
1861
|
+
b3._bx_Joint_WakeBodies(joint);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const primary = this._primaryAxis(cdata);
|
|
1865
|
+
if (primary === null) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
for (const [axis, state] of cdata.axes) {
|
|
1869
|
+
if (!this._isPrimaryAxis(cdata, axis)) {
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET) {
|
|
1873
|
+
if (axis === PhysicsConstraintAxis.ANGULAR_X) {
|
|
1874
|
+
// twist about the primary axis (frame z in box3d)
|
|
1875
|
+
b3._bx_Joint_SetTwistLimits(joint, state.mode === PhysicsConstraintAxisLimitMode.FREE ? 0 : 1, state.min, state.max);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const limited = state.mode !== PhysicsConstraintAxisLimitMode.FREE;
|
|
1880
|
+
if (limited) {
|
|
1881
|
+
const min = state.mode === PhysicsConstraintAxisLimitMode.LOCKED ? 0 : state.min;
|
|
1882
|
+
const max = state.mode === PhysicsConstraintAxisLimitMode.LOCKED ? 0 : state.max;
|
|
1883
|
+
b3._bx_Joint_SetLimits(joint, min, max);
|
|
1884
|
+
}
|
|
1885
|
+
b3._bx_Joint_EnableLimit(joint, limited ? 1 : 0);
|
|
1886
|
+
if (cdata.type !== PhysicsConstraintType.BALL_AND_SOCKET) {
|
|
1887
|
+
this._applyMotor(joint, state);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET) {
|
|
1891
|
+
this._applySphericalMotor(cdata, joint);
|
|
1892
|
+
}
|
|
1893
|
+
b3._bx_Joint_WakeBodies(joint);
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Box3D's spherical joint has one motor for the whole joint: a relative angular velocity, and one spring towards a
|
|
1897
|
+
* target rotation. Babylon has a motor per axis, so the three angular axes are combined into one target in the
|
|
1898
|
+
* joint frame (ANGULAR_X about frame z, ANGULAR_Y about frame x, ANGULAR_Z about frame y).
|
|
1899
|
+
*/
|
|
1900
|
+
_applySphericalMotor(cdata, joint) {
|
|
1901
|
+
const b3 = this._b3;
|
|
1902
|
+
const twist = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_X);
|
|
1903
|
+
const swing1 = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_Y);
|
|
1904
|
+
const swing2 = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_Z);
|
|
1905
|
+
const states = [twist, swing1, swing2];
|
|
1906
|
+
const velocity = (state) => (state?.motor === PhysicsConstraintMotorType.VELOCITY ? state.target : 0);
|
|
1907
|
+
const hasVelocity = states.some((state) => state?.motor === PhysicsConstraintMotorType.VELOCITY);
|
|
1908
|
+
const hasPosition = states.some((state) => state?.motor === PhysicsConstraintMotorType.POSITION);
|
|
1909
|
+
const maxTorque = Math.max(...states.map((state) => (state?.motor === PhysicsConstraintMotorType.VELOCITY ? state.maxForce : 0)));
|
|
1910
|
+
b3._bx_Joint_SetSphericalMotor(joint, hasVelocity ? 1 : 0, velocity(swing1), velocity(swing2), velocity(twist), maxTorque);
|
|
1911
|
+
const target = velocity(swing1) !== 0 || velocity(swing2) !== 0 || velocity(twist) !== 0;
|
|
1912
|
+
if (hasVelocity && target) {
|
|
1913
|
+
// the world space target has to follow body A as it turns
|
|
1914
|
+
this._motorJoints.add(joint);
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
this._motorJoints.delete(joint);
|
|
1918
|
+
}
|
|
1919
|
+
b3._bx_Joint_EnableSpring(joint, hasPosition ? 1 : 0);
|
|
1920
|
+
if (hasPosition) {
|
|
1921
|
+
const position = (state) => (state?.motor === PhysicsConstraintMotorType.POSITION ? state.target : 0);
|
|
1922
|
+
const rotation = this._tmpQuat[0];
|
|
1923
|
+
Quaternion.RotationYawPitchRollToRef(0, 0, position(twist), rotation);
|
|
1924
|
+
const swing = this._tmpQuat[1];
|
|
1925
|
+
Quaternion.RotationYawPitchRollToRef(position(swing2), position(swing1), 0, swing);
|
|
1926
|
+
rotation.multiplyInPlace(swing);
|
|
1927
|
+
b3._bx_Joint_SetSpring(joint, 5, 1);
|
|
1928
|
+
b3._bx_Joint_SetSphericalTarget(joint, rotation.x, rotation.y, rotation.z, rotation.w);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
_applyMotor(joint, state) {
|
|
1932
|
+
const b3 = this._b3;
|
|
1933
|
+
switch (state.motor) {
|
|
1934
|
+
case PhysicsConstraintMotorType.VELOCITY:
|
|
1935
|
+
b3._bx_Joint_EnableSpring(joint, 0);
|
|
1936
|
+
b3._bx_Joint_EnableMotor(joint, 1);
|
|
1937
|
+
b3._bx_Joint_SetMotorSpeed(joint, state.target);
|
|
1938
|
+
b3._bx_Joint_SetMaxMotorForce(joint, state.maxForce);
|
|
1939
|
+
break;
|
|
1940
|
+
case PhysicsConstraintMotorType.POSITION:
|
|
1941
|
+
b3._bx_Joint_EnableMotor(joint, 0);
|
|
1942
|
+
b3._bx_Joint_EnableSpring(joint, 1);
|
|
1943
|
+
b3._bx_Joint_SetSpring(joint, 5, 1);
|
|
1944
|
+
b3._bx_Joint_SetTarget(joint, state.target);
|
|
1945
|
+
break;
|
|
1946
|
+
default:
|
|
1947
|
+
b3._bx_Joint_EnableMotor(joint, 0);
|
|
1948
|
+
b3._bx_Joint_EnableSpring(joint, 0);
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/** Applies a SIX_DOF plan's limits, springs and motors to one of its Box3D joints. */
|
|
1953
|
+
_applySixDofPlan(cdata, joint, bodyA, bodyB) {
|
|
1954
|
+
const b3 = this._b3;
|
|
1955
|
+
const plan = cdata.plan;
|
|
1956
|
+
switch (plan.jointType) {
|
|
1957
|
+
case Box3DJointType.SPHERICAL:
|
|
1958
|
+
if (plan.coneAngle >= 0) {
|
|
1959
|
+
b3._bx_Joint_SetLimits(joint, -plan.coneAngle, plan.coneAngle);
|
|
1960
|
+
}
|
|
1961
|
+
b3._bx_Joint_EnableLimit(joint, plan.coneAngle >= 0 ? 1 : 0);
|
|
1962
|
+
b3._bx_Joint_SetTwistLimits(joint, plan.twist ? 1 : 0, plan.twist ? plan.twist[0] : 0, plan.twist ? plan.twist[1] : 0);
|
|
1963
|
+
break;
|
|
1964
|
+
case Box3DJointType.REVOLUTE:
|
|
1965
|
+
case Box3DJointType.PRISMATIC:
|
|
1966
|
+
if (plan.limit) {
|
|
1967
|
+
b3._bx_Joint_SetLimits(joint, plan.limit[0], plan.limit[1]);
|
|
1968
|
+
}
|
|
1969
|
+
b3._bx_Joint_EnableLimit(joint, plan.limit ? 1 : 0);
|
|
1970
|
+
break;
|
|
1971
|
+
case Box3DJointType.DISTANCE:
|
|
1972
|
+
if (plan.spring) {
|
|
1973
|
+
const spring = this._springToHertz(plan.stiffness, plan.damping, false, bodyA, bodyB);
|
|
1974
|
+
b3._bx_Joint_EnableLimit(joint, 0);
|
|
1975
|
+
b3._bx_Joint_EnableSpring(joint, 1);
|
|
1976
|
+
b3._bx_Joint_SetSpring(joint, spring.hertz, spring.dampingRatio);
|
|
1977
|
+
b3._bx_Joint_SetTarget(joint, cdata.length);
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
// an enabled spring with 0 hertz leaves the length free between the limits (a rope); Box3D keeps
|
|
1981
|
+
// equal limits rigid
|
|
1982
|
+
b3._bx_Joint_SetLimits(joint, plan.limit[0], plan.limit[1]);
|
|
1983
|
+
b3._bx_Joint_EnableLimit(joint, 1);
|
|
1984
|
+
b3._bx_Joint_EnableSpring(joint, 1);
|
|
1985
|
+
b3._bx_Joint_SetSpring(joint, 0, 0);
|
|
1986
|
+
}
|
|
1987
|
+
break;
|
|
1988
|
+
default:
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
if (plan.stiffness > 0 && !plan.spring) {
|
|
1992
|
+
this._warnOnce("sixdof-softness", "SIX_DOF: limit stiffness and damping are approximated with Box3D's per joint constraint softness, which applies to every axis of the joint.");
|
|
1993
|
+
const soft = this._springToHertz(plan.stiffness, plan.damping, plan.angularStiffness, bodyA, bodyB);
|
|
1994
|
+
b3._bx_Joint_SetConstraintTuning(joint, soft.hertz, soft.dampingRatio);
|
|
1995
|
+
}
|
|
1996
|
+
if (plan.jointType === Box3DJointType.SPHERICAL) {
|
|
1997
|
+
this._applySphericalMotor(cdata, joint);
|
|
1998
|
+
}
|
|
1999
|
+
else if (plan.primaryAxis !== null && plan.jointType !== Box3DJointType.DISTANCE) {
|
|
2000
|
+
const state = cdata.axes.get(plan.primaryAxis);
|
|
2001
|
+
if (state) {
|
|
2002
|
+
this._applyMotor(joint, state);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
_setAxis(constraint, axis, apply) {
|
|
2007
|
+
const cdata = constraint._pluginData;
|
|
2008
|
+
if (!cdata) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const state = this._axisState(cdata, axis);
|
|
2012
|
+
const previousMotor = { motor: state.motor, target: state.target, maxForce: state.maxForce };
|
|
2013
|
+
apply(state);
|
|
2014
|
+
if (cdata.plan) {
|
|
2015
|
+
// SIX_DOF: every axis shapes the plan. A different Box3D joint type or frame needs new joints.
|
|
2016
|
+
const plan = this._planSixDof(cdata);
|
|
2017
|
+
const rebuild = plan.jointType !== cdata.plan.jointType || String(plan.basis) !== String(cdata.plan.basis);
|
|
2018
|
+
cdata.plan = plan;
|
|
2019
|
+
cdata.jointType = plan.jointType;
|
|
2020
|
+
const motorChanged = previousMotor.motor !== state.motor || previousMotor.target !== state.target || previousMotor.maxForce !== state.maxForce;
|
|
2021
|
+
if (motorChanged && state.motor !== PhysicsConstraintMotorType.NONE && !this._isPrimaryAxis(cdata, axis)) {
|
|
2022
|
+
this._warnOnce(`axis-motor-${axis}`, `SIX_DOF: a motor on axis ${axis} has no effect with the Box3D joint this constraint maps to.`);
|
|
2023
|
+
}
|
|
2024
|
+
const first = cdata.pairs[0];
|
|
2025
|
+
if (first) {
|
|
2026
|
+
// the rest length of a distance joint follows its limits, whether or not the joint is rebuilt
|
|
2027
|
+
this._updateJointLength(cdata, constraint.options.maxDistance, first.parentData, first.childData);
|
|
2028
|
+
}
|
|
2029
|
+
if (rebuild) {
|
|
2030
|
+
if (first) {
|
|
2031
|
+
this._buildFrames(cdata, constraint, first.parentData, first.childData);
|
|
2032
|
+
}
|
|
2033
|
+
this._recreateJoints(cdata);
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
else if (!this._isPrimaryAxis(cdata, axis)) {
|
|
2038
|
+
this._warnOnce(`axis-${cdata.type}-${axis}`, `axis ${axis} is not configurable on constraint type ${cdata.type} with Box3D; the value is stored but has no effect.`);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
for (let i = 0; i < cdata.joints.length; i++) {
|
|
2042
|
+
if (cdata.joints[i]) {
|
|
2043
|
+
this._applyAxisState(cdata, cdata.joints[i], cdata.pairs[i].parentData, cdata.pairs[i].childData);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
setAxisFriction(constraint, axis, friction) {
|
|
2048
|
+
const cdata = constraint._pluginData;
|
|
2049
|
+
if (cdata) {
|
|
2050
|
+
this._axisState(cdata, axis).friction = friction;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
getAxisFriction(constraint, axis) {
|
|
2054
|
+
return constraint._pluginData?.axes.get(axis)?.friction ?? null;
|
|
2055
|
+
}
|
|
2056
|
+
setAxisMode(constraint, axis, limitMode) {
|
|
2057
|
+
this._setAxis(constraint, axis, (state) => (state.mode = limitMode));
|
|
2058
|
+
}
|
|
2059
|
+
getAxisMode(constraint, axis) {
|
|
2060
|
+
return constraint._pluginData?.axes.get(axis)?.mode ?? null;
|
|
2061
|
+
}
|
|
2062
|
+
setAxisMinLimit(constraint, axis, minLimit) {
|
|
2063
|
+
this._setAxis(constraint, axis, (state) => {
|
|
2064
|
+
state.min = minLimit;
|
|
2065
|
+
if (state.mode === PhysicsConstraintAxisLimitMode.FREE) {
|
|
2066
|
+
state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
getAxisMinLimit(constraint, axis) {
|
|
2071
|
+
return constraint._pluginData?.axes.get(axis)?.min ?? null;
|
|
2072
|
+
}
|
|
2073
|
+
setAxisMaxLimit(constraint, axis, limit) {
|
|
2074
|
+
this._setAxis(constraint, axis, (state) => {
|
|
2075
|
+
state.max = limit;
|
|
2076
|
+
if (state.mode === PhysicsConstraintAxisLimitMode.FREE) {
|
|
2077
|
+
state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
getAxisMaxLimit(constraint, axis) {
|
|
2082
|
+
return constraint._pluginData?.axes.get(axis)?.max ?? null;
|
|
2083
|
+
}
|
|
2084
|
+
setAxisMotorType(constraint, axis, motorType) {
|
|
2085
|
+
this._setAxis(constraint, axis, (state) => (state.motor = motorType));
|
|
2086
|
+
}
|
|
2087
|
+
getAxisMotorType(constraint, axis) {
|
|
2088
|
+
return constraint._pluginData?.axes.get(axis)?.motor ?? null;
|
|
2089
|
+
}
|
|
2090
|
+
setAxisMotorTarget(constraint, axis, target) {
|
|
2091
|
+
this._setAxis(constraint, axis, (state) => (state.target = target));
|
|
2092
|
+
}
|
|
2093
|
+
getAxisMotorTarget(constraint, axis) {
|
|
2094
|
+
return constraint._pluginData?.axes.get(axis)?.target ?? null;
|
|
2095
|
+
}
|
|
2096
|
+
setAxisMotorMaxForce(constraint, axis, maxForce) {
|
|
2097
|
+
this._setAxis(constraint, axis, (state) => (state.maxForce = maxForce));
|
|
2098
|
+
}
|
|
2099
|
+
getAxisMotorMaxForce(constraint, axis) {
|
|
2100
|
+
return constraint._pluginData?.axes.get(axis)?.maxForce ?? null;
|
|
2101
|
+
}
|
|
2102
|
+
disposeConstraint(constraint) {
|
|
2103
|
+
const cdata = constraint._pluginData;
|
|
2104
|
+
if (!cdata) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
for (const joint of cdata.joints) {
|
|
2108
|
+
if (joint) {
|
|
2109
|
+
this._destroyJoint(joint);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
cdata.joints.length = 0;
|
|
2113
|
+
cdata.pairs.length = 0;
|
|
2114
|
+
}
|
|
2115
|
+
getBodiesUsingConstraint(constraint) {
|
|
2116
|
+
const cdata = constraint._pluginData;
|
|
2117
|
+
if (!cdata) {
|
|
2118
|
+
return [];
|
|
2119
|
+
}
|
|
2120
|
+
return cdata.pairs.map((p) => ({ parentBody: p.parent, parentBodyIndex: p.parentIndex, childBody: p.child, childBodyIndex: p.childIndex }));
|
|
2121
|
+
}
|
|
2122
|
+
addConstraint(body, childBody, constraint, instanceIndex, childInstanceIndex) {
|
|
2123
|
+
this.initConstraint(constraint, body, childBody, instanceIndex, childInstanceIndex);
|
|
2124
|
+
}
|
|
2125
|
+
// ----------------------------------------------------------------------------------------
|
|
2126
|
+
// Box3D specific extras
|
|
2127
|
+
// ----------------------------------------------------------------------------------------
|
|
2128
|
+
/**
|
|
2129
|
+
* Applies a radial impulse to every shape within the radius, like a grenade.
|
|
2130
|
+
* @param position world position of the explosion
|
|
2131
|
+
* @param radius radius of the explosion
|
|
2132
|
+
* @param impulsePerArea impulse per surface area of the affected shapes
|
|
2133
|
+
* @param falloff distance beyond the radius over which the impulse fades to zero
|
|
2134
|
+
*/
|
|
2135
|
+
explode(position, radius, impulsePerArea, falloff = 0) {
|
|
2136
|
+
this._b3._bx_World_Explode(this._worldSlot, position.x, position.y, position.z, radius, falloff, impulsePerArea);
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Box2D style collision group. Shapes sharing a negative group never collide with each other,
|
|
2140
|
+
* shapes sharing a positive group always collide. Zero means no group.
|
|
2141
|
+
* @param shape the shape
|
|
2142
|
+
* @param groupIndex the group index
|
|
2143
|
+
*/
|
|
2144
|
+
setShapeFilterGroup(shape, groupIndex) {
|
|
2145
|
+
const data = shape._pluginData;
|
|
2146
|
+
this._b3._bx_ShapeDesc_SetGroup(data.slot, groupIndex | 0);
|
|
2147
|
+
this._syncShapeUsers(data, Box3DShapeSync.FILTER);
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Rolling resistance for spheres and capsules (Babylon materials have no equivalent).
|
|
2151
|
+
* @param shape the shape
|
|
2152
|
+
* @param value resistance, typically in [0, 1]
|
|
2153
|
+
*/
|
|
2154
|
+
setShapeRollingResistance(shape, value) {
|
|
2155
|
+
const data = shape._pluginData;
|
|
2156
|
+
this._b3._bx_ShapeDesc_SetRollingResistance(data.slot, value);
|
|
2157
|
+
this._syncShapeUsers(data, Box3DShapeSync.MATERIAL);
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Lets a body spin faster than the default angular speed cap (wheels).
|
|
2161
|
+
* @param body the body
|
|
2162
|
+
* @param allow true to allow fast rotation
|
|
2163
|
+
* @param instanceIndex optional thin instance index
|
|
2164
|
+
*/
|
|
2165
|
+
setAllowFastRotation(body, allow, instanceIndex) {
|
|
2166
|
+
this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_AllowFastRotation(data.slot, allow ? 1 : 0), instanceIndex);
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Creates a Box3D wheel joint between a chassis and a wheel body.
|
|
2170
|
+
* @param chassis the chassis body
|
|
2171
|
+
* @param wheel the wheel body
|
|
2172
|
+
* @param options wheel joint options
|
|
2173
|
+
* @returns a handle used to drive and steer the wheel
|
|
2174
|
+
*/
|
|
2175
|
+
createWheelJoint(chassis, wheel, options) {
|
|
2176
|
+
const s = this._scratch();
|
|
2177
|
+
const axisA = (options.axisA ?? Vector3.UpReadOnly).normalizeToNew();
|
|
2178
|
+
const axleA = (options.axleA ?? Vector3.Forward()).normalizeToNew();
|
|
2179
|
+
// suspension travels along frame A x, the wheel spins about frame z
|
|
2180
|
+
axleA.subtractInPlace(axisA.scale(Vector3.Dot(axisA, axleA))).normalize();
|
|
2181
|
+
this._frameToBuffer(s, 0, options.pivotA, axisA, Vector3.Cross(axleA, axisA), axleA);
|
|
2182
|
+
// frame B must coincide with frame A in world space at creation, compute it from the current poses
|
|
2183
|
+
const pivotB = options.pivotB ?? Vector3.ZeroReadOnly;
|
|
2184
|
+
s[7] = pivotB.x;
|
|
2185
|
+
s[8] = pivotB.y;
|
|
2186
|
+
s[9] = pivotB.z;
|
|
2187
|
+
const chassisData = this._getPluginReference(chassis);
|
|
2188
|
+
const wheelData = this._getPluginReference(wheel);
|
|
2189
|
+
this._b3._bx_ComputeAlignedFrameB(chassisData.slot, wheelData.slot);
|
|
2190
|
+
const susp = options.suspensionLimits ?? [-0.2, 0.2];
|
|
2191
|
+
const steer = options.steeringLimits ?? [-Math.PI / 4, Math.PI / 4];
|
|
2192
|
+
s.set([
|
|
2193
|
+
1,
|
|
2194
|
+
options.suspensionHertz ?? 4,
|
|
2195
|
+
options.suspensionDampingRatio ?? 0.7,
|
|
2196
|
+
1,
|
|
2197
|
+
susp[0],
|
|
2198
|
+
susp[1],
|
|
2199
|
+
options.enableSpinMotor ? 1 : 0,
|
|
2200
|
+
options.maxSpinTorque ?? 5,
|
|
2201
|
+
0,
|
|
2202
|
+
options.enableSteering ? 1 : 0,
|
|
2203
|
+
options.steeringHertz ?? 10,
|
|
2204
|
+
options.steeringDampingRatio ?? 0.7,
|
|
2205
|
+
0,
|
|
2206
|
+
options.maxSteeringTorque ?? 5,
|
|
2207
|
+
1,
|
|
2208
|
+
steer[0],
|
|
2209
|
+
steer[1],
|
|
2210
|
+
], 14);
|
|
2211
|
+
const slot = this._b3._bx_CreateJoint(this._worldSlot, Box3DJointType.WHEEL, chassisData.slot, wheelData.slot, 0, 0);
|
|
2212
|
+
if (!slot) {
|
|
2213
|
+
throw new Error("Box3DPlugin: wheel joint creation failed");
|
|
2214
|
+
}
|
|
2215
|
+
return new Box3DWheelJoint(this, slot);
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Creates a Box3D parallel joint: a spring that keeps a local axis of bodyB parallel to an axis
|
|
2219
|
+
* of bodyA, for example to keep a vehicle upright without locking its motion.
|
|
2220
|
+
* @param bodyA first body (often a static body)
|
|
2221
|
+
* @param bodyB body to align
|
|
2222
|
+
* @param axisA axis in bodyA local space
|
|
2223
|
+
* @param axisB axis in bodyB local space
|
|
2224
|
+
* @param hertz spring stiffness
|
|
2225
|
+
* @param dampingRatio spring damping
|
|
2226
|
+
* @param maxTorque maximum spring torque
|
|
2227
|
+
* @returns a joint slot that can be passed to disposeExtraJoint
|
|
2228
|
+
*/
|
|
2229
|
+
createParallelJoint(bodyA, bodyB, axisA, axisB, hertz = 1, dampingRatio = 1, maxTorque = 3.4e38) {
|
|
2230
|
+
const s = this._scratch();
|
|
2231
|
+
const a = axisA.normalizeToNew();
|
|
2232
|
+
const pa = a.getNormalToRef(new Vector3());
|
|
2233
|
+
this._frameToBuffer(s, 0, Vector3.ZeroReadOnly, pa, Vector3.Cross(a, pa), a);
|
|
2234
|
+
const b = axisB.normalizeToNew();
|
|
2235
|
+
const pb = b.getNormalToRef(new Vector3());
|
|
2236
|
+
this._frameToBuffer(s, 7, Vector3.ZeroReadOnly, pb, Vector3.Cross(b, pb), b);
|
|
2237
|
+
s.set([hertz, dampingRatio, maxTorque], 14);
|
|
2238
|
+
return this._b3._bx_CreateJoint(this._worldSlot, Box3DJointType.PARALLEL, this._getPluginReference(bodyA).slot, this._getPluginReference(bodyB).slot, 1, 0);
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Destroys a joint created by createParallelJoint.
|
|
2242
|
+
* @param slot the joint slot
|
|
2243
|
+
*/
|
|
2244
|
+
disposeExtraJoint(slot) {
|
|
2245
|
+
this._destroyExtraJoint(slot);
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* @internal
|
|
2249
|
+
*/
|
|
2250
|
+
_destroyExtraJoint(slot) {
|
|
2251
|
+
if (slot) {
|
|
2252
|
+
this._destroyJoint(slot);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* @internal
|
|
2257
|
+
*/
|
|
2258
|
+
_wheelCall(fn, slot, ...args) {
|
|
2259
|
+
if (slot) {
|
|
2260
|
+
this._b3[fn](slot, ...args);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
// ----------------------------------------------------------------------------------------
|
|
2264
|
+
// queries
|
|
2265
|
+
// ----------------------------------------------------------------------------------------
|
|
2266
|
+
raycast(from, to, result, query) {
|
|
2267
|
+
const results = Array.isArray(result) ? result : [result];
|
|
2268
|
+
for (const r of results) {
|
|
2269
|
+
r.reset(from, to);
|
|
2270
|
+
}
|
|
2271
|
+
const membership = (query?.membership ?? ~0) >>> 0;
|
|
2272
|
+
const collideWith = (query?.collideWith ?? ~0) >>> 0;
|
|
2273
|
+
const ignore = query?.ignoreBody ? (query.ignoreBody._pluginData?.slot ?? 0) : 0;
|
|
2274
|
+
const closestOnly = results.length === 1;
|
|
2275
|
+
const count = this._b3._bx_World_CastRay(this._worldSlot, from.x, from.y, from.z, to.x - from.x, to.y - from.y, to.z - from.z, membership, collideWith, ignore, query?.shouldHitTriggers ? 1 : 0, closestOnly ? 1 : 0);
|
|
2276
|
+
if (count <= 0) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const buffer = new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_RayHitsPtr(), count * RAY_STRIDE);
|
|
2280
|
+
const hits = [];
|
|
2281
|
+
for (let i = 0; i < count; i++) {
|
|
2282
|
+
hits.push(i);
|
|
2283
|
+
}
|
|
2284
|
+
hits.sort((a, b) => buffer[a * RAY_STRIDE + 6] - buffer[b * RAY_STRIDE + 6]);
|
|
2285
|
+
if (!results.length) {
|
|
2286
|
+
for (let i = 0; i < count; i++) {
|
|
2287
|
+
const r = new PhysicsRaycastResult();
|
|
2288
|
+
r.reset(from, to);
|
|
2289
|
+
results.push(r);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const n = Math.min(count, results.length);
|
|
2293
|
+
for (let i = 0; i < n; i++) {
|
|
2294
|
+
const o = hits[i] * RAY_STRIDE;
|
|
2295
|
+
const r = results[i];
|
|
2296
|
+
const bodyRef = this._bodies[buffer[o + 7]];
|
|
2297
|
+
r.body = bodyRef?.body;
|
|
2298
|
+
r.bodyIndex = bodyRef?.index;
|
|
2299
|
+
r.shape = bodyRef?.body.shape ?? undefined;
|
|
2300
|
+
r.setHitData({ x: buffer[o + 3], y: buffer[o + 4], z: buffer[o + 5] }, { x: buffer[o], y: buffer[o + 1], z: buffer[o + 2] }, buffer[o + 9]);
|
|
2301
|
+
r.calculateHitDistance();
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
// ----------------------------------------------------------------------------------------
|
|
2305
|
+
// events
|
|
2306
|
+
// ----------------------------------------------------------------------------------------
|
|
2307
|
+
/**
|
|
2308
|
+
* Copies `count` event records out of wasm memory and resolves their two body slots (at `slotOffset`) to the plugin's
|
|
2309
|
+
* body references. Observers run user code that can grow the wasm memory, which detaches every HEAPF32 view (creating
|
|
2310
|
+
* bodies or shapes, casting rays), or dispose bodies and hand their slots to new ones, so nothing is read from wasm
|
|
2311
|
+
* memory or the slot table once the first observer has been called.
|
|
2312
|
+
*/
|
|
2313
|
+
_readEvents(pointer, count, stride, slotOffset) {
|
|
2314
|
+
const records = new Float32Array(this._b3.HEAPF32.buffer, pointer, count * stride).slice();
|
|
2315
|
+
const refs = new Array(count * 2);
|
|
2316
|
+
for (let i = 0; i < count; i++) {
|
|
2317
|
+
refs[i * 2] = this._bodies[records[i * stride + slotOffset]];
|
|
2318
|
+
refs[i * 2 + 1] = this._bodies[records[i * stride + slotOffset + 1]];
|
|
2319
|
+
}
|
|
2320
|
+
return { records, refs };
|
|
2321
|
+
}
|
|
2322
|
+
_notifyCollisions() {
|
|
2323
|
+
const count = this._b3._bx_World_GetContactEvents(this._worldSlot);
|
|
2324
|
+
if (!count) {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const { records: buffer, refs } = this._readEvents(this._b3._bx_ContactEventsPtr(), count, CONTACT_STRIDE, 3);
|
|
2328
|
+
for (let i = 0; i < count; i++) {
|
|
2329
|
+
const o = i * CONTACT_STRIDE;
|
|
2330
|
+
const kind = buffer[o];
|
|
2331
|
+
const refA = refs[i * 2];
|
|
2332
|
+
const refB = refs[i * 2 + 1];
|
|
2333
|
+
// a body disposed by an earlier observer has slot 0, its remaining events are dropped
|
|
2334
|
+
if (!refA || !refB || !refA.data.slot || !refB.data.slot) {
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
// like Havok, an event is reported when either body asked for that event type
|
|
2338
|
+
const bit = kind === 0 ? Box3DEventBits.COLLISION_STARTED : kind === 1 ? Box3DEventBits.COLLISION_FINISHED : Box3DEventBits.COLLISION_CONTINUED;
|
|
2339
|
+
if (((refA.data.eventMask | refB.data.eventMask) & bit) === 0) {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (kind === 1) {
|
|
2343
|
+
const ended = {
|
|
2344
|
+
collider: refA.body,
|
|
2345
|
+
colliderIndex: refA.index,
|
|
2346
|
+
collidedAgainst: refB.body,
|
|
2347
|
+
collidedAgainstIndex: refB.index,
|
|
2348
|
+
type: PhysicsEventType.COLLISION_FINISHED,
|
|
2349
|
+
};
|
|
2350
|
+
this.onCollisionEndedObservable.notifyObservers(ended);
|
|
2351
|
+
this._bodyCollisionEndedObservable.get(refA.data.slot)?.notifyObservers(ended);
|
|
2352
|
+
const endedB = this._bodyCollisionEndedObservable.get(refB.data.slot);
|
|
2353
|
+
if (endedB) {
|
|
2354
|
+
endedB.notifyObservers({ ...ended, collider: refB.body, colliderIndex: refB.index, collidedAgainst: refA.body, collidedAgainstIndex: refA.index });
|
|
2355
|
+
}
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
// begin events carry the manifold point, normal and normal impulse, hit events the approach speed
|
|
2359
|
+
const type = kind === 0 ? PhysicsEventType.COLLISION_STARTED : PhysicsEventType.COLLISION_CONTINUED;
|
|
2360
|
+
const point = new Vector3(buffer[o + 5], buffer[o + 6], buffer[o + 7]);
|
|
2361
|
+
const normal = new Vector3(buffer[o + 8], buffer[o + 9], buffer[o + 10]);
|
|
2362
|
+
const info = {
|
|
2363
|
+
collider: refA.body,
|
|
2364
|
+
colliderIndex: refA.index,
|
|
2365
|
+
collidedAgainst: refB.body,
|
|
2366
|
+
collidedAgainstIndex: refB.index,
|
|
2367
|
+
type,
|
|
2368
|
+
point,
|
|
2369
|
+
normal,
|
|
2370
|
+
distance: 0,
|
|
2371
|
+
impulse: buffer[o + 11],
|
|
2372
|
+
};
|
|
2373
|
+
this.onCollisionObservable.notifyObservers(info);
|
|
2374
|
+
if (this._bodyCollisionObservable.size) {
|
|
2375
|
+
this._bodyCollisionObservable.get(refA.data.slot)?.notifyObservers(info);
|
|
2376
|
+
const obsB = this._bodyCollisionObservable.get(refB.data.slot);
|
|
2377
|
+
if (obsB) {
|
|
2378
|
+
obsB.notifyObservers({
|
|
2379
|
+
...info,
|
|
2380
|
+
collider: refB.body,
|
|
2381
|
+
colliderIndex: refB.index,
|
|
2382
|
+
collidedAgainst: refA.body,
|
|
2383
|
+
collidedAgainstIndex: refA.index,
|
|
2384
|
+
normal: normal ? normal.negate() : null,
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
_notifyTriggers() {
|
|
2391
|
+
const count = this._b3._bx_World_GetSensorEvents(this._worldSlot);
|
|
2392
|
+
if (!count || !this.onTriggerCollisionObservable.hasObservers()) {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const { records: buffer, refs } = this._readEvents(this._b3._bx_SensorEventsPtr(), count, SENSOR_STRIDE, 3);
|
|
2396
|
+
for (let i = 0; i < count; i++) {
|
|
2397
|
+
const o = i * SENSOR_STRIDE;
|
|
2398
|
+
const refA = refs[i * 2];
|
|
2399
|
+
const refB = refs[i * 2 + 1];
|
|
2400
|
+
if (!refA || !refB || !refA.data.slot || !refB.data.slot) {
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
this.onTriggerCollisionObservable.notifyObservers({
|
|
2404
|
+
collider: refA.body,
|
|
2405
|
+
colliderIndex: refA.index,
|
|
2406
|
+
collidedAgainst: refB.body,
|
|
2407
|
+
collidedAgainstIndex: refB.index,
|
|
2408
|
+
type: buffer[o] === 0 ? PhysicsEventType.TRIGGER_ENTERED : PhysicsEventType.TRIGGER_EXITED,
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
dispose() {
|
|
2413
|
+
this._motorJoints.clear();
|
|
2414
|
+
this.onCollisionObservable.clear();
|
|
2415
|
+
this.onCollisionEndedObservable.clear();
|
|
2416
|
+
this.onTriggerCollisionObservable.clear();
|
|
2417
|
+
this._bodyCollisionObservable.clear();
|
|
2418
|
+
this._bodyCollisionEndedObservable.clear();
|
|
2419
|
+
for (const shape of this._shapes) {
|
|
2420
|
+
if (shape) {
|
|
2421
|
+
this._b3._bx_ShapeDesc_Destroy(shape.slot);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
this._shapes.length = 0;
|
|
2425
|
+
this._bodies.length = 0;
|
|
2426
|
+
if (this._worldSlot) {
|
|
2427
|
+
this._b3._bx_DestroyWorld(this._worldSlot);
|
|
2428
|
+
this._worldSlot = 0;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
//# sourceMappingURL=box3dPlugin.js.map
|