scimesh 0.3.2
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
gltf_io.h
Go to the documentation of this file.
1
37#pragma once
38
39#include <scimesh/scene.h>
40#include <scimesh/camera.h>
41#include <scimesh/mesh.h>
42#include <scimesh/normals.h>
43#include <string>
44#include <vector>
45#include <cstdint>
46#include <cctype>
47#include <algorithm>
48#include <fstream>
49#include <sstream>
50#include <iomanip>
51#include <stdexcept>
52
53namespace scimesh {
54namespace gltf_io {
55
56namespace detail {
57
58inline std::string json_escape(const std::string &s) {
59 std::ostringstream o;
60 for (char ch : s) {
61 switch (ch) {
62 case '"': o << "\\\""; break;
63 case '\\': o << "\\\\"; break;
64 case '\n': o << "\\n"; break;
65 case '\r': o << "\\r"; break;
66 case '\t': o << "\\t"; break;
67 default:
68 if (static_cast<unsigned char>(ch) < 0x20) {
69 o << "\\u" << std::hex << std::setw(4) << std::setfill('0')
70 << static_cast<int>(ch) << std::dec;
71 } else {
72 o << ch;
73 }
74 }
75 }
76 return o.str();
77}
78
80inline std::string fmt(float v) {
81 std::ostringstream o;
82 o << std::setprecision(9) << static_cast<double>(v);
83 return o.str();
84}
85
86inline std::string join_impl(const std::vector<std::string> &v);
87
89struct PackedMesh {
90 std::vector<float> positions; // x,y,z per vertex
91 std::vector<float> normals; // x,y,z per vertex (empty if absent)
92 std::vector<uint8_t> colors; // r,g,b,a per vertex (empty if absent)
93 std::vector<uint32_t> indices; // 3 per triangle
94 bool has_colors = false; // whether a COLOR_0 attribute is present
95 float base_r = 1.0f, base_g = 1.0f, base_b = 1.0f, base_a = 1.0f; // material base color
96};
97
98inline void append_vertex(PackedMesh &pm, const Vec3 &v,
99 const std::vector<Vec3> *normals, uint32_t idx,
100 const Color *color) {
101 pm.positions.push_back(v.x);
102 pm.positions.push_back(v.y);
103 pm.positions.push_back(v.z);
104 if (normals != nullptr && !normals->empty()) {
105 const Vec3 &n = (*normals)[idx];
106 pm.normals.push_back(n.x);
107 pm.normals.push_back(n.y);
108 pm.normals.push_back(n.z);
109 }
110 if (color != nullptr) {
111 pm.colors.push_back(static_cast<uint8_t>(std::min(1.0f, std::max(0.0f, color->r)) * 255.0f + 0.5f));
112 pm.colors.push_back(static_cast<uint8_t>(std::min(1.0f, std::max(0.0f, color->g)) * 255.0f + 0.5f));
113 pm.colors.push_back(static_cast<uint8_t>(std::min(1.0f, std::max(0.0f, color->b)) * 255.0f + 0.5f));
114 pm.colors.push_back(static_cast<uint8_t>(std::min(1.0f, std::max(0.0f, color->a)) * 255.0f + 0.5f));
115 }
116}
117
118inline PackedMesh pack_mesh(const Mesh &mesh) {
119 PackedMesh pm;
120 pm.base_r = mesh.default_color.r;
121 pm.base_g = mesh.default_color.g;
122 pm.base_b = mesh.default_color.b;
123 pm.base_a = mesh.default_color.a;
124
125 const std::vector<Vec3> *normals = nullptr;
126 std::vector<Vec3> computed_normals;
127 if (mesh.has_normals()) {
128 normals = &mesh.normals;
129 } else if (!mesh.triangles.empty()) {
130 // scimesh computes normals at render time when a mesh lacks them;
131 // export them so standard viewers shade the mesh correctly too.
132 compute_vertex_normals(mesh, computed_normals);
133 normals = &computed_normals;
134 }
135
136 if (mesh.has_face_colors()) {
137 // glTF colors are per-vertex; split vertices so each face carries its
138 // own color (matching scimesh's flat face-coloring).
139 pm.has_colors = true;
140 for (size_t t = 0; t < mesh.triangles.size(); ++t) {
141 const Triangle &tri = mesh.triangles[t];
142 const Color &fc = mesh.face_colors[t];
143 uint32_t base = static_cast<uint32_t>(pm.positions.size() / 3);
144 const uint32_t idxs[3] = {tri.v0, tri.v1, tri.v2};
145 for (int k = 0; k < 3; ++k) {
146 append_vertex(pm, mesh.vertices[idxs[k]], normals, idxs[k], &fc);
147 pm.indices.push_back(base + static_cast<uint32_t>(k));
148 }
149 }
150 } else {
151 pm.has_colors = mesh.has_colors();
152 for (size_t i = 0; i < mesh.vertices.size(); ++i) {
153 const Color *c = nullptr;
154 if (pm.has_colors) c = &mesh.colors[i];
155 append_vertex(pm, mesh.vertices[i], normals, static_cast<uint32_t>(i), c);
156 }
157 for (const Triangle &tri : mesh.triangles) {
158 pm.indices.push_back(tri.v0);
159 pm.indices.push_back(tri.v1);
160 pm.indices.push_back(tri.v2);
161 }
162 }
163 return pm;
164}
165
168 std::string json;
169 std::vector<uint8_t> bin;
170 bool has_camera = false;
171};
172
173inline void pad_bin(std::vector<uint8_t> &bin, size_t align = 4) {
174 while (bin.size() % align != 0) bin.push_back(0);
175}
176
177inline GltfOutput build(const Scene &scene, const Camera *camera) {
178 std::vector<PackedMesh> meshes;
179 meshes.reserve(scene.meshes.size());
180 for (size_t i = 0; i < scene.meshes.size(); ++i) {
181 if (!scene.meshes[i].empty())
182 meshes.push_back(pack_mesh(scene.meshes[i]));
183 }
184
185 GltfOutput out;
186 std::vector<uint8_t> &bin = out.bin;
187
188 // ---- pack the binary buffer (positions, normals, colors, indices) ----
189 std::vector<size_t> pos_off(meshes.size()), nor_off(meshes.size()),
190 col_off(meshes.size()), idx_off(meshes.size());
191 std::vector<size_t> pos_cnt(meshes.size()), idx_cnt(meshes.size());
192 for (size_t i = 0; i < meshes.size(); ++i) {
193 const PackedMesh &m = meshes[i];
194 pad_bin(bin);
195 pos_off[i] = bin.size();
196 for (float f : m.positions) { const uint8_t *p = reinterpret_cast<const uint8_t *>(&f); bin.insert(bin.end(), p, p + 4); }
197 pos_cnt[i] = m.positions.size() / 3;
198
199 if (!m.normals.empty()) {
200 pad_bin(bin);
201 nor_off[i] = bin.size();
202 for (float f : m.normals) { const uint8_t *p = reinterpret_cast<const uint8_t *>(&f); bin.insert(bin.end(), p, p + 4); }
203 }
204 if (!m.colors.empty()) {
205 pad_bin(bin);
206 col_off[i] = bin.size();
207 bin.insert(bin.end(), m.colors.begin(), m.colors.end());
208 }
209 pad_bin(bin);
210 idx_off[i] = bin.size();
211 for (uint32_t v : m.indices) {
212 const uint8_t *p = reinterpret_cast<const uint8_t *>(&v);
213 bin.insert(bin.end(), p, p + 4);
214 }
215 idx_cnt[i] = m.indices.size();
216 }
217
218 // ---- build the JSON document ----
219 std::ostringstream j;
220
221 j << "{\n";
222 j << " \"asset\": {\"version\": \"2.0\", \"generator\": \"scimesh\"},\n";
223
224 // buffers
225 j << " \"buffers\": [{\"byteLength\": " << bin.size();
226 j << "}],\n"; // uri filled in later for .gltf
227
228 // bufferViews + accessors per mesh
229 int next_accessor = 0;
230 std::vector<std::string> accessors_json;
231 std::vector<std::string> views_json;
232 std::vector<std::vector<int>> mesh_accessors; // {POS, NOR, COL, IDX} per mesh
233 for (size_t i = 0; i < meshes.size(); ++i) {
234 const PackedMesh &m = meshes[i];
235 std::vector<int> acc;
236 // POSITION
237 {
238 float mn[3] = {0,0,0}, mx[3] = {0,0,0};
239 for (size_t k = 0; k < m.positions.size(); k += 3) {
240 for (int d = 0; d < 3; ++d) {
241 float v = m.positions[k + d];
242 if (k == 0 || v < mn[d]) mn[d] = v;
243 if (k == 0 || v > mx[d]) mx[d] = v;
244 }
245 }
246 views_json.push_back("{\"buffer\": 0, \"byteOffset\": " + std::to_string(pos_off[i]) +
247 ", \"byteLength\": " + std::to_string(m.positions.size() * 4) + "}");
248 accessors_json.push_back(
249 "{\"bufferView\": " + std::to_string(views_json.size() - 1) +
250 ", \"componentType\": 5126, \"count\": " + std::to_string(pos_cnt[i]) +
251 ", \"type\": \"VEC3\", \"min\": [" + fmt(mn[0]) + ", " + fmt(mn[1]) + ", " + fmt(mn[2]) +
252 "], \"max\": [" + fmt(mx[0]) + ", " + fmt(mx[1]) + ", " + fmt(mx[2]) + "]}");
253 acc.push_back(next_accessor++);
254 }
255 // NORMAL
256 if (!m.normals.empty()) {
257 views_json.push_back("{\"buffer\": 0, \"byteOffset\": " + std::to_string(nor_off[i]) +
258 ", \"byteLength\": " + std::to_string(m.normals.size() * 4) + "}");
259 accessors_json.push_back(
260 "{\"bufferView\": " + std::to_string(views_json.size() - 1) +
261 ", \"componentType\": 5126, \"count\": " + std::to_string(pos_cnt[i]) +
262 ", \"type\": \"VEC3\"}");
263 acc.push_back(next_accessor++);
264 } else {
265 acc.push_back(-1);
266 }
267 // COLOR_0
268 if (!m.colors.empty()) {
269 views_json.push_back("{\"buffer\": 0, \"byteOffset\": " + std::to_string(col_off[i]) +
270 ", \"byteLength\": " + std::to_string(m.colors.size()) + "}");
271 accessors_json.push_back(
272 "{\"bufferView\": " + std::to_string(views_json.size() - 1) +
273 ", \"componentType\": 5121, \"count\": " + std::to_string(pos_cnt[i]) +
274 ", \"normalized\": true, \"type\": \"VEC4\"}");
275 acc.push_back(next_accessor++);
276 } else {
277 acc.push_back(-1);
278 }
279 // INDICES
280 views_json.push_back("{\"buffer\": 0, \"byteOffset\": " + std::to_string(idx_off[i]) +
281 ", \"byteLength\": " + std::to_string(m.indices.size() * 4) + "}");
282 accessors_json.push_back(
283 "{\"bufferView\": " + std::to_string(views_json.size() - 1) +
284 ", \"componentType\": 5125, \"count\": " + std::to_string(idx_cnt[i]) +
285 ", \"type\": \"SCALAR\"}");
286 acc.push_back(next_accessor++);
287 mesh_accessors.push_back(acc);
288 }
289
290 j << " \"bufferViews\": [\n " << join_impl(views_json) << "\n ],\n";
291 j << " \"accessors\": [\n " << join_impl(accessors_json) << "\n ],\n";
292
293 // materials (one per mesh, dedup not required for correctness)
294 std::vector<std::string> materials_json;
295 for (const PackedMesh &m : meshes) {
296 float r = m.has_colors ? 1.0f : m.base_r;
297 float g = m.has_colors ? 1.0f : m.base_g;
298 float b = m.has_colors ? 1.0f : m.base_b;
299 float a = m.has_colors ? 1.0f : m.base_a;
300 std::ostringstream mat;
301 // Fully diffuse (metallic 0 / roughness 1): scimesh is not PBR, and the
302 // glTF default (metallic 1 / roughness 1) renders black under simple
303 // lighting in most viewers.
304 mat << "{\"pbrMetallicRoughness\": {\"baseColorFactor\": ["
305 << fmt(r) << ", " << fmt(g) << ", " << fmt(b) << ", " << fmt(a)
306 << "], \"metallicFactor\": 0.0, \"roughnessFactor\": 1.0}}";
307 materials_json.push_back(mat.str());
308 }
309 j << " \"materials\": [\n " << join_impl(materials_json) << "\n ],\n";
310
311 // meshes
312 std::vector<std::string> meshes_json;
313 for (size_t i = 0; i < meshes.size(); ++i) {
314 const auto &acc = mesh_accessors[i];
315 std::ostringstream attrs;
316 attrs << "\"POSITION\": " << acc[0];
317 if (acc[1] >= 0) attrs << ", \"NORMAL\": " << acc[1];
318 if (acc[2] >= 0) attrs << ", \"COLOR_0\": " << acc[2];
319 std::ostringstream prim;
320 prim << "{\"attributes\": {" << attrs.str() << "}, \"indices\": "
321 << acc[3] << ", \"material\": " << i << "}";
322 meshes_json.push_back("{\"primitives\": [" + prim.str() + "]}");
323 }
324 j << " \"meshes\": [\n " << join_impl(meshes_json) << "\n ],\n";
325
326 // cameras
327 out.has_camera = (camera != nullptr && camera->projection == ProjectionType::PERSPECTIVE);
328 if (out.has_camera) {
329 float yfov = camera->fov_degrees * 3.14159265358979323846f / 180.0f;
330 j << " \"cameras\": [{\"type\": \"perspective\", \"perspective\": {\"yfov\": "
331 << fmt(yfov) << ", \"znear\": 0.1}}],\n";
332 }
333
334 // nodes
335 std::vector<std::string> nodes_json;
336 for (size_t i = 0; i < meshes.size(); ++i) {
337 std::ostringstream n;
338 n << "{\"mesh\": " << i;
339 if (!scene.name(i).empty())
340 n << ", \"name\": \"" << json_escape(scene.name(i)) << "\"";
341 const Mat4 &m = scene.transform(i);
342 bool identity = (m == Mat4(1.0f));
343 if (!identity) {
344 n << ", \"matrix\": [";
345 for (int c = 0; c < 4; ++c)
346 for (int r = 0; r < 4; ++r) {
347 if (c != 0 || r != 0) n << ", ";
348 n << fmt(m[c][r]);
349 }
350 n << "]";
351 }
352 n << "}";
353 nodes_json.push_back(n.str());
354 }
355 if (out.has_camera) {
356 std::ostringstream n;
357 n << "{\"camera\": 0, \"name\": \"camera\"}";
358 nodes_json.push_back(n.str());
359 }
360 j << " \"nodes\": [\n " << join_impl(nodes_json) << "\n ],\n";
361
362 // scenes
363 std::vector<std::string> node_ids;
364 for (size_t i = 0; i < nodes_json.size(); ++i) node_ids.push_back(std::to_string(i));
365 j << " \"scenes\": [{\"nodes\": [" << join_impl(node_ids) << "]}],\n";
366 j << " \"scene\": 0\n";
367 j << "}\n";
368
369 out.json = j.str();
370 return out;
371}
372
373inline std::string join_impl(const std::vector<std::string> &v) {
374 std::ostringstream o;
375 for (size_t i = 0; i < v.size(); ++i) {
376 if (i) o << ", ";
377 o << v[i];
378 }
379 return o.str();
380}
381
382inline std::string basename(const std::string &path) {
383 size_t pos = path.find_last_of("/\\");
384 return pos == std::string::npos ? path : path.substr(pos + 1);
385}
386
387inline std::string dirname(const std::string &path) {
388 size_t pos = path.find_last_of("/\\");
389 return pos == std::string::npos ? std::string() : path.substr(0, pos + 1);
390}
391
392inline std::string stem(const std::string &path) {
393 std::string b = basename(path);
394 size_t dot = b.find_last_of('.');
395 return (dot == std::string::npos) ? b : b.substr(0, dot);
396}
397
398inline void write_bytes(const std::string &path, const std::vector<uint8_t> &data) {
399 std::ofstream f(path, std::ios::binary);
400 if (!f) throw std::runtime_error("glTF: cannot open '" + path + "' for writing");
401 f.write(reinterpret_cast<const char *>(data.data()),
402 static_cast<std::streamsize>(data.size()));
403}
404
405} // namespace detail
406
416inline void write_gltf(const std::string &path, const Scene &scene,
417 const Camera *camera = nullptr) {
418 detail::GltfOutput out = detail::build(scene, camera);
419
420 // Fill in the buffer uri: relative reference to the sibling .bin file.
421 std::string bin_name = detail::stem(path) + ".bin";
422 std::string uri = detail::basename(bin_name);
423 std::string uri_escaped = uri;
424 // The buffer entry is exactly '{"byteLength": N}' — splice in the uri.
425 std::string needle = "\"byteLength\": " + std::to_string(out.bin.size()) + "}";
426 std::string repl = "\"byteLength\": " + std::to_string(out.bin.size()) +
427 ", \"uri\": \"" + uri_escaped + "\"}";
428 size_t pos = out.json.find(needle);
429 if (pos != std::string::npos)
430 out.json.replace(pos, needle.size(), repl);
431
432 std::string bin_path = detail::dirname(path) + bin_name;
433 detail::write_bytes(bin_path, out.bin);
434 std::ofstream jf(path);
435 if (!jf) throw std::runtime_error("glTF: cannot open '" + path + "' for writing");
436 jf << out.json;
437}
438
446inline void write_glb(const std::string &path, const Scene &scene,
447 const Camera *camera = nullptr) {
448 detail::GltfOutput out = detail::build(scene, camera);
449
450 // JSON chunk must be padded to 4 bytes with spaces (0x20).
451 std::vector<uint8_t> json_chunk(out.json.begin(), out.json.end());
452 while (json_chunk.size() % 4 != 0) json_chunk.push_back(0x20);
453 // BIN chunk padded to 4 bytes with zeros.
454 std::vector<uint8_t> bin = out.bin;
455 while (bin.size() % 4 != 0) bin.push_back(0);
456
457 const uint32_t kMagic = 0x46546C67; // "glTF"
458 const uint32_t kVersion = 2;
459 const uint32_t kJsonType = 0x4E4F534A; // "JSON"
460 const uint32_t kBinType = 0x004E4942; // "BIN\0"
461 uint32_t total = 12 + 8 + static_cast<uint32_t>(json_chunk.size()) +
462 8 + static_cast<uint32_t>(bin.size());
463
464 std::vector<uint8_t> out_bytes;
465 auto put_u32 = [&out_bytes](uint32_t v) {
466 out_bytes.push_back(v & 0xFF);
467 out_bytes.push_back((v >> 8) & 0xFF);
468 out_bytes.push_back((v >> 16) & 0xFF);
469 out_bytes.push_back((v >> 24) & 0xFF);
470 };
471 put_u32(kMagic);
472 put_u32(kVersion);
473 put_u32(total);
474 put_u32(static_cast<uint32_t>(json_chunk.size()));
475 put_u32(kJsonType);
476 out_bytes.insert(out_bytes.end(), json_chunk.begin(), json_chunk.end());
477 put_u32(static_cast<uint32_t>(bin.size()));
478 put_u32(kBinType);
479 out_bytes.insert(out_bytes.end(), bin.begin(), bin.end());
480
481 detail::write_bytes(path, out_bytes);
482}
483
488inline void write(const std::string &path, const Scene &scene,
489 const Camera *camera = nullptr) {
490 std::string ext;
491 std::string b = detail::basename(path);
492 size_t dot = b.find_last_of('.');
493 if (dot != std::string::npos)
494 ext = b.substr(dot + 1);
495 for (auto &c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
496 if (ext == "glb")
497 write_glb(path, scene, camera);
498 else
499 write_gltf(path, scene, camera);
500}
501
502} // namespace gltf_io
503} // namespace scimesh
Camera definition and helper functions for view setup.
The Mesh — the central data structure for 3D geometry in scimesh.
void write_bytes(const std::string &path, const std::vector< uint8_t > &data)
Definition gltf_io.h:398
GltfOutput build(const Scene &scene, const Camera *camera)
Definition gltf_io.h:177
std::string json_escape(const std::string &s)
Definition gltf_io.h:58
std::string fmt(float v)
Print a float compactly with enough digits to round-trip a float32.
Definition gltf_io.h:80
void pad_bin(std::vector< uint8_t > &bin, size_t align=4)
Definition gltf_io.h:173
std::string dirname(const std::string &path)
Definition gltf_io.h:387
std::string basename(const std::string &path)
Definition gltf_io.h:382
PackedMesh pack_mesh(const Mesh &mesh)
Definition gltf_io.h:118
void append_vertex(PackedMesh &pm, const Vec3 &v, const std::vector< Vec3 > *normals, uint32_t idx, const Color *color)
Definition gltf_io.h:98
std::string join_impl(const std::vector< std::string > &v)
Definition gltf_io.h:373
std::string stem(const std::string &path)
Definition gltf_io.h:392
void write(const std::string &path, const Scene &scene, const Camera *camera=nullptr)
Write a scene as glTF, choosing the format from the file extension.
Definition gltf_io.h:488
void write_gltf(const std::string &path, const Scene &scene, const Camera *camera=nullptr)
Write a scene as glTF 2.0 (JSON document + external .bin file).
Definition gltf_io.h:416
void write_glb(const std::string &path, const Scene &scene, const Camera *camera=nullptr)
Write a scene as a single binary glTF file (.glb).
Definition gltf_io.h:446
glm::mat4 Mat4
4×4 floating-point matrix.
Definition types.h:65
void compute_vertex_normals(const Mesh &mesh, std::vector< Vec3 > &normals)
Compute per-vertex normals by averaging adjacent face normals.
Definition normals.cpp:5
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
@ PERSPECTIVE
Perspective projection: objects farther away appear smaller.
Compute per-vertex surface normals for lighting.
The Scene — a collection of meshes rendered together.
A virtual camera that defines the viewpoint for rendering.
Definition camera.h:77
ProjectionType projection
Which projection type to use.
Definition camera.h:99
float fov_degrees
Vertical field of view in degrees (perspective only).
Definition camera.h:109
An RGBA color with floating-point components.
Definition types.h:88
float g
Green channel, [0, 1].
Definition types.h:90
float r
Red channel, [0, 1].
Definition types.h:89
float b
Blue channel, [0, 1].
Definition types.h:91
float a
Alpha (opacity) channel, [0, 1]. 1.0 = fully opaque.
Definition types.h:92
A 3D triangle mesh using an indexed face set representation.
Definition mesh.h:76
bool has_face_colors() const
Does the mesh have per-face colors?
Definition mesh.h:182
std::vector< Color > face_colors
Per-face RGBA colors.
Definition mesh.h:115
std::vector< Color > colors
Per-vertex RGBA colors.
Definition mesh.h:107
bool has_colors() const
Does the mesh have per-vertex colors?
Definition mesh.h:177
std::vector< Vec3 > normals
Per-vertex surface normals (unit-length direction vectors).
Definition mesh.h:125
bool has_normals() const
Does the mesh have per-vertex normals?
Definition mesh.h:187
std::vector< Vec3 > vertices
3D vertex positions.
Definition mesh.h:86
std::vector< Triangle > triangles
Triangle index triplets.
Definition mesh.h:94
Color default_color
Fallback color when no per-vertex or per-face color is set.
Definition mesh.h:160
A collection of Mesh objects to be rendered together.
Definition scene.h:57
std::vector< Mesh > meshes
The meshes in this scene, drawn in order.
Definition scene.h:59
const Mat4 & transform(size_t index) const
The placement transform of the mesh at index (identity when unset).
Definition scene.h:101
const std::string & name(size_t index) const
The name of the mesh at index (empty when unset).
Definition scene.h:109
A triangle defined by three vertex indices.
Definition types.h:127
uint32_t v2
Index of the third vertex in Mesh::vertices.
Definition types.h:133
uint32_t v1
Index of the second vertex in Mesh::vertices.
Definition types.h:131
uint32_t v0
Index of the first vertex in Mesh::vertices.
Definition types.h:129
Serialized glTF scene: the JSON document plus the binary buffer.
Definition gltf_io.h:167
std::vector< uint8_t > bin
Definition gltf_io.h:169
One mesh, packed into glTF-ready flat arrays.
Definition gltf_io.h:89
std::vector< uint32_t > indices
Definition gltf_io.h:93
std::vector< float > normals
Definition gltf_io.h:91
std::vector< float > positions
Definition gltf_io.h:90
std::vector< uint8_t > colors
Definition gltf_io.h:92