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