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.
- package/README.md +3 -0
- package/dist/ammo-loader.d.ts +4 -0
- package/dist/ammo-loader.d.ts.map +1 -0
- package/dist/ammo-loader.js +29 -0
- package/dist/camera.d.ts +46 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +303 -0
- package/dist/engine.d.ts +88 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +990 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/math.d.ts +52 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +389 -0
- package/dist/model.d.ts +102 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +416 -0
- package/dist/physics.d.ts +82 -0
- package/dist/physics.d.ts.map +1 -0
- package/dist/physics.js +503 -0
- package/dist/pmx-loader.d.ts +47 -0
- package/dist/pmx-loader.d.ts.map +1 -0
- package/dist/pmx-loader.js +938 -0
- package/package.json +42 -0
- package/src/ammo-loader.ts +35 -0
- package/src/camera.ts +358 -0
- package/src/engine.ts +1166 -0
- package/src/index.ts +1 -0
- package/src/math.ts +505 -0
- package/src/model.ts +586 -0
- package/src/physics.ts +680 -0
- package/src/pmx-loader.ts +1031 -0
package/dist/physics.js
ADDED
|
@@ -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"}
|