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,938 @@
1
+ import { Model } from "./model";
2
+ import { Mat4, Vec3 } from "./math";
3
+ export class PmxLoader {
4
+ constructor(buffer) {
5
+ this.offset = 0;
6
+ this.encoding = 0;
7
+ this.additionalVec4Count = 0;
8
+ this.vertexIndexSize = 0;
9
+ this.textureIndexSize = 0;
10
+ this.materialIndexSize = 0;
11
+ this.boneIndexSize = 0;
12
+ this.morphIndexSize = 0;
13
+ this.rigidBodyIndexSize = 0;
14
+ this.textures = [];
15
+ this.materials = [];
16
+ this.bones = [];
17
+ this.inverseBindMatrices = null;
18
+ this.joints0 = null;
19
+ this.weights0 = null;
20
+ this.rigidbodies = [];
21
+ this.joints = [];
22
+ this.view = new DataView(buffer);
23
+ }
24
+ static async load(url) {
25
+ const loader = new PmxLoader(await fetch(url).then((r) => r.arrayBuffer()));
26
+ return loader.parse();
27
+ }
28
+ parse() {
29
+ this.parseHeader();
30
+ const { positions, normals, uvs } = this.parseVertices();
31
+ const indices = this.parseIndices();
32
+ this.parseTextures();
33
+ this.parseMaterials();
34
+ this.parseBones();
35
+ // Skip morphs and display frames before parsing rigidbodies
36
+ this.skipMorphs();
37
+ this.skipDisplayFrames();
38
+ this.parseRigidbodies();
39
+ this.parseJoints();
40
+ this.computeInverseBind();
41
+ return this.toModel(positions, normals, uvs, indices);
42
+ }
43
+ parseHeader() {
44
+ if (this.getString(3) !== "PMX")
45
+ throw new Error("Not a PMX file");
46
+ // PMX: 1-byte alignment after signature
47
+ this.getUint8();
48
+ // PMX: version (float32)
49
+ const version = this.getFloat32();
50
+ if (version < 2.0 || version > 2.2) {
51
+ // Continue, but warn for unexpected version
52
+ console.warn(`PMX version ${version} may not be fully supported`);
53
+ }
54
+ // PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
55
+ const globalsCount = this.getUint8();
56
+ // Validate globalsCount (must be at least 8 for PMX 2.x)
57
+ if (globalsCount < 8) {
58
+ throw new Error(`Invalid globalsCount: ${globalsCount}, expected at least 8`);
59
+ }
60
+ // Read globals (first 8 bytes are standard for PMX 2.x)
61
+ this.encoding = this.getUint8(); // 0:utf16le, 1:utf8
62
+ this.additionalVec4Count = this.getUint8();
63
+ this.vertexIndexSize = this.getUint8();
64
+ this.textureIndexSize = this.getUint8();
65
+ this.materialIndexSize = this.getUint8();
66
+ this.boneIndexSize = this.getUint8();
67
+ this.morphIndexSize = this.getUint8();
68
+ this.rigidBodyIndexSize = this.getUint8();
69
+ // Skip any extra globals beyond the first 8 (for future format extensions)
70
+ if (globalsCount > 8) {
71
+ for (let i = 8; i < globalsCount; i++) {
72
+ this.getUint8();
73
+ }
74
+ }
75
+ this.decoder = new TextDecoder(this.encoding === 0 ? "utf-16le" : "utf-8");
76
+ // Skip model info (4 text fields)
77
+ this.getText();
78
+ this.getText();
79
+ this.getText();
80
+ this.getText();
81
+ }
82
+ parseVertices() {
83
+ const count = this.getInt32();
84
+ const positions = [];
85
+ const normals = [];
86
+ const uvs = [];
87
+ // Prepare skinning arrays (4 influences per vertex)
88
+ const joints = new Uint16Array(count * 4);
89
+ const weights = new Uint8Array(count * 4); // UNORM8, will be normalized to 255
90
+ for (let i = 0; i < count; i++) {
91
+ const px = this.getFloat32();
92
+ const py = this.getFloat32();
93
+ const pz = this.getFloat32();
94
+ positions.push(px, py, pz);
95
+ const nx = this.getFloat32();
96
+ const ny = this.getFloat32();
97
+ const nz = this.getFloat32();
98
+ normals.push(nx, ny, nz);
99
+ const u = this.getFloat32();
100
+ const v = this.getFloat32();
101
+ // PMX UVs are in the same orientation as WebGPU sampling; no flip
102
+ uvs.push(u, v);
103
+ this.offset += this.additionalVec4Count * 16;
104
+ const type = this.getUint8();
105
+ const base = i * 4;
106
+ // Initialize defaults
107
+ joints[base] = 0;
108
+ joints[base + 1] = 0;
109
+ joints[base + 2] = 0;
110
+ joints[base + 3] = 0;
111
+ weights[base] = 255;
112
+ weights[base + 1] = 0;
113
+ weights[base + 2] = 0;
114
+ weights[base + 3] = 0;
115
+ if (type === 0) {
116
+ // BDEF1
117
+ const j0 = this.getNonVertexIndex(this.boneIndexSize);
118
+ joints[base] = j0 >= 0 ? j0 : 0;
119
+ // weight stays [255,0,0,0]
120
+ }
121
+ else if (type === 1 || type === 3) {
122
+ // BDEF2 or SDEF (treated as BDEF2)
123
+ const j0 = this.getNonVertexIndex(this.boneIndexSize);
124
+ const j1 = this.getNonVertexIndex(this.boneIndexSize);
125
+ const w0f = this.getFloat32();
126
+ const w0 = Math.max(0, Math.min(255, Math.round(w0f * 255)));
127
+ const w1 = Math.max(0, Math.min(255, 255 - w0));
128
+ joints[base] = j0 >= 0 ? j0 : 0;
129
+ joints[base + 1] = j1 >= 0 ? j1 : 0;
130
+ weights[base] = w0;
131
+ weights[base + 1] = w1;
132
+ // SDEF has extra 3 vec3 (C, R0, R1)
133
+ if (type === 3) {
134
+ this.offset += 36; // 9 floats * 4 bytes
135
+ }
136
+ }
137
+ else if (type === 2 || type === 4) {
138
+ // BDEF4 or QDEF (treat as LBS4)
139
+ let sum = 0;
140
+ for (let k = 0; k < 4; k++) {
141
+ const j = this.getNonVertexIndex(this.boneIndexSize);
142
+ joints[base + k] = j >= 0 ? j : 0;
143
+ }
144
+ const wf = [this.getFloat32(), this.getFloat32(), this.getFloat32(), this.getFloat32()];
145
+ const ws = wf.map((x) => Math.max(0, Math.min(1, x)));
146
+ const w8 = ws.map((x) => Math.round(x * 255));
147
+ sum = w8[0] + w8[1] + w8[2] + w8[3];
148
+ if (sum === 0) {
149
+ weights[base] = 255;
150
+ }
151
+ else {
152
+ // Normalize to 255
153
+ const scale = 255 / sum;
154
+ let accum = 0;
155
+ for (let k = 0; k < 3; k++) {
156
+ const v = Math.max(0, Math.min(255, Math.round(w8[k] * scale)));
157
+ weights[base + k] = v;
158
+ accum += v;
159
+ }
160
+ weights[base + 3] = Math.max(0, Math.min(255, 255 - accum));
161
+ }
162
+ }
163
+ else {
164
+ throw new Error(`Invalid bone weight type: ${type}`);
165
+ }
166
+ this.offset += 4; // edge scale
167
+ }
168
+ this.joints0 = joints;
169
+ this.weights0 = weights;
170
+ return { positions, normals, uvs };
171
+ }
172
+ // (removed) skipBoneWeight – replaced by inline parsing
173
+ parseIndices() {
174
+ const count = this.getInt32();
175
+ const indices = [];
176
+ for (let i = 0; i < count; i++) {
177
+ indices.push(this.getIndex(this.vertexIndexSize));
178
+ }
179
+ return indices;
180
+ }
181
+ parseTextures() {
182
+ try {
183
+ const count = this.getInt32();
184
+ this.textures = [];
185
+ for (let i = 0; i < count; i++) {
186
+ const textureName = this.getText();
187
+ this.textures.push({
188
+ path: textureName,
189
+ name: textureName.split("/").pop() || textureName, // Extract filename
190
+ });
191
+ }
192
+ }
193
+ catch (error) {
194
+ console.error("Error parsing textures:", error);
195
+ this.textures = [];
196
+ }
197
+ }
198
+ parseMaterials() {
199
+ try {
200
+ const count = this.getInt32();
201
+ this.materials = [];
202
+ for (let i = 0; i < count; i++) {
203
+ const name = this.getText();
204
+ this.getText(); // englishName (skip)
205
+ const diffuse = [this.getFloat32(), this.getFloat32(), this.getFloat32(), this.getFloat32()];
206
+ const specular = [this.getFloat32(), this.getFloat32(), this.getFloat32()];
207
+ const shininess = this.getFloat32();
208
+ const ambient = [this.getFloat32(), this.getFloat32(), this.getFloat32()];
209
+ const flag = this.getUint8();
210
+ // edgeColor vec4
211
+ const edgeColor = [
212
+ this.getFloat32(),
213
+ this.getFloat32(),
214
+ this.getFloat32(),
215
+ this.getFloat32(),
216
+ ];
217
+ // edgeSize float
218
+ const edgeSize = this.getFloat32();
219
+ const textureIndex = this.getNonVertexIndex(this.textureIndexSize);
220
+ const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize);
221
+ const sphereTextureMode = this.getUint8();
222
+ const isSharedToonTexture = this.getUint8() === 1;
223
+ const toonTextureIndex = isSharedToonTexture ? this.getUint8() : this.getNonVertexIndex(this.textureIndexSize);
224
+ this.getText(); // comment (skip)
225
+ const vertexCount = this.getInt32();
226
+ // PMX material flag bits:
227
+ // Bit 0 (0x01): Double-sided rendering
228
+ // Bit 4 (0x10): Edge drawing (outline)
229
+ this.materials.push({
230
+ name,
231
+ diffuse,
232
+ specular,
233
+ ambient,
234
+ shininess,
235
+ diffuseTextureIndex: textureIndex,
236
+ normalTextureIndex: -1, // Not used in basic PMX
237
+ sphereTextureIndex,
238
+ sphereMode: sphereTextureMode,
239
+ toonTextureIndex,
240
+ edgeFlag: flag,
241
+ edgeColor,
242
+ edgeSize,
243
+ vertexCount,
244
+ });
245
+ }
246
+ }
247
+ catch (error) {
248
+ console.error("Error parsing materials:", error);
249
+ this.materials = [];
250
+ }
251
+ }
252
+ parseBones() {
253
+ try {
254
+ const count = this.getInt32();
255
+ const bones = [];
256
+ const abs = new Array(count);
257
+ // PMX 2.x bone flags (best-effort common masks)
258
+ const FLAG_TAIL_IS_BONE = 0x0001;
259
+ const FLAG_IK = 0x0020;
260
+ const FLAG_APPEND_ROTATE = 0x0100;
261
+ const FLAG_APPEND_MOVE = 0x0200;
262
+ const FLAG_AXIS_LIMIT = 0x0400;
263
+ const FLAG_LOCAL_AXIS = 0x0800;
264
+ const FLAG_EXTERNAL_PARENT = 0x2000;
265
+ for (let i = 0; i < count; i++) {
266
+ const name = this.getText();
267
+ this.getText(); // englishName (skip)
268
+ // PMX is left-handed, engine is now left-handed - no conversion needed
269
+ const x = this.getFloat32();
270
+ const y = this.getFloat32();
271
+ const z = this.getFloat32();
272
+ const parentIndex = this.getNonVertexIndex(this.boneIndexSize);
273
+ this.getInt32(); // transform order (skip)
274
+ const flags = this.getUint16();
275
+ // Tail: bone index or offset vector3
276
+ if ((flags & FLAG_TAIL_IS_BONE) !== 0) {
277
+ this.getNonVertexIndex(this.boneIndexSize);
278
+ }
279
+ else {
280
+ // tail offset vec3
281
+ this.getFloat32();
282
+ this.getFloat32();
283
+ this.getFloat32();
284
+ }
285
+ // Append transform (inherit/ratio)
286
+ let appendParent = undefined;
287
+ let appendRatio = undefined;
288
+ let appendRotate = false;
289
+ let appendMove = false;
290
+ if ((flags & (FLAG_APPEND_ROTATE | FLAG_APPEND_MOVE)) !== 0) {
291
+ appendParent = this.getNonVertexIndex(this.boneIndexSize); // append parent
292
+ appendRatio = this.getFloat32(); // ratio
293
+ appendRotate = (flags & FLAG_APPEND_ROTATE) !== 0;
294
+ appendMove = (flags & FLAG_APPEND_MOVE) !== 0;
295
+ }
296
+ // Axis limit
297
+ if ((flags & FLAG_AXIS_LIMIT) !== 0) {
298
+ this.getFloat32();
299
+ this.getFloat32();
300
+ this.getFloat32();
301
+ }
302
+ // Local axis (two vectors x and z)
303
+ if ((flags & FLAG_LOCAL_AXIS) !== 0) {
304
+ // local axis X
305
+ this.getFloat32();
306
+ this.getFloat32();
307
+ this.getFloat32();
308
+ // local axis Z
309
+ this.getFloat32();
310
+ this.getFloat32();
311
+ this.getFloat32();
312
+ }
313
+ // External parent transform
314
+ if ((flags & FLAG_EXTERNAL_PARENT) !== 0) {
315
+ this.getInt32();
316
+ }
317
+ // IK block
318
+ if ((flags & FLAG_IK) !== 0) {
319
+ this.getNonVertexIndex(this.boneIndexSize); // target
320
+ this.getInt32(); // iteration
321
+ this.getFloat32(); // rotationConstraint
322
+ const linksCount = this.getInt32();
323
+ for (let li = 0; li < linksCount; li++) {
324
+ this.getNonVertexIndex(this.boneIndexSize); // link target
325
+ const hasLimit = this.getUint8() === 1;
326
+ if (hasLimit) {
327
+ // min and max angles (vec3 each)
328
+ this.getFloat32();
329
+ this.getFloat32();
330
+ this.getFloat32();
331
+ this.getFloat32();
332
+ this.getFloat32();
333
+ this.getFloat32();
334
+ }
335
+ }
336
+ }
337
+ // Stash minimal bone info; append data will be merged later
338
+ abs[i] = { name, parent: parentIndex, x, y, z, appendParent, appendRatio, appendRotate, appendMove };
339
+ }
340
+ for (let i = 0; i < count; i++) {
341
+ const a = abs[i];
342
+ if (a.parent >= 0 && a.parent < count) {
343
+ const p = abs[a.parent];
344
+ bones.push({
345
+ name: a.name,
346
+ parentIndex: a.parent,
347
+ bindTranslation: [a.x - p.x, a.y - p.y, a.z - p.z],
348
+ children: [], // Will be populated later when building skeleton
349
+ appendParentIndex: a.appendParent,
350
+ appendRatio: a.appendRatio,
351
+ appendRotate: a.appendRotate,
352
+ appendMove: a.appendMove,
353
+ });
354
+ }
355
+ else {
356
+ bones.push({
357
+ name: a.name,
358
+ parentIndex: a.parent,
359
+ bindTranslation: [a.x, a.y, a.z],
360
+ children: [], // Will be populated later when building skeleton
361
+ appendParentIndex: a.appendParent,
362
+ appendRatio: a.appendRatio,
363
+ appendRotate: a.appendRotate,
364
+ appendMove: a.appendMove,
365
+ });
366
+ }
367
+ }
368
+ this.bones = bones;
369
+ }
370
+ catch (e) {
371
+ console.warn("Error parsing bones:", e);
372
+ this.bones = [];
373
+ }
374
+ }
375
+ skipMorphs() {
376
+ try {
377
+ // Check if we have enough bytes to read the count
378
+ if (this.offset + 4 > this.view.buffer.byteLength) {
379
+ return false;
380
+ }
381
+ const count = this.getInt32();
382
+ if (count < 0 || count > 100000) {
383
+ // Suspicious count, likely corrupted - restore offset
384
+ this.offset -= 4;
385
+ return false;
386
+ }
387
+ for (let i = 0; i < count; i++) {
388
+ // Check bounds before reading each morph
389
+ if (this.offset >= this.view.buffer.byteLength) {
390
+ return false;
391
+ }
392
+ try {
393
+ this.getText(); // name
394
+ this.getText(); // englishName
395
+ this.getUint8(); // panelType
396
+ const morphType = this.getUint8();
397
+ const offsetCount = this.getInt32();
398
+ // Skip offsets based on morph type
399
+ for (let j = 0; j < offsetCount; j++) {
400
+ if (this.offset >= this.view.buffer.byteLength) {
401
+ return false;
402
+ }
403
+ if (morphType === 0) {
404
+ // Group morph
405
+ this.getNonVertexIndex(this.morphIndexSize); // morphIndex
406
+ this.getFloat32(); // ratio
407
+ }
408
+ else if (morphType === 1) {
409
+ // Vertex morph
410
+ this.getIndex(this.vertexIndexSize); // vertexIndex
411
+ this.getFloat32(); // x
412
+ this.getFloat32(); // y
413
+ this.getFloat32(); // z
414
+ }
415
+ else if (morphType === 2) {
416
+ // Bone morph
417
+ this.getNonVertexIndex(this.boneIndexSize); // boneIndex
418
+ this.getFloat32(); // x
419
+ this.getFloat32(); // y
420
+ this.getFloat32(); // z
421
+ this.getFloat32(); // rx
422
+ this.getFloat32(); // ry
423
+ this.getFloat32(); // rz
424
+ }
425
+ else if (morphType === 3) {
426
+ // UV morph
427
+ this.getIndex(this.vertexIndexSize); // vertexIndex
428
+ this.getFloat32(); // u
429
+ this.getFloat32(); // v
430
+ }
431
+ else if (morphType === 4 || morphType === 5 || morphType === 6 || morphType === 7) {
432
+ // UV morph types 4-7 (additional UV channels)
433
+ this.getIndex(this.vertexIndexSize); // vertexIndex
434
+ this.getFloat32(); // u
435
+ this.getFloat32(); // v
436
+ }
437
+ else if (morphType === 8) {
438
+ // Material morph
439
+ this.getNonVertexIndex(this.materialIndexSize); // materialIndex
440
+ this.getUint8(); // offsetType
441
+ this.getFloat32(); // diffuse r
442
+ this.getFloat32(); // diffuse g
443
+ this.getFloat32(); // diffuse b
444
+ this.getFloat32(); // diffuse a
445
+ this.getFloat32(); // specular r
446
+ this.getFloat32(); // specular g
447
+ this.getFloat32(); // specular b
448
+ this.getFloat32(); // specular power
449
+ this.getFloat32(); // ambient r
450
+ this.getFloat32(); // ambient g
451
+ this.getFloat32(); // ambient b
452
+ this.getFloat32(); // edgeColor r
453
+ this.getFloat32(); // edgeColor g
454
+ this.getFloat32(); // edgeColor b
455
+ this.getFloat32(); // edgeColor a
456
+ this.getFloat32(); // edgeSize
457
+ this.getFloat32(); // textureCoeff r
458
+ this.getFloat32(); // textureCoeff g
459
+ this.getFloat32(); // textureCoeff b
460
+ this.getFloat32(); // textureCoeff a
461
+ this.getFloat32(); // sphereCoeff r
462
+ this.getFloat32(); // sphereCoeff g
463
+ this.getFloat32(); // sphereCoeff b
464
+ this.getFloat32(); // sphereCoeff a
465
+ this.getFloat32(); // toonCoeff r
466
+ this.getFloat32(); // toonCoeff g
467
+ this.getFloat32(); // toonCoeff b
468
+ this.getFloat32(); // toonCoeff a
469
+ }
470
+ }
471
+ }
472
+ catch (e) {
473
+ // If we fail to read a morph, stop skipping
474
+ console.warn(`Error reading morph ${i}:`, e);
475
+ return false;
476
+ }
477
+ }
478
+ return true;
479
+ }
480
+ catch (e) {
481
+ console.warn("Error skipping morphs:", e);
482
+ return false;
483
+ }
484
+ }
485
+ skipDisplayFrames() {
486
+ try {
487
+ // Check if we have enough bytes to read the count
488
+ if (this.offset + 4 > this.view.buffer.byteLength) {
489
+ return false;
490
+ }
491
+ const count = this.getInt32();
492
+ if (count < 0 || count > 100000) {
493
+ // Suspicious count, likely corrupted - restore offset
494
+ this.offset -= 4;
495
+ return false;
496
+ }
497
+ for (let i = 0; i < count; i++) {
498
+ // Check bounds before reading each frame
499
+ if (this.offset >= this.view.buffer.byteLength) {
500
+ return false;
501
+ }
502
+ try {
503
+ this.getText(); // name
504
+ this.getText(); // englishName
505
+ this.getUint8(); // flag
506
+ const elementCount = this.getInt32();
507
+ for (let j = 0; j < elementCount; j++) {
508
+ if (this.offset >= this.view.buffer.byteLength) {
509
+ return false;
510
+ }
511
+ const elementType = this.getUint8();
512
+ if (elementType === 0) {
513
+ // Bone frame
514
+ this.getNonVertexIndex(this.boneIndexSize);
515
+ }
516
+ else if (elementType === 1) {
517
+ // Morph frame
518
+ this.getNonVertexIndex(this.morphIndexSize);
519
+ }
520
+ }
521
+ }
522
+ catch (e) {
523
+ // If we fail to read a frame, stop skipping
524
+ console.warn(`Error reading display frame ${i}:`, e);
525
+ return false;
526
+ }
527
+ }
528
+ return true;
529
+ }
530
+ catch (e) {
531
+ console.warn("Error skipping display frames:", e);
532
+ return false;
533
+ }
534
+ }
535
+ parseRigidbodies() {
536
+ try {
537
+ // Note: morphs and display frames are already skipped in parse() before calling this
538
+ // Check bounds before reading rigidbody count
539
+ if (this.offset + 4 > this.view.buffer.byteLength) {
540
+ console.warn("Not enough bytes for rigidbody count");
541
+ this.rigidbodies = [];
542
+ return;
543
+ }
544
+ const count = this.getInt32();
545
+ if (count < 0 || count > 10000) {
546
+ // Suspicious count
547
+ console.warn(`Suspicious rigidbody count: ${count}`);
548
+ this.rigidbodies = [];
549
+ return;
550
+ }
551
+ this.rigidbodies = [];
552
+ for (let i = 0; i < count; i++) {
553
+ try {
554
+ // Check bounds before reading each rigidbody
555
+ if (this.offset >= this.view.buffer.byteLength) {
556
+ console.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`);
557
+ break;
558
+ }
559
+ const name = this.getText();
560
+ const englishName = this.getText();
561
+ const boneIndex = this.getNonVertexIndex(this.boneIndexSize);
562
+ const group = this.getUint8();
563
+ const collisionMask = this.getUint16();
564
+ const shape = this.getUint8(); // 0=sphere, 1=box, 2=capsule
565
+ // Size parameters (depends on shape)
566
+ const sizeX = this.getFloat32();
567
+ const sizeY = this.getFloat32();
568
+ const sizeZ = this.getFloat32();
569
+ const posX = this.getFloat32();
570
+ const posY = this.getFloat32();
571
+ const posZ = this.getFloat32();
572
+ // Rotation (Euler angles in RADIANS in PMX file - PMX Editor displays them as degrees for convenience)
573
+ // ZXY order (left-handed system)
574
+ const rotX = this.getFloat32();
575
+ const rotY = this.getFloat32();
576
+ const rotZ = this.getFloat32();
577
+ // Physical properties
578
+ const mass = this.getFloat32();
579
+ const linearDamping = this.getFloat32();
580
+ const angularDamping = this.getFloat32();
581
+ const restitution = this.getFloat32();
582
+ const friction = this.getFloat32();
583
+ const type = this.getUint8(); // 0=static, 1=dynamic, 2=kinematic
584
+ this.rigidbodies.push({
585
+ name,
586
+ englishName,
587
+ boneIndex,
588
+ group,
589
+ collisionMask,
590
+ shape: shape,
591
+ size: new Vec3(sizeX, sizeY, sizeZ),
592
+ shapePosition: new Vec3(posX, posY, posZ),
593
+ shapeRotation: new Vec3(rotX, rotY, rotZ),
594
+ mass,
595
+ linearDamping,
596
+ angularDamping,
597
+ restitution,
598
+ friction,
599
+ type: type,
600
+ bodyOffsetMatrixInverse: Mat4.identity(),
601
+ });
602
+ }
603
+ catch (e) {
604
+ console.warn(`Error reading rigidbody ${i} of ${count}:`, e);
605
+ // Stop parsing if we encounter an error
606
+ break;
607
+ }
608
+ }
609
+ }
610
+ catch (e) {
611
+ console.warn("Error parsing rigidbodies:", e);
612
+ this.rigidbodies = [];
613
+ }
614
+ }
615
+ parseJoints() {
616
+ try {
617
+ // Check bounds before reading joint count
618
+ if (this.offset + 4 > this.view.buffer.byteLength) {
619
+ console.warn("Not enough bytes for joint count");
620
+ this.joints = [];
621
+ return;
622
+ }
623
+ const count = this.getInt32();
624
+ if (count < 0 || count > 10000) {
625
+ console.warn(`Suspicious joint count: ${count}`);
626
+ this.joints = [];
627
+ return;
628
+ }
629
+ this.joints = [];
630
+ for (let i = 0; i < count; i++) {
631
+ try {
632
+ // Check bounds before reading each joint
633
+ if (this.offset >= this.view.buffer.byteLength) {
634
+ console.warn(`Reached end of buffer while reading joint ${i} of ${count}`);
635
+ break;
636
+ }
637
+ const name = this.getText();
638
+ const englishName = this.getText();
639
+ const type = this.getUint8();
640
+ // Rigidbody indices use rigidBodyIndexSize (not boneIndexSize)
641
+ const rigidbodyIndexA = this.getNonVertexIndex(this.rigidBodyIndexSize);
642
+ const rigidbodyIndexB = this.getNonVertexIndex(this.rigidBodyIndexSize);
643
+ const posX = this.getFloat32();
644
+ const posY = this.getFloat32();
645
+ const posZ = this.getFloat32();
646
+ // Rotation (Euler angles in RADIANS in PMX file - PMX Editor displays them as degrees for convenience)
647
+ // ZXY order (left-handed system)
648
+ const rotX = this.getFloat32();
649
+ const rotY = this.getFloat32();
650
+ const rotZ = this.getFloat32();
651
+ const posMinX = this.getFloat32();
652
+ const posMinY = this.getFloat32();
653
+ const posMinZ = this.getFloat32();
654
+ const posMaxX = this.getFloat32();
655
+ const posMaxY = this.getFloat32();
656
+ const posMaxZ = this.getFloat32();
657
+ // Rotation constraints (in RADIANS in PMX file)
658
+ const rotMinX = this.getFloat32();
659
+ const rotMinY = this.getFloat32();
660
+ const rotMinZ = this.getFloat32();
661
+ const rotMaxX = this.getFloat32();
662
+ const rotMaxY = this.getFloat32();
663
+ const rotMaxZ = this.getFloat32();
664
+ // Spring parameters
665
+ const springPosX = this.getFloat32();
666
+ const springPosY = this.getFloat32();
667
+ const springPosZ = this.getFloat32();
668
+ // Spring rotation (stiffness values in PMX file)
669
+ const springRotX = this.getFloat32();
670
+ const springRotY = this.getFloat32();
671
+ const springRotZ = this.getFloat32();
672
+ // Store rotations and springs as Vec3 (Euler angles), not Quat
673
+ this.joints.push({
674
+ name,
675
+ englishName,
676
+ type,
677
+ rigidbodyIndexA,
678
+ rigidbodyIndexB,
679
+ position: new Vec3(posX, posY, posZ),
680
+ rotation: new Vec3(rotX, rotY, rotZ), // Store as Vec3 (Euler angles)
681
+ positionMin: new Vec3(posMinX, posMinY, posMinZ),
682
+ positionMax: new Vec3(posMaxX, posMaxY, posMaxZ),
683
+ rotationMin: new Vec3(rotMinX, rotMinY, rotMinZ), // Store as Vec3 (Euler angles)
684
+ rotationMax: new Vec3(rotMaxX, rotMaxY, rotMaxZ), // Store as Vec3 (Euler angles)
685
+ springPosition: new Vec3(springPosX, springPosY, springPosZ),
686
+ springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
687
+ });
688
+ }
689
+ catch (e) {
690
+ console.warn(`Error reading joint ${i} of ${count}:`, e);
691
+ // Stop parsing if we encounter an error
692
+ break;
693
+ }
694
+ }
695
+ }
696
+ catch (e) {
697
+ console.warn("Error parsing joints:", e);
698
+ this.joints = [];
699
+ }
700
+ }
701
+ computeInverseBind() {
702
+ if (!this.bones || this.bones.length === 0) {
703
+ this.inverseBindMatrices = new Float32Array(0);
704
+ return;
705
+ }
706
+ const n = this.bones.length;
707
+ const world = new Array(n).fill(null);
708
+ const inv = new Float32Array(n * 16);
709
+ const computeWorld = (i) => {
710
+ if (world[i])
711
+ return world[i];
712
+ const bone = this.bones[i];
713
+ const local = Mat4.identity().translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]);
714
+ let w;
715
+ if (bone.parentIndex >= 0 && bone.parentIndex < n) {
716
+ w = computeWorld(bone.parentIndex).multiply(local);
717
+ }
718
+ else {
719
+ w = local;
720
+ }
721
+ world[i] = w;
722
+ return w;
723
+ };
724
+ for (let i = 0; i < n; i++) {
725
+ const w = computeWorld(i);
726
+ const invm = Mat4.identity().translateInPlace(-w.values[12], -w.values[13], -w.values[14]);
727
+ inv.set(invm.values, i * 16);
728
+ }
729
+ this.inverseBindMatrices = inv;
730
+ }
731
+ toModel(positions, normals, uvs, indices) {
732
+ // Create indexed vertex buffer
733
+ const vertexCount = positions.length / 3;
734
+ const vertexData = new Float32Array(vertexCount * 8);
735
+ for (let i = 0; i < vertexCount; i++) {
736
+ const pi = i * 3;
737
+ const ui = i * 2;
738
+ const vi = i * 8;
739
+ vertexData[vi] = positions[pi];
740
+ vertexData[vi + 1] = positions[pi + 1];
741
+ vertexData[vi + 2] = positions[pi + 2];
742
+ vertexData[vi + 3] = normals[pi];
743
+ vertexData[vi + 4] = normals[pi + 1];
744
+ vertexData[vi + 5] = normals[pi + 2];
745
+ vertexData[vi + 6] = uvs[ui];
746
+ vertexData[vi + 7] = uvs[ui + 1];
747
+ }
748
+ // Create index buffer
749
+ const indexData = new Uint32Array(indices);
750
+ // Create skeleton (required)
751
+ const skeleton = {
752
+ bones: this.bones.length > 0 ? this.bones : [],
753
+ inverseBindMatrices: this.inverseBindMatrices || new Float32Array(0),
754
+ };
755
+ let skinning;
756
+ if (this.joints0 && this.weights0) {
757
+ // Clamp joints to valid range now that we know bone count, and renormalize weights
758
+ const boneCount = this.bones.length;
759
+ const joints = this.joints0;
760
+ const weights = this.weights0;
761
+ for (let i = 0; i < joints.length; i += 4) {
762
+ // First pass: identify and zero out invalid joints, collect valid weight sum
763
+ let validWeightSum = 0;
764
+ let validCount = 0;
765
+ for (let k = 0; k < 4; k++) {
766
+ const j = joints[i + k];
767
+ if (j < 0 || j >= boneCount) {
768
+ // Invalid joint: zero the weight but keep joint index for debugging
769
+ weights[i + k] = 0;
770
+ // Optionally clamp to valid range
771
+ if (j < 0) {
772
+ joints[i + k] = 0;
773
+ }
774
+ else {
775
+ joints[i + k] = boneCount > 0 ? boneCount - 1 : 0;
776
+ }
777
+ }
778
+ else {
779
+ validWeightSum += weights[i + k];
780
+ validCount++;
781
+ }
782
+ }
783
+ // If no valid weights, assign all weight to first bone (bone 0)
784
+ if (validWeightSum === 0 || validCount === 0) {
785
+ weights[i] = 255;
786
+ weights[i + 1] = 0;
787
+ weights[i + 2] = 0;
788
+ weights[i + 3] = 0;
789
+ joints[i] = boneCount > 0 ? 0 : 0;
790
+ joints[i + 1] = 0;
791
+ joints[i + 2] = 0;
792
+ joints[i + 3] = 0;
793
+ }
794
+ else if (validWeightSum !== 255) {
795
+ // Normalize valid weights to sum to exactly 255
796
+ const scale = 255 / validWeightSum;
797
+ let accum = 0;
798
+ for (let k = 0; k < 3; k++) {
799
+ if (joints[i + k] >= 0 && joints[i + k] < boneCount) {
800
+ const v = Math.max(0, Math.min(255, Math.round(weights[i + k] * scale)));
801
+ weights[i + k] = v;
802
+ accum += v;
803
+ }
804
+ else {
805
+ weights[i + k] = 0;
806
+ }
807
+ }
808
+ // Handle the 4th weight
809
+ if (joints[i + 3] >= 0 && joints[i + 3] < boneCount) {
810
+ weights[i + 3] = Math.max(0, Math.min(255, 255 - accum));
811
+ }
812
+ else {
813
+ weights[i + 3] = 0;
814
+ // Redistribute the remainder to the last valid weight
815
+ if (accum < 255) {
816
+ for (let k = 2; k >= 0; k--) {
817
+ if (joints[i + k] >= 0 && joints[i + k] < boneCount && weights[i + k] > 0) {
818
+ weights[i + k] = Math.min(255, weights[i + k] + (255 - accum));
819
+ break;
820
+ }
821
+ }
822
+ }
823
+ }
824
+ // Final safety check: ensure sum is exactly 255
825
+ const finalSum = weights[i] + weights[i + 1] + weights[i + 2] + weights[i + 3];
826
+ if (finalSum !== 255) {
827
+ const diff = 255 - finalSum;
828
+ // Add/subtract difference to/from the largest valid weight
829
+ let maxIdx = 0;
830
+ let maxWeight = weights[i];
831
+ for (let k = 1; k < 4; k++) {
832
+ if (weights[i + k] > maxWeight && joints[i + k] >= 0 && joints[i + k] < boneCount) {
833
+ maxWeight = weights[i + k];
834
+ maxIdx = k;
835
+ }
836
+ }
837
+ if (joints[i + maxIdx] >= 0 && joints[i + maxIdx] < boneCount) {
838
+ weights[i + maxIdx] = Math.max(0, Math.min(255, weights[i + maxIdx] + diff));
839
+ }
840
+ }
841
+ }
842
+ }
843
+ skinning = { joints, weights };
844
+ }
845
+ else {
846
+ // Create default skinning (single bone per vertex)
847
+ const vertexCount = positions.length / 3;
848
+ const joints = new Uint16Array(vertexCount * 4);
849
+ const weights = new Uint8Array(vertexCount * 4);
850
+ for (let i = 0; i < vertexCount; i++) {
851
+ joints[i * 4] = 0;
852
+ weights[i * 4] = 255;
853
+ }
854
+ skinning = { joints, weights };
855
+ }
856
+ return new Model(vertexData, indexData, this.textures, this.materials, skeleton, skinning, this.rigidbodies, this.joints);
857
+ }
858
+ getUint8() {
859
+ if (this.offset >= this.view.buffer.byteLength) {
860
+ throw new RangeError(`Offset ${this.offset} exceeds buffer bounds ${this.view.buffer.byteLength}`);
861
+ }
862
+ return this.view.getUint8(this.offset++);
863
+ }
864
+ getUint16() {
865
+ if (this.offset + 2 > this.view.buffer.byteLength) {
866
+ throw new RangeError(`Offset ${this.offset} + 2 exceeds buffer bounds ${this.view.buffer.byteLength}`);
867
+ }
868
+ const v = this.view.getUint16(this.offset, true);
869
+ this.offset += 2;
870
+ return v;
871
+ }
872
+ // Vertex index: 1->uint8, 2->uint16, 4->int32
873
+ getVertexIndex(size) {
874
+ if (size === 1)
875
+ return this.getUint8();
876
+ if (size === 2) {
877
+ const v = this.view.getUint16(this.offset, true);
878
+ this.offset += 2;
879
+ return v;
880
+ }
881
+ return this.getInt32();
882
+ }
883
+ // Non-vertex indices (texture/material/bone/morph/rigid): 1->int8, 2->int16, 4->int32
884
+ getNonVertexIndex(size) {
885
+ if (size === 1) {
886
+ const v = this.view.getInt8(this.offset);
887
+ this.offset += 1;
888
+ return v;
889
+ }
890
+ if (size === 2) {
891
+ const v = this.view.getInt16(this.offset, true);
892
+ this.offset += 2;
893
+ return v;
894
+ }
895
+ return this.getInt32();
896
+ }
897
+ getInt32() {
898
+ if (this.offset + 4 > this.view.buffer.byteLength) {
899
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
900
+ }
901
+ const v = this.view.getInt32(this.offset, true);
902
+ this.offset += 4;
903
+ return v;
904
+ }
905
+ getFloat32() {
906
+ if (this.offset + 4 > this.view.buffer.byteLength) {
907
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
908
+ }
909
+ const v = this.view.getFloat32(this.offset, true);
910
+ this.offset += 4;
911
+ return v;
912
+ }
913
+ getString(len) {
914
+ const bytes = new Uint8Array(this.view.buffer, this.offset, len);
915
+ this.offset += len;
916
+ return String.fromCharCode(...bytes);
917
+ }
918
+ getText() {
919
+ const len = this.getInt32();
920
+ if (len <= 0)
921
+ return "";
922
+ // Debug: log problematic string lengths
923
+ if (len > 1000 || len < -1000) {
924
+ throw new RangeError(`Suspicious string length: ${len} at offset ${this.offset - 4}`);
925
+ }
926
+ // Ensure we don't read beyond buffer bounds
927
+ if (this.offset + len > this.view.buffer.byteLength) {
928
+ throw new RangeError(`String length ${len} exceeds buffer bounds at offset ${this.offset - 4}`);
929
+ }
930
+ const bytes = new Uint8Array(this.view.buffer, this.offset, len);
931
+ this.offset += len;
932
+ return this.decoder.decode(bytes);
933
+ }
934
+ getIndex(size) {
935
+ // Backward-compat helper: defaults to vertex index behavior
936
+ return this.getVertexIndex(size);
937
+ }
938
+ }