Skip to content

Commit

Permalink
Merge pull request #578 from andre-schulz/glb_write_support
Browse files Browse the repository at this point in the history
MVE: Implement Binary glTF 2.0 mesh export
  • Loading branch information
simonfuhrmann authored Dec 23, 2024
2 parents d716f1b + fc40763 commit d4bc2ee
Show file tree
Hide file tree
Showing 3 changed files with 363 additions and 0 deletions.
3 changes: 3 additions & 0 deletions libs/mve/mesh_io.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "mve/mesh_io_pbrt.h"
#include "mve/mesh_io_smf.h"
#include "mve/mesh_io_obj.h"
#include "mve/mesh_io_glb.h"

MVE_NAMESPACE_BEGIN
MVE_GEOM_NAMESPACE_BEGIN
Expand Down Expand Up @@ -60,6 +61,8 @@ save_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename)
save_smf_mesh(mesh, filename);
else if (util::string::right(filename, 4) == ".obj")
save_obj_mesh(mesh, filename);
else if (util::string::right(filename, 4) == ".glb")
save_glb_mesh(mesh, filename);
else
throw std::runtime_error("Extension not recognized");
}
Expand Down
332 changes: 332 additions & 0 deletions libs/mve/mesh_io_glb.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
/*
* Copyright (C) 2024, Andre Schulz
* TU Darmstadt - Graphics, Capture and Massively Parallel Computing
* All rights reserved.
*
* This software may be modified and distributed under the terms
* of the BSD 3-Clause license. See the LICENSE.txt file for details.
*/

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>

#include "mve/mesh_io_glb.h"
#include "mve/mesh_tools.h"
#include "util/exception.h"

MVE_NAMESPACE_BEGIN
MVE_GEOM_NAMESPACE_BEGIN

void
save_glb_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename)
{
if (mesh == nullptr)
throw std::invalid_argument("Null mesh given");
if (filename.empty())
throw std::invalid_argument("No filename given");

TriangleMesh::VertexList const& verts(mesh->get_vertices());
std::size_t const verts_size_bytes = verts.size() * sizeof(verts[0]);

TriangleMesh::ColorList const& vcolors(mesh->get_vertex_colors());
std::size_t const vcolors_size_bytes = vcolors.size() * sizeof(vcolors[0]);

TriangleMesh::NormalList const& vnormals(mesh->get_vertex_normals());
std::size_t const vnormals_size_bytes = vnormals.size() * sizeof(vnormals[0]);

TriangleMesh::TexCoordList const& vtexcoords(mesh->get_vertex_texcoords());
std::size_t const vtexcoords_size_bytes = vtexcoords.size() * sizeof(vtexcoords[0]);

TriangleMesh::FaceList const& faces(mesh->get_faces());
std::size_t const index_buf_size_bytes = faces.size() * sizeof(faces[0]);

if (faces.size() % 3 != 0)
throw std::invalid_argument("Triangle indices not divisible by 3");

std::size_t total_bin_size_bytes = verts_size_bytes + vcolors_size_bytes
+ vnormals_size_bytes + vtexcoords_size_bytes + index_buf_size_bytes;

/* Ensure binary buffer's end is aligned to 4-byte boundary according to
* glTF 2.0 spec section 4.4.3.1.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#chunks-overview
*/
std::size_t const bin_buf_padding_bytes = (4 - total_bin_size_bytes % 4) % 4;
total_bin_size_bytes += bin_buf_padding_bytes;
if (total_bin_size_bytes > std::numeric_limits<std::uint32_t>::max())
throw std::length_error("Binary buffer exceeds uint32 limit!");

/* Create glTF JSON. */
std::stringstream ss;
ss << "{";

/* Write asset.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-asset
*/
ss << "\"asset\":{"
"\"generator\":\"MVE (https://github.com/simonfuhrmann/mve)\","
"\"version\":\"2.0\""
"},";

/* Write buffers.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-buffer
*/
ss << "\"buffers\":["
"{\"byteLength\":" << total_bin_size_bytes << "}"
"],";

/* Write buffer views.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-bufferview
*/
std::uint32_t constexpr GLTF_ARRAY_BUFFER = 34962;
std::uint32_t constexpr GLTF_ELEMENT_ARRAY_BUFFER = 34963;

std::uint32_t buffer_view_id_counter = 0;
std::size_t byte_offset = 0;
ss << "\"bufferViews\":[";

/* Position buffer view. */
std::uint32_t verts_buffer_view_id = buffer_view_id_counter++;
ss << "{"
"\"buffer\":0,"
"\"byteOffset\":" << byte_offset << ","
"\"byteLength\":" << verts_size_bytes << ","
"\"target\":" << GLTF_ARRAY_BUFFER;
ss << "}";
byte_offset += verts_size_bytes;

/* Color buffer view. */
std::uint32_t vcolors_buffer_view_id = 0;
if (!vcolors.empty())
{
vcolors_buffer_view_id = buffer_view_id_counter++;
ss << ",{"
"\"buffer\":0,"
"\"byteOffset\":" << byte_offset << ","
"\"byteLength\":" << vcolors_size_bytes << ","
"\"target\":" << GLTF_ARRAY_BUFFER;
ss << "}";
byte_offset += vcolors_size_bytes;
}

/* Normal buffer view. */
std::uint32_t vnormals_buffer_view_id = 0;
if (!vnormals.empty())
{
vnormals_buffer_view_id = buffer_view_id_counter++;
ss << ",{"
"\"buffer\":0,"
"\"byteOffset\":" << byte_offset << ","
"\"byteLength\":" << vnormals_size_bytes << ","
"\"target\":" << GLTF_ARRAY_BUFFER;
ss << "}";
byte_offset += vnormals_size_bytes;
}

/* Texcoord buffer view. */
std::uint32_t vtexcoords_buffer_view_id = 0;
if (!vtexcoords.empty())
{
vtexcoords_buffer_view_id = buffer_view_id_counter++;
ss << ",{"
"\"buffer\":0,"
"\"byteOffset\":" << byte_offset << ","
"\"byteLength\":" << vtexcoords_size_bytes << ","
"\"target\":" << GLTF_ARRAY_BUFFER;
ss << "}";
byte_offset += vtexcoords_size_bytes;
}

/* Index buffer view. */
std::uint32_t index_buffer_view_id = buffer_view_id_counter++;
ss << ",{"
"\"buffer\":0,"
"\"byteOffset\":" << byte_offset << ","
"\"byteLength\":" << index_buf_size_bytes << ","
"\"target\":" << GLTF_ELEMENT_ARRAY_BUFFER;
ss << "}";

/* End of buffer views array. */
ss << "],";

/* Write accessors.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-accessor
*/
std::uint32_t constexpr GLTF_UNSIGNED_INT = 5125;
std::uint32_t constexpr GLTF_FLOAT = 5126;

std::uint32_t accessor_id_counter = 0;
ss << "\"accessors\":[";

/* Vertex position accessor. */
math::Vec3f aabb_min, aabb_max;
geom::mesh_find_aabb(mesh, aabb_min, aabb_max);
std::uint32_t verts_accessor_id = accessor_id_counter++;
ss << "{"
"\"bufferView\":" << verts_buffer_view_id << ","
"\"componentType\":" << GLTF_FLOAT << ","
"\"count\":" << verts.size() << ",";
ss << std::setprecision(std::numeric_limits<float>::max_digits10);
ss << "\"min\":[" << aabb_min[0] << "," << aabb_min[1] << "," << aabb_min[2] << "],"
"\"max\":[" << aabb_max[0] << "," << aabb_max[1] << "," << aabb_max[2] << "],"
"\"type\":\"VEC3\""
"}";

/* Color accessor. */
std::uint32_t vcolors_accessor_id = 0;
if (!vcolors.empty())
{
vcolors_accessor_id = accessor_id_counter++;
ss << ",{"
"\"bufferView\":" << vcolors_buffer_view_id << ","
"\"componentType\":" << GLTF_FLOAT << ","
"\"count\":" << vcolors.size() << ","
"\"type\":\"VEC4\""
"}";
}

/* Normal accessor. */
std::uint32_t vnormals_accessor_id = 0;
if (!vnormals.empty())
{
vnormals_accessor_id = accessor_id_counter++;
ss << ",{"
"\"bufferView\":" << vnormals_buffer_view_id << ","
"\"componentType\":" << GLTF_FLOAT << ","
"\"count\":" << vnormals.size() << ","
"\"type\":\"VEC3\""
"}";
}

/* Texcoord accessor. */
std::uint32_t vtexcoords_accessor_id = 0;
if (!vtexcoords.empty())
{
vtexcoords_accessor_id = accessor_id_counter++;
ss << ",{"
"\"bufferView\":" << vtexcoords_buffer_view_id << ","
"\"componentType\":" << GLTF_FLOAT << ","
"\"count\":" << vtexcoords.size() << ","
"\"type\":\"VEC2\""
"}";
}

/* Index buffer accessor. */
std::uint32_t index_accessor_id = accessor_id_counter++;
ss << ",{"
"\"bufferView\":" << index_buffer_view_id << ","
"\"componentType\":" << GLTF_UNSIGNED_INT << ","
"\"count\":" << faces.size() << ","
"\"type\":\"SCALAR\""
"}";

/* End of accessors array. */
ss << "],";

/* Write mesh and mesh primitive.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh-primitive
*/
std::uint32_t constexpr GLTF_TRIANGLES = 4;
ss << "\"meshes\":[{"
"\"primitives\":[{"
"\"attributes\":{"
"\"POSITION\":" << verts_accessor_id;
if (!vcolors.empty())
ss << ",\"COLOR_0\":" << vcolors_accessor_id;
if (!vnormals.empty())
ss << ",\"NORMAL\":" << vnormals_accessor_id;
if (!vtexcoords.empty())
ss << ",\"TEXCOORD_0\":" << vtexcoords_accessor_id;
ss << "}," /* End of attributes object. */
"\"indices\":" << index_accessor_id << ","
"\"mode\":" << GLTF_TRIANGLES;
ss << "}]" /* End of primitives array. */
"}],"; /* End of meshes array. */

/* Nodes and scene(s).
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-scene
*/
ss << "\"nodes\":[{\"mesh\":0}],"
"\"scene\":0,"
"\"scenes\":[{\"nodes\":[0]}]";

/* End of glTF JSON. */
ss << "}";

/* Ensure glTF JSON's end is aligned to 4-byte boundary with spaces
* according to glTF 2.0 spec sections 4.4.3.1 [1] and 4.4.3.2 [2].
* [1] https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#chunks-overview
* [2] https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#structured-json-content
*/
std::size_t const json_chunk_padding_bytes = (4 - ss.tellp() % 4) % 4;
if (json_chunk_padding_bytes > 0)
ss.write(" ", json_chunk_padding_bytes);

std::string const json_chunk = ss.str();
if (json_chunk.size() > std::numeric_limits<std::uint32_t>::max())
throw std::length_error("JSON chunk exceeds uint32 limit!");
std::uint32_t const json_chunk_len = static_cast<std::uint32_t>(json_chunk.size());

std::size_t const glb_length = 12 + 8 + json_chunk_len + 8 + total_bin_size_bytes;
if (glb_length > std::numeric_limits<std::uint32_t>::max())
throw std::length_error("GLB length exceeds uint32 limit!");

/* Open output file. */
std::ofstream out(filename, std::ios::binary);
if (!out.good())
throw util::FileException(filename, std::strerror(errno));

/* Write GLB header. */
std::uint32_t const gltf_magic = 0x46546C67; /* "glTF" */
out.write(reinterpret_cast<char const*>(&gltf_magic), sizeof(gltf_magic));

std::uint32_t const gltf_version = 2;
out.write(reinterpret_cast<char const*>(&gltf_version), sizeof(gltf_version));

std::uint32_t const glb_length_u32 = static_cast<std::uint32_t>(glb_length);
out.write(reinterpret_cast<char const*>(&glb_length_u32), sizeof(glb_length_u32));

/* Write JSON chunk. */
out.write(reinterpret_cast<char const*>(&json_chunk_len), sizeof(json_chunk_len));

std::uint32_t const json_chunk_type = 0x4E4F534A; /* "JSON" */
out.write(reinterpret_cast<char const*>(&json_chunk_type), sizeof(json_chunk_type));

out.write(json_chunk.data(), json_chunk_len);

/* Write binary buffer chunk. */
std::uint32_t const total_bin_size_bytes_u32
= static_cast<std::uint32_t>(total_bin_size_bytes);
out.write(reinterpret_cast<char const*>(&total_bin_size_bytes_u32),
sizeof(total_bin_size_bytes_u32));

std::uint32_t const bin_chunk_type = 0x004E4942; /* "BIN\0" */
out.write(reinterpret_cast<char const*>(&bin_chunk_type), sizeof(bin_chunk_type));

out.write((char const*)verts.data(), verts_size_bytes);
if (!vcolors.empty())
out.write((char const*)vcolors.data(), vcolors_size_bytes);
if (!vnormals.empty())
out.write((char const*)vnormals.data(), vnormals_size_bytes);
if (!vtexcoords.empty())
out.write((char const*)vtexcoords.data(), vtexcoords_size_bytes);
out.write((char const*)faces.data(), index_buf_size_bytes);

/* Ensure binary buffer's end is aligned to 4-byte boundary with zeros
* according to glTF 2.0 spec section 4.4.3.3.
* https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-buffer
*/
if (bin_buf_padding_bytes > 0)
out.write("\0\0\0", bin_buf_padding_bytes);
}

MVE_GEOM_NAMESPACE_END
MVE_NAMESPACE_END
28 changes: 28 additions & 0 deletions libs/mve/mesh_io_glb.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2024, Andre Schulz
* TU Darmstadt - Graphics, Capture and Massively Parallel Computing
* All rights reserved.
*
* This software may be modified and distributed under the terms
* of the BSD 3-Clause license. See the LICENSE.txt file for details.
*/

#ifndef MVE_MESH_IO_GLB_HEADER
#define MVE_MESH_IO_GLB_HEADER

#include <string>

#include "mve/defines.h"
#include "mve/mesh.h"

MVE_NAMESPACE_BEGIN
MVE_GEOM_NAMESPACE_BEGIN

/** Saves a triangle mesh as a Binary glTF 2.0 file. */
void
save_glb_mesh (TriangleMesh::ConstPtr mesh, std::string const& filename);

MVE_GEOM_NAMESPACE_END
MVE_NAMESPACE_END

#endif /* MVE_MESH_IO_GLB_HEADER */

0 comments on commit d4bc2ee

Please sign in to comment.