reze-engine 0.3.12 → 0.3.13

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 (44) hide show
  1. package/README.md +66 -66
  2. package/dist/bezier-interpolate.d.ts +15 -0
  3. package/dist/bezier-interpolate.d.ts.map +1 -0
  4. package/dist/bezier-interpolate.js +40 -0
  5. package/dist/engine.d.ts +3 -8
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +19 -30
  8. package/dist/engine_ts.d.ts +143 -0
  9. package/dist/engine_ts.d.ts.map +1 -0
  10. package/dist/engine_ts.js +1575 -0
  11. package/dist/ik.d.ts +32 -0
  12. package/dist/ik.d.ts.map +1 -0
  13. package/dist/ik.js +337 -0
  14. package/dist/player.d.ts +64 -0
  15. package/dist/player.d.ts.map +1 -0
  16. package/dist/player.js +220 -0
  17. package/dist/pool-scene.d.ts +52 -0
  18. package/dist/pool-scene.d.ts.map +1 -0
  19. package/dist/pool-scene.js +1122 -0
  20. package/dist/pool.d.ts +38 -0
  21. package/dist/pool.d.ts.map +1 -0
  22. package/dist/pool.js +422 -0
  23. package/dist/rzm-converter.d.ts +12 -0
  24. package/dist/rzm-converter.d.ts.map +1 -0
  25. package/dist/rzm-converter.js +40 -0
  26. package/dist/rzm-loader.d.ts +24 -0
  27. package/dist/rzm-loader.d.ts.map +1 -0
  28. package/dist/rzm-loader.js +488 -0
  29. package/dist/rzm-writer.d.ts +27 -0
  30. package/dist/rzm-writer.d.ts.map +1 -0
  31. package/dist/rzm-writer.js +701 -0
  32. package/package.json +1 -1
  33. package/src/camera.ts +358 -358
  34. package/src/engine.ts +28 -28
  35. package/src/ik-solver.ts +411 -411
  36. package/src/math.ts +584 -584
  37. package/src/physics.ts +742 -742
  38. package/src/vmd-loader.ts +276 -276
  39. package/dist/audio.d.ts +0 -29
  40. package/dist/audio.d.ts.map +0 -1
  41. package/dist/audio.js +0 -116
  42. package/dist/particles.d.ts +0 -67
  43. package/dist/particles.d.ts.map +0 -1
  44. package/dist/particles.js +0 -266
@@ -0,0 +1,701 @@
1
+ /**
2
+ * RZM (Reze Model) Binary Format Writer
3
+ *
4
+ * Format Structure:
5
+ * - Header (32 bytes)
6
+ * - Magic: "RZM\0" (4 bytes)
7
+ * - Version: uint32 (1)
8
+ * - File size: uint64
9
+ * - Section count: uint32
10
+ * - Reserved: 16 bytes
11
+ *
12
+ * - Section Table (variable)
13
+ * - For each section: type (uint32), offset (uint64), size (uint64)
14
+ *
15
+ * - Sections (in order):
16
+ * - GEOMETRY: vertices, indices
17
+ * - SKELETON: bones, inverse bind matrices
18
+ * - SKINNING: joints, weights
19
+ * - MORPHING: morphs, offsets buffer
20
+ * - MATERIALS: material data
21
+ * - TEXTURES: texture metadata + embedded image data
22
+ * - PHYSICS: rigidbodies, joints
23
+ */
24
+ var SectionType;
25
+ (function (SectionType) {
26
+ SectionType[SectionType["GEOMETRY"] = 1] = "GEOMETRY";
27
+ SectionType[SectionType["SKELETON"] = 2] = "SKELETON";
28
+ SectionType[SectionType["SKINNING"] = 3] = "SKINNING";
29
+ SectionType[SectionType["MORPHING"] = 4] = "MORPHING";
30
+ SectionType[SectionType["MATERIALS"] = 5] = "MATERIALS";
31
+ SectionType[SectionType["TEXTURES"] = 6] = "TEXTURES";
32
+ SectionType[SectionType["PHYSICS"] = 7] = "PHYSICS";
33
+ })(SectionType || (SectionType = {}));
34
+ export class RzmWriter {
35
+ constructor() {
36
+ this.sections = [];
37
+ this.currentOffset = 0;
38
+ }
39
+ /**
40
+ * Convert a Model to RZM binary format
41
+ * @param model The model to convert
42
+ * @param textureData Map of texture paths to their binary data (PNG/JPEG bytes)
43
+ * @returns RZM binary data as ArrayBuffer
44
+ */
45
+ static async write(model, textureData) {
46
+ const writer = new RzmWriter();
47
+ await writer.writeModel(model, textureData);
48
+ return writer.finalize();
49
+ }
50
+ async writeModel(model, textureData) {
51
+ // Calculate header + section table size
52
+ const textures = model.getTextures();
53
+ const sectionCount = 7; // All sections are always present
54
+ const sectionTableSize = sectionCount * RzmWriter.SECTION_TABLE_ENTRY_SIZE;
55
+ this.currentOffset = RzmWriter.HEADER_SIZE + sectionTableSize;
56
+ // Write all sections
57
+ this.writeGeometry(model);
58
+ this.writeSkeleton(model);
59
+ this.writeSkinning(model);
60
+ this.writeMorphing(model);
61
+ this.writeMaterials(model);
62
+ await this.writeTextures(model, textureData);
63
+ this.writePhysics(model);
64
+ }
65
+ writeGeometry(model) {
66
+ const vertices = model.getVertices();
67
+ const indices = model.getIndices();
68
+ // Layout: vertexCount (uint32), vertexData (Float32Array), indexCount (uint32), indexData (Uint32Array)
69
+ const vertexBytes = new Uint8Array(vertices.buffer);
70
+ const indexBytes = new Uint8Array(indices.buffer);
71
+ const size = 4 + vertexBytes.length + 4 + indexBytes.length;
72
+ const data = new Uint8Array(size);
73
+ const view = new DataView(data.buffer);
74
+ let offset = 0;
75
+ view.setUint32(offset, vertices.length / 8, true); // vertexCount (8 floats per vertex)
76
+ offset += 4;
77
+ data.set(vertexBytes, offset);
78
+ offset += vertexBytes.length;
79
+ view.setUint32(offset, indices.length, true); // indexCount
80
+ offset += 4;
81
+ data.set(indexBytes, offset);
82
+ this.addSection(SectionType.GEOMETRY, data);
83
+ }
84
+ writeSkeleton(model) {
85
+ const skeleton = model.getSkeleton();
86
+ const bones = skeleton.bones;
87
+ const inverseBindMatrices = skeleton.inverseBindMatrices;
88
+ // Layout: boneCount (uint32), bones[], inverseBindMatrices (Float32Array)
89
+ // Each bone: nameLen (uint32), name (utf8), parentIndex (int32), bindTranslation (3x float32),
90
+ // appendParentIndex (int32, -1 if none), appendRatio (float32), appendRotate (uint8), appendMove (uint8),
91
+ // ikTargetIndex (int32, -1 if none), ikIteration (int32), ikLimitAngle (float32),
92
+ // ikLinkCount (uint32), ikLinks[]
93
+ // Each IK link: boneIndex (int32), hasLimit (uint8), minAngle (3x float32, if hasLimit), maxAngle (3x float32, if hasLimit)
94
+ const boneData = [];
95
+ let totalSize = 4; // boneCount
96
+ for (const bone of bones) {
97
+ const nameBytes = new TextEncoder().encode(bone.name);
98
+ const boneSize = 4 + nameBytes.length + 4 + 12 + 4 + 4 + 1 + 1 + 4 + 4 + 4 + 4; // base size
99
+ const boneBuffer = new Uint8Array(boneSize);
100
+ const boneView = new DataView(boneBuffer.buffer);
101
+ let off = 0;
102
+ boneView.setUint32(off, nameBytes.length, true);
103
+ off += 4;
104
+ boneBuffer.set(nameBytes, off);
105
+ off += nameBytes.length;
106
+ boneView.setInt32(off, bone.parentIndex ?? -1, true);
107
+ off += 4;
108
+ boneView.setFloat32(off, bone.bindTranslation[0], true);
109
+ off += 4;
110
+ boneView.setFloat32(off, bone.bindTranslation[1], true);
111
+ off += 4;
112
+ boneView.setFloat32(off, bone.bindTranslation[2], true);
113
+ off += 4;
114
+ boneView.setInt32(off, bone.appendParentIndex ?? -1, true);
115
+ off += 4;
116
+ boneView.setFloat32(off, bone.appendRatio ?? 0, true);
117
+ off += 4;
118
+ boneView.setUint8(off, bone.appendRotate ? 1 : 0);
119
+ off += 1;
120
+ boneView.setUint8(off, bone.appendMove ? 1 : 0);
121
+ off += 1;
122
+ boneView.setInt32(off, bone.ikTargetIndex ?? -1, true);
123
+ off += 4;
124
+ boneView.setInt32(off, bone.ikIteration ?? 0, true);
125
+ off += 4;
126
+ boneView.setFloat32(off, bone.ikLimitAngle ?? 0, true);
127
+ off += 4;
128
+ // IK links
129
+ const ikLinks = bone.ikLinks || [];
130
+ boneView.setUint32(off, ikLinks.length, true);
131
+ off += 4;
132
+ // Extend buffer for IK links
133
+ const ikLinkSize = ikLinks.length * (4 + 1 + (ikLinks.some((l) => l.hasLimit) ? 24 : 0));
134
+ const extendedBuffer = new Uint8Array(boneSize + ikLinkSize);
135
+ extendedBuffer.set(boneBuffer, 0);
136
+ const extendedView = new DataView(extendedBuffer.buffer);
137
+ for (const link of ikLinks) {
138
+ extendedView.setInt32(off, link.boneIndex, true);
139
+ off += 4;
140
+ extendedView.setUint8(off, link.hasLimit ? 1 : 0);
141
+ off += 1;
142
+ if (link.hasLimit && link.minAngle && link.maxAngle) {
143
+ extendedView.setFloat32(off, link.minAngle.x, true);
144
+ off += 4;
145
+ extendedView.setFloat32(off, link.minAngle.y, true);
146
+ off += 4;
147
+ extendedView.setFloat32(off, link.minAngle.z, true);
148
+ off += 4;
149
+ extendedView.setFloat32(off, link.maxAngle.x, true);
150
+ off += 4;
151
+ extendedView.setFloat32(off, link.maxAngle.y, true);
152
+ off += 4;
153
+ extendedView.setFloat32(off, link.maxAngle.z, true);
154
+ off += 4;
155
+ }
156
+ }
157
+ boneData.push(extendedBuffer);
158
+ totalSize += extendedBuffer.length;
159
+ }
160
+ // Inverse bind matrices
161
+ const invBindBytes = new Uint8Array(inverseBindMatrices.buffer);
162
+ totalSize += invBindBytes.length;
163
+ // Combine all data
164
+ const data = new Uint8Array(totalSize);
165
+ const view = new DataView(data.buffer);
166
+ let offset = 0;
167
+ view.setUint32(offset, bones.length, true);
168
+ offset += 4;
169
+ for (const boneBuffer of boneData) {
170
+ data.set(boneBuffer, offset);
171
+ offset += boneBuffer.length;
172
+ }
173
+ data.set(invBindBytes, offset);
174
+ this.addSection(SectionType.SKELETON, data);
175
+ }
176
+ writeSkinning(model) {
177
+ const skinning = model.getSkinning();
178
+ const joints = skinning.joints;
179
+ const weights = skinning.weights;
180
+ // Layout: joints (Uint16Array), weights (Uint8Array)
181
+ const jointsBytes = new Uint8Array(joints.buffer);
182
+ const weightsBytes = new Uint8Array(weights.buffer);
183
+ const size = 4 + jointsBytes.length + 4 + weightsBytes.length;
184
+ const data = new Uint8Array(size);
185
+ const view = new DataView(data.buffer);
186
+ let offset = 0;
187
+ view.setUint32(offset, joints.length, true);
188
+ offset += 4;
189
+ data.set(jointsBytes, offset);
190
+ offset += jointsBytes.length;
191
+ view.setUint32(offset, weights.length, true);
192
+ offset += 4;
193
+ data.set(weightsBytes, offset);
194
+ this.addSection(SectionType.SKINNING, data);
195
+ }
196
+ writeMorphing(model) {
197
+ const morphing = model.getMorphing();
198
+ const morphs = morphing.morphs;
199
+ const offsetsBuffer = morphing.offsetsBuffer;
200
+ // Layout: morphCount (uint32), morphs[], offsetsBuffer (Float32Array)
201
+ // Each morph: nameLen (uint32), name (utf8), type (uint8), vertexOffsetCount (uint32), vertexOffsets[],
202
+ // groupRefCount (uint32), groupRefs[]
203
+ // Each vertex offset: vertexIndex (uint32), positionOffset (3x float32)
204
+ // Each group ref: morphIndex (int32), ratio (float32)
205
+ const morphData = [];
206
+ let totalSize = 4; // morphCount
207
+ for (const morph of morphs) {
208
+ const nameBytes = new TextEncoder().encode(morph.name);
209
+ let morphSize = 4 + nameBytes.length + 1 + 4; // nameLen + name + type + vertexOffsetCount
210
+ // Vertex offsets
211
+ const vertexOffsets = morph.vertexOffsets || [];
212
+ morphSize += vertexOffsets.length * (4 + 12); // vertexIndex + positionOffset
213
+ // Group references
214
+ const groupRefs = morph.groupReferences || [];
215
+ morphSize += 4 + groupRefs.length * (4 + 4); // groupRefCount + (morphIndex + ratio)
216
+ const morphBuffer = new Uint8Array(morphSize);
217
+ const morphView = new DataView(morphBuffer.buffer);
218
+ let off = 0;
219
+ morphView.setUint32(off, nameBytes.length, true);
220
+ off += 4;
221
+ morphBuffer.set(nameBytes, off);
222
+ off += nameBytes.length;
223
+ morphView.setUint8(off, morph.type);
224
+ off += 1;
225
+ morphView.setUint32(off, vertexOffsets.length, true);
226
+ off += 4;
227
+ for (const vo of vertexOffsets) {
228
+ morphView.setUint32(off, vo.vertexIndex, true);
229
+ off += 4;
230
+ morphView.setFloat32(off, vo.positionOffset[0], true);
231
+ off += 4;
232
+ morphView.setFloat32(off, vo.positionOffset[1], true);
233
+ off += 4;
234
+ morphView.setFloat32(off, vo.positionOffset[2], true);
235
+ off += 4;
236
+ }
237
+ morphView.setUint32(off, groupRefs.length, true);
238
+ off += 4;
239
+ for (const gr of groupRefs) {
240
+ morphView.setInt32(off, gr.morphIndex, true);
241
+ off += 4;
242
+ morphView.setFloat32(off, gr.ratio, true);
243
+ off += 4;
244
+ }
245
+ morphData.push(morphBuffer);
246
+ totalSize += morphBuffer.length;
247
+ }
248
+ // Offsets buffer
249
+ const offsetsBytes = new Uint8Array(offsetsBuffer.buffer);
250
+ totalSize += offsetsBytes.length;
251
+ // Combine all data
252
+ const data = new Uint8Array(totalSize);
253
+ const view = new DataView(data.buffer);
254
+ let offset = 0;
255
+ view.setUint32(offset, morphs.length, true);
256
+ offset += 4;
257
+ for (const morphBuffer of morphData) {
258
+ data.set(morphBuffer, offset);
259
+ offset += morphBuffer.length;
260
+ }
261
+ data.set(offsetsBytes, offset);
262
+ this.addSection(SectionType.MORPHING, data);
263
+ }
264
+ writeMaterials(model) {
265
+ const materials = model.getMaterials();
266
+ // Layout: materialCount (uint32), materials[]
267
+ // Each material: nameLen (uint32), name (utf8), diffuse (4x float32), specular (3x float32),
268
+ // shininess (float32), ambient (3x float32), diffuseTextureIndex (int32),
269
+ // normalTextureIndex (int32), sphereTextureIndex (int32), sphereMode (uint8),
270
+ // toonTextureIndex (int32), edgeFlag (uint8), edgeColor (4x float32),
271
+ // edgeSize (float32), vertexCount (uint32), isEye (uint8), isFace (uint8), isHair (uint8)
272
+ const materialData = [];
273
+ let totalSize = 4; // materialCount
274
+ for (const mat of materials) {
275
+ const nameBytes = new TextEncoder().encode(mat.name);
276
+ const size = 4 + nameBytes.length + 4 * 4 + 3 * 4 + 4 + 3 * 4 + 4 + 4 + 4 + 1 + 4 + 1 + 4 * 4 + 4 + 4 + 1 + 1 + 1;
277
+ const buffer = new Uint8Array(size);
278
+ const view = new DataView(buffer.buffer);
279
+ let off = 0;
280
+ view.setUint32(off, nameBytes.length, true);
281
+ off += 4;
282
+ buffer.set(nameBytes, off);
283
+ off += nameBytes.length;
284
+ view.setFloat32(off, mat.diffuse[0], true);
285
+ off += 4;
286
+ view.setFloat32(off, mat.diffuse[1], true);
287
+ off += 4;
288
+ view.setFloat32(off, mat.diffuse[2], true);
289
+ off += 4;
290
+ view.setFloat32(off, mat.diffuse[3], true);
291
+ off += 4;
292
+ view.setFloat32(off, mat.specular[0], true);
293
+ off += 4;
294
+ view.setFloat32(off, mat.specular[1], true);
295
+ off += 4;
296
+ view.setFloat32(off, mat.specular[2], true);
297
+ off += 4;
298
+ view.setFloat32(off, mat.shininess, true);
299
+ off += 4;
300
+ view.setFloat32(off, mat.ambient[0], true);
301
+ off += 4;
302
+ view.setFloat32(off, mat.ambient[1], true);
303
+ off += 4;
304
+ view.setFloat32(off, mat.ambient[2], true);
305
+ off += 4;
306
+ view.setInt32(off, mat.diffuseTextureIndex, true);
307
+ off += 4;
308
+ view.setInt32(off, mat.normalTextureIndex, true);
309
+ off += 4;
310
+ view.setInt32(off, mat.sphereTextureIndex, true);
311
+ off += 4;
312
+ view.setUint8(off, mat.sphereMode);
313
+ off += 1;
314
+ view.setInt32(off, mat.toonTextureIndex, true);
315
+ off += 4;
316
+ view.setUint8(off, mat.edgeFlag);
317
+ off += 1;
318
+ view.setFloat32(off, mat.edgeColor[0], true);
319
+ off += 4;
320
+ view.setFloat32(off, mat.edgeColor[1], true);
321
+ off += 4;
322
+ view.setFloat32(off, mat.edgeColor[2], true);
323
+ off += 4;
324
+ view.setFloat32(off, mat.edgeColor[3], true);
325
+ off += 4;
326
+ view.setFloat32(off, mat.edgeSize, true);
327
+ off += 4;
328
+ view.setUint32(off, mat.vertexCount, true);
329
+ off += 4;
330
+ view.setUint8(off, mat.isEye ? 1 : 0);
331
+ off += 1;
332
+ view.setUint8(off, mat.isFace ? 1 : 0);
333
+ off += 1;
334
+ view.setUint8(off, mat.isHair ? 1 : 0);
335
+ off += 1;
336
+ materialData.push(buffer);
337
+ totalSize += buffer.length;
338
+ }
339
+ const data = new Uint8Array(totalSize);
340
+ const view = new DataView(data.buffer);
341
+ let offset = 0;
342
+ view.setUint32(offset, materials.length, true);
343
+ offset += 4;
344
+ for (const buffer of materialData) {
345
+ data.set(buffer, offset);
346
+ offset += buffer.length;
347
+ }
348
+ this.addSection(SectionType.MATERIALS, data);
349
+ }
350
+ async writeTextures(model, textureData) {
351
+ const textures = model.getTextures();
352
+ // Layout: textureCount (uint32), textures[]
353
+ // Each texture: nameLen (uint32), name (utf8), pathLen (uint32), path (utf8),
354
+ // width (uint32), height (uint32), format (uint8), dataSize (uint32), data (bytes)
355
+ const textureDataArray = [];
356
+ let totalSize = 4; // textureCount
357
+ for (const texture of textures) {
358
+ const nameBytes = new TextEncoder().encode(texture.name);
359
+ const pathBytes = new TextEncoder().encode(texture.path);
360
+ // Try to get texture data
361
+ const data = textureData.get(texture.path) || textureData.get(texture.name);
362
+ if (!data) {
363
+ console.warn(`[RzmWriter] Texture data not found for: ${texture.path}`);
364
+ // Create empty texture entry
365
+ const size = 4 + nameBytes.length + 4 + pathBytes.length + 4 + 4 + 1 + 4;
366
+ const buffer = new Uint8Array(size);
367
+ const view = new DataView(buffer.buffer);
368
+ let off = 0;
369
+ view.setUint32(off, nameBytes.length, true);
370
+ off += 4;
371
+ buffer.set(nameBytes, off);
372
+ off += nameBytes.length;
373
+ view.setUint32(off, pathBytes.length, true);
374
+ off += 4;
375
+ buffer.set(pathBytes, off);
376
+ off += pathBytes.length;
377
+ view.setUint32(off, 0, true); // width
378
+ off += 4;
379
+ view.setUint32(off, 0, true); // height
380
+ off += 4;
381
+ view.setUint8(off, 0); // format (0 = unknown)
382
+ off += 1;
383
+ view.setUint32(off, 0, true); // dataSize
384
+ off += 4;
385
+ textureDataArray.push(buffer);
386
+ totalSize += buffer.length;
387
+ continue;
388
+ }
389
+ // Decode image to get dimensions
390
+ // For now, we'll store the raw bytes and dimensions will be determined by loader
391
+ // Format: 0 = PNG, 1 = JPEG, 2 = WebP, 3 = raw RGBA
392
+ let format = 0;
393
+ if (texture.path.toLowerCase().endsWith(".jpg") || texture.path.toLowerCase().endsWith(".jpeg")) {
394
+ format = 1;
395
+ }
396
+ else if (texture.path.toLowerCase().endsWith(".webp")) {
397
+ format = 2;
398
+ }
399
+ else if (texture.path.toLowerCase().endsWith(".png")) {
400
+ format = 0;
401
+ }
402
+ else {
403
+ format = 3; // Assume raw RGBA
404
+ }
405
+ // Try to decode to get dimensions
406
+ let width = 0;
407
+ let height = 0;
408
+ try {
409
+ const blob = new Blob([data]);
410
+ const imageBitmap = await createImageBitmap(blob);
411
+ width = imageBitmap.width;
412
+ height = imageBitmap.height;
413
+ imageBitmap.close();
414
+ }
415
+ catch (e) {
416
+ console.warn(`[RzmWriter] Failed to decode image for dimensions: ${texture.path}`, e);
417
+ }
418
+ const dataBytes = new Uint8Array(data);
419
+ const size = 4 + nameBytes.length + 4 + pathBytes.length + 4 + 4 + 1 + 4 + dataBytes.length;
420
+ const buffer = new Uint8Array(size);
421
+ const view = new DataView(buffer.buffer);
422
+ let off = 0;
423
+ view.setUint32(off, nameBytes.length, true);
424
+ off += 4;
425
+ buffer.set(nameBytes, off);
426
+ off += nameBytes.length;
427
+ view.setUint32(off, pathBytes.length, true);
428
+ off += 4;
429
+ buffer.set(pathBytes, off);
430
+ off += pathBytes.length;
431
+ view.setUint32(off, width, true);
432
+ off += 4;
433
+ view.setUint32(off, height, true);
434
+ off += 4;
435
+ view.setUint8(off, format);
436
+ off += 1;
437
+ view.setUint32(off, dataBytes.length, true);
438
+ off += 4;
439
+ buffer.set(dataBytes, off);
440
+ textureDataArray.push(buffer);
441
+ totalSize += buffer.length;
442
+ }
443
+ const data = new Uint8Array(totalSize);
444
+ const view = new DataView(data.buffer);
445
+ let offset = 0;
446
+ view.setUint32(offset, textures.length, true);
447
+ offset += 4;
448
+ for (const buffer of textureDataArray) {
449
+ data.set(buffer, offset);
450
+ offset += buffer.length;
451
+ }
452
+ this.addSection(SectionType.TEXTURES, data);
453
+ }
454
+ writePhysics(model) {
455
+ const rigidbodies = model.getRigidbodies();
456
+ const joints = model.getJoints();
457
+ // Layout: rigidbodyCount (uint32), rigidbodies[], jointCount (uint32), joints[]
458
+ // Each rigidbody: nameLen (uint32), name (utf8), englishNameLen (uint32), englishName (utf8),
459
+ // boneIndex (int32), group (uint8), collisionMask (uint16), shape (uint8),
460
+ // size (3x float32), shapePosition (3x float32), shapeRotation (3x float32),
461
+ // mass (float32), linearDamping (float32), angularDamping (float32),
462
+ // restitution (float32), friction (float32), type (uint8)
463
+ // Each joint: nameLen (uint32), name (utf8), englishNameLen (uint32), englishName (utf8),
464
+ // type (uint8), rigidbodyIndexA (int32), rigidbodyIndexB (int32),
465
+ // position (3x float32), rotation (3x float32),
466
+ // positionMin (3x float32), positionMax (3x float32),
467
+ // rotationMin (3x float32), rotationMax (3x float32),
468
+ // springPosition (3x float32), springRotation (3x float32)
469
+ const rigidbodyData = [];
470
+ let totalSize = 4; // rigidbodyCount
471
+ for (const rb of rigidbodies) {
472
+ const nameBytes = new TextEncoder().encode(rb.name);
473
+ const englishNameBytes = new TextEncoder().encode(rb.englishName);
474
+ // Calculate size: nameLen(4) + name + englishNameLen(4) + englishName + boneIndex(4) + group(1) +
475
+ // collisionMask(2) + shape(1) + size(12) + shapePosition(12) + shapeRotation(12) +
476
+ // mass(4) + linearDamping(4) + angularDamping(4) + restitution(4) + friction(4) + type(1)
477
+ const fixedSize = 4 + 4 + 4 + 1 + 2 + 1 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 1; // 73 bytes
478
+ const size = fixedSize + nameBytes.length + englishNameBytes.length;
479
+ // Create ArrayBuffer directly to ensure correct size
480
+ const arrayBuffer = new ArrayBuffer(size);
481
+ const buffer = new Uint8Array(arrayBuffer);
482
+ const view = new DataView(arrayBuffer);
483
+ let off = 0;
484
+ view.setUint32(off, nameBytes.length, true);
485
+ off += 4;
486
+ buffer.set(nameBytes, off);
487
+ off += nameBytes.length;
488
+ view.setUint32(off, englishNameBytes.length, true);
489
+ off += 4;
490
+ buffer.set(englishNameBytes, off);
491
+ off += englishNameBytes.length;
492
+ view.setInt32(off, rb.boneIndex, true);
493
+ off += 4;
494
+ view.setUint8(off, rb.group);
495
+ off += 1;
496
+ view.setUint16(off, rb.collisionMask, true);
497
+ off += 2;
498
+ view.setUint8(off, rb.shape);
499
+ off += 1;
500
+ view.setFloat32(off, rb.size.x, true);
501
+ off += 4;
502
+ view.setFloat32(off, rb.size.y, true);
503
+ off += 4;
504
+ view.setFloat32(off, rb.size.z, true);
505
+ off += 4;
506
+ // Bounds check before writing
507
+ if (off + 4 > buffer.byteLength) {
508
+ throw new Error(`Rigidbody buffer overflow at shapePosition.x: off=${off}, bufferSize=${buffer.byteLength}, calculatedSize=${size}, name="${rb.name}"`);
509
+ }
510
+ view.setFloat32(off, rb.shapePosition.x, true);
511
+ off += 4;
512
+ if (off + 4 > buffer.byteLength) {
513
+ throw new Error(`Rigidbody buffer overflow at shapePosition.y: off=${off}, bufferSize=${buffer.byteLength}, calculatedSize=${size}, name="${rb.name}"`);
514
+ }
515
+ view.setFloat32(off, rb.shapePosition.y, true);
516
+ off += 4;
517
+ if (off + 4 > buffer.byteLength) {
518
+ throw new Error(`Rigidbody buffer overflow at shapePosition.z: off=${off}, bufferSize=${buffer.byteLength}, calculatedSize=${size}, name="${rb.name}"`);
519
+ }
520
+ view.setFloat32(off, rb.shapePosition.z, true);
521
+ off += 4;
522
+ view.setFloat32(off, rb.shapeRotation.x, true);
523
+ off += 4;
524
+ view.setFloat32(off, rb.shapeRotation.y, true);
525
+ off += 4;
526
+ view.setFloat32(off, rb.shapeRotation.z, true);
527
+ off += 4;
528
+ view.setFloat32(off, rb.mass, true);
529
+ off += 4;
530
+ view.setFloat32(off, rb.linearDamping, true);
531
+ off += 4;
532
+ view.setFloat32(off, rb.angularDamping, true);
533
+ off += 4;
534
+ view.setFloat32(off, rb.restitution, true);
535
+ off += 4;
536
+ view.setFloat32(off, rb.friction, true);
537
+ off += 4;
538
+ view.setUint8(off, rb.type);
539
+ off += 1;
540
+ // Verify buffer size matches written data
541
+ if (off !== size) {
542
+ throw new Error(`Rigidbody buffer size mismatch: expected ${size} bytes, wrote ${off} bytes for "${rb.name}" (nameLen=${nameBytes.length}, englishNameLen=${englishNameBytes.length})`);
543
+ }
544
+ rigidbodyData.push(buffer);
545
+ totalSize += buffer.length;
546
+ }
547
+ const jointData = [];
548
+ totalSize += 4; // jointCount
549
+ for (const joint of joints) {
550
+ const nameBytes = new TextEncoder().encode(joint.name);
551
+ const englishNameBytes = new TextEncoder().encode(joint.englishName);
552
+ // Calculate size: nameLen(4) + name + englishNameLen(4) + englishName + type(1) +
553
+ // rigidbodyIndexA(4) + rigidbodyIndexB(4) + position(12) + rotation(12) +
554
+ // positionMin(12) + positionMax(12) + rotationMin(12) + rotationMax(12) +
555
+ // springPosition(12) + springRotation(12)
556
+ const fixedSize = 4 + 4 + 1 + 4 + 4 + 12 + 12 + 12 + 12 + 12 + 12 + 12 + 12; // 113 bytes
557
+ const size = fixedSize + nameBytes.length + englishNameBytes.length;
558
+ // Create ArrayBuffer directly to ensure correct size
559
+ const arrayBuffer = new ArrayBuffer(size);
560
+ const buffer = new Uint8Array(arrayBuffer);
561
+ const view = new DataView(arrayBuffer);
562
+ let off = 0;
563
+ view.setUint32(off, nameBytes.length, true);
564
+ off += 4;
565
+ buffer.set(nameBytes, off);
566
+ off += nameBytes.length;
567
+ view.setUint32(off, englishNameBytes.length, true);
568
+ off += 4;
569
+ buffer.set(englishNameBytes, off);
570
+ off += englishNameBytes.length;
571
+ view.setUint8(off, joint.type);
572
+ off += 1;
573
+ view.setInt32(off, joint.rigidbodyIndexA, true);
574
+ off += 4;
575
+ view.setInt32(off, joint.rigidbodyIndexB, true);
576
+ off += 4;
577
+ view.setFloat32(off, joint.position.x, true);
578
+ off += 4;
579
+ view.setFloat32(off, joint.position.y, true);
580
+ off += 4;
581
+ view.setFloat32(off, joint.position.z, true);
582
+ off += 4;
583
+ view.setFloat32(off, joint.rotation.x, true);
584
+ off += 4;
585
+ view.setFloat32(off, joint.rotation.y, true);
586
+ off += 4;
587
+ view.setFloat32(off, joint.rotation.z, true);
588
+ off += 4;
589
+ view.setFloat32(off, joint.positionMin.x, true);
590
+ off += 4;
591
+ view.setFloat32(off, joint.positionMin.y, true);
592
+ off += 4;
593
+ view.setFloat32(off, joint.positionMin.z, true);
594
+ off += 4;
595
+ view.setFloat32(off, joint.positionMax.x, true);
596
+ off += 4;
597
+ view.setFloat32(off, joint.positionMax.y, true);
598
+ off += 4;
599
+ view.setFloat32(off, joint.positionMax.z, true);
600
+ off += 4;
601
+ view.setFloat32(off, joint.rotationMin.x, true);
602
+ off += 4;
603
+ view.setFloat32(off, joint.rotationMin.y, true);
604
+ off += 4;
605
+ view.setFloat32(off, joint.rotationMin.z, true);
606
+ off += 4;
607
+ view.setFloat32(off, joint.rotationMax.x, true);
608
+ off += 4;
609
+ view.setFloat32(off, joint.rotationMax.y, true);
610
+ off += 4;
611
+ view.setFloat32(off, joint.rotationMax.z, true);
612
+ off += 4;
613
+ view.setFloat32(off, joint.springPosition.x, true);
614
+ off += 4;
615
+ view.setFloat32(off, joint.springPosition.y, true);
616
+ off += 4;
617
+ view.setFloat32(off, joint.springPosition.z, true);
618
+ off += 4;
619
+ view.setFloat32(off, joint.springRotation.x, true);
620
+ off += 4;
621
+ view.setFloat32(off, joint.springRotation.y, true);
622
+ off += 4;
623
+ view.setFloat32(off, joint.springRotation.z, true);
624
+ off += 4;
625
+ // Verify buffer size matches written data
626
+ if (off !== size) {
627
+ throw new Error(`Joint buffer size mismatch: expected ${size} bytes, wrote ${off} bytes for "${joint.name}"`);
628
+ }
629
+ jointData.push(buffer);
630
+ totalSize += buffer.length;
631
+ }
632
+ const data = new Uint8Array(totalSize);
633
+ const view = new DataView(data.buffer);
634
+ let offset = 0;
635
+ view.setUint32(offset, rigidbodies.length, true);
636
+ offset += 4;
637
+ for (const buffer of rigidbodyData) {
638
+ data.set(buffer, offset);
639
+ offset += buffer.length;
640
+ }
641
+ view.setUint32(offset, joints.length, true);
642
+ offset += 4;
643
+ for (const buffer of jointData) {
644
+ data.set(buffer, offset);
645
+ offset += buffer.length;
646
+ }
647
+ this.addSection(SectionType.PHYSICS, data);
648
+ }
649
+ addSection(type, data) {
650
+ this.sections.push({
651
+ type,
652
+ offset: this.currentOffset,
653
+ size: data.length,
654
+ data,
655
+ });
656
+ this.currentOffset += data.length;
657
+ }
658
+ finalize() {
659
+ const sectionCount = this.sections.length;
660
+ const sectionTableSize = sectionCount * RzmWriter.SECTION_TABLE_ENTRY_SIZE;
661
+ const fileSize = RzmWriter.HEADER_SIZE + sectionTableSize + this.currentOffset;
662
+ const buffer = new ArrayBuffer(fileSize);
663
+ const view = new DataView(buffer);
664
+ const uint8View = new Uint8Array(buffer);
665
+ // Write header
666
+ let offset = 0;
667
+ // Magic
668
+ uint8View[0] = "R".charCodeAt(0);
669
+ uint8View[1] = "Z".charCodeAt(0);
670
+ uint8View[2] = "M".charCodeAt(0);
671
+ uint8View[3] = 0;
672
+ offset += 4;
673
+ view.setUint32(offset, RzmWriter.VERSION, true);
674
+ offset += 4;
675
+ view.setBigUint64(offset, BigInt(fileSize), true);
676
+ offset += 8;
677
+ view.setUint32(offset, sectionCount, true);
678
+ offset += 4;
679
+ // Reserved (16 bytes)
680
+ offset += 16;
681
+ // Write section table
682
+ for (const section of this.sections) {
683
+ view.setUint32(offset, section.type, true);
684
+ offset += 4;
685
+ view.setBigUint64(offset, BigInt(section.offset), true);
686
+ offset += 8;
687
+ view.setBigUint64(offset, BigInt(section.size), true);
688
+ offset += 8;
689
+ }
690
+ // Write section data
691
+ for (const section of this.sections) {
692
+ uint8View.set(section.data, section.offset);
693
+ }
694
+ return buffer;
695
+ }
696
+ }
697
+ // Header size: 4 (magic) + 4 (version) + 8 (file size) + 4 (section count) + 16 (reserved) = 36 bytes
698
+ RzmWriter.HEADER_SIZE = 36;
699
+ RzmWriter.SECTION_TABLE_ENTRY_SIZE = 20; // 4 (type) + 8 (offset) + 8 (size)
700
+ RzmWriter.MAGIC = "RZM\0";
701
+ RzmWriter.VERSION = 1;