reze-engine 0.1.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.
@@ -0,0 +1,503 @@
1
+ import { Quat, Vec3, Mat4 } from "./math";
2
+ import { loadAmmo } from "./ammo-loader";
3
+ export var RigidbodyShape;
4
+ (function (RigidbodyShape) {
5
+ RigidbodyShape[RigidbodyShape["Sphere"] = 0] = "Sphere";
6
+ RigidbodyShape[RigidbodyShape["Box"] = 1] = "Box";
7
+ RigidbodyShape[RigidbodyShape["Capsule"] = 2] = "Capsule";
8
+ })(RigidbodyShape || (RigidbodyShape = {}));
9
+ export var RigidbodyType;
10
+ (function (RigidbodyType) {
11
+ RigidbodyType[RigidbodyType["Static"] = 0] = "Static";
12
+ RigidbodyType[RigidbodyType["Dynamic"] = 1] = "Dynamic";
13
+ RigidbodyType[RigidbodyType["Kinematic"] = 2] = "Kinematic";
14
+ })(RigidbodyType || (RigidbodyType = {}));
15
+ export class Physics {
16
+ constructor(rigidbodies, joints = []) {
17
+ this.gravity = new Vec3(0, -98, 0); // Gravity acceleration (cm/s²), MMD-style default
18
+ this.ammoInitialized = false;
19
+ this.ammoPromise = null;
20
+ this.ammo = null;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ this.dynamicsWorld = null; // btDiscreteDynamicsWorld
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ this.ammoRigidbodies = []; // btRigidBody instances
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ this.ammoConstraints = []; // btTypedConstraint instances
27
+ this.rigidbodiesInitialized = false; // bodyOffsetMatrixInverse computed and bodies positioned
28
+ this.jointsCreated = false; // Joints delayed until after rigidbodies are positioned
29
+ this.firstFrame = true; // Needed to reposition bodies before creating joints
30
+ this.forceDisableOffsetForConstraintFrame = true; // MMD compatibility (Bullet 2.75 behavior)
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ this.zeroVector = null; // Cached zero vector for velocity clearing
33
+ this.rigidbodies = rigidbodies;
34
+ this.joints = joints;
35
+ this.initAmmo();
36
+ }
37
+ async initAmmo() {
38
+ if (this.ammoInitialized || this.ammoPromise)
39
+ return;
40
+ this.ammoPromise = loadAmmo();
41
+ try {
42
+ this.ammo = await this.ammoPromise;
43
+ this.createAmmoWorld();
44
+ this.ammoInitialized = true;
45
+ }
46
+ catch (error) {
47
+ console.error("[Physics] Failed to initialize Ammo:", error);
48
+ this.ammoPromise = null;
49
+ }
50
+ }
51
+ setGravity(gravity) {
52
+ this.gravity = gravity;
53
+ if (this.dynamicsWorld && this.ammo) {
54
+ const Ammo = this.ammo;
55
+ const gravityVec = new Ammo.btVector3(gravity.x, gravity.y, gravity.z);
56
+ this.dynamicsWorld.setGravity(gravityVec);
57
+ Ammo.destroy(gravityVec);
58
+ }
59
+ }
60
+ getGravity() {
61
+ return this.gravity;
62
+ }
63
+ getRigidbodies() {
64
+ return this.rigidbodies;
65
+ }
66
+ getJoints() {
67
+ return this.joints;
68
+ }
69
+ getRigidbodyTransforms() {
70
+ const transforms = [];
71
+ if (!this.ammo || !this.ammoRigidbodies.length) {
72
+ for (let i = 0; i < this.rigidbodies.length; i++) {
73
+ transforms.push({
74
+ position: new Vec3(this.rigidbodies[i].shapePosition.x, this.rigidbodies[i].shapePosition.y, this.rigidbodies[i].shapePosition.z),
75
+ rotation: Quat.fromEuler(this.rigidbodies[i].shapeRotation.x, this.rigidbodies[i].shapeRotation.y, this.rigidbodies[i].shapeRotation.z),
76
+ });
77
+ }
78
+ return transforms;
79
+ }
80
+ for (let i = 0; i < this.ammoRigidbodies.length; i++) {
81
+ const ammoBody = this.ammoRigidbodies[i];
82
+ if (!ammoBody) {
83
+ const rb = this.rigidbodies[i];
84
+ transforms.push({
85
+ position: new Vec3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z),
86
+ rotation: Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z),
87
+ });
88
+ continue;
89
+ }
90
+ const transform = ammoBody.getWorldTransform();
91
+ const origin = transform.getOrigin();
92
+ const rotQuat = transform.getRotation();
93
+ transforms.push({
94
+ position: new Vec3(origin.x(), origin.y(), origin.z()),
95
+ rotation: new Quat(rotQuat.x(), rotQuat.y(), rotQuat.z(), rotQuat.w()),
96
+ });
97
+ }
98
+ return transforms;
99
+ }
100
+ createAmmoWorld() {
101
+ if (!this.ammo)
102
+ return;
103
+ const Ammo = this.ammo;
104
+ const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
105
+ const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
106
+ const overlappingPairCache = new Ammo.btDbvtBroadphase();
107
+ const solver = new Ammo.btSequentialImpulseConstraintSolver();
108
+ this.dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
109
+ const gravityVec = new Ammo.btVector3(this.gravity.x, this.gravity.y, this.gravity.z);
110
+ this.dynamicsWorld.setGravity(gravityVec);
111
+ Ammo.destroy(gravityVec);
112
+ this.createAmmoRigidbodies();
113
+ }
114
+ createAmmoRigidbodies() {
115
+ if (!this.ammo || !this.dynamicsWorld)
116
+ return;
117
+ const Ammo = this.ammo;
118
+ this.ammoRigidbodies = [];
119
+ for (let i = 0; i < this.rigidbodies.length; i++) {
120
+ const rb = this.rigidbodies[i];
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ let shape = null;
123
+ const size = rb.size;
124
+ switch (rb.shape) {
125
+ case RigidbodyShape.Sphere:
126
+ const radius = size.x;
127
+ shape = new Ammo.btSphereShape(radius);
128
+ break;
129
+ case RigidbodyShape.Box:
130
+ const sizeVector = new Ammo.btVector3(size.x, size.y, size.z);
131
+ shape = new Ammo.btBoxShape(sizeVector);
132
+ Ammo.destroy(sizeVector);
133
+ break;
134
+ case RigidbodyShape.Capsule:
135
+ const capsuleRadius = size.x;
136
+ const capsuleHalfHeight = size.y;
137
+ shape = new Ammo.btCapsuleShape(capsuleRadius, capsuleHalfHeight);
138
+ break;
139
+ default:
140
+ const defaultHalfExtents = new Ammo.btVector3(size.x / 2, size.y / 2, size.z / 2);
141
+ shape = new Ammo.btBoxShape(defaultHalfExtents);
142
+ Ammo.destroy(defaultHalfExtents);
143
+ break;
144
+ }
145
+ // Bodies must start at correct position to avoid explosions when joints are created
146
+ const transform = new Ammo.btTransform();
147
+ transform.setIdentity();
148
+ const shapePos = new Ammo.btVector3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z);
149
+ transform.setOrigin(shapePos);
150
+ Ammo.destroy(shapePos);
151
+ const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
152
+ const quat = new Ammo.btQuaternion(shapeRotQuat.x, shapeRotQuat.y, shapeRotQuat.z, shapeRotQuat.w);
153
+ transform.setRotation(quat);
154
+ Ammo.destroy(quat);
155
+ // All types use the same motionState constructor
156
+ const motionState = new Ammo.btDefaultMotionState(transform);
157
+ const mass = rb.type === RigidbodyType.Dynamic ? rb.mass : 0;
158
+ const isDynamic = rb.type === RigidbodyType.Dynamic;
159
+ const localInertia = new Ammo.btVector3(0, 0, 0);
160
+ if (isDynamic && mass > 0) {
161
+ shape.calculateLocalInertia(mass, localInertia);
162
+ }
163
+ const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
164
+ rbInfo.set_m_restitution(rb.restitution);
165
+ rbInfo.set_m_friction(rb.friction);
166
+ rbInfo.set_m_linearDamping(rb.linearDamping);
167
+ rbInfo.set_m_angularDamping(rb.angularDamping);
168
+ const body = new Ammo.btRigidBody(rbInfo);
169
+ body.setSleepingThresholds(0.0, 0.0);
170
+ // Static (FollowBone) should be kinematic, not static - must follow bones
171
+ if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
172
+ body.setCollisionFlags(body.getCollisionFlags() | 2); // CF_KINEMATIC_OBJECT
173
+ body.setActivationState(4); // DISABLE_DEACTIVATION
174
+ }
175
+ const collisionGroup = 1 << rb.group;
176
+ const collisionMask = rb.collisionMask;
177
+ const isZeroVolume = (rb.shape === RigidbodyShape.Sphere && rb.size.x === 0) ||
178
+ (rb.shape === RigidbodyShape.Box && (rb.size.x === 0 || rb.size.y === 0 || rb.size.z === 0)) ||
179
+ (rb.shape === RigidbodyShape.Capsule && (rb.size.x === 0 || rb.size.y === 0));
180
+ if (collisionMask === 0 || isZeroVolume) {
181
+ body.setCollisionFlags(body.getCollisionFlags() | 4); // CF_NO_CONTACT_RESPONSE
182
+ }
183
+ this.dynamicsWorld.addRigidBody(body, collisionGroup, collisionMask);
184
+ this.ammoRigidbodies.push(body);
185
+ Ammo.destroy(rbInfo);
186
+ Ammo.destroy(localInertia);
187
+ }
188
+ }
189
+ createAmmoJoints() {
190
+ if (!this.ammo || !this.dynamicsWorld || this.ammoRigidbodies.length === 0)
191
+ return;
192
+ const Ammo = this.ammo;
193
+ this.ammoConstraints = [];
194
+ for (const joint of this.joints) {
195
+ const rbIndexA = joint.rigidbodyIndexA;
196
+ const rbIndexB = joint.rigidbodyIndexB;
197
+ if (rbIndexA < 0 ||
198
+ rbIndexA >= this.ammoRigidbodies.length ||
199
+ rbIndexB < 0 ||
200
+ rbIndexB >= this.ammoRigidbodies.length) {
201
+ console.warn(`[Physics] Invalid joint indices: ${rbIndexA}, ${rbIndexB}`);
202
+ continue;
203
+ }
204
+ const bodyA = this.ammoRigidbodies[rbIndexA];
205
+ const bodyB = this.ammoRigidbodies[rbIndexB];
206
+ if (!bodyA || !bodyB) {
207
+ console.warn(`[Physics] Body not found for joint ${joint.name}: bodyA=${!!bodyA}, bodyB=${!!bodyB}`);
208
+ continue;
209
+ }
210
+ // Compute joint frames using actual current body positions (after repositioning)
211
+ const bodyATransform = bodyA.getWorldTransform();
212
+ const bodyBTransform = bodyB.getWorldTransform();
213
+ const bodyAOrigin = bodyATransform.getOrigin();
214
+ const bodyARotQuat = bodyATransform.getRotation();
215
+ const bodyAPos = new Vec3(bodyAOrigin.x(), bodyAOrigin.y(), bodyAOrigin.z());
216
+ const bodyARot = new Quat(bodyARotQuat.x(), bodyARotQuat.y(), bodyARotQuat.z(), bodyARotQuat.w());
217
+ const bodyAMat = Mat4.fromPositionRotation(bodyAPos, bodyARot);
218
+ const bodyBOrigin = bodyBTransform.getOrigin();
219
+ const bodyBRotQuat = bodyBTransform.getRotation();
220
+ const bodyBPos = new Vec3(bodyBOrigin.x(), bodyBOrigin.y(), bodyBOrigin.z());
221
+ const bodyBRot = new Quat(bodyBRotQuat.x(), bodyBRotQuat.y(), bodyBRotQuat.z(), bodyBRotQuat.w());
222
+ const bodyBMat = Mat4.fromPositionRotation(bodyBPos, bodyBRot);
223
+ const scalingFactor = 1.0;
224
+ const jointRotQuat = Quat.fromEuler(joint.rotation.x, joint.rotation.y, joint.rotation.z);
225
+ const jointPos = new Vec3(joint.position.x * scalingFactor, joint.position.y * scalingFactor, joint.position.z * scalingFactor);
226
+ const jointTransform = Mat4.fromPositionRotation(jointPos, jointRotQuat);
227
+ // Transform joint world position to body A's local space
228
+ const frameInAMat = bodyAMat.inverse().multiply(jointTransform);
229
+ const framePosA = frameInAMat.getPosition();
230
+ const frameRotA = frameInAMat.toQuat();
231
+ // Transform joint world position to body B's local space
232
+ const frameInBMat = bodyBMat.inverse().multiply(jointTransform);
233
+ const framePosB = frameInBMat.getPosition();
234
+ const frameRotB = frameInBMat.toQuat();
235
+ const frameInA = new Ammo.btTransform();
236
+ frameInA.setIdentity();
237
+ const pivotInA = new Ammo.btVector3(framePosA.x, framePosA.y, framePosA.z);
238
+ frameInA.setOrigin(pivotInA);
239
+ const quatA = new Ammo.btQuaternion(frameRotA.x, frameRotA.y, frameRotA.z, frameRotA.w);
240
+ frameInA.setRotation(quatA);
241
+ const frameInB = new Ammo.btTransform();
242
+ frameInB.setIdentity();
243
+ const pivotInB = new Ammo.btVector3(framePosB.x, framePosB.y, framePosB.z);
244
+ frameInB.setOrigin(pivotInB);
245
+ const quatB = new Ammo.btQuaternion(frameRotB.x, frameRotB.y, frameRotB.z, frameRotB.w);
246
+ frameInB.setRotation(quatB);
247
+ const useLinearReferenceFrameA = true;
248
+ const constraint = new Ammo.btGeneric6DofSpringConstraint(bodyA, bodyB, frameInA, frameInB, useLinearReferenceFrameA);
249
+ // Disable offset for constraint frame for MMD compatibility (Bullet 2.75 behavior)
250
+ if (this.forceDisableOffsetForConstraintFrame) {
251
+ let jointPtr;
252
+ if (typeof Ammo.getPointer === "function") {
253
+ jointPtr = Ammo.getPointer(constraint);
254
+ }
255
+ else {
256
+ const constraintWithPtr = constraint;
257
+ jointPtr = constraintWithPtr.ptr;
258
+ }
259
+ if (jointPtr !== undefined && Ammo.HEAP8) {
260
+ const heap8 = Ammo.HEAP8;
261
+ // jointPtr + 1300 = m_useLinearReferenceFrameA, jointPtr + 1301 = m_useOffsetForConstraintFrame
262
+ if (heap8[jointPtr + 1300] === (useLinearReferenceFrameA ? 1 : 0) && heap8[jointPtr + 1301] === 1) {
263
+ heap8[jointPtr + 1301] = 0;
264
+ }
265
+ }
266
+ }
267
+ for (let i = 0; i < 6; ++i) {
268
+ constraint.setParam(2, 0.475, i); // BT_CONSTRAINT_STOP_ERP
269
+ }
270
+ const lowerLinear = new Ammo.btVector3(joint.positionMin.x, joint.positionMin.y, joint.positionMin.z);
271
+ const upperLinear = new Ammo.btVector3(joint.positionMax.x, joint.positionMax.y, joint.positionMax.z);
272
+ constraint.setLinearLowerLimit(lowerLinear);
273
+ constraint.setLinearUpperLimit(upperLinear);
274
+ const lowerAngular = new Ammo.btVector3(this.normalizeAngle(joint.rotationMin.x), this.normalizeAngle(joint.rotationMin.y), this.normalizeAngle(joint.rotationMin.z));
275
+ const upperAngular = new Ammo.btVector3(this.normalizeAngle(joint.rotationMax.x), this.normalizeAngle(joint.rotationMax.y), this.normalizeAngle(joint.rotationMax.z));
276
+ constraint.setAngularLowerLimit(lowerAngular);
277
+ constraint.setAngularUpperLimit(upperAngular);
278
+ // Linear springs: only enable if stiffness is non-zero
279
+ if (joint.springPosition.x !== 0) {
280
+ constraint.setStiffness(0, joint.springPosition.x);
281
+ constraint.enableSpring(0, true);
282
+ }
283
+ else {
284
+ constraint.enableSpring(0, false);
285
+ }
286
+ if (joint.springPosition.y !== 0) {
287
+ constraint.setStiffness(1, joint.springPosition.y);
288
+ constraint.enableSpring(1, true);
289
+ }
290
+ else {
291
+ constraint.enableSpring(1, false);
292
+ }
293
+ if (joint.springPosition.z !== 0) {
294
+ constraint.setStiffness(2, joint.springPosition.z);
295
+ constraint.enableSpring(2, true);
296
+ }
297
+ else {
298
+ constraint.enableSpring(2, false);
299
+ }
300
+ // Angular springs: always enable
301
+ constraint.setStiffness(3, joint.springRotation.x);
302
+ constraint.enableSpring(3, true);
303
+ constraint.setStiffness(4, joint.springRotation.y);
304
+ constraint.enableSpring(4, true);
305
+ constraint.setStiffness(5, joint.springRotation.z);
306
+ constraint.enableSpring(5, true);
307
+ this.dynamicsWorld.addConstraint(constraint, false);
308
+ this.ammoConstraints.push(constraint);
309
+ Ammo.destroy(pivotInA);
310
+ Ammo.destroy(pivotInB);
311
+ Ammo.destroy(quatA);
312
+ Ammo.destroy(quatB);
313
+ Ammo.destroy(lowerLinear);
314
+ Ammo.destroy(upperLinear);
315
+ Ammo.destroy(lowerAngular);
316
+ Ammo.destroy(upperAngular);
317
+ }
318
+ }
319
+ // Normalize angle to [-π, π] range
320
+ normalizeAngle(angle) {
321
+ const pi = Math.PI;
322
+ const twoPi = 2 * pi;
323
+ angle = angle % twoPi;
324
+ if (angle < -pi) {
325
+ angle += twoPi;
326
+ }
327
+ else if (angle > pi) {
328
+ angle -= twoPi;
329
+ }
330
+ return angle;
331
+ }
332
+ // Syncs bones to rigidbodies, simulates dynamics, solves constraints
333
+ // Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
334
+ step(dt, boneWorldMatrices, boneInverseBindMatrices) {
335
+ // Wait for Ammo to initialize
336
+ if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
337
+ return;
338
+ }
339
+ const boneCount = boneWorldMatrices.length / 16;
340
+ if (this.firstFrame) {
341
+ if (!this.rigidbodiesInitialized) {
342
+ this.computeBodyOffsets(boneInverseBindMatrices, boneCount);
343
+ this.rigidbodiesInitialized = true;
344
+ }
345
+ // Position bodies based on current bone poses (not bind pose) before creating joints
346
+ this.positionBodiesFromBones(boneWorldMatrices, boneCount);
347
+ if (!this.jointsCreated) {
348
+ this.createAmmoJoints();
349
+ this.jointsCreated = true;
350
+ }
351
+ if (this.dynamicsWorld.stepSimulation) {
352
+ this.dynamicsWorld.stepSimulation(0, 0, 0);
353
+ }
354
+ this.firstFrame = false;
355
+ }
356
+ // Step order: 1) Sync Static/Kinematic from bones, 2) Step physics, 3) Apply dynamic to bones
357
+ this.syncFromBones(boneWorldMatrices, boneInverseBindMatrices, boneCount);
358
+ this.stepAmmoPhysics(dt);
359
+ this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneInverseBindMatrices, boneCount);
360
+ }
361
+ // Compute bodyOffsetMatrixInverse for all rigidbodies (called once during initialization)
362
+ computeBodyOffsets(boneInverseBindMatrices, boneCount) {
363
+ if (!this.ammo || !this.dynamicsWorld)
364
+ return;
365
+ for (let i = 0; i < this.rigidbodies.length; i++) {
366
+ const rb = this.rigidbodies[i];
367
+ if (rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
368
+ const boneIdx = rb.boneIndex;
369
+ const invBindIdx = boneIdx * 16;
370
+ const invBindMat = new Mat4(boneInverseBindMatrices.subarray(invBindIdx, invBindIdx + 16));
371
+ // Compute shape transform in bone-local space: shapeLocal = boneInverseBind × shapeWorldBind
372
+ const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
373
+ const shapeWorldBind = Mat4.fromPositionRotation(rb.shapePosition, shapeRotQuat);
374
+ // shapeLocal = boneInverseBind × shapeWorldBind (not shapeWorldBind × boneInverseBind)
375
+ const bodyOffsetMatrix = invBindMat.multiply(shapeWorldBind);
376
+ rb.bodyOffsetMatrixInverse = bodyOffsetMatrix.inverse();
377
+ rb.bodyOffsetMatrix = bodyOffsetMatrix; // Cache non-inverse to avoid expensive inverse() calls
378
+ }
379
+ else {
380
+ rb.bodyOffsetMatrixInverse = Mat4.identity();
381
+ rb.bodyOffsetMatrix = Mat4.identity(); // Cache non-inverse
382
+ }
383
+ }
384
+ }
385
+ // Position bodies based on current bone transforms (called on first frame only)
386
+ positionBodiesFromBones(boneWorldMatrices, boneCount) {
387
+ if (!this.ammo || !this.dynamicsWorld)
388
+ return;
389
+ const Ammo = this.ammo;
390
+ for (let i = 0; i < this.rigidbodies.length; i++) {
391
+ const rb = this.rigidbodies[i];
392
+ const ammoBody = this.ammoRigidbodies[i];
393
+ if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount)
394
+ continue;
395
+ const boneIdx = rb.boneIndex;
396
+ const worldMatIdx = boneIdx * 16;
397
+ const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16));
398
+ // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
399
+ const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
400
+ const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
401
+ const worldPos = nodeWorldMatrix.getPosition();
402
+ const worldRot = nodeWorldMatrix.toQuat();
403
+ const transform = new Ammo.btTransform();
404
+ const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
405
+ const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
406
+ transform.setOrigin(pos);
407
+ transform.setRotation(quat);
408
+ if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
409
+ ammoBody.setCollisionFlags(ammoBody.getCollisionFlags() | 2); // CF_KINEMATIC_OBJECT
410
+ ammoBody.setActivationState(4); // DISABLE_DEACTIVATION
411
+ }
412
+ ammoBody.setWorldTransform(transform);
413
+ ammoBody.getMotionState().setWorldTransform(transform);
414
+ if (!this.zeroVector) {
415
+ this.zeroVector = new Ammo.btVector3(0, 0, 0);
416
+ }
417
+ ammoBody.setLinearVelocity(this.zeroVector);
418
+ ammoBody.setAngularVelocity(this.zeroVector);
419
+ Ammo.destroy(pos);
420
+ Ammo.destroy(quat);
421
+ Ammo.destroy(transform);
422
+ }
423
+ }
424
+ // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
425
+ syncFromBones(boneWorldMatrices, boneInverseBindMatrices, boneCount) {
426
+ if (!this.ammo || !this.dynamicsWorld)
427
+ return;
428
+ const Ammo = this.ammo;
429
+ for (let i = 0; i < this.rigidbodies.length; i++) {
430
+ const rb = this.rigidbodies[i];
431
+ const ammoBody = this.ammoRigidbodies[i];
432
+ if (!ammoBody)
433
+ continue;
434
+ // Sync both Static (FollowBone) and Kinematic bodies - they both follow bones
435
+ if ((rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) &&
436
+ rb.boneIndex >= 0 &&
437
+ rb.boneIndex < boneCount) {
438
+ const boneIdx = rb.boneIndex;
439
+ const worldMatIdx = boneIdx * 16;
440
+ const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16));
441
+ // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
442
+ const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
443
+ const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
444
+ const worldPos = nodeWorldMatrix.getPosition();
445
+ const worldRot = nodeWorldMatrix.toQuat();
446
+ const transform = new Ammo.btTransform();
447
+ const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
448
+ const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
449
+ transform.setOrigin(pos);
450
+ transform.setRotation(quat);
451
+ ammoBody.setWorldTransform(transform);
452
+ ammoBody.getMotionState().setWorldTransform(transform);
453
+ if (!this.zeroVector) {
454
+ this.zeroVector = new Ammo.btVector3(0, 0, 0);
455
+ }
456
+ ammoBody.setLinearVelocity(this.zeroVector);
457
+ ammoBody.setAngularVelocity(this.zeroVector);
458
+ Ammo.destroy(pos);
459
+ Ammo.destroy(quat);
460
+ Ammo.destroy(transform);
461
+ }
462
+ }
463
+ }
464
+ // Step Ammo physics simulation
465
+ stepAmmoPhysics(dt) {
466
+ if (!this.ammo || !this.dynamicsWorld)
467
+ return;
468
+ const fixedTimeStep = 1 / 75;
469
+ const maxSubSteps = 10;
470
+ this.dynamicsWorld.stepSimulation(dt, maxSubSteps, fixedTimeStep);
471
+ }
472
+ // Apply dynamic rigidbody world transforms to bone world matrices in-place
473
+ applyAmmoRigidbodiesToBones(boneWorldMatrices, boneInverseBindMatrices, boneCount) {
474
+ if (!this.ammo || !this.dynamicsWorld)
475
+ return;
476
+ for (let i = 0; i < this.rigidbodies.length; i++) {
477
+ const rb = this.rigidbodies[i];
478
+ const ammoBody = this.ammoRigidbodies[i];
479
+ if (!ammoBody)
480
+ continue;
481
+ // Only dynamic rigidbodies drive bones (Static/Kinematic follow bones)
482
+ if (rb.type === RigidbodyType.Dynamic && rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
483
+ const boneIdx = rb.boneIndex;
484
+ const worldMatIdx = boneIdx * 16;
485
+ const transform = ammoBody.getWorldTransform();
486
+ const origin = transform.getOrigin();
487
+ const rotation = transform.getRotation();
488
+ const nodePos = new Vec3(origin.x(), origin.y(), origin.z());
489
+ const nodeRot = new Quat(rotation.x(), rotation.y(), rotation.z(), rotation.w());
490
+ const nodeWorldMatrix = Mat4.fromPositionRotation(nodePos, nodeRot);
491
+ // boneWorld = nodeWorld × bodyOffsetMatrixInverse (not bodyOffsetMatrixInverse × nodeWorld)
492
+ const boneWorldMat = nodeWorldMatrix.multiply(rb.bodyOffsetMatrixInverse);
493
+ const values = boneWorldMat.values;
494
+ if (!isNaN(values[0]) && !isNaN(values[15]) && Math.abs(values[0]) < 1e6 && Math.abs(values[15]) < 1e6) {
495
+ boneWorldMatrices.set(values, worldMatIdx);
496
+ }
497
+ else {
498
+ console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`);
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
@@ -0,0 +1,47 @@
1
+ import { Model } from "./model";
2
+ export declare class PmxLoader {
3
+ private view;
4
+ private offset;
5
+ private decoder;
6
+ private encoding;
7
+ private additionalVec4Count;
8
+ private vertexIndexSize;
9
+ private textureIndexSize;
10
+ private materialIndexSize;
11
+ private boneIndexSize;
12
+ private morphIndexSize;
13
+ private rigidBodyIndexSize;
14
+ private textures;
15
+ private materials;
16
+ private bones;
17
+ private inverseBindMatrices;
18
+ private joints0;
19
+ private weights0;
20
+ private rigidbodies;
21
+ private joints;
22
+ private constructor();
23
+ static load(url: string): Promise<Model>;
24
+ private parse;
25
+ private parseHeader;
26
+ private parseVertices;
27
+ private parseIndices;
28
+ private parseTextures;
29
+ private parseMaterials;
30
+ private parseBones;
31
+ private skipMorphs;
32
+ private skipDisplayFrames;
33
+ private parseRigidbodies;
34
+ private parseJoints;
35
+ private computeInverseBind;
36
+ private toModel;
37
+ private getUint8;
38
+ private getUint16;
39
+ private getVertexIndex;
40
+ private getNonVertexIndex;
41
+ private getInt32;
42
+ private getFloat32;
43
+ private getString;
44
+ private getText;
45
+ private getIndex;
46
+ }
47
+ //# sourceMappingURL=pmx-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA+C,MAAM,SAAS,CAAA;AAI5E,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C,OAAO,CAAC,KAAK;IAgBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAkEtB,OAAO,CAAC,UAAU;IA2IlB,OAAO,CAAC,UAAU;IAyGlB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IA2If,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}