reze-engine 0.54.16 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/engine.d.ts +210 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +1063 -11
  4. package/dist/graph/presets/pool_floor.d.ts +3 -0
  5. package/dist/graph/presets/pool_floor.d.ts.map +1 -0
  6. package/dist/graph/presets/pool_floor.js +42 -0
  7. package/dist/graph/presets/water.d.ts +3 -0
  8. package/dist/graph/presets/water.d.ts.map +1 -0
  9. package/dist/graph/presets/water.js +31 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +8 -0
  13. package/dist/model.d.ts.map +1 -1
  14. package/dist/model.js +10 -0
  15. package/dist/overlay.d.ts +200 -0
  16. package/dist/overlay.d.ts.map +1 -0
  17. package/dist/overlay.js +806 -0
  18. package/dist/physics/autofit.d.ts +147 -0
  19. package/dist/physics/autofit.d.ts.map +1 -0
  20. package/dist/physics/autofit.js +501 -0
  21. package/dist/pmx-document.d.ts +218 -0
  22. package/dist/pmx-document.d.ts.map +1 -0
  23. package/dist/pmx-document.js +685 -0
  24. package/dist/shaders/passes/field-blit.d.ts +26 -0
  25. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  26. package/dist/shaders/passes/field-blit.js +65 -0
  27. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  28. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  29. package/dist/shaders/passes/ground-noise.js +88 -0
  30. package/dist/shaders/passes/overlay.d.ts +3 -0
  31. package/dist/shaders/passes/overlay.d.ts.map +1 -0
  32. package/dist/shaders/passes/overlay.js +164 -0
  33. package/dist/shaders/passes/sim.d.ts +34 -0
  34. package/dist/shaders/passes/sim.d.ts.map +1 -0
  35. package/dist/shaders/passes/sim.js +169 -0
  36. package/dist/shaders/passes/wireframe.d.ts +2 -0
  37. package/dist/shaders/passes/wireframe.d.ts.map +1 -0
  38. package/dist/shaders/passes/wireframe.js +107 -0
  39. package/dist/shaders/score-api.d.ts +10 -0
  40. package/dist/shaders/score-api.d.ts.map +1 -0
  41. package/dist/shaders/score-api.js +114 -0
  42. package/package.json +1 -1
  43. package/src/engine.ts +1147 -13
  44. package/src/index.ts +50 -0
  45. package/src/model.ts +9 -0
  46. package/src/overlay.ts +1000 -0
  47. package/src/pmx-document.ts +882 -0
  48. package/src/shaders/passes/overlay.ts +166 -0
  49. package/src/shaders/passes/wireframe.ts +108 -0
@@ -0,0 +1,882 @@
1
+ // The PMX file, as data, losslessly.
2
+ //
3
+ // SEPARATE from Model on purpose. Model is what the renderer needs and drops
4
+ // everything else — English names, memos, display frames, additional UVs, SDEF
5
+ // and QDEF weights, per-vertex edge scale. That is correct for drawing: making
6
+ // Model lossless would make every viewer pay per-vertex costs for a scene it
7
+ // never edits. It is fatal for editing. A writer fed from Model would turn
8
+ // "rename a bone and save" into "rename a bone and lose every morph's panel
9
+ // assignment", and the user would rightly call that a broken editor.
10
+ //
11
+ // So this reads the file into a structure that mirrors it one for one, and
12
+ // writes that structure back. Read and write live in ONE module and each section
13
+ // keeps its reader and writer adjacent, because the only real failure mode here
14
+ // is the two drifting apart in field order — which the format cannot detect and
15
+ // which produces a file that loads as garbage.
16
+ //
17
+ // Round-tripping is checked byte-for-byte against real models in
18
+ // tests/pmx-roundtrip.test.mjs. That test is the product promise in executable
19
+ // form, so it is worth more than any of the code below.
20
+
21
+ /** 0 = UTF-16LE, 1 = UTF-8. Preserved: writing back in the other one is a
22
+ * different file, even where it decodes the same. */
23
+ export type PmxEncoding = 0 | 1
24
+
25
+ export interface PmxGlobals {
26
+ encoding: PmxEncoding
27
+ additionalUvCount: number
28
+ /** Vertex indices are UNSIGNED; every other index is signed, with -1 for none. */
29
+ vertexIndexSize: 1 | 2 | 4
30
+ textureIndexSize: 1 | 2 | 4
31
+ materialIndexSize: 1 | 2 | 4
32
+ boneIndexSize: 1 | 2 | 4
33
+ morphIndexSize: 1 | 2 | 4
34
+ rigidbodyIndexSize: 1 | 2 | 4
35
+ /** Anything past the eight PMX 2.0 knows about, kept so the header's own
36
+ * length survives a round trip. */
37
+ extra: number[]
38
+ }
39
+
40
+ export type PmxWeightType = 0 | 1 | 2 | 3 | 4 // BDEF1, BDEF2, BDEF4, SDEF, QDEF
41
+
42
+ export interface PmxVertex {
43
+ position: [number, number, number]
44
+ normal: [number, number, number]
45
+ uv: [number, number]
46
+ /** additionalUvCount entries, each a vec4. */
47
+ additionalUv: [number, number, number, number][]
48
+ weightType: PmxWeightType
49
+ /** 1, 2 or 4 entries depending on weightType. */
50
+ bones: number[]
51
+ /** BDEF2 keeps one, BDEF4 and QDEF keep four, BDEF1 keeps none. */
52
+ weights: number[]
53
+ /** SDEF only: C, R0, R1. */
54
+ sdef?: { c: [number, number, number]; r0: [number, number, number]; r1: [number, number, number] }
55
+ edgeScale: number
56
+ }
57
+
58
+ export interface PmxMaterial {
59
+ name: string
60
+ nameEn: string
61
+ diffuse: [number, number, number, number]
62
+ specular: [number, number, number]
63
+ specularPower: number
64
+ ambient: [number, number, number]
65
+ /** Bit 0 double-sided, 1 ground shadow, 2 to self-shadow map, 3 from
66
+ * self-shadow, 4 edge, 5 vertex colour, 6 point draw, 7 line draw. */
67
+ drawFlags: number
68
+ edgeColor: [number, number, number, number]
69
+ edgeSize: number
70
+ textureIndex: number
71
+ sphereIndex: number
72
+ /** 0 none, 1 multiply, 2 add, 3 subtexture. */
73
+ sphereMode: number
74
+ /** 0 = toonIndex is a texture index, 1 = it is a shared toon 0-9. */
75
+ toonShared: number
76
+ toonIndex: number
77
+ memo: string
78
+ /** How many INDICES this material owns, not faces. Materials own a contiguous
79
+ * run of the index buffer, which is why face order is document identity. */
80
+ indexCount: number
81
+ }
82
+
83
+ export interface PmxIkLink {
84
+ boneIndex: number
85
+ hasLimit: boolean
86
+ limitMin?: [number, number, number]
87
+ limitMax?: [number, number, number]
88
+ }
89
+
90
+ export interface PmxBone {
91
+ name: string
92
+ nameEn: string
93
+ position: [number, number, number]
94
+ parentIndex: number
95
+ layer: number
96
+ /** 0x0001 tail is a bone · 0x0002 rotatable · 0x0004 movable · 0x0008 visible
97
+ * 0x0010 operable · 0x0020 IK · 0x0080 local append · 0x0100 append rotate
98
+ * 0x0200 append move · 0x0400 fixed axis · 0x0800 local axes
99
+ * 0x1000 after physics · 0x2000 external parent */
100
+ flags: number
101
+ tailBoneIndex?: number
102
+ tailPosition?: [number, number, number]
103
+ appendParentIndex?: number
104
+ appendRatio?: number
105
+ fixedAxis?: [number, number, number]
106
+ localAxisX?: [number, number, number]
107
+ localAxisZ?: [number, number, number]
108
+ externalKey?: number
109
+ ik?: {
110
+ targetIndex: number
111
+ loopCount: number
112
+ limitAngle: number
113
+ links: PmxIkLink[]
114
+ }
115
+ }
116
+
117
+ export type PmxMorphOffset =
118
+ | { kind: "group"; index: number; influence: number }
119
+ | { kind: "vertex"; index: number; offset: [number, number, number] }
120
+ | { kind: "bone"; index: number; translation: [number, number, number]; rotation: [number, number, number, number] }
121
+ | { kind: "uv"; index: number; offset: [number, number, number, number] }
122
+ | {
123
+ kind: "material"
124
+ index: number
125
+ /** 0 multiply, 1 add. */
126
+ operation: number
127
+ diffuse: [number, number, number, number]
128
+ specular: [number, number, number]
129
+ specularPower: number
130
+ ambient: [number, number, number]
131
+ edgeColor: [number, number, number, number]
132
+ edgeSize: number
133
+ textureTint: [number, number, number, number]
134
+ sphereTint: [number, number, number, number]
135
+ toonTint: [number, number, number, number]
136
+ }
137
+ | { kind: "flip"; index: number; influence: number }
138
+ | {
139
+ kind: "impulse"
140
+ index: number
141
+ local: number
142
+ velocity: [number, number, number]
143
+ torque: [number, number, number]
144
+ }
145
+
146
+ export interface PmxMorph {
147
+ name: string
148
+ nameEn: string
149
+ /** 0 reserved, 1 eyebrow, 2 eye, 3 mouth, 4 other. What MMD groups sliders by. */
150
+ panel: number
151
+ /** 0 group, 1 vertex, 2 bone, 3-7 UV, 8 material, 9 flip, 10 impulse. */
152
+ type: number
153
+ offsets: PmxMorphOffset[]
154
+ }
155
+
156
+ export interface PmxDisplayFrame {
157
+ name: string
158
+ nameEn: string
159
+ /** 1 for the two frames MMD reserves (Root and 表情). */
160
+ special: number
161
+ /** type 0 = bone index, 1 = morph index. */
162
+ elements: { type: number; index: number }[]
163
+ }
164
+
165
+ export interface PmxRigidbody {
166
+ name: string
167
+ nameEn: string
168
+ boneIndex: number
169
+ group: number
170
+ nonCollideMask: number
171
+ /** 0 sphere, 1 box, 2 capsule. */
172
+ shape: number
173
+ size: [number, number, number]
174
+ position: [number, number, number]
175
+ rotation: [number, number, number]
176
+ mass: number
177
+ linearDamping: number
178
+ angularDamping: number
179
+ restitution: number
180
+ friction: number
181
+ /** 0 follows its bone, 1 physics, 2 physics then aligned to its bone. */
182
+ physicsMode: number
183
+ }
184
+
185
+ export interface PmxJoint {
186
+ name: string
187
+ nameEn: string
188
+ /** 0 is 6DOF-with-spring, the only one PMX 2.0 has. 2.1 adds 1-5. */
189
+ type: number
190
+ rigidbodyA: number
191
+ rigidbodyB: number
192
+ position: [number, number, number]
193
+ rotation: [number, number, number]
194
+ positionMin: [number, number, number]
195
+ positionMax: [number, number, number]
196
+ rotationMin: [number, number, number]
197
+ rotationMax: [number, number, number]
198
+ springPosition: [number, number, number]
199
+ springRotation: [number, number, number]
200
+ }
201
+
202
+ export interface PmxDocument {
203
+ version: number
204
+ globals: PmxGlobals
205
+ name: string
206
+ nameEn: string
207
+ comment: string
208
+ commentEn: string
209
+ vertices: PmxVertex[]
210
+ /** Flat triples. Materials own contiguous runs of this, so its ORDER is
211
+ * identity: reordering it silently re-assigns geometry between materials. */
212
+ indices: Uint32Array
213
+ textures: string[]
214
+ materials: PmxMaterial[]
215
+ bones: PmxBone[]
216
+ morphs: PmxMorph[]
217
+ displayFrames: PmxDisplayFrame[]
218
+ rigidbodies: PmxRigidbody[]
219
+ joints: PmxJoint[]
220
+ /** PMX 2.1 soft bodies, kept as the bytes they arrived as. They are vanishingly
221
+ * rare and nothing here edits them — but a model that has them must still save
222
+ * as the same file, and preserving the tail costs nothing. */
223
+ trailing: Uint8Array | null
224
+ }
225
+
226
+ // ──────────────────────────────────────────────────────────────────
227
+ // Bytes
228
+
229
+ class Cursor {
230
+ readonly view: DataView
231
+ readonly bytes: Uint8Array
232
+ at = 0
233
+ constructor(buffer: ArrayBuffer) {
234
+ this.view = new DataView(buffer)
235
+ this.bytes = new Uint8Array(buffer)
236
+ }
237
+ u8() {
238
+ return this.view.getUint8(this.at++)
239
+ }
240
+ u16() {
241
+ const v = this.view.getUint16(this.at, true)
242
+ this.at += 2
243
+ return v
244
+ }
245
+ i32() {
246
+ const v = this.view.getInt32(this.at, true)
247
+ this.at += 4
248
+ return v
249
+ }
250
+ f32() {
251
+ const v = this.view.getFloat32(this.at, true)
252
+ this.at += 4
253
+ return v
254
+ }
255
+ vec2(): [number, number] {
256
+ return [this.f32(), this.f32()]
257
+ }
258
+ vec3(): [number, number, number] {
259
+ return [this.f32(), this.f32(), this.f32()]
260
+ }
261
+ vec4(): [number, number, number, number] {
262
+ return [this.f32(), this.f32(), this.f32(), this.f32()]
263
+ }
264
+ /** Unsigned — vertex indices only. */
265
+ uindex(size: number): number {
266
+ if (size === 1) return this.u8()
267
+ if (size === 2) return this.u16()
268
+ return this.view.getUint32((this.at += 4) - 4, true)
269
+ }
270
+ /** Signed, with the all-ones pattern meaning "none". Every index but vertex. */
271
+ index(size: number): number {
272
+ if (size === 1) return this.view.getInt8(this.at++)
273
+ if (size === 2) {
274
+ const v = this.view.getInt16(this.at, true)
275
+ this.at += 2
276
+ return v
277
+ }
278
+ return this.i32()
279
+ }
280
+ text(encoding: PmxEncoding): string {
281
+ const length = this.i32()
282
+ const slice = this.bytes.subarray(this.at, this.at + length)
283
+ this.at += length
284
+ if (encoding === 1) return new TextDecoder("utf-8").decode(slice)
285
+ return new TextDecoder("utf-16le").decode(slice)
286
+ }
287
+ }
288
+
289
+ class Sink {
290
+ private chunks: Uint8Array[] = []
291
+ private buf = new Uint8Array(65536)
292
+ private view = new DataView(this.buf.buffer)
293
+ private at = 0
294
+ private need(n: number) {
295
+ if (this.at + n <= this.buf.length) return
296
+ this.chunks.push(this.buf.subarray(0, this.at))
297
+ this.buf = new Uint8Array(Math.max(65536, n))
298
+ this.view = new DataView(this.buf.buffer)
299
+ this.at = 0
300
+ }
301
+ u8(v: number) {
302
+ this.need(1)
303
+ this.view.setUint8(this.at++, v)
304
+ }
305
+ u16(v: number) {
306
+ this.need(2)
307
+ this.view.setUint16(this.at, v, true)
308
+ this.at += 2
309
+ }
310
+ i32(v: number) {
311
+ this.need(4)
312
+ this.view.setInt32(this.at, v, true)
313
+ this.at += 4
314
+ }
315
+ f32(v: number) {
316
+ this.need(4)
317
+ this.view.setFloat32(this.at, v, true)
318
+ this.at += 4
319
+ }
320
+ vec(v: readonly number[]) {
321
+ for (const n of v) this.f32(n)
322
+ }
323
+ uindex(size: number, v: number) {
324
+ if (size === 1) this.u8(v)
325
+ else if (size === 2) this.u16(v)
326
+ else this.i32(v | 0)
327
+ }
328
+ index(size: number, v: number) {
329
+ if (size === 1) this.u8(v & 0xff)
330
+ else if (size === 2) this.u16(v & 0xffff)
331
+ else this.i32(v)
332
+ }
333
+ raw(v: Uint8Array) {
334
+ this.need(v.length)
335
+ this.buf.set(v, this.at)
336
+ this.at += v.length
337
+ }
338
+ text(encoding: PmxEncoding, v: string) {
339
+ const bytes = encoding === 1 ? new TextEncoder().encode(v) : utf16le(v)
340
+ this.i32(bytes.length)
341
+ this.raw(bytes)
342
+ }
343
+ finish(): ArrayBuffer {
344
+ this.chunks.push(this.buf.subarray(0, this.at))
345
+ let total = 0
346
+ for (const c of this.chunks) total += c.length
347
+ const out = new Uint8Array(total)
348
+ let at = 0
349
+ for (const c of this.chunks) {
350
+ out.set(c, at)
351
+ at += c.length
352
+ }
353
+ return out.buffer
354
+ }
355
+ }
356
+
357
+ /** TextEncoder has no utf-16le, so the surrogate pairs are written by hand. */
358
+ function utf16le(s: string): Uint8Array {
359
+ const out = new Uint8Array(s.length * 2)
360
+ const view = new DataView(out.buffer)
361
+ for (let i = 0; i < s.length; i++) view.setUint16(i * 2, s.charCodeAt(i), true)
362
+ return out
363
+ }
364
+
365
+ // ──────────────────────────────────────────────────────────────────
366
+ // Read and write, one section at a time, each pair adjacent
367
+
368
+ /** Parses a PMX file into a document that writes back byte for byte. */
369
+ export function readPmxDocument(buffer: ArrayBuffer): PmxDocument {
370
+ const c = new Cursor(buffer)
371
+
372
+ // Signature. "Pmx " appears in the wild alongside "PMX ", and PMD is the older
373
+ // format entirely — say which it was rather than "not a PMX file".
374
+ const sig = String.fromCharCode(c.u8(), c.u8(), c.u8(), c.u8())
375
+ if (sig.slice(0, 3).toUpperCase() !== "PMX") {
376
+ if (sig.slice(0, 3).toUpperCase() === "PMD") {
377
+ throw new Error("This is a PMD file (MMD's older format). Convert it to PMX in PMXEditor first.")
378
+ }
379
+ throw new Error(`Not a PMX file (signature "${sig.replace(/[^\x20-\x7e]/g, "?")}")`)
380
+ }
381
+ const version = c.f32()
382
+ const globalsCount = c.u8()
383
+ if (globalsCount < 8) throw new Error(`PMX header declares ${globalsCount} globals, expected at least 8`)
384
+ const globals: PmxGlobals = {
385
+ encoding: c.u8() as PmxEncoding,
386
+ additionalUvCount: c.u8(),
387
+ vertexIndexSize: c.u8() as 1 | 2 | 4,
388
+ textureIndexSize: c.u8() as 1 | 2 | 4,
389
+ materialIndexSize: c.u8() as 1 | 2 | 4,
390
+ boneIndexSize: c.u8() as 1 | 2 | 4,
391
+ morphIndexSize: c.u8() as 1 | 2 | 4,
392
+ rigidbodyIndexSize: c.u8() as 1 | 2 | 4,
393
+ extra: [],
394
+ }
395
+ for (let i = 8; i < globalsCount; i++) globals.extra.push(c.u8())
396
+ const e = globals.encoding
397
+
398
+ const doc: PmxDocument = {
399
+ version,
400
+ globals,
401
+ name: c.text(e),
402
+ nameEn: c.text(e),
403
+ comment: c.text(e),
404
+ commentEn: c.text(e),
405
+ vertices: [],
406
+ indices: new Uint32Array(0),
407
+ textures: [],
408
+ materials: [],
409
+ bones: [],
410
+ morphs: [],
411
+ displayFrames: [],
412
+ rigidbodies: [],
413
+ joints: [],
414
+ trailing: null,
415
+ }
416
+
417
+ // ── Vertices ──
418
+ const vertexCount = c.i32()
419
+ doc.vertices.length = vertexCount
420
+ for (let i = 0; i < vertexCount; i++) {
421
+ const v: PmxVertex = {
422
+ position: c.vec3(),
423
+ normal: c.vec3(),
424
+ uv: c.vec2(),
425
+ additionalUv: [],
426
+ weightType: 0,
427
+ bones: [],
428
+ weights: [],
429
+ edgeScale: 0,
430
+ }
431
+ for (let k = 0; k < globals.additionalUvCount; k++) v.additionalUv.push(c.vec4())
432
+ v.weightType = c.u8() as PmxWeightType
433
+ const bi = globals.boneIndexSize
434
+ switch (v.weightType) {
435
+ case 0:
436
+ v.bones = [c.index(bi)]
437
+ break
438
+ case 1:
439
+ v.bones = [c.index(bi), c.index(bi)]
440
+ v.weights = [c.f32()]
441
+ break
442
+ case 2:
443
+ case 4:
444
+ v.bones = [c.index(bi), c.index(bi), c.index(bi), c.index(bi)]
445
+ v.weights = [c.f32(), c.f32(), c.f32(), c.f32()]
446
+ break
447
+ case 3:
448
+ v.bones = [c.index(bi), c.index(bi)]
449
+ v.weights = [c.f32()]
450
+ v.sdef = { c: c.vec3(), r0: c.vec3(), r1: c.vec3() }
451
+ break
452
+ }
453
+ v.edgeScale = c.f32()
454
+ doc.vertices[i] = v
455
+ }
456
+
457
+ // ── Faces ──
458
+ const indexCount = c.i32()
459
+ const indices = new Uint32Array(indexCount)
460
+ for (let i = 0; i < indexCount; i++) indices[i] = c.uindex(globals.vertexIndexSize)
461
+ doc.indices = indices
462
+
463
+ // ── Textures ──
464
+ const textureCount = c.i32()
465
+ for (let i = 0; i < textureCount; i++) doc.textures.push(c.text(e))
466
+
467
+ // ── Materials ──
468
+ const materialCount = c.i32()
469
+ for (let i = 0; i < materialCount; i++) {
470
+ const m: PmxMaterial = {
471
+ name: c.text(e),
472
+ nameEn: c.text(e),
473
+ diffuse: c.vec4(),
474
+ specular: c.vec3(),
475
+ specularPower: c.f32(),
476
+ ambient: c.vec3(),
477
+ drawFlags: c.u8(),
478
+ edgeColor: c.vec4(),
479
+ edgeSize: c.f32(),
480
+ textureIndex: c.index(globals.textureIndexSize),
481
+ sphereIndex: c.index(globals.textureIndexSize),
482
+ sphereMode: c.u8(),
483
+ toonShared: c.u8(),
484
+ toonIndex: 0,
485
+ memo: "",
486
+ indexCount: 0,
487
+ }
488
+ m.toonIndex = m.toonShared === 1 ? c.u8() : c.index(globals.textureIndexSize)
489
+ m.memo = c.text(e)
490
+ m.indexCount = c.i32()
491
+ doc.materials.push(m)
492
+ }
493
+
494
+ // ── Bones ──
495
+ const boneCount = c.i32()
496
+ const bi = globals.boneIndexSize
497
+ for (let i = 0; i < boneCount; i++) {
498
+ const b: PmxBone = {
499
+ name: c.text(e),
500
+ nameEn: c.text(e),
501
+ position: c.vec3(),
502
+ parentIndex: c.index(bi),
503
+ layer: c.i32(),
504
+ flags: c.u16(),
505
+ }
506
+ if (b.flags & 0x0001) b.tailBoneIndex = c.index(bi)
507
+ else b.tailPosition = c.vec3()
508
+ if (b.flags & 0x0300) {
509
+ b.appendParentIndex = c.index(bi)
510
+ b.appendRatio = c.f32()
511
+ }
512
+ if (b.flags & 0x0400) b.fixedAxis = c.vec3()
513
+ if (b.flags & 0x0800) {
514
+ b.localAxisX = c.vec3()
515
+ b.localAxisZ = c.vec3()
516
+ }
517
+ if (b.flags & 0x2000) b.externalKey = c.i32()
518
+ if (b.flags & 0x0020) {
519
+ const targetIndex = c.index(bi)
520
+ const loopCount = c.i32()
521
+ const limitAngle = c.f32()
522
+ const linkCount = c.i32()
523
+ const links: PmxIkLink[] = []
524
+ for (let k = 0; k < linkCount; k++) {
525
+ const boneIndex = c.index(bi)
526
+ const hasLimit = c.u8() === 1
527
+ links.push(hasLimit ? { boneIndex, hasLimit, limitMin: c.vec3(), limitMax: c.vec3() } : { boneIndex, hasLimit })
528
+ }
529
+ b.ik = { targetIndex, loopCount, limitAngle, links }
530
+ }
531
+ doc.bones.push(b)
532
+ }
533
+
534
+ // ── Morphs ──
535
+ const morphCount = c.i32()
536
+ for (let i = 0; i < morphCount; i++) {
537
+ const name = c.text(e)
538
+ const nameEn = c.text(e)
539
+ const panel = c.u8()
540
+ const type = c.u8()
541
+ const offsetCount = c.i32()
542
+ const offsets: PmxMorphOffset[] = []
543
+ for (let k = 0; k < offsetCount; k++) {
544
+ switch (type) {
545
+ case 0:
546
+ offsets.push({ kind: "group", index: c.index(globals.morphIndexSize), influence: c.f32() })
547
+ break
548
+ case 1:
549
+ offsets.push({ kind: "vertex", index: c.uindex(globals.vertexIndexSize), offset: c.vec3() })
550
+ break
551
+ case 2:
552
+ offsets.push({ kind: "bone", index: c.index(bi), translation: c.vec3(), rotation: c.vec4() })
553
+ break
554
+ case 3:
555
+ case 4:
556
+ case 5:
557
+ case 6:
558
+ case 7:
559
+ offsets.push({ kind: "uv", index: c.uindex(globals.vertexIndexSize), offset: c.vec4() })
560
+ break
561
+ case 8:
562
+ offsets.push({
563
+ kind: "material",
564
+ index: c.index(globals.materialIndexSize),
565
+ operation: c.u8(),
566
+ diffuse: c.vec4(),
567
+ specular: c.vec3(),
568
+ specularPower: c.f32(),
569
+ ambient: c.vec3(),
570
+ edgeColor: c.vec4(),
571
+ edgeSize: c.f32(),
572
+ textureTint: c.vec4(),
573
+ sphereTint: c.vec4(),
574
+ toonTint: c.vec4(),
575
+ })
576
+ break
577
+ case 9:
578
+ offsets.push({ kind: "flip", index: c.index(globals.morphIndexSize), influence: c.f32() })
579
+ break
580
+ case 10:
581
+ offsets.push({
582
+ kind: "impulse",
583
+ index: c.index(globals.rigidbodyIndexSize),
584
+ local: c.u8(),
585
+ velocity: c.vec3(),
586
+ torque: c.vec3(),
587
+ })
588
+ break
589
+ default:
590
+ throw new Error(`Morph "${name}" has unknown type ${type}`)
591
+ }
592
+ }
593
+ doc.morphs.push({ name, nameEn, panel, type, offsets })
594
+ }
595
+
596
+ // ── Display frames ──
597
+ const frameCount = c.i32()
598
+ for (let i = 0; i < frameCount; i++) {
599
+ const name = c.text(e)
600
+ const nameEn = c.text(e)
601
+ const special = c.u8()
602
+ const elementCount = c.i32()
603
+ const elements: { type: number; index: number }[] = []
604
+ for (let k = 0; k < elementCount; k++) {
605
+ const type = c.u8()
606
+ elements.push({ type, index: c.index(type === 1 ? globals.morphIndexSize : bi) })
607
+ }
608
+ doc.displayFrames.push({ name, nameEn, special, elements })
609
+ }
610
+
611
+ // ── Rigidbodies ──
612
+ const bodyCount = c.i32()
613
+ for (let i = 0; i < bodyCount; i++) {
614
+ doc.rigidbodies.push({
615
+ name: c.text(e),
616
+ nameEn: c.text(e),
617
+ boneIndex: c.index(bi),
618
+ group: c.u8(),
619
+ nonCollideMask: c.u16(),
620
+ shape: c.u8(),
621
+ size: c.vec3(),
622
+ position: c.vec3(),
623
+ rotation: c.vec3(),
624
+ mass: c.f32(),
625
+ linearDamping: c.f32(),
626
+ angularDamping: c.f32(),
627
+ restitution: c.f32(),
628
+ friction: c.f32(),
629
+ physicsMode: c.u8(),
630
+ })
631
+ }
632
+
633
+ // ── Joints ──
634
+ const jointCount = c.i32()
635
+ const ri = globals.rigidbodyIndexSize
636
+ for (let i = 0; i < jointCount; i++) {
637
+ doc.joints.push({
638
+ name: c.text(e),
639
+ nameEn: c.text(e),
640
+ type: c.u8(),
641
+ rigidbodyA: c.index(ri),
642
+ rigidbodyB: c.index(ri),
643
+ position: c.vec3(),
644
+ rotation: c.vec3(),
645
+ positionMin: c.vec3(),
646
+ positionMax: c.vec3(),
647
+ rotationMin: c.vec3(),
648
+ rotationMax: c.vec3(),
649
+ springPosition: c.vec3(),
650
+ springRotation: c.vec3(),
651
+ })
652
+ }
653
+
654
+ if (c.at < c.bytes.length) doc.trailing = c.bytes.slice(c.at)
655
+ return doc
656
+ }
657
+
658
+ /** Writes a document back to PMX bytes. */
659
+ export function writePmxDocument(doc: PmxDocument): ArrayBuffer {
660
+ const w = new Sink()
661
+ const g = doc.globals
662
+ const e = g.encoding
663
+ const bi = g.boneIndexSize
664
+
665
+ for (const ch of "PMX ") w.u8(ch.charCodeAt(0))
666
+ w.f32(doc.version)
667
+ w.u8(8 + g.extra.length)
668
+ w.u8(g.encoding)
669
+ w.u8(g.additionalUvCount)
670
+ w.u8(g.vertexIndexSize)
671
+ w.u8(g.textureIndexSize)
672
+ w.u8(g.materialIndexSize)
673
+ w.u8(g.boneIndexSize)
674
+ w.u8(g.morphIndexSize)
675
+ w.u8(g.rigidbodyIndexSize)
676
+ for (const x of g.extra) w.u8(x)
677
+ w.text(e, doc.name)
678
+ w.text(e, doc.nameEn)
679
+ w.text(e, doc.comment)
680
+ w.text(e, doc.commentEn)
681
+
682
+ w.i32(doc.vertices.length)
683
+ for (const v of doc.vertices) {
684
+ w.vec(v.position)
685
+ w.vec(v.normal)
686
+ w.vec(v.uv)
687
+ for (let k = 0; k < g.additionalUvCount; k++) w.vec(v.additionalUv[k] ?? [0, 0, 0, 0])
688
+ w.u8(v.weightType)
689
+ switch (v.weightType) {
690
+ case 0:
691
+ w.index(bi, v.bones[0])
692
+ break
693
+ case 1:
694
+ w.index(bi, v.bones[0])
695
+ w.index(bi, v.bones[1])
696
+ w.f32(v.weights[0])
697
+ break
698
+ case 2:
699
+ case 4:
700
+ for (let k = 0; k < 4; k++) w.index(bi, v.bones[k])
701
+ for (let k = 0; k < 4; k++) w.f32(v.weights[k])
702
+ break
703
+ case 3:
704
+ w.index(bi, v.bones[0])
705
+ w.index(bi, v.bones[1])
706
+ w.f32(v.weights[0])
707
+ w.vec(v.sdef!.c)
708
+ w.vec(v.sdef!.r0)
709
+ w.vec(v.sdef!.r1)
710
+ break
711
+ }
712
+ w.f32(v.edgeScale)
713
+ }
714
+
715
+ w.i32(doc.indices.length)
716
+ for (let i = 0; i < doc.indices.length; i++) w.uindex(g.vertexIndexSize, doc.indices[i])
717
+
718
+ w.i32(doc.textures.length)
719
+ for (const t of doc.textures) w.text(e, t)
720
+
721
+ w.i32(doc.materials.length)
722
+ for (const m of doc.materials) {
723
+ w.text(e, m.name)
724
+ w.text(e, m.nameEn)
725
+ w.vec(m.diffuse)
726
+ w.vec(m.specular)
727
+ w.f32(m.specularPower)
728
+ w.vec(m.ambient)
729
+ w.u8(m.drawFlags)
730
+ w.vec(m.edgeColor)
731
+ w.f32(m.edgeSize)
732
+ w.index(g.textureIndexSize, m.textureIndex)
733
+ w.index(g.textureIndexSize, m.sphereIndex)
734
+ w.u8(m.sphereMode)
735
+ w.u8(m.toonShared)
736
+ if (m.toonShared === 1) w.u8(m.toonIndex)
737
+ else w.index(g.textureIndexSize, m.toonIndex)
738
+ w.text(e, m.memo)
739
+ w.i32(m.indexCount)
740
+ }
741
+
742
+ w.i32(doc.bones.length)
743
+ for (const b of doc.bones) {
744
+ w.text(e, b.name)
745
+ w.text(e, b.nameEn)
746
+ w.vec(b.position)
747
+ w.index(bi, b.parentIndex)
748
+ w.i32(b.layer)
749
+ w.u16(b.flags)
750
+ if (b.flags & 0x0001) w.index(bi, b.tailBoneIndex!)
751
+ else w.vec(b.tailPosition!)
752
+ if (b.flags & 0x0300) {
753
+ w.index(bi, b.appendParentIndex!)
754
+ w.f32(b.appendRatio!)
755
+ }
756
+ if (b.flags & 0x0400) w.vec(b.fixedAxis!)
757
+ if (b.flags & 0x0800) {
758
+ w.vec(b.localAxisX!)
759
+ w.vec(b.localAxisZ!)
760
+ }
761
+ if (b.flags & 0x2000) w.i32(b.externalKey!)
762
+ if (b.flags & 0x0020) {
763
+ const ik = b.ik!
764
+ w.index(bi, ik.targetIndex)
765
+ w.i32(ik.loopCount)
766
+ w.f32(ik.limitAngle)
767
+ w.i32(ik.links.length)
768
+ for (const l of ik.links) {
769
+ w.index(bi, l.boneIndex)
770
+ w.u8(l.hasLimit ? 1 : 0)
771
+ if (l.hasLimit) {
772
+ w.vec(l.limitMin!)
773
+ w.vec(l.limitMax!)
774
+ }
775
+ }
776
+ }
777
+ }
778
+
779
+ w.i32(doc.morphs.length)
780
+ for (const m of doc.morphs) {
781
+ w.text(e, m.name)
782
+ w.text(e, m.nameEn)
783
+ w.u8(m.panel)
784
+ w.u8(m.type)
785
+ w.i32(m.offsets.length)
786
+ for (const o of m.offsets) {
787
+ switch (o.kind) {
788
+ case "group":
789
+ w.index(g.morphIndexSize, o.index)
790
+ w.f32(o.influence)
791
+ break
792
+ case "vertex":
793
+ w.uindex(g.vertexIndexSize, o.index)
794
+ w.vec(o.offset)
795
+ break
796
+ case "bone":
797
+ w.index(bi, o.index)
798
+ w.vec(o.translation)
799
+ w.vec(o.rotation)
800
+ break
801
+ case "uv":
802
+ w.uindex(g.vertexIndexSize, o.index)
803
+ w.vec(o.offset)
804
+ break
805
+ case "material":
806
+ w.index(g.materialIndexSize, o.index)
807
+ w.u8(o.operation)
808
+ w.vec(o.diffuse)
809
+ w.vec(o.specular)
810
+ w.f32(o.specularPower)
811
+ w.vec(o.ambient)
812
+ w.vec(o.edgeColor)
813
+ w.f32(o.edgeSize)
814
+ w.vec(o.textureTint)
815
+ w.vec(o.sphereTint)
816
+ w.vec(o.toonTint)
817
+ break
818
+ case "flip":
819
+ w.index(g.morphIndexSize, o.index)
820
+ w.f32(o.influence)
821
+ break
822
+ case "impulse":
823
+ w.index(g.rigidbodyIndexSize, o.index)
824
+ w.u8(o.local)
825
+ w.vec(o.velocity)
826
+ w.vec(o.torque)
827
+ break
828
+ }
829
+ }
830
+ }
831
+
832
+ w.i32(doc.displayFrames.length)
833
+ for (const f of doc.displayFrames) {
834
+ w.text(e, f.name)
835
+ w.text(e, f.nameEn)
836
+ w.u8(f.special)
837
+ w.i32(f.elements.length)
838
+ for (const el of f.elements) {
839
+ w.u8(el.type)
840
+ w.index(el.type === 1 ? g.morphIndexSize : bi, el.index)
841
+ }
842
+ }
843
+
844
+ w.i32(doc.rigidbodies.length)
845
+ for (const b of doc.rigidbodies) {
846
+ w.text(e, b.name)
847
+ w.text(e, b.nameEn)
848
+ w.index(bi, b.boneIndex)
849
+ w.u8(b.group)
850
+ w.u16(b.nonCollideMask)
851
+ w.u8(b.shape)
852
+ w.vec(b.size)
853
+ w.vec(b.position)
854
+ w.vec(b.rotation)
855
+ w.f32(b.mass)
856
+ w.f32(b.linearDamping)
857
+ w.f32(b.angularDamping)
858
+ w.f32(b.restitution)
859
+ w.f32(b.friction)
860
+ w.u8(b.physicsMode)
861
+ }
862
+
863
+ w.i32(doc.joints.length)
864
+ for (const j of doc.joints) {
865
+ w.text(e, j.name)
866
+ w.text(e, j.nameEn)
867
+ w.u8(j.type)
868
+ w.index(g.rigidbodyIndexSize, j.rigidbodyA)
869
+ w.index(g.rigidbodyIndexSize, j.rigidbodyB)
870
+ w.vec(j.position)
871
+ w.vec(j.rotation)
872
+ w.vec(j.positionMin)
873
+ w.vec(j.positionMax)
874
+ w.vec(j.rotationMin)
875
+ w.vec(j.rotationMax)
876
+ w.vec(j.springPosition)
877
+ w.vec(j.springRotation)
878
+ }
879
+
880
+ if (doc.trailing) w.raw(doc.trailing)
881
+ return w.finish()
882
+ }