Skip to content

Commit d4bc2ee

Browse files
Merge pull request simonfuhrmann#578 from andre-schulz/glb_write_support
MVE: Implement Binary glTF 2.0 mesh export
2 parents d716f1b + fc40763 commit d4bc2ee

3 files changed

Lines changed: 363 additions & 0 deletions

File tree

libs/mve/mesh_io.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "mve/mesh_io_pbrt.h"
1919
#include "mve/mesh_io_smf.h"
2020
#include "mve/mesh_io_obj.h"
21+
#include "mve/mesh_io_glb.h"
2122

2223
MVE_NAMESPACE_BEGIN
2324
MVE_GEOM_NAMESPACE_BEGIN
@@ -60,6 +61,8 @@ save_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename)
6061
save_smf_mesh(mesh, filename);
6162
else if (util::string::right(filename, 4) == ".obj")
6263
save_obj_mesh(mesh, filename);
64+
else if (util::string::right(filename, 4) == ".glb")
65+
save_glb_mesh(mesh, filename);
6366
else
6467
throw std::runtime_error("Extension not recognized");
6568
}

libs/mve/mesh_io_glb.cc

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*
2+
* Copyright (C) 2024, Andre Schulz
3+
* TU Darmstadt - Graphics, Capture and Massively Parallel Computing
4+
* All rights reserved.
5+
*
6+
* This software may be modified and distributed under the terms
7+
* of the BSD 3-Clause license. See the LICENSE.txt file for details.
8+
*/
9+
10+
#include <cstddef>
11+
#include <cstdint>
12+
#include <cstring>
13+
#include <fstream>
14+
#include <iomanip>
15+
#include <iostream>
16+
#include <limits>
17+
#include <sstream>
18+
#include <stdexcept>
19+
20+
#include "mve/mesh_io_glb.h"
21+
#include "mve/mesh_tools.h"
22+
#include "util/exception.h"
23+
24+
MVE_NAMESPACE_BEGIN
25+
MVE_GEOM_NAMESPACE_BEGIN
26+
27+
void
28+
save_glb_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename)
29+
{
30+
if (mesh == nullptr)
31+
throw std::invalid_argument("Null mesh given");
32+
if (filename.empty())
33+
throw std::invalid_argument("No filename given");
34+
35+
TriangleMesh::VertexList const& verts(mesh->get_vertices());
36+
std::size_t const verts_size_bytes = verts.size() * sizeof(verts[0]);
37+
38+
TriangleMesh::ColorList const& vcolors(mesh->get_vertex_colors());
39+
std::size_t const vcolors_size_bytes = vcolors.size() * sizeof(vcolors[0]);
40+
41+
TriangleMesh::NormalList const& vnormals(mesh->get_vertex_normals());
42+
std::size_t const vnormals_size_bytes = vnormals.size() * sizeof(vnormals[0]);
43+
44+
TriangleMesh::TexCoordList const& vtexcoords(mesh->get_vertex_texcoords());
45+
std::size_t const vtexcoords_size_bytes = vtexcoords.size() * sizeof(vtexcoords[0]);
46+
47+
TriangleMesh::FaceList const& faces(mesh->get_faces());
48+
std::size_t const index_buf_size_bytes = faces.size() * sizeof(faces[0]);
49+
50+
if (faces.size() % 3 != 0)
51+
throw std::invalid_argument("Triangle indices not divisible by 3");
52+
53+
std::size_t total_bin_size_bytes = verts_size_bytes + vcolors_size_bytes
54+
+ vnormals_size_bytes + vtexcoords_size_bytes + index_buf_size_bytes;
55+
56+
/* Ensure binary buffer's end is aligned to 4-byte boundary according to
57+
* glTF 2.0 spec section 4.4.3.1.
58+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#chunks-overview
59+
*/
60+
std::size_t const bin_buf_padding_bytes = (4 - total_bin_size_bytes % 4) % 4;
61+
total_bin_size_bytes += bin_buf_padding_bytes;
62+
if (total_bin_size_bytes > std::numeric_limits<std::uint32_t>::max())
63+
throw std::length_error("Binary buffer exceeds uint32 limit!");
64+
65+
/* Create glTF JSON. */
66+
std::stringstream ss;
67+
ss << "{";
68+
69+
/* Write asset.
70+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-asset
71+
*/
72+
ss << "\"asset\":{"
73+
"\"generator\":\"MVE (https://github.com/simonfuhrmann/mve)\","
74+
"\"version\":\"2.0\""
75+
"},";
76+
77+
/* Write buffers.
78+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-buffer
79+
*/
80+
ss << "\"buffers\":["
81+
"{\"byteLength\":" << total_bin_size_bytes << "}"
82+
"],";
83+
84+
/* Write buffer views.
85+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-bufferview
86+
*/
87+
std::uint32_t constexpr GLTF_ARRAY_BUFFER = 34962;
88+
std::uint32_t constexpr GLTF_ELEMENT_ARRAY_BUFFER = 34963;
89+
90+
std::uint32_t buffer_view_id_counter = 0;
91+
std::size_t byte_offset = 0;
92+
ss << "\"bufferViews\":[";
93+
94+
/* Position buffer view. */
95+
std::uint32_t verts_buffer_view_id = buffer_view_id_counter++;
96+
ss << "{"
97+
"\"buffer\":0,"
98+
"\"byteOffset\":" << byte_offset << ","
99+
"\"byteLength\":" << verts_size_bytes << ","
100+
"\"target\":" << GLTF_ARRAY_BUFFER;
101+
ss << "}";
102+
byte_offset += verts_size_bytes;
103+
104+
/* Color buffer view. */
105+
std::uint32_t vcolors_buffer_view_id = 0;
106+
if (!vcolors.empty())
107+
{
108+
vcolors_buffer_view_id = buffer_view_id_counter++;
109+
ss << ",{"
110+
"\"buffer\":0,"
111+
"\"byteOffset\":" << byte_offset << ","
112+
"\"byteLength\":" << vcolors_size_bytes << ","
113+
"\"target\":" << GLTF_ARRAY_BUFFER;
114+
ss << "}";
115+
byte_offset += vcolors_size_bytes;
116+
}
117+
118+
/* Normal buffer view. */
119+
std::uint32_t vnormals_buffer_view_id = 0;
120+
if (!vnormals.empty())
121+
{
122+
vnormals_buffer_view_id = buffer_view_id_counter++;
123+
ss << ",{"
124+
"\"buffer\":0,"
125+
"\"byteOffset\":" << byte_offset << ","
126+
"\"byteLength\":" << vnormals_size_bytes << ","
127+
"\"target\":" << GLTF_ARRAY_BUFFER;
128+
ss << "}";
129+
byte_offset += vnormals_size_bytes;
130+
}
131+
132+
/* Texcoord buffer view. */
133+
std::uint32_t vtexcoords_buffer_view_id = 0;
134+
if (!vtexcoords.empty())
135+
{
136+
vtexcoords_buffer_view_id = buffer_view_id_counter++;
137+
ss << ",{"
138+
"\"buffer\":0,"
139+
"\"byteOffset\":" << byte_offset << ","
140+
"\"byteLength\":" << vtexcoords_size_bytes << ","
141+
"\"target\":" << GLTF_ARRAY_BUFFER;
142+
ss << "}";
143+
byte_offset += vtexcoords_size_bytes;
144+
}
145+
146+
/* Index buffer view. */
147+
std::uint32_t index_buffer_view_id = buffer_view_id_counter++;
148+
ss << ",{"
149+
"\"buffer\":0,"
150+
"\"byteOffset\":" << byte_offset << ","
151+
"\"byteLength\":" << index_buf_size_bytes << ","
152+
"\"target\":" << GLTF_ELEMENT_ARRAY_BUFFER;
153+
ss << "}";
154+
155+
/* End of buffer views array. */
156+
ss << "],";
157+
158+
/* Write accessors.
159+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-accessor
160+
*/
161+
std::uint32_t constexpr GLTF_UNSIGNED_INT = 5125;
162+
std::uint32_t constexpr GLTF_FLOAT = 5126;
163+
164+
std::uint32_t accessor_id_counter = 0;
165+
ss << "\"accessors\":[";
166+
167+
/* Vertex position accessor. */
168+
math::Vec3f aabb_min, aabb_max;
169+
geom::mesh_find_aabb(mesh, aabb_min, aabb_max);
170+
std::uint32_t verts_accessor_id = accessor_id_counter++;
171+
ss << "{"
172+
"\"bufferView\":" << verts_buffer_view_id << ","
173+
"\"componentType\":" << GLTF_FLOAT << ","
174+
"\"count\":" << verts.size() << ",";
175+
ss << std::setprecision(std::numeric_limits<float>::max_digits10);
176+
ss << "\"min\":[" << aabb_min[0] << "," << aabb_min[1] << "," << aabb_min[2] << "],"
177+
"\"max\":[" << aabb_max[0] << "," << aabb_max[1] << "," << aabb_max[2] << "],"
178+
"\"type\":\"VEC3\""
179+
"}";
180+
181+
/* Color accessor. */
182+
std::uint32_t vcolors_accessor_id = 0;
183+
if (!vcolors.empty())
184+
{
185+
vcolors_accessor_id = accessor_id_counter++;
186+
ss << ",{"
187+
"\"bufferView\":" << vcolors_buffer_view_id << ","
188+
"\"componentType\":" << GLTF_FLOAT << ","
189+
"\"count\":" << vcolors.size() << ","
190+
"\"type\":\"VEC4\""
191+
"}";
192+
}
193+
194+
/* Normal accessor. */
195+
std::uint32_t vnormals_accessor_id = 0;
196+
if (!vnormals.empty())
197+
{
198+
vnormals_accessor_id = accessor_id_counter++;
199+
ss << ",{"
200+
"\"bufferView\":" << vnormals_buffer_view_id << ","
201+
"\"componentType\":" << GLTF_FLOAT << ","
202+
"\"count\":" << vnormals.size() << ","
203+
"\"type\":\"VEC3\""
204+
"}";
205+
}
206+
207+
/* Texcoord accessor. */
208+
std::uint32_t vtexcoords_accessor_id = 0;
209+
if (!vtexcoords.empty())
210+
{
211+
vtexcoords_accessor_id = accessor_id_counter++;
212+
ss << ",{"
213+
"\"bufferView\":" << vtexcoords_buffer_view_id << ","
214+
"\"componentType\":" << GLTF_FLOAT << ","
215+
"\"count\":" << vtexcoords.size() << ","
216+
"\"type\":\"VEC2\""
217+
"}";
218+
}
219+
220+
/* Index buffer accessor. */
221+
std::uint32_t index_accessor_id = accessor_id_counter++;
222+
ss << ",{"
223+
"\"bufferView\":" << index_buffer_view_id << ","
224+
"\"componentType\":" << GLTF_UNSIGNED_INT << ","
225+
"\"count\":" << faces.size() << ","
226+
"\"type\":\"SCALAR\""
227+
"}";
228+
229+
/* End of accessors array. */
230+
ss << "],";
231+
232+
/* Write mesh and mesh primitive.
233+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh
234+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh-primitive
235+
*/
236+
std::uint32_t constexpr GLTF_TRIANGLES = 4;
237+
ss << "\"meshes\":[{"
238+
"\"primitives\":[{"
239+
"\"attributes\":{"
240+
"\"POSITION\":" << verts_accessor_id;
241+
if (!vcolors.empty())
242+
ss << ",\"COLOR_0\":" << vcolors_accessor_id;
243+
if (!vnormals.empty())
244+
ss << ",\"NORMAL\":" << vnormals_accessor_id;
245+
if (!vtexcoords.empty())
246+
ss << ",\"TEXCOORD_0\":" << vtexcoords_accessor_id;
247+
ss << "}," /* End of attributes object. */
248+
"\"indices\":" << index_accessor_id << ","
249+
"\"mode\":" << GLTF_TRIANGLES;
250+
ss << "}]" /* End of primitives array. */
251+
"}],"; /* End of meshes array. */
252+
253+
/* Nodes and scene(s).
254+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node
255+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-scene
256+
*/
257+
ss << "\"nodes\":[{\"mesh\":0}],"
258+
"\"scene\":0,"
259+
"\"scenes\":[{\"nodes\":[0]}]";
260+
261+
/* End of glTF JSON. */
262+
ss << "}";
263+
264+
/* Ensure glTF JSON's end is aligned to 4-byte boundary with spaces
265+
* according to glTF 2.0 spec sections 4.4.3.1 [1] and 4.4.3.2 [2].
266+
* [1] https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#chunks-overview
267+
* [2] https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#structured-json-content
268+
*/
269+
std::size_t const json_chunk_padding_bytes = (4 - ss.tellp() % 4) % 4;
270+
if (json_chunk_padding_bytes > 0)
271+
ss.write(" ", json_chunk_padding_bytes);
272+
273+
std::string const json_chunk = ss.str();
274+
if (json_chunk.size() > std::numeric_limits<std::uint32_t>::max())
275+
throw std::length_error("JSON chunk exceeds uint32 limit!");
276+
std::uint32_t const json_chunk_len = static_cast<std::uint32_t>(json_chunk.size());
277+
278+
std::size_t const glb_length = 12 + 8 + json_chunk_len + 8 + total_bin_size_bytes;
279+
if (glb_length > std::numeric_limits<std::uint32_t>::max())
280+
throw std::length_error("GLB length exceeds uint32 limit!");
281+
282+
/* Open output file. */
283+
std::ofstream out(filename, std::ios::binary);
284+
if (!out.good())
285+
throw util::FileException(filename, std::strerror(errno));
286+
287+
/* Write GLB header. */
288+
std::uint32_t const gltf_magic = 0x46546C67; /* "glTF" */
289+
out.write(reinterpret_cast<char const*>(&gltf_magic), sizeof(gltf_magic));
290+
291+
std::uint32_t const gltf_version = 2;
292+
out.write(reinterpret_cast<char const*>(&gltf_version), sizeof(gltf_version));
293+
294+
std::uint32_t const glb_length_u32 = static_cast<std::uint32_t>(glb_length);
295+
out.write(reinterpret_cast<char const*>(&glb_length_u32), sizeof(glb_length_u32));
296+
297+
/* Write JSON chunk. */
298+
out.write(reinterpret_cast<char const*>(&json_chunk_len), sizeof(json_chunk_len));
299+
300+
std::uint32_t const json_chunk_type = 0x4E4F534A; /* "JSON" */
301+
out.write(reinterpret_cast<char const*>(&json_chunk_type), sizeof(json_chunk_type));
302+
303+
out.write(json_chunk.data(), json_chunk_len);
304+
305+
/* Write binary buffer chunk. */
306+
std::uint32_t const total_bin_size_bytes_u32
307+
= static_cast<std::uint32_t>(total_bin_size_bytes);
308+
out.write(reinterpret_cast<char const*>(&total_bin_size_bytes_u32),
309+
sizeof(total_bin_size_bytes_u32));
310+
311+
std::uint32_t const bin_chunk_type = 0x004E4942; /* "BIN\0" */
312+
out.write(reinterpret_cast<char const*>(&bin_chunk_type), sizeof(bin_chunk_type));
313+
314+
out.write((char const*)verts.data(), verts_size_bytes);
315+
if (!vcolors.empty())
316+
out.write((char const*)vcolors.data(), vcolors_size_bytes);
317+
if (!vnormals.empty())
318+
out.write((char const*)vnormals.data(), vnormals_size_bytes);
319+
if (!vtexcoords.empty())
320+
out.write((char const*)vtexcoords.data(), vtexcoords_size_bytes);
321+
out.write((char const*)faces.data(), index_buf_size_bytes);
322+
323+
/* Ensure binary buffer's end is aligned to 4-byte boundary with zeros
324+
* according to glTF 2.0 spec section 4.4.3.3.
325+
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-buffer
326+
*/
327+
if (bin_buf_padding_bytes > 0)
328+
out.write("\0\0\0", bin_buf_padding_bytes);
329+
}
330+
331+
MVE_GEOM_NAMESPACE_END
332+
MVE_NAMESPACE_END

libs/mve/mesh_io_glb.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (C) 2024, Andre Schulz
3+
* TU Darmstadt - Graphics, Capture and Massively Parallel Computing
4+
* All rights reserved.
5+
*
6+
* This software may be modified and distributed under the terms
7+
* of the BSD 3-Clause license. See the LICENSE.txt file for details.
8+
*/
9+
10+
#ifndef MVE_MESH_IO_GLB_HEADER
11+
#define MVE_MESH_IO_GLB_HEADER
12+
13+
#include <string>
14+
15+
#include "mve/defines.h"
16+
#include "mve/mesh.h"
17+
18+
MVE_NAMESPACE_BEGIN
19+
MVE_GEOM_NAMESPACE_BEGIN
20+
21+
/** Saves a triangle mesh as a Binary glTF 2.0 file. */
22+
void
23+
save_glb_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename);
24+
25+
MVE_GEOM_NAMESPACE_END
26+
MVE_NAMESPACE_END
27+
28+
#endif /* MVE_MESH_IO_GLB_HEADER */

0 commit comments

Comments
 (0)