occt-gltf-addon 0.1.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.
@@ -0,0 +1,545 @@
1
+ #include "gltf_writer.h"
2
+
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <cstdint>
6
+ #include <cstring>
7
+ #include <fstream>
8
+ #include <limits>
9
+ #include <sstream>
10
+ #include <stdexcept>
11
+ #include <string>
12
+ #include <unordered_map>
13
+ #include <vector>
14
+
15
+ #ifdef OCCT_GLTF_WITH_DRACO
16
+ #include <draco/compression/encode.h>
17
+ #include <draco/core/encoder_buffer.h>
18
+ #include <draco/mesh/mesh.h>
19
+ #include <draco/attributes/geometry_attribute.h>
20
+ #endif
21
+
22
+ static inline uint32_t Align4(uint32_t v) { return (v + 3u) & ~3u; }
23
+
24
+ static void AppendU32(std::vector<uint8_t>& out, uint32_t v)
25
+ {
26
+ out.push_back(static_cast<uint8_t>(v & 0xFFu));
27
+ out.push_back(static_cast<uint8_t>((v >> 8) & 0xFFu));
28
+ out.push_back(static_cast<uint8_t>((v >> 16) & 0xFFu));
29
+ out.push_back(static_cast<uint8_t>((v >> 24) & 0xFFu));
30
+ }
31
+
32
+ static void AppendBytes(std::vector<uint8_t>& out, const void* data, size_t size)
33
+ {
34
+ const auto* p = static_cast<const uint8_t*>(data);
35
+ out.insert(out.end(), p, p + size);
36
+ }
37
+
38
+ static void AppendFloat32(std::vector<uint8_t>& out, float f)
39
+ {
40
+ static_assert(sizeof(float) == 4, "float must be 32-bit");
41
+ uint32_t u = 0;
42
+ std::memcpy(&u, &f, 4);
43
+ AppendU32(out, u);
44
+ }
45
+
46
+ static std::array<float, 3> Min3Init()
47
+ {
48
+ return {std::numeric_limits<float>::infinity(),
49
+ std::numeric_limits<float>::infinity(),
50
+ std::numeric_limits<float>::infinity()};
51
+ }
52
+ static std::array<float, 3> Max3Init()
53
+ {
54
+ return {-std::numeric_limits<float>::infinity(),
55
+ -std::numeric_limits<float>::infinity(),
56
+ -std::numeric_limits<float>::infinity()};
57
+ }
58
+
59
+ static bool IsIdentityMatrix(const std::array<float, 16>& m)
60
+ {
61
+ const std::array<float, 16> I = {
62
+ 1,0,0,0,
63
+ 0,1,0,0,
64
+ 0,0,1,0,
65
+ 0,0,0,1
66
+ };
67
+ return m == I;
68
+ }
69
+
70
+ static std::string JsonEscapeUtf8(const std::string& s)
71
+ {
72
+ std::string out;
73
+ out.reserve(s.size() + 16);
74
+ static const char hex[] = "0123456789ABCDEF";
75
+
76
+ for (unsigned char uc : s)
77
+ {
78
+ switch (uc)
79
+ {
80
+ case '\\': out += "\\\\"; break;
81
+ case '"': out += "\\\""; break;
82
+ case '\b': out += "\\b"; break;
83
+ case '\f': out += "\\f"; break;
84
+ case '\n': out += "\\n"; break;
85
+ case '\r': out += "\\r"; break;
86
+ case '\t': out += "\\t"; break;
87
+ default:
88
+ {
89
+ // JSON does not allow unescaped control characters (0x00-0x1F).
90
+ if (uc < 0x20)
91
+ {
92
+ out += "\\u00";
93
+ out.push_back(hex[(uc >> 4) & 0x0F]);
94
+ out.push_back(hex[uc & 0x0F]);
95
+ }
96
+ else
97
+ {
98
+ out.push_back(static_cast<char>(uc));
99
+ }
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+
107
+ struct MeshChunk
108
+ {
109
+ // Uncompressed layout
110
+ uint32_t posOff = 0, posLen = 0;
111
+ uint32_t nrmOff = 0, nrmLen = 0;
112
+
113
+ // Draco-compressed layout
114
+ uint32_t dracoOff = 0, dracoLen = 0;
115
+ int dracoPosId = 0;
116
+ int dracoNrmId = 1;
117
+
118
+ uint32_t vCount = 0;
119
+ std::array<float, 3> minP = Min3Init();
120
+ std::array<float, 3> maxP = Max3Init();
121
+ };
122
+
123
+ static void PadBinTo4(std::vector<uint8_t>& bin)
124
+ {
125
+ const uint32_t pad = Align4(static_cast<uint32_t>(bin.size())) - static_cast<uint32_t>(bin.size());
126
+ for (uint32_t i = 0; i < pad; ++i) bin.push_back(0);
127
+ }
128
+
129
+ #ifdef OCCT_GLTF_WITH_DRACO
130
+ static std::vector<uint8_t> EncodeMeshDraco(const MeshData& mesh, const GltfWriteOptions& opt, int& posAttId, int& nrmAttId)
131
+ {
132
+ const uint32_t vCount = static_cast<uint32_t>(mesh.positions.size() / 3);
133
+ if (vCount == 0)
134
+ {
135
+ posAttId = 0;
136
+ nrmAttId = 1;
137
+ return {};
138
+ }
139
+ if (vCount % 3 != 0)
140
+ {
141
+ throw std::runtime_error("Draco encode expects triangle-list with vertexCount % 3 == 0.");
142
+ }
143
+
144
+ const uint32_t faceCount = vCount / 3;
145
+
146
+ draco::Mesh dmesh;
147
+ dmesh.SetNumFaces(faceCount);
148
+ dmesh.set_num_points(vCount);
149
+
150
+ draco::GeometryAttribute posAtt;
151
+ posAtt.Init(draco::GeometryAttribute::POSITION,
152
+ /*attribute_name*/ nullptr,
153
+ /*num_components*/ 3,
154
+ draco::DT_FLOAT32,
155
+ /*normalized*/ false,
156
+ /*byte_stride*/ sizeof(float) * 3,
157
+ /*byte_offset*/ 0);
158
+ posAttId = dmesh.AddAttribute(posAtt, /*identity_mapping*/ true, vCount);
159
+
160
+ draco::GeometryAttribute nrmAtt;
161
+ nrmAtt.Init(draco::GeometryAttribute::NORMAL,
162
+ /*attribute_name*/ nullptr,
163
+ /*num_components*/ 3,
164
+ draco::DT_FLOAT32,
165
+ /*normalized*/ false,
166
+ /*byte_stride*/ sizeof(float) * 3,
167
+ /*byte_offset*/ 0);
168
+ nrmAttId = dmesh.AddAttribute(nrmAtt, /*identity_mapping*/ true, vCount);
169
+
170
+ auto* posPtr = dmesh.attribute(posAttId);
171
+ auto* nrmPtr = dmesh.attribute(nrmAttId);
172
+ if (posPtr == nullptr || nrmPtr == nullptr)
173
+ {
174
+ throw std::runtime_error("Draco: failed to create attributes.");
175
+ }
176
+
177
+ for (uint32_t i = 0; i < vCount; ++i)
178
+ {
179
+ const draco::PointIndex pi(i);
180
+ posPtr->SetAttributeValue(posPtr->mapped_index(pi), &mesh.positions[i * 3]);
181
+ nrmPtr->SetAttributeValue(nrmPtr->mapped_index(pi), &mesh.normals[i * 3]);
182
+ }
183
+
184
+ for (uint32_t f = 0; f < faceCount; ++f)
185
+ {
186
+ const draco::FaceIndex fi(f);
187
+ draco::Mesh::Face face;
188
+ face[0] = draco::PointIndex(f * 3 + 0);
189
+ face[1] = draco::PointIndex(f * 3 + 1);
190
+ face[2] = draco::PointIndex(f * 3 + 2);
191
+ dmesh.SetFace(fi, face);
192
+ }
193
+
194
+ draco::Encoder encoder;
195
+ const int encSpeed = std::max(0, std::min(10, 10 - opt.dracoCompressionLevel));
196
+ encoder.SetSpeedOptions(encSpeed, /*decode_speed*/ 10);
197
+ encoder.SetEncodingMethod(draco::MESH_EDGEBREAKER_ENCODING);
198
+
199
+ encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, opt.dracoQuantBitsPosition);
200
+ encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, opt.dracoQuantBitsNormal);
201
+
202
+ draco::EncoderBuffer buffer;
203
+ const draco::Status status = encoder.EncodeMeshToBuffer(dmesh, &buffer);
204
+ if (!status.ok())
205
+ {
206
+ throw std::runtime_error(std::string("Draco encode failed: ") + status.error_msg());
207
+ }
208
+
209
+ const uint8_t* p = reinterpret_cast<const uint8_t*>(buffer.data());
210
+ return std::vector<uint8_t>(p, p + buffer.size());
211
+ }
212
+ #endif
213
+
214
+ std::uint64_t WriteGLB(const std::string& outputPath, const SceneData& scene, const GltfWriteOptions& opt)
215
+ {
216
+ std::vector<uint8_t> bin;
217
+ bin.reserve(1024 * 1024);
218
+
219
+ std::vector<MeshChunk> chunks;
220
+ chunks.reserve(scene.meshes.size());
221
+
222
+ if (opt.dracoEnabled)
223
+ {
224
+ #ifndef OCCT_GLTF_WITH_DRACO
225
+ throw std::runtime_error("Draco compression requested, but addon was built without Draco (OCCT_GLTF_WITH_DRACO).");
226
+ #endif
227
+ }
228
+
229
+ // Build BIN and per-mesh metadata.
230
+ for (const auto& mesh : scene.meshes)
231
+ {
232
+ if (mesh.positions.size() % 3 != 0 || mesh.normals.size() != mesh.positions.size())
233
+ {
234
+ throw std::runtime_error("Invalid mesh buffers (positions/normals mismatch).");
235
+ }
236
+
237
+ MeshChunk ch;
238
+ ch.vCount = static_cast<uint32_t>(mesh.positions.size() / 3);
239
+
240
+ // min/max for POSITION
241
+ for (uint32_t i = 0; i < ch.vCount; ++i)
242
+ {
243
+ const float x = mesh.positions[i * 3 + 0];
244
+ const float y = mesh.positions[i * 3 + 1];
245
+ const float z = mesh.positions[i * 3 + 2];
246
+ ch.minP[0] = std::min(ch.minP[0], x); ch.minP[1] = std::min(ch.minP[1], y); ch.minP[2] = std::min(ch.minP[2], z);
247
+ ch.maxP[0] = std::max(ch.maxP[0], x); ch.maxP[1] = std::max(ch.maxP[1], y); ch.maxP[2] = std::max(ch.maxP[2], z);
248
+ }
249
+
250
+ if (!opt.dracoEnabled)
251
+ {
252
+ ch.posOff = static_cast<uint32_t>(bin.size());
253
+ for (float f : mesh.positions) AppendFloat32(bin, f);
254
+ ch.posLen = static_cast<uint32_t>(bin.size()) - ch.posOff;
255
+
256
+ ch.nrmOff = static_cast<uint32_t>(bin.size());
257
+ for (float f : mesh.normals) AppendFloat32(bin, f);
258
+ ch.nrmLen = static_cast<uint32_t>(bin.size()) - ch.nrmOff;
259
+ }
260
+ else
261
+ {
262
+ #ifdef OCCT_GLTF_WITH_DRACO
263
+ PadBinTo4(bin);
264
+ ch.dracoOff = static_cast<uint32_t>(bin.size());
265
+ int posId = 0, nrmId = 1;
266
+ const std::vector<uint8_t> draco = EncodeMeshDraco(mesh, opt, posId, nrmId);
267
+ ch.dracoPosId = posId;
268
+ ch.dracoNrmId = nrmId;
269
+ AppendBytes(bin, draco.data(), draco.size());
270
+ ch.dracoLen = static_cast<uint32_t>(bin.size()) - ch.dracoOff;
271
+ PadBinTo4(bin);
272
+ #endif
273
+ }
274
+
275
+ chunks.push_back(ch);
276
+ }
277
+
278
+ // Build glTF JSON (UTF-8) preserving nodes hierarchy.
279
+ std::ostringstream json;
280
+ json << "{";
281
+ json << "\"asset\":{\"version\":\"2.0\",\"generator\":\"occt-gltf-addon\"},";
282
+ json << "\"scene\":0,";
283
+ if (opt.dracoEnabled)
284
+ {
285
+ json << "\"extensionsUsed\":[\"KHR_draco_mesh_compression\"],";
286
+ json << "\"extensionsRequired\":[\"KHR_draco_mesh_compression\"],";
287
+ }
288
+
289
+ // scenes
290
+ json << "\"scenes\":[{\"nodes\":[";
291
+ for (size_t i = 0; i < scene.roots.size(); ++i)
292
+ {
293
+ if (i) json << ",";
294
+ json << scene.roots[i];
295
+ }
296
+ json << "]}],";
297
+
298
+ // nodes
299
+ json << "\"nodes\":[";
300
+ for (size_t i = 0; i < scene.nodes.size(); ++i)
301
+ {
302
+ const SceneNode& n = scene.nodes[i];
303
+ if (i) json << ",";
304
+
305
+ std::ostringstream node;
306
+ node << "{";
307
+ bool first = true;
308
+
309
+ if (!n.name.empty())
310
+ {
311
+ node << "\"name\":\"" << JsonEscapeUtf8(n.name) << "\"";
312
+ first = false;
313
+ }
314
+ if (n.meshIndex >= 0)
315
+ {
316
+ if (!first) node << ",";
317
+ node << "\"mesh\":" << n.meshIndex;
318
+ first = false;
319
+ }
320
+ if (!n.children.empty())
321
+ {
322
+ if (!first) node << ",";
323
+ node << "\"children\":[";
324
+ for (size_t ci = 0; ci < n.children.size(); ++ci)
325
+ {
326
+ if (ci) node << ",";
327
+ node << n.children[ci];
328
+ }
329
+ node << "]";
330
+ first = false;
331
+ }
332
+ if (!IsIdentityMatrix(n.matrix))
333
+ {
334
+ if (!first) node << ",";
335
+ node << "\"matrix\":[";
336
+ for (int k = 0; k < 16; ++k)
337
+ {
338
+ if (k) node << ",";
339
+ node << n.matrix[(size_t)k];
340
+ }
341
+ node << "]";
342
+ }
343
+
344
+ node << "}";
345
+ json << node.str();
346
+ }
347
+ json << "],";
348
+
349
+ // meshes
350
+ json << "\"meshes\":[";
351
+ for (size_t mi = 0; mi < scene.meshes.size(); ++mi)
352
+ {
353
+ if (mi) json << ",";
354
+ const MeshData& mesh = scene.meshes[mi];
355
+ const uint32_t posAccessor = static_cast<uint32_t>(mi * 2);
356
+ const uint32_t nrmAccessor = static_cast<uint32_t>(mi * 2 + 1);
357
+ json << "{"
358
+ << "\"primitives\":[{"
359
+ << "\"attributes\":{"
360
+ << "\"POSITION\":" << posAccessor << ","
361
+ << "\"NORMAL\":" << nrmAccessor
362
+ << "},\"mode\":4";
363
+ if (mesh.materialIndex >= 0)
364
+ {
365
+ json << ",\"material\":" << mesh.materialIndex;
366
+ }
367
+ if (opt.dracoEnabled)
368
+ {
369
+ const MeshChunk& ch = chunks[mi];
370
+ json << ",\"extensions\":{"
371
+ << "\"KHR_draco_mesh_compression\":{"
372
+ << "\"bufferView\":" << mi
373
+ << ",\"attributes\":{"
374
+ << "\"POSITION\":" << ch.dracoPosId << ","
375
+ << "\"NORMAL\":" << ch.dracoNrmId
376
+ << "}}}";
377
+ }
378
+ json
379
+ << "}]}";
380
+ }
381
+ json << "],";
382
+
383
+ // materials (optional)
384
+ if (!scene.materials.empty())
385
+ {
386
+ json << "\"materials\":[";
387
+ for (size_t i = 0; i < scene.materials.size(); ++i)
388
+ {
389
+ const MaterialData& m = scene.materials[i];
390
+ if (i) json << ",";
391
+ json << "{"
392
+ << "\"pbrMetallicRoughness\":{"
393
+ << "\"baseColorFactor\":["
394
+ << m.baseColorFactor[0] << "," << m.baseColorFactor[1] << "," << m.baseColorFactor[2] << "," << m.baseColorFactor[3]
395
+ << "],"
396
+ << "\"metallicFactor\":" << m.metallicFactor << ","
397
+ << "\"roughnessFactor\":" << m.roughnessFactor
398
+ << "},"
399
+ << "\"doubleSided\":" << (m.doubleSided ? "true" : "false");
400
+ if (m.baseColorFactor[3] < 1.0f)
401
+ {
402
+ json << ",\"alphaMode\":\"BLEND\"";
403
+ }
404
+ json << "}";
405
+ }
406
+ json << "],";
407
+ }
408
+
409
+ // bufferViews
410
+ json << "\"bufferViews\":[";
411
+ if (!opt.dracoEnabled)
412
+ {
413
+ // 2 per mesh (POSITION, NORMAL)
414
+ for (size_t mi = 0; mi < chunks.size(); ++mi)
415
+ {
416
+ const MeshChunk& ch = chunks[mi];
417
+ if (mi) json << ",";
418
+ json << "{"
419
+ << "\"buffer\":0,\"byteOffset\":" << ch.posOff << ",\"byteLength\":" << ch.posLen << ",\"target\":34962"
420
+ << "},"
421
+ << "{"
422
+ << "\"buffer\":0,\"byteOffset\":" << ch.nrmOff << ",\"byteLength\":" << ch.nrmLen << ",\"target\":34962"
423
+ << "}";
424
+ }
425
+ }
426
+ else
427
+ {
428
+ // 1 per mesh (Draco compressed data)
429
+ for (size_t mi = 0; mi < chunks.size(); ++mi)
430
+ {
431
+ const MeshChunk& ch = chunks[mi];
432
+ if (mi) json << ",";
433
+ json << "{"
434
+ << "\"buffer\":0,\"byteOffset\":" << ch.dracoOff << ",\"byteLength\":" << ch.dracoLen
435
+ << "}";
436
+ }
437
+ }
438
+ json << "],";
439
+
440
+ // accessors (2 per mesh)
441
+ json << "\"accessors\":[";
442
+ for (size_t mi = 0; mi < chunks.size(); ++mi)
443
+ {
444
+ const MeshChunk& ch = chunks[mi];
445
+ if (mi) json << ",";
446
+ if (!opt.dracoEnabled)
447
+ {
448
+ const uint32_t posView = static_cast<uint32_t>(mi * 2);
449
+ const uint32_t nrmView = static_cast<uint32_t>(mi * 2 + 1);
450
+ json << "{"
451
+ << "\"bufferView\":" << posView
452
+ << ",\"byteOffset\":0,\"componentType\":5126,\"count\":" << ch.vCount
453
+ << ",\"type\":\"VEC3\""
454
+ << ",\"min\":[" << ch.minP[0] << "," << ch.minP[1] << "," << ch.minP[2] << "]"
455
+ << ",\"max\":[" << ch.maxP[0] << "," << ch.maxP[1] << "," << ch.maxP[2] << "]"
456
+ << "},"
457
+ << "{"
458
+ << "\"bufferView\":" << nrmView
459
+ << ",\"byteOffset\":0,\"componentType\":5126,\"count\":" << ch.vCount
460
+ << ",\"type\":\"VEC3\""
461
+ << "}";
462
+ }
463
+ else
464
+ {
465
+ // For Draco, accessors describe the attribute type/count; data comes from the extension bufferView.
466
+ json << "{"
467
+ << "\"componentType\":5126,\"count\":" << ch.vCount
468
+ << ",\"type\":\"VEC3\""
469
+ << ",\"min\":[" << ch.minP[0] << "," << ch.minP[1] << "," << ch.minP[2] << "]"
470
+ << ",\"max\":[" << ch.maxP[0] << "," << ch.maxP[1] << "," << ch.maxP[2] << "]"
471
+ << "},"
472
+ << "{"
473
+ << "\"componentType\":5126,\"count\":" << ch.vCount
474
+ << ",\"type\":\"VEC3\""
475
+ << "}";
476
+ }
477
+ }
478
+ json << "],";
479
+
480
+ json << "\"buffers\":[{\"byteLength\":" << bin.size() << "}]}";
481
+
482
+ std::string jsonStr = json.str();
483
+
484
+ // JSON chunk padded to 4 bytes with spaces.
485
+ const uint32_t jsonLen = static_cast<uint32_t>(jsonStr.size());
486
+ const uint32_t jsonPaddedLen = Align4(jsonLen);
487
+ jsonStr.resize(jsonPaddedLen, ' ');
488
+
489
+ // BIN chunk padded to 4 bytes with zeros.
490
+ const uint32_t binLen = static_cast<uint32_t>(bin.size());
491
+ const uint32_t binPaddedLen = Align4(binLen);
492
+ bin.resize(binPaddedLen, 0);
493
+
494
+ const uint32_t totalLen = 12u
495
+ + 8u + static_cast<uint32_t>(jsonStr.size())
496
+ + 8u + static_cast<uint32_t>(bin.size());
497
+
498
+ std::vector<uint8_t> glb;
499
+ glb.reserve(totalLen);
500
+
501
+ AppendU32(glb, 0x46546C67u); // 'glTF'
502
+ AppendU32(glb, 2u);
503
+ AppendU32(glb, totalLen);
504
+
505
+ // JSON chunk
506
+ AppendU32(glb, static_cast<uint32_t>(jsonStr.size()));
507
+ AppendU32(glb, 0x4E4F534Au); // 'JSON'
508
+ AppendBytes(glb, jsonStr.data(), jsonStr.size());
509
+
510
+ // BIN chunk
511
+ AppendU32(glb, static_cast<uint32_t>(bin.size()));
512
+ AppendU32(glb, 0x004E4942u); // 'BIN\0'
513
+ AppendBytes(glb, bin.data(), bin.size());
514
+
515
+ if (glb.size() != totalLen)
516
+ {
517
+ throw std::runtime_error("Internal error: GLB size mismatch.");
518
+ }
519
+
520
+ std::ofstream ofs(outputPath, std::ios::binary);
521
+ if (!ofs)
522
+ {
523
+ throw std::runtime_error("Failed to open output file: " + outputPath);
524
+ }
525
+ ofs.write(reinterpret_cast<const char*>(glb.data()), static_cast<std::streamsize>(glb.size()));
526
+ if (!ofs)
527
+ {
528
+ throw std::runtime_error("Failed to write output file: " + outputPath);
529
+ }
530
+
531
+ return static_cast<std::uint64_t>(glb.size());
532
+ }
533
+
534
+ std::uint64_t WriteGLB(const std::string& outputPath, const MeshData& mesh, const GltfWriteOptions& opt)
535
+ {
536
+ SceneData scene;
537
+ scene.meshes.push_back(mesh);
538
+ SceneNode root;
539
+ root.name = "Root";
540
+ root.meshIndex = 0;
541
+ scene.nodes.push_back(root);
542
+ scene.roots.push_back(0);
543
+ return WriteGLB(outputPath, scene, opt);
544
+ }
545
+
@@ -0,0 +1,78 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <string>
5
+ #include <array>
6
+ #include <vector>
7
+
8
+ struct MeshData
9
+ {
10
+ // Unindexed triangle list. Each 3 floats = one vertex position.
11
+ std::vector<float> positions; // xyz xyz xyz ...
12
+ std::vector<float> normals; // xyz xyz xyz ...
13
+
14
+ // glTF material index (SceneData::materials), -1 for default.
15
+ int materialIndex = -1;
16
+ };
17
+
18
+ struct MaterialData
19
+ {
20
+ // glTF PBR baseColorFactor (linear RGBA in [0..1])
21
+ std::array<float, 4> baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f};
22
+ float metallicFactor = 0.0f;
23
+ float roughnessFactor = 1.0f;
24
+ bool doubleSided = true;
25
+ };
26
+
27
+ struct ConvertStats
28
+ {
29
+ // Inputs
30
+ double linearDeflection = 0.1;
31
+ double angularDeflection = 0.5;
32
+
33
+ // STEP / mesh stats
34
+ std::uint64_t facesTotal = 0;
35
+ std::uint64_t facesWithTriangulation = 0;
36
+ std::uint64_t triangles = 0;
37
+ std::uint64_t vertices = 0; // expanded vertices (triangle list)
38
+
39
+ // Output stats
40
+ std::uint64_t glbBytes = 0;
41
+
42
+ // Timings (milliseconds)
43
+ std::uint64_t msRead = 0;
44
+ std::uint64_t msTransfer = 0;
45
+ std::uint64_t msMesh = 0;
46
+ std::uint64_t msExtract = 0;
47
+ std::uint64_t msWrite = 0;
48
+ std::uint64_t msTotal = 0;
49
+
50
+ // Scene graph stats
51
+ std::uint64_t nodes = 0;
52
+ std::uint64_t meshes = 0;
53
+
54
+ // Diagnostics
55
+ std::uint64_t skippedMeshesNoTriangulation = 0;
56
+ std::uint64_t skippedSmallParts = 0;
57
+ };
58
+
59
+ struct SceneNode
60
+ {
61
+ std::string name; // UTF-8 (supports Chinese)
62
+ std::vector<int> children; // indices into SceneData::nodes
63
+ int meshIndex = -1; // index into SceneData::meshes, -1 for none
64
+ std::array<float, 16> matrix = { // glTF column-major 4x4
65
+ 1,0,0,0,
66
+ 0,1,0,0,
67
+ 0,0,1,0,
68
+ 0,0,0,1
69
+ };
70
+ };
71
+
72
+ struct SceneData
73
+ {
74
+ std::vector<MeshData> meshes;
75
+ std::vector<MaterialData> materials;
76
+ std::vector<SceneNode> nodes;
77
+ std::vector<int> roots; // root node indices
78
+ };