reze-engine 0.50.6 → 0.50.7
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/dist/_dbg-loader.mjs +1100 -0
- package/dist/physics-debug.d.ts +30 -0
- package/dist/physics-debug.d.ts.map +1 -0
- package/dist/physics-debug.js +526 -0
- package/dist/shaders/materials/body.d.ts +2 -0
- package/dist/shaders/materials/body.d.ts.map +1 -0
- package/dist/shaders/materials/body.js +95 -0
- package/dist/shaders/materials/cloth_rough.d.ts +2 -0
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_rough.js +69 -0
- package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_smooth.js +61 -0
- package/dist/shaders/materials/default.d.ts +2 -0
- package/dist/shaders/materials/default.d.ts.map +1 -0
- package/dist/shaders/materials/default.js +43 -0
- package/dist/shaders/materials/eye.d.ts +2 -0
- package/dist/shaders/materials/eye.d.ts.map +1 -0
- package/dist/shaders/materials/eye.js +60 -0
- package/dist/shaders/materials/face.d.ts +2 -0
- package/dist/shaders/materials/face.d.ts.map +1 -0
- package/dist/shaders/materials/face.js +95 -0
- package/dist/shaders/materials/hair.d.ts +2 -0
- package/dist/shaders/materials/hair.d.ts.map +1 -0
- package/dist/shaders/materials/hair.js +90 -0
- package/dist/shaders/materials/metal.d.ts +2 -0
- package/dist/shaders/materials/metal.d.ts.map +1 -0
- package/dist/shaders/materials/metal.js +77 -0
- package/dist/shaders/materials/mmd_classic.d.ts +2 -0
- package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
- package/dist/shaders/materials/mmd_classic.js +66 -0
- package/dist/shaders/materials/stockings.d.ts +2 -0
- package/dist/shaders/materials/stockings.d.ts.map +1 -0
- package/dist/shaders/materials/stockings.js +122 -0
- package/dist/shaders/passes/physics-debug.d.ts +2 -0
- package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
- package/dist/shaders/passes/physics-debug.js +69 -0
- package/dist/vmd-writer.d.ts +11 -6
- package/dist/vmd-writer.d.ts.map +1 -1
- package/dist/vmd-writer.js +35 -17
- package/package.json +1 -1
- package/src/vmd-writer.ts +36 -16
- package/dist/physics/autofit.d.ts +0 -147
- package/dist/physics/autofit.d.ts.map +0 -1
- package/dist/physics/autofit.js +0 -501
- package/dist/shaders/passes/field-blit.d.ts +0 -26
- package/dist/shaders/passes/field-blit.d.ts.map +0 -1
- package/dist/shaders/passes/field-blit.js +0 -65
- package/dist/shaders/passes/ground-noise.d.ts +0 -7
- package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
- package/dist/shaders/passes/ground-noise.js +0 -88
- package/dist/shaders/passes/sim.d.ts +0 -34
- package/dist/shaders/passes/sim.d.ts.map +0 -1
- package/dist/shaders/passes/sim.js +0 -169
- package/dist/shaders/score-api.d.ts +0 -10
- package/dist/shaders/score-api.d.ts.map +0 -1
- 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
|
+
}
|