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.
- package/README.md +66 -66
- package/dist/bezier-interpolate.d.ts +15 -0
- package/dist/bezier-interpolate.d.ts.map +1 -0
- package/dist/bezier-interpolate.js +40 -0
- package/dist/engine.d.ts +3 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +19 -30
- package/dist/engine_ts.d.ts +143 -0
- package/dist/engine_ts.d.ts.map +1 -0
- package/dist/engine_ts.js +1575 -0
- package/dist/ik.d.ts +32 -0
- package/dist/ik.d.ts.map +1 -0
- package/dist/ik.js +337 -0
- package/dist/player.d.ts +64 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +220 -0
- package/dist/pool-scene.d.ts +52 -0
- package/dist/pool-scene.d.ts.map +1 -0
- package/dist/pool-scene.js +1122 -0
- package/dist/pool.d.ts +38 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +422 -0
- package/dist/rzm-converter.d.ts +12 -0
- package/dist/rzm-converter.d.ts.map +1 -0
- package/dist/rzm-converter.js +40 -0
- package/dist/rzm-loader.d.ts +24 -0
- package/dist/rzm-loader.d.ts.map +1 -0
- package/dist/rzm-loader.js +488 -0
- package/dist/rzm-writer.d.ts +27 -0
- package/dist/rzm-writer.d.ts.map +1 -0
- package/dist/rzm-writer.js +701 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +28 -28
- package/src/ik-solver.ts +411 -411
- package/src/math.ts +584 -584
- package/src/physics.ts +742 -742
- package/src/vmd-loader.ts +276 -276
- package/dist/audio.d.ts +0 -29
- package/dist/audio.d.ts.map +0 -1
- package/dist/audio.js +0 -116
- package/dist/particles.d.ts +0 -67
- package/dist/particles.d.ts.map +0 -1
- 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;
|