reze-engine 0.50.6 → 0.50.8

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.
Files changed (84) hide show
  1. package/dist/_dbg-loader.mjs +1100 -0
  2. package/dist/engine.d.ts +71 -0
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +114 -1
  5. package/dist/graph/slots.d.ts.map +1 -1
  6. package/dist/graph/slots.js +35 -4
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/physics-debug.d.ts +30 -0
  10. package/dist/physics-debug.d.ts.map +1 -0
  11. package/dist/physics-debug.js +526 -0
  12. package/dist/shaders/cast-api.d.ts +1 -1
  13. package/dist/shaders/cast-api.d.ts.map +1 -1
  14. package/dist/shaders/cast-api.js +11 -0
  15. package/dist/shaders/materials/body.d.ts +2 -0
  16. package/dist/shaders/materials/body.d.ts.map +1 -0
  17. package/dist/shaders/materials/body.js +95 -0
  18. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  19. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  20. package/dist/shaders/materials/cloth_rough.js +69 -0
  21. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  22. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  23. package/dist/shaders/materials/cloth_smooth.js +61 -0
  24. package/dist/shaders/materials/common.d.ts +20 -0
  25. package/dist/shaders/materials/common.d.ts.map +1 -1
  26. package/dist/shaders/materials/common.js +85 -1
  27. package/dist/shaders/materials/default.d.ts +2 -0
  28. package/dist/shaders/materials/default.d.ts.map +1 -0
  29. package/dist/shaders/materials/default.js +43 -0
  30. package/dist/shaders/materials/eye.d.ts +2 -0
  31. package/dist/shaders/materials/eye.d.ts.map +1 -0
  32. package/dist/shaders/materials/eye.js +60 -0
  33. package/dist/shaders/materials/face.d.ts +2 -0
  34. package/dist/shaders/materials/face.d.ts.map +1 -0
  35. package/dist/shaders/materials/face.js +95 -0
  36. package/dist/shaders/materials/hair.d.ts +2 -0
  37. package/dist/shaders/materials/hair.d.ts.map +1 -0
  38. package/dist/shaders/materials/hair.js +90 -0
  39. package/dist/shaders/materials/metal.d.ts +2 -0
  40. package/dist/shaders/materials/metal.d.ts.map +1 -0
  41. package/dist/shaders/materials/metal.js +77 -0
  42. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  43. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  44. package/dist/shaders/materials/mmd_classic.js +66 -0
  45. package/dist/shaders/materials/stockings.d.ts +2 -0
  46. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  47. package/dist/shaders/materials/stockings.js +122 -0
  48. package/dist/shaders/passes/composite.d.ts +1 -1
  49. package/dist/shaders/passes/composite.d.ts.map +1 -1
  50. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  51. package/dist/shaders/passes/depth-prepass.js +21 -0
  52. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  53. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  54. package/dist/shaders/passes/physics-debug.js +69 -0
  55. package/dist/shaders/passes/shadow.d.ts +1 -1
  56. package/dist/shaders/passes/shadow.d.ts.map +1 -1
  57. package/dist/shaders/passes/shadow.js +19 -1
  58. package/dist/vmd-writer.d.ts +11 -6
  59. package/dist/vmd-writer.d.ts.map +1 -1
  60. package/dist/vmd-writer.js +35 -17
  61. package/package.json +1 -1
  62. package/src/engine.ts +139 -1
  63. package/src/graph/slots.ts +37 -4
  64. package/src/index.ts +1 -0
  65. package/src/shaders/cast-api.ts +11 -0
  66. package/src/shaders/materials/common.ts +86 -1
  67. package/src/shaders/passes/depth-prepass.ts +21 -0
  68. package/src/shaders/passes/shadow.ts +20 -1
  69. package/src/vmd-writer.ts +36 -16
  70. package/dist/physics/autofit.d.ts +0 -147
  71. package/dist/physics/autofit.d.ts.map +0 -1
  72. package/dist/physics/autofit.js +0 -501
  73. package/dist/shaders/passes/field-blit.d.ts +0 -26
  74. package/dist/shaders/passes/field-blit.d.ts.map +0 -1
  75. package/dist/shaders/passes/field-blit.js +0 -65
  76. package/dist/shaders/passes/ground-noise.d.ts +0 -7
  77. package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
  78. package/dist/shaders/passes/ground-noise.js +0 -88
  79. package/dist/shaders/passes/sim.d.ts +0 -34
  80. package/dist/shaders/passes/sim.d.ts.map +0 -1
  81. package/dist/shaders/passes/sim.js +0 -169
  82. package/dist/shaders/score-api.d.ts +0 -10
  83. package/dist/shaders/score-api.d.ts.map +0 -1
  84. package/dist/shaders/score-api.js +0 -114
@@ -0,0 +1,1100 @@
1
+ import { Model, } from "./model";
2
+ import { Mat4, Vec3 } from "./math";
3
+ import { RigidbodyType } from "./physics";
4
+ import { createFetchAssetReader } from "./asset-reader";
5
+ export class PmxLoader {
6
+ constructor(buffer) {
7
+ this.offset = 0;
8
+ this.encoding = 0;
9
+ this.additionalVec4Count = 0;
10
+ this.vertexIndexSize = 0;
11
+ this.textureIndexSize = 0;
12
+ this.materialIndexSize = 0;
13
+ this.boneIndexSize = 0;
14
+ this.morphIndexSize = 0;
15
+ this.rigidBodyIndexSize = 0;
16
+ this.textures = [];
17
+ this.materials = [];
18
+ this.bones = [];
19
+ this.inverseBindMatrices = null;
20
+ this.joints0 = null;
21
+ this.weights0 = null;
22
+ this.morphs = [];
23
+ this.vertexCount = 0;
24
+ this.rigidbodies = [];
25
+ this.joints = [];
26
+ this.warnings = [];
27
+ this.view = new DataView(buffer);
28
+ }
29
+ // Parse problems are non-fatal (the mesh may still render), but they must
30
+ // be queryable afterwards — a rig whose rigidbody section failed to parse
31
+ // silently loses physics, and console.warn alone drowns in browser log
32
+ // limits. Collected warnings ship on the Model (getLoadWarnings()).
33
+ warn(message, error) {
34
+ const full = error instanceof Error ? `${message}: ${error.message}` : message;
35
+ this.warnings.push(full);
36
+ console.warn(message, error ?? "");
37
+ }
38
+ static async load(url) {
39
+ return PmxLoader.loadFromReader(createFetchAssetReader(), url);
40
+ }
41
+ static loadFromBuffer(buffer) {
42
+ return new PmxLoader(buffer).parse();
43
+ }
44
+ static async loadFromReader(reader, pmxLogicalPath) {
45
+ const buffer = await reader.readBinary(pmxLogicalPath);
46
+ return PmxLoader.loadFromBuffer(buffer);
47
+ }
48
+ parse() {
49
+ this.parseHeader();
50
+ const { positions, normals, uvs } = this.parseVertices();
51
+ this.vertexCount = positions.length / 3;
52
+ const indices = this.parseIndices();
53
+ this.parseTextures();
54
+ this.parseMaterials();
55
+ this.parseBones();
56
+ // Parse morphs and display frames before parsing rigidbodies
57
+ this.parseMorphs();
58
+ this.skipDisplayFrames();
59
+ this.parseRigidbodies();
60
+ this.parseJoints();
61
+ this.computeInverseBind();
62
+ return this.toModel(positions, normals, uvs, indices);
63
+ }
64
+ parseHeader() {
65
+ // The signature is four bytes. PMX 2.x writes "PMX ", but exporters in the
66
+ // wild also write "Pmx " — and a strict uppercase compare rejected those
67
+ // models outright. Compare case-insensitively and name what was actually
68
+ // found when it is something else, so a PMD (the older format, signature
69
+ // "Pmd") says so instead of "not a PMX file".
70
+ const signature = this.getString(4);
71
+ if (signature.slice(0, 3).toUpperCase() !== "PMX") {
72
+ const head = signature.slice(0, 3).toUpperCase();
73
+ if (head === "PMD") {
74
+ throw new Error("This is a PMD file (MMD's older format). Convert it to PMX in PMXEditor first.");
75
+ }
76
+ throw new Error(`Not a PMX file (signature "${signature.replace(/[^\x20-\x7e]/g, "?")}")`);
77
+ }
78
+ // PMX: version (float32)
79
+ const version = this.getFloat32();
80
+ if (version < 2.0 || version > 2.2) {
81
+ // Continue, but warn for unexpected version
82
+ this.warn(`PMX version ${version} may not be fully supported`);
83
+ }
84
+ // PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
85
+ const globalsCount = this.getUint8();
86
+ // Validate globalsCount (must be at least 8 for PMX 2.x)
87
+ if (globalsCount < 8) {
88
+ throw new Error(`Invalid globalsCount: ${globalsCount}, expected at least 8`);
89
+ }
90
+ // Read globals (first 8 bytes are standard for PMX 2.x)
91
+ this.encoding = this.getUint8(); // 0:utf16le, 1:utf8
92
+ this.additionalVec4Count = this.getUint8();
93
+ this.vertexIndexSize = this.getUint8();
94
+ this.textureIndexSize = this.getUint8();
95
+ this.materialIndexSize = this.getUint8();
96
+ this.boneIndexSize = this.getUint8();
97
+ this.morphIndexSize = this.getUint8();
98
+ this.rigidBodyIndexSize = this.getUint8();
99
+ // Skip any extra globals beyond the first 8 (for future format extensions)
100
+ if (globalsCount > 8) {
101
+ for (let i = 8; i < globalsCount; i++) {
102
+ this.getUint8();
103
+ }
104
+ }
105
+ this.decoder = new TextDecoder(this.encoding === 0 ? "utf-16le" : "utf-8");
106
+ // Skip model info (4 text fields)
107
+ this.getText();
108
+ this.getText();
109
+ this.getText();
110
+ this.getText();
111
+ }
112
+ parseVertices() {
113
+ const count = this.getInt32();
114
+ const positions = [];
115
+ const normals = [];
116
+ const uvs = [];
117
+ // Prepare skinning arrays (4 influences per vertex)
118
+ const joints = new Uint16Array(count * 4);
119
+ const weights = new Uint8Array(count * 4); // UNORM8, will be normalized to 255
120
+ for (let i = 0; i < count; i++) {
121
+ const px = this.getFloat32();
122
+ const py = this.getFloat32();
123
+ const pz = this.getFloat32();
124
+ positions.push(px, py, pz);
125
+ const nx = this.getFloat32();
126
+ const ny = this.getFloat32();
127
+ const nz = this.getFloat32();
128
+ normals.push(nx, ny, nz);
129
+ const u = this.getFloat32();
130
+ const v = this.getFloat32();
131
+ // PMX UVs are in the same orientation as WebGPU sampling; no flip
132
+ uvs.push(u, v);
133
+ this.offset += this.additionalVec4Count * 16;
134
+ const type = this.getUint8();
135
+ const base = i * 4;
136
+ // Initialize defaults
137
+ joints[base] = 0;
138
+ joints[base + 1] = 0;
139
+ joints[base + 2] = 0;
140
+ joints[base + 3] = 0;
141
+ weights[base] = 255;
142
+ weights[base + 1] = 0;
143
+ weights[base + 2] = 0;
144
+ weights[base + 3] = 0;
145
+ if (type === 0) {
146
+ // BDEF1
147
+ const j0 = this.getNonVertexIndex(this.boneIndexSize);
148
+ joints[base] = j0 >= 0 ? j0 : 0;
149
+ // weight stays [255,0,0,0]
150
+ }
151
+ else if (type === 1 || type === 3) {
152
+ // BDEF2 or SDEF (treated as BDEF2)
153
+ const j0 = this.getNonVertexIndex(this.boneIndexSize);
154
+ const j1 = this.getNonVertexIndex(this.boneIndexSize);
155
+ const w0f = this.getFloat32();
156
+ const w0 = Math.max(0, Math.min(255, Math.round(w0f * 255)));
157
+ const w1 = Math.max(0, Math.min(255, 255 - w0));
158
+ joints[base] = j0 >= 0 ? j0 : 0;
159
+ joints[base + 1] = j1 >= 0 ? j1 : 0;
160
+ weights[base] = w0;
161
+ weights[base + 1] = w1;
162
+ // SDEF has extra 3 vec3 (C, R0, R1)
163
+ if (type === 3) {
164
+ this.offset += 36; // 9 floats * 4 bytes
165
+ }
166
+ }
167
+ else if (type === 2 || type === 4) {
168
+ // BDEF4 or QDEF (treat as LBS4)
169
+ let sum = 0;
170
+ for (let k = 0; k < 4; k++) {
171
+ const j = this.getNonVertexIndex(this.boneIndexSize);
172
+ joints[base + k] = j >= 0 ? j : 0;
173
+ }
174
+ const wf = [this.getFloat32(), this.getFloat32(), this.getFloat32(), this.getFloat32()];
175
+ const ws = wf.map((x) => Math.max(0, Math.min(1, x)));
176
+ const w8 = ws.map((x) => Math.round(x * 255));
177
+ sum = w8[0] + w8[1] + w8[2] + w8[3];
178
+ if (sum === 0) {
179
+ weights[base] = 255;
180
+ }
181
+ else {
182
+ // Normalize to 255
183
+ const scale = 255 / sum;
184
+ let accum = 0;
185
+ for (let k = 0; k < 3; k++) {
186
+ const v = Math.max(0, Math.min(255, Math.round(w8[k] * scale)));
187
+ weights[base + k] = v;
188
+ accum += v;
189
+ }
190
+ weights[base + 3] = Math.max(0, Math.min(255, 255 - accum));
191
+ }
192
+ }
193
+ else {
194
+ throw new Error(`Invalid bone weight type: ${type}`);
195
+ }
196
+ this.offset += 4; // edge scale
197
+ }
198
+ this.joints0 = joints;
199
+ this.weights0 = weights;
200
+ return { positions, normals, uvs };
201
+ }
202
+ // (removed) skipBoneWeight – replaced by inline parsing
203
+ parseIndices() {
204
+ const count = this.getInt32();
205
+ const indices = [];
206
+ for (let i = 0; i < count; i++) {
207
+ indices.push(this.getIndex(this.vertexIndexSize));
208
+ }
209
+ return indices;
210
+ }
211
+ parseTextures() {
212
+ try {
213
+ const count = this.getInt32();
214
+ this.textures = [];
215
+ for (let i = 0; i < count; i++) {
216
+ const textureName = this.getText();
217
+ this.textures.push({
218
+ path: textureName,
219
+ name: textureName.split("/").pop() || textureName, // Extract filename
220
+ });
221
+ }
222
+ }
223
+ catch (error) {
224
+ console.error("Error parsing textures:", error);
225
+ this.textures = [];
226
+ }
227
+ }
228
+ parseMaterials() {
229
+ try {
230
+ const count = this.getInt32();
231
+ this.materials = [];
232
+ for (let i = 0; i < count; i++) {
233
+ const name = this.getText();
234
+ this.getText(); // englishName (skip)
235
+ const diffuse = [this.getFloat32(), this.getFloat32(), this.getFloat32(), this.getFloat32()];
236
+ const specular = [this.getFloat32(), this.getFloat32(), this.getFloat32()];
237
+ const shininess = this.getFloat32();
238
+ const ambient = [this.getFloat32(), this.getFloat32(), this.getFloat32()];
239
+ const flag = this.getUint8();
240
+ // edgeColor vec4
241
+ const edgeColor = [
242
+ this.getFloat32(),
243
+ this.getFloat32(),
244
+ this.getFloat32(),
245
+ this.getFloat32(),
246
+ ];
247
+ // edgeSize float (outline width in PMX units; engine scales by camera distance for stable screen thickness)
248
+ const edgeSize = this.getFloat32();
249
+ const textureIndex = this.getNonVertexIndex(this.textureIndexSize);
250
+ const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize);
251
+ const sphereTextureMode = this.getUint8();
252
+ const isSharedToonTexture = this.getUint8() === 1;
253
+ const toonTextureIndex = isSharedToonTexture ? this.getUint8() : this.getNonVertexIndex(this.textureIndexSize);
254
+ this.getText(); // comment (skip)
255
+ const vertexCount = this.getInt32();
256
+ // PMX material flag bits:
257
+ // Bit 0 (0x01): Double-sided rendering
258
+ // Bit 4 (0x10): Edge drawing (outline)
259
+ const mat = {
260
+ name,
261
+ diffuse,
262
+ specular,
263
+ ambient,
264
+ shininess,
265
+ diffuseTextureIndex: textureIndex,
266
+ normalTextureIndex: -1, // Not used in basic PMX
267
+ sphereTextureIndex,
268
+ sphereMode: sphereTextureMode,
269
+ toonTextureIndex,
270
+ sharedToon: isSharedToonTexture,
271
+ edgeFlag: flag,
272
+ edgeColor,
273
+ edgeSize,
274
+ vertexCount,
275
+ };
276
+ this.materials.push(mat);
277
+ }
278
+ }
279
+ catch (error) {
280
+ console.error("Error parsing materials:", error);
281
+ this.materials = [];
282
+ }
283
+ }
284
+ parseBones() {
285
+ try {
286
+ const count = this.getInt32();
287
+ const bones = [];
288
+ const abs = new Array(count);
289
+ // PMX 2.x bone flags (best-effort common masks)
290
+ const FLAG_TAIL_IS_BONE = 0x0001;
291
+ const FLAG_IK = 0x0020;
292
+ const FLAG_APPEND_ROTATE = 0x0100;
293
+ const FLAG_APPEND_MOVE = 0x0200;
294
+ const FLAG_AXIS_LIMIT = 0x0400;
295
+ const FLAG_LOCAL_AXIS = 0x0800;
296
+ const FLAG_EXTERNAL_PARENT = 0x2000;
297
+ for (let i = 0; i < count; i++) {
298
+ const name = this.getText();
299
+ this.getText(); // englishName (skip)
300
+ // PMX is left-handed, engine is now left-handed - no conversion needed
301
+ const x = this.getFloat32();
302
+ const y = this.getFloat32();
303
+ const z = this.getFloat32();
304
+ const parentIndex = this.getNonVertexIndex(this.boneIndexSize);
305
+ const deformLayer = this.getInt32(); // 変形階層
306
+ const flags = this.getUint16();
307
+ // Tail: bone index or offset vector3
308
+ if ((flags & FLAG_TAIL_IS_BONE) !== 0) {
309
+ this.getNonVertexIndex(this.boneIndexSize);
310
+ }
311
+ else {
312
+ // tail offset vec3
313
+ this.getFloat32();
314
+ this.getFloat32();
315
+ this.getFloat32();
316
+ }
317
+ // Append transform (inherit/ratio)
318
+ let appendParent = undefined;
319
+ let appendRatio = undefined;
320
+ let appendRotate = false;
321
+ let appendMove = false;
322
+ if ((flags & (FLAG_APPEND_ROTATE | FLAG_APPEND_MOVE)) !== 0) {
323
+ appendParent = this.getNonVertexIndex(this.boneIndexSize); // append parent
324
+ appendRatio = this.getFloat32(); // ratio
325
+ appendRotate = (flags & FLAG_APPEND_ROTATE) !== 0;
326
+ appendMove = (flags & FLAG_APPEND_MOVE) !== 0;
327
+ }
328
+ // Axis limit (軸制限)
329
+ let fixedAxis = undefined;
330
+ if ((flags & FLAG_AXIS_LIMIT) !== 0) {
331
+ fixedAxis = [this.getFloat32(), this.getFloat32(), this.getFloat32()];
332
+ }
333
+ // Local axis (two vectors x and z)
334
+ if ((flags & FLAG_LOCAL_AXIS) !== 0) {
335
+ // local axis X
336
+ this.getFloat32();
337
+ this.getFloat32();
338
+ this.getFloat32();
339
+ // local axis Z
340
+ this.getFloat32();
341
+ this.getFloat32();
342
+ this.getFloat32();
343
+ }
344
+ // External parent transform
345
+ if ((flags & FLAG_EXTERNAL_PARENT) !== 0) {
346
+ this.getInt32();
347
+ }
348
+ // IK block
349
+ let ikTargetIndex = undefined;
350
+ let ikIteration = undefined;
351
+ let ikLimitAngle = undefined;
352
+ let ikLinks = undefined;
353
+ if ((flags & FLAG_IK) !== 0) {
354
+ ikTargetIndex = this.getNonVertexIndex(this.boneIndexSize); // target
355
+ ikIteration = this.getInt32(); // iteration
356
+ ikLimitAngle = this.getFloat32(); // rotationConstraint
357
+ const linksCount = this.getInt32();
358
+ ikLinks = [];
359
+ for (let li = 0; li < linksCount; li++) {
360
+ const linkBoneIndex = this.getNonVertexIndex(this.boneIndexSize); // link target
361
+ const hasLimit = this.getUint8() === 1;
362
+ let minAngle = undefined;
363
+ let maxAngle = undefined;
364
+ if (hasLimit) {
365
+ // min and max angles (vec3 each)
366
+ const minX = this.getFloat32();
367
+ const minY = this.getFloat32();
368
+ const minZ = this.getFloat32();
369
+ const maxX = this.getFloat32();
370
+ const maxY = this.getFloat32();
371
+ const maxZ = this.getFloat32();
372
+ minAngle = new Vec3(minX, minY, minZ);
373
+ maxAngle = new Vec3(maxX, maxY, maxZ);
374
+ }
375
+ ikLinks.push({
376
+ boneIndex: linkBoneIndex,
377
+ hasLimit,
378
+ minAngle,
379
+ maxAngle,
380
+ });
381
+ }
382
+ }
383
+ // Stash minimal bone info; append data will be merged later
384
+ abs[i] = {
385
+ name,
386
+ parent: parentIndex,
387
+ x,
388
+ y,
389
+ z,
390
+ appendParent,
391
+ appendRatio,
392
+ appendRotate,
393
+ appendMove,
394
+ fixedAxis,
395
+ deformLayer,
396
+ ikTargetIndex,
397
+ ikIteration,
398
+ ikLimitAngle,
399
+ ikLinks,
400
+ };
401
+ }
402
+ for (let i = 0; i < count; i++) {
403
+ const a = abs[i];
404
+ const boneData = {
405
+ name: a.name,
406
+ parentIndex: a.parent,
407
+ bindTranslation: a.parent >= 0 && a.parent < count
408
+ ? [a.x - abs[a.parent].x, a.y - abs[a.parent].y, a.z - abs[a.parent].z]
409
+ : [a.x, a.y, a.z],
410
+ children: [], // Will be populated later when building skeleton
411
+ appendParentIndex: a.appendParent,
412
+ appendRatio: a.appendRatio,
413
+ appendRotate: a.appendRotate,
414
+ appendMove: a.appendMove,
415
+ fixedAxis: a.fixedAxis,
416
+ deformLayer: a.deformLayer,
417
+ ikTargetIndex: a.ikTargetIndex,
418
+ ikIteration: a.ikIteration,
419
+ ikLimitAngle: a.ikLimitAngle,
420
+ ikLinks: a.ikLinks,
421
+ };
422
+ bones.push(boneData);
423
+ }
424
+ this.bones = bones;
425
+ }
426
+ catch (e) {
427
+ this.warn("Error parsing bones:", e);
428
+ this.bones = [];
429
+ }
430
+ }
431
+ parseMorphs() {
432
+ try {
433
+ // Check if we have enough bytes to read the count
434
+ if (this.offset + 4 > this.view.buffer.byteLength) {
435
+ this.morphs = [];
436
+ return;
437
+ }
438
+ const count = this.getInt32();
439
+ if (count < 0 || count > 100000) {
440
+ // Suspicious count, likely corrupted
441
+ this.warn(`Suspicious morph count: ${count}`);
442
+ this.morphs = [];
443
+ return;
444
+ }
445
+ this.morphs = [];
446
+ for (let i = 0; i < count; i++) {
447
+ // Check bounds before reading each morph
448
+ if (this.offset >= this.view.buffer.byteLength) {
449
+ break;
450
+ }
451
+ try {
452
+ const name = this.getText();
453
+ this.getText(); // englishName (skip)
454
+ this.getUint8(); // panelType (skip)
455
+ const morphType = this.getUint8();
456
+ const offsetCount = this.getInt32();
457
+ (globalThis.__DECL ||= []).push([name, morphType, offsetCount]);
458
+ const morph = {
459
+ name,
460
+ type: morphType,
461
+ vertexOffsets: [],
462
+ groupReferences: [],
463
+ };
464
+ // Allocated only for the type that uses them, so `morph.boneOffsets`
465
+ // being undefined means "not that kind of morph" rather than "empty".
466
+ if (morphType === 2)
467
+ morph.boneOffsets = [];
468
+ else if (morphType >= 3 && morphType <= 7)
469
+ morph.uvOffsets = [];
470
+ else if (morphType === 8)
471
+ morph.materialOffsets = [];
472
+ // Parse vertex morphs (type 1)
473
+ if (morphType === 1) {
474
+ for (let j = 0; j < offsetCount; j++) {
475
+ if (this.offset >= this.view.buffer.byteLength) {
476
+ break;
477
+ }
478
+ const vertexIndex = this.getIndex(this.vertexIndexSize);
479
+ const x = this.getFloat32();
480
+ const y = this.getFloat32();
481
+ const z = this.getFloat32();
482
+ if (!(vertexIndex >= 0 && vertexIndex < this.vertexCount)) (globalThis.__OOR ||= []).push([name, vertexIndex]);
483
+ if (vertexIndex >= 0 && vertexIndex < this.vertexCount) {
484
+ morph.vertexOffsets.push({
485
+ vertexIndex,
486
+ positionOffset: [x, y, z],
487
+ });
488
+ }
489
+ }
490
+ }
491
+ else if (morphType === 0) {
492
+ // Parse group morphs
493
+ for (let j = 0; j < offsetCount; j++) {
494
+ if (this.offset >= this.view.buffer.byteLength) {
495
+ break;
496
+ }
497
+ const morphIndex = this.getNonVertexIndex(this.morphIndexSize);
498
+ const ratio = this.getFloat32();
499
+ if (morphIndex >= 0) {
500
+ morph.groupReferences.push({
501
+ morphIndex,
502
+ ratio,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ else {
508
+ // Bone, UV and material morphs are kept; flip and impulse (PMX 2.1)
509
+ // are still read past — nothing in the wild uses them, and impulse
510
+ // would need to drive rigidbodies.
511
+ for (let j = 0; j < offsetCount; j++) {
512
+ if (this.offset >= this.view.buffer.byteLength) {
513
+ break;
514
+ }
515
+ if (morphType === 2) {
516
+ // Bone morph
517
+ const boneIndex = this.getNonVertexIndex(this.boneIndexSize);
518
+ const tx = this.getFloat32();
519
+ const ty = this.getFloat32();
520
+ const tz = this.getFloat32();
521
+ const rx = this.getFloat32();
522
+ const ry = this.getFloat32();
523
+ const rz = this.getFloat32();
524
+ const rw = this.getFloat32();
525
+ if (boneIndex >= 0) {
526
+ const offset = {
527
+ boneIndex,
528
+ translation: [tx, ty, tz],
529
+ rotation: [rx, ry, rz, rw],
530
+ };
531
+ morph.boneOffsets.push(offset);
532
+ }
533
+ }
534
+ else if (morphType >= 3 && morphType <= 7) {
535
+ // UV morph + additional UV channels 1-4; offset is always a Float4 per spec
536
+ const vertexIndex = this.getIndex(this.vertexIndexSize);
537
+ const x = this.getFloat32();
538
+ const y = this.getFloat32();
539
+ const z = this.getFloat32();
540
+ const w = this.getFloat32();
541
+ if (vertexIndex >= 0 && vertexIndex < this.vertexCount) {
542
+ const offset = { vertexIndex, offset: [x, y, z, w] };
543
+ morph.uvOffsets.push(offset);
544
+ }
545
+ }
546
+ else if (morphType === 8) {
547
+ // Material morph
548
+ const materialIndex = this.getNonVertexIndex(this.materialIndexSize);
549
+ const offsetType = this.getUint8();
550
+ const diffuseR = this.getFloat32();
551
+ const diffuseG = this.getFloat32();
552
+ const diffuseB = this.getFloat32();
553
+ const diffuseA = this.getFloat32();
554
+ const specularR = this.getFloat32();
555
+ const specularG = this.getFloat32();
556
+ const specularB = this.getFloat32();
557
+ const shininess = this.getFloat32();
558
+ const ambientR = this.getFloat32();
559
+ const ambientG = this.getFloat32();
560
+ const ambientB = this.getFloat32();
561
+ const edgeR = this.getFloat32();
562
+ const edgeG = this.getFloat32();
563
+ const edgeB = this.getFloat32();
564
+ const edgeA = this.getFloat32();
565
+ const edgeSize = this.getFloat32();
566
+ const texR = this.getFloat32();
567
+ const texG = this.getFloat32();
568
+ const texB = this.getFloat32();
569
+ const texA = this.getFloat32();
570
+ const sphR = this.getFloat32();
571
+ const sphG = this.getFloat32();
572
+ const sphB = this.getFloat32();
573
+ const sphA = this.getFloat32();
574
+ const toonR = this.getFloat32();
575
+ const toonG = this.getFloat32();
576
+ const toonB = this.getFloat32();
577
+ const toonA = this.getFloat32();
578
+ // -1 is legal and means "every material" — kept as-is so the
579
+ // applier can fan it out without a second convention.
580
+ if (materialIndex >= -1) {
581
+ const offset = {
582
+ materialIndex,
583
+ offsetType,
584
+ diffuse: [diffuseR, diffuseG, diffuseB, diffuseA],
585
+ specular: [specularR, specularG, specularB],
586
+ shininess,
587
+ ambient: [ambientR, ambientG, ambientB],
588
+ edgeColor: [edgeR, edgeG, edgeB, edgeA],
589
+ edgeSize,
590
+ textureCoeff: [texR, texG, texB, texA],
591
+ sphereCoeff: [sphR, sphG, sphB, sphA],
592
+ toonCoeff: [toonR, toonG, toonB, toonA],
593
+ };
594
+ morph.materialOffsets.push(offset);
595
+ }
596
+ }
597
+ else if (morphType === 9) {
598
+ // Flip morph (PMX 2.1): same layout as group morph
599
+ this.getNonVertexIndex(this.morphIndexSize); // morphIndex
600
+ this.getFloat32(); // ratio
601
+ }
602
+ else if (morphType === 10) {
603
+ // Impulse morph (PMX 2.1)
604
+ this.getNonVertexIndex(this.rigidBodyIndexSize); // rigidbodyIndex
605
+ this.getUint8(); // local flag
606
+ this.getFloat32(); // velocity x
607
+ this.getFloat32(); // velocity y
608
+ this.getFloat32(); // velocity z
609
+ this.getFloat32(); // torque x
610
+ this.getFloat32(); // torque y
611
+ this.getFloat32(); // torque z
612
+ }
613
+ }
614
+ }
615
+ this.morphs.push(morph);
616
+ }
617
+ catch (e) {
618
+ // If we fail to read a morph, skip it
619
+ this.warn(`Error reading morph ${i}:`, e);
620
+ }
621
+ }
622
+ }
623
+ catch (e) {
624
+ this.warn("Error parsing morphs:", e);
625
+ this.morphs = [];
626
+ }
627
+ }
628
+ skipDisplayFrames() {
629
+ try {
630
+ // Check if we have enough bytes to read the count
631
+ if (this.offset + 4 > this.view.buffer.byteLength) {
632
+ return false;
633
+ }
634
+ const count = this.getInt32();
635
+ if (count < 0 || count > 100000) {
636
+ // Suspicious count, likely corrupted - restore offset
637
+ this.offset -= 4;
638
+ return false;
639
+ }
640
+ for (let i = 0; i < count; i++) {
641
+ // Check bounds before reading each frame
642
+ if (this.offset >= this.view.buffer.byteLength) {
643
+ return false;
644
+ }
645
+ try {
646
+ this.getText(); // name
647
+ this.getText(); // englishName
648
+ this.getUint8(); // flag
649
+ const elementCount = this.getInt32();
650
+ for (let j = 0; j < elementCount; j++) {
651
+ if (this.offset >= this.view.buffer.byteLength) {
652
+ return false;
653
+ }
654
+ const elementType = this.getUint8();
655
+ if (elementType === 0) {
656
+ // Bone frame
657
+ this.getNonVertexIndex(this.boneIndexSize);
658
+ }
659
+ else if (elementType === 1) {
660
+ // Morph frame
661
+ this.getNonVertexIndex(this.morphIndexSize);
662
+ }
663
+ }
664
+ }
665
+ catch (e) {
666
+ // If we fail to read a frame, stop skipping
667
+ this.warn(`Error reading display frame ${i}:`, e);
668
+ return false;
669
+ }
670
+ }
671
+ return true;
672
+ }
673
+ catch (e) {
674
+ this.warn("Error skipping display frames:", e);
675
+ return false;
676
+ }
677
+ }
678
+ parseRigidbodies() {
679
+ try {
680
+ // Note: morphs and display frames are already skipped in parse() before calling this
681
+ // Check bounds before reading rigidbody count
682
+ if (this.offset + 4 > this.view.buffer.byteLength) {
683
+ this.warn("Not enough bytes for rigidbody count");
684
+ this.rigidbodies = [];
685
+ return;
686
+ }
687
+ const count = this.getInt32();
688
+ if (count < 0 || count > 10000) {
689
+ // Suspicious count
690
+ this.warn(`Suspicious rigidbody count: ${count}`);
691
+ this.rigidbodies = [];
692
+ return;
693
+ }
694
+ this.rigidbodies = [];
695
+ for (let i = 0; i < count; i++) {
696
+ try {
697
+ // Check bounds before reading each rigidbody
698
+ if (this.offset >= this.view.buffer.byteLength) {
699
+ this.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`);
700
+ break;
701
+ }
702
+ const name = this.getText();
703
+ const englishName = this.getText();
704
+ const boneIndex = this.getNonVertexIndex(this.boneIndexSize);
705
+ const group = this.getUint8();
706
+ const collisionMask = this.getUint16();
707
+ const shape = this.getUint8(); // 0=sphere, 1=box, 2=capsule
708
+ // Size parameters (depends on shape)
709
+ const sizeX = this.getFloat32();
710
+ const sizeY = this.getFloat32();
711
+ const sizeZ = this.getFloat32();
712
+ const posX = this.getFloat32();
713
+ const posY = this.getFloat32();
714
+ const posZ = this.getFloat32();
715
+ // Rotation (Euler angles in RADIANS in PMX file - PMX Editor displays them as degrees for convenience)
716
+ // ZXY order (left-handed system)
717
+ const rotX = this.getFloat32();
718
+ const rotY = this.getFloat32();
719
+ const rotZ = this.getFloat32();
720
+ // Physical properties
721
+ const mass = this.getFloat32();
722
+ const linearDamping = this.getFloat32();
723
+ const angularDamping = this.getFloat32();
724
+ const restitution = this.getFloat32();
725
+ const friction = this.getFloat32();
726
+ // PMX physics mode: 0 = follow bone, 1 = physics,
727
+ // 2 = physics + bone-position alignment. Mode 2 is dynamic — it
728
+ // must NOT be cast onto RigidbodyType (2 = Kinematic there, which
729
+ // freezes the body to its bone and kills physics for most modern
730
+ // cloth rigs and every 胸_回転 breast body).
731
+ const mode = this.getUint8();
732
+ this.rigidbodies.push({
733
+ name,
734
+ englishName,
735
+ boneIndex,
736
+ group,
737
+ collisionMask,
738
+ shape: shape,
739
+ size: new Vec3(sizeX, sizeY, sizeZ),
740
+ shapePosition: new Vec3(posX, posY, posZ),
741
+ shapeRotation: new Vec3(rotX, rotY, rotZ),
742
+ mass,
743
+ linearDamping,
744
+ angularDamping,
745
+ restitution,
746
+ friction,
747
+ type: mode === 0 ? RigidbodyType.Static : RigidbodyType.Dynamic,
748
+ aligned: mode === 2,
749
+ bodyOffsetMatrixInverse: Mat4.identity(),
750
+ });
751
+ }
752
+ catch (e) {
753
+ this.warn(`Error reading rigidbody ${i} of ${count}:`, e);
754
+ // Stop parsing if we encounter an error
755
+ break;
756
+ }
757
+ }
758
+ }
759
+ catch (e) {
760
+ this.warn("Error parsing rigidbodies:", e);
761
+ this.rigidbodies = [];
762
+ }
763
+ }
764
+ parseJoints() {
765
+ try {
766
+ // Check bounds before reading joint count
767
+ if (this.offset + 4 > this.view.buffer.byteLength) {
768
+ this.warn("Not enough bytes for joint count");
769
+ this.joints = [];
770
+ return;
771
+ }
772
+ const count = this.getInt32();
773
+ if (count < 0 || count > 10000) {
774
+ this.warn(`Suspicious joint count: ${count}`);
775
+ this.joints = [];
776
+ return;
777
+ }
778
+ this.joints = [];
779
+ for (let i = 0; i < count; i++) {
780
+ try {
781
+ // Check bounds before reading each joint
782
+ if (this.offset >= this.view.buffer.byteLength) {
783
+ this.warn(`Reached end of buffer while reading joint ${i} of ${count}`);
784
+ break;
785
+ }
786
+ const name = this.getText();
787
+ const englishName = this.getText();
788
+ const type = this.getUint8();
789
+ // Rigidbody indices use rigidBodyIndexSize (not boneIndexSize)
790
+ const rigidbodyIndexA = this.getNonVertexIndex(this.rigidBodyIndexSize);
791
+ const rigidbodyIndexB = this.getNonVertexIndex(this.rigidBodyIndexSize);
792
+ const posX = this.getFloat32();
793
+ const posY = this.getFloat32();
794
+ const posZ = this.getFloat32();
795
+ // Rotation (Euler angles in RADIANS in PMX file - PMX Editor displays them as degrees for convenience)
796
+ // ZXY order (left-handed system)
797
+ const rotX = this.getFloat32();
798
+ const rotY = this.getFloat32();
799
+ const rotZ = this.getFloat32();
800
+ const posMinX = this.getFloat32();
801
+ const posMinY = this.getFloat32();
802
+ const posMinZ = this.getFloat32();
803
+ const posMaxX = this.getFloat32();
804
+ const posMaxY = this.getFloat32();
805
+ const posMaxZ = this.getFloat32();
806
+ // Rotation constraints (in RADIANS in PMX file)
807
+ const rotMinX = this.getFloat32();
808
+ const rotMinY = this.getFloat32();
809
+ const rotMinZ = this.getFloat32();
810
+ const rotMaxX = this.getFloat32();
811
+ const rotMaxY = this.getFloat32();
812
+ const rotMaxZ = this.getFloat32();
813
+ // Spring parameters
814
+ const springPosX = this.getFloat32();
815
+ const springPosY = this.getFloat32();
816
+ const springPosZ = this.getFloat32();
817
+ // Spring rotation (stiffness values in PMX file)
818
+ const springRotX = this.getFloat32();
819
+ const springRotY = this.getFloat32();
820
+ const springRotZ = this.getFloat32();
821
+ // Store rotations and springs as Vec3 (Euler angles), not Quat
822
+ this.joints.push({
823
+ name,
824
+ englishName,
825
+ type,
826
+ rigidbodyIndexA,
827
+ rigidbodyIndexB,
828
+ position: new Vec3(posX, posY, posZ),
829
+ rotation: new Vec3(rotX, rotY, rotZ), // Store as Vec3 (Euler angles)
830
+ positionMin: new Vec3(posMinX, posMinY, posMinZ),
831
+ positionMax: new Vec3(posMaxX, posMaxY, posMaxZ),
832
+ rotationMin: new Vec3(rotMinX, rotMinY, rotMinZ), // Store as Vec3 (Euler angles)
833
+ rotationMax: new Vec3(rotMaxX, rotMaxY, rotMaxZ), // Store as Vec3 (Euler angles)
834
+ springPosition: new Vec3(springPosX, springPosY, springPosZ),
835
+ springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
836
+ });
837
+ }
838
+ catch (e) {
839
+ this.warn(`Error reading joint ${i} of ${count}:`, e);
840
+ // Stop parsing if we encounter an error
841
+ break;
842
+ }
843
+ }
844
+ }
845
+ catch (e) {
846
+ this.warn("Error parsing joints:", e);
847
+ this.joints = [];
848
+ }
849
+ }
850
+ computeInverseBind() {
851
+ if (!this.bones || this.bones.length === 0) {
852
+ this.inverseBindMatrices = new Float32Array(0);
853
+ return;
854
+ }
855
+ const n = this.bones.length;
856
+ const world = new Array(n).fill(null);
857
+ const inv = new Float32Array(n * 16);
858
+ const computeWorld = (i) => {
859
+ if (world[i])
860
+ return world[i];
861
+ const bone = this.bones[i];
862
+ const local = Mat4.identity().translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]);
863
+ let w;
864
+ if (bone.parentIndex >= 0 && bone.parentIndex < n) {
865
+ w = computeWorld(bone.parentIndex).multiply(local);
866
+ }
867
+ else {
868
+ w = local;
869
+ }
870
+ world[i] = w;
871
+ return w;
872
+ };
873
+ for (let i = 0; i < n; i++) {
874
+ const w = computeWorld(i);
875
+ const invm = Mat4.identity().translateInPlace(-w.values[12], -w.values[13], -w.values[14]);
876
+ inv.set(invm.values, i * 16);
877
+ }
878
+ this.inverseBindMatrices = inv;
879
+ }
880
+ toModel(positions, normals, uvs, indices) {
881
+ // Create indexed vertex buffer
882
+ // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, ...]
883
+ const vertexData = new Float32Array(this.vertexCount * 8);
884
+ for (let i = 0; i < this.vertexCount; i++) {
885
+ const pi = i * 3;
886
+ const ui = i * 2;
887
+ const vi = i * 8;
888
+ vertexData[vi] = positions[pi];
889
+ vertexData[vi + 1] = positions[pi + 1];
890
+ vertexData[vi + 2] = positions[pi + 2];
891
+ vertexData[vi + 3] = normals[pi];
892
+ vertexData[vi + 4] = normals[pi + 1];
893
+ vertexData[vi + 5] = normals[pi + 2];
894
+ vertexData[vi + 6] = uvs[ui];
895
+ vertexData[vi + 7] = uvs[ui + 1];
896
+ }
897
+ // Create index buffer
898
+ const indexData = new Uint32Array(indices);
899
+ // Create skeleton (required)
900
+ const skeleton = {
901
+ bones: this.bones.length > 0 ? this.bones : [],
902
+ inverseBindMatrices: this.inverseBindMatrices || new Float32Array(0),
903
+ };
904
+ let skinning;
905
+ if (this.joints0 && this.weights0) {
906
+ // Clamp joints to valid range now that we know bone count, and renormalize weights
907
+ const boneCount = this.bones.length;
908
+ const joints = this.joints0;
909
+ const weights = this.weights0;
910
+ for (let i = 0; i < joints.length; i += 4) {
911
+ // First pass: identify and zero out invalid joints, collect valid weight sum
912
+ let validWeightSum = 0;
913
+ let validCount = 0;
914
+ for (let k = 0; k < 4; k++) {
915
+ const j = joints[i + k];
916
+ if (j < 0 || j >= boneCount) {
917
+ // Invalid joint: zero the weight but keep joint index for debugging
918
+ weights[i + k] = 0;
919
+ // Optionally clamp to valid range
920
+ if (j < 0) {
921
+ joints[i + k] = 0;
922
+ }
923
+ else {
924
+ joints[i + k] = boneCount > 0 ? boneCount - 1 : 0;
925
+ }
926
+ }
927
+ else {
928
+ validWeightSum += weights[i + k];
929
+ validCount++;
930
+ }
931
+ }
932
+ // If no valid weights, assign all weight to first bone (bone 0)
933
+ if (validWeightSum === 0 || validCount === 0) {
934
+ weights[i] = 255;
935
+ weights[i + 1] = 0;
936
+ weights[i + 2] = 0;
937
+ weights[i + 3] = 0;
938
+ joints[i] = boneCount > 0 ? 0 : 0;
939
+ joints[i + 1] = 0;
940
+ joints[i + 2] = 0;
941
+ joints[i + 3] = 0;
942
+ }
943
+ else if (validWeightSum !== 255) {
944
+ // Normalize valid weights to sum to exactly 255
945
+ const scale = 255 / validWeightSum;
946
+ let accum = 0;
947
+ for (let k = 0; k < 3; k++) {
948
+ if (joints[i + k] >= 0 && joints[i + k] < boneCount) {
949
+ const v = Math.max(0, Math.min(255, Math.round(weights[i + k] * scale)));
950
+ weights[i + k] = v;
951
+ accum += v;
952
+ }
953
+ else {
954
+ weights[i + k] = 0;
955
+ }
956
+ }
957
+ // Handle the 4th weight
958
+ if (joints[i + 3] >= 0 && joints[i + 3] < boneCount) {
959
+ weights[i + 3] = Math.max(0, Math.min(255, 255 - accum));
960
+ }
961
+ else {
962
+ weights[i + 3] = 0;
963
+ // Redistribute the remainder to the last valid weight
964
+ if (accum < 255) {
965
+ for (let k = 2; k >= 0; k--) {
966
+ if (joints[i + k] >= 0 && joints[i + k] < boneCount && weights[i + k] > 0) {
967
+ weights[i + k] = Math.min(255, weights[i + k] + (255 - accum));
968
+ break;
969
+ }
970
+ }
971
+ }
972
+ }
973
+ // Final safety check: ensure sum is exactly 255
974
+ const finalSum = weights[i] + weights[i + 1] + weights[i + 2] + weights[i + 3];
975
+ if (finalSum !== 255) {
976
+ const diff = 255 - finalSum;
977
+ // Add/subtract difference to/from the largest valid weight
978
+ let maxIdx = 0;
979
+ let maxWeight = weights[i];
980
+ for (let k = 1; k < 4; k++) {
981
+ if (weights[i + k] > maxWeight && joints[i + k] >= 0 && joints[i + k] < boneCount) {
982
+ maxWeight = weights[i + k];
983
+ maxIdx = k;
984
+ }
985
+ }
986
+ if (joints[i + maxIdx] >= 0 && joints[i + maxIdx] < boneCount) {
987
+ weights[i + maxIdx] = Math.max(0, Math.min(255, weights[i + maxIdx] + diff));
988
+ }
989
+ }
990
+ }
991
+ }
992
+ skinning = { joints, weights };
993
+ }
994
+ else {
995
+ // Create default skinning (single bone per vertex)
996
+ const joints = new Uint16Array(this.vertexCount * 4);
997
+ const weights = new Uint8Array(this.vertexCount * 4);
998
+ for (let i = 0; i < this.vertexCount; i++) {
999
+ joints[i * 4] = 0;
1000
+ weights[i * 4] = 255;
1001
+ }
1002
+ skinning = { joints, weights };
1003
+ }
1004
+ // Morphs are applied from the sparse per-morph vertexOffsets lists (see
1005
+ // Model.applyMorphs); no dense morphCount*vertexCount buffer is needed.
1006
+ const morphing = {
1007
+ morphs: this.morphs,
1008
+ };
1009
+ return new Model(vertexData, indexData, this.textures, this.materials, skeleton, skinning, morphing, this.rigidbodies, this.joints, this.warnings);
1010
+ }
1011
+ getUint8() {
1012
+ if (this.offset >= this.view.buffer.byteLength) {
1013
+ throw new RangeError(`Offset ${this.offset} exceeds buffer bounds ${this.view.buffer.byteLength}`);
1014
+ }
1015
+ return this.view.getUint8(this.offset++);
1016
+ }
1017
+ getUint16() {
1018
+ if (this.offset + 2 > this.view.buffer.byteLength) {
1019
+ throw new RangeError(`Offset ${this.offset} + 2 exceeds buffer bounds ${this.view.buffer.byteLength}`);
1020
+ }
1021
+ const v = this.view.getUint16(this.offset, true);
1022
+ this.offset += 2;
1023
+ return v;
1024
+ }
1025
+ // Vertex index: 1->uint8, 2->uint16, 4->int32
1026
+ getVertexIndex(size) {
1027
+ if (size === 1)
1028
+ return this.getUint8();
1029
+ if (size === 2) {
1030
+ const v = this.view.getUint16(this.offset, true);
1031
+ this.offset += 2;
1032
+ return v;
1033
+ }
1034
+ return this.getInt32();
1035
+ }
1036
+ // Non-vertex indices (texture/material/bone/morph/rigid): 1->int8, 2->int16, 4->int32
1037
+ getNonVertexIndex(size) {
1038
+ if (size === 1) {
1039
+ const v = this.view.getInt8(this.offset);
1040
+ this.offset += 1;
1041
+ return v;
1042
+ }
1043
+ if (size === 2) {
1044
+ const v = this.view.getInt16(this.offset, true);
1045
+ this.offset += 2;
1046
+ return v;
1047
+ }
1048
+ return this.getInt32();
1049
+ }
1050
+ getInt32() {
1051
+ if (this.offset + 4 > this.view.buffer.byteLength) {
1052
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
1053
+ }
1054
+ const v = this.view.getInt32(this.offset, true);
1055
+ this.offset += 4;
1056
+ return v;
1057
+ }
1058
+ getFloat32() {
1059
+ if (this.offset + 4 > this.view.buffer.byteLength) {
1060
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
1061
+ }
1062
+ const v = this.view.getFloat32(this.offset, true);
1063
+ this.offset += 4;
1064
+ return v;
1065
+ }
1066
+ getString(len) {
1067
+ // byteOffset matters: a DataView built over a slice of a larger buffer has
1068
+ // its own origin, and reading the raw buffer without it lands elsewhere.
1069
+ const bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len);
1070
+ this.offset += len;
1071
+ return String.fromCharCode(...bytes);
1072
+ }
1073
+ getText() {
1074
+ const len = this.getInt32();
1075
+ if (len <= 0)
1076
+ return "";
1077
+ // No arbitrary length ceiling. A model's comment field carries credits and
1078
+ // terms of use and routinely runs past a thousand bytes — roughly five
1079
+ // hundred characters in UTF-16 — so a "this looks too long" heuristic
1080
+ // rejected perfectly ordinary models from several distributors. The bounds
1081
+ // check below is the real guard: a length that would read past the end of
1082
+ // the file is corrupt, and any length that fits is the author's business.
1083
+ // Ensure we don't read beyond buffer bounds
1084
+ if (this.offset + len > this.view.byteLength) {
1085
+ throw new RangeError(`String length ${len} exceeds buffer bounds at offset ${this.offset - 4}`);
1086
+ }
1087
+ let bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len);
1088
+ this.offset += len;
1089
+ // UTF-16 needs whole code units. A stray odd length (seen from a few
1090
+ // exporters) made TextDecoder emit a replacement char and, worse, shifted
1091
+ // nothing — better to drop the orphan byte than to carry it into the name.
1092
+ if (this.encoding === 0 && len % 2 === 1)
1093
+ bytes = bytes.subarray(0, len - 1);
1094
+ return this.decoder.decode(bytes);
1095
+ }
1096
+ getIndex(size) {
1097
+ // Backward-compat helper: defaults to vertex index behavior
1098
+ return this.getVertexIndex(size);
1099
+ }
1100
+ }