From fc407638f5b844f4fd9100bec51791330ed28744 Mon Sep 17 00:00:00 2001 From: Andre Schulz Date: Sun, 25 Feb 2024 23:29:26 +0100 Subject: [PATCH] MVE: Implement Binary glTF 2.0 mesh export Implemented according to official glTF 2.0 spec: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html Tested by validating the resulting GLB with glTF Validator: https://github.khronos.org/glTF-Validator/ Fixes #571 --- libs/mve/mesh_io.cc | 3 + libs/mve/mesh_io_glb.cc | 332 ++++++++++++++++++++++++++++++++++++++++ libs/mve/mesh_io_glb.h | 28 ++++ 3 files changed, 363 insertions(+) create mode 100644 libs/mve/mesh_io_glb.cc create mode 100644 libs/mve/mesh_io_glb.h diff --git a/libs/mve/mesh_io.cc b/libs/mve/mesh_io.cc index 281ace43b..fad451963 100644 --- a/libs/mve/mesh_io.cc +++ b/libs/mve/mesh_io.cc @@ -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 @@ -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"); } diff --git a/libs/mve/mesh_io_glb.cc b/libs/mve/mesh_io_glb.cc new file mode 100644 index 000000000..7631994b4 --- /dev/null +++ b/libs/mve/mesh_io_glb.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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::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::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::max()) + throw std::length_error("JSON chunk exceeds uint32 limit!"); + std::uint32_t const json_chunk_len = static_cast(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::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(&gltf_magic), sizeof(gltf_magic)); + + std::uint32_t const gltf_version = 2; + out.write(reinterpret_cast(&gltf_version), sizeof(gltf_version)); + + std::uint32_t const glb_length_u32 = static_cast(glb_length); + out.write(reinterpret_cast(&glb_length_u32), sizeof(glb_length_u32)); + + /* Write JSON chunk. */ + out.write(reinterpret_cast(&json_chunk_len), sizeof(json_chunk_len)); + + std::uint32_t const json_chunk_type = 0x4E4F534A; /* "JSON" */ + out.write(reinterpret_cast(&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(total_bin_size_bytes); + out.write(reinterpret_cast(&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(&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 diff --git a/libs/mve/mesh_io_glb.h b/libs/mve/mesh_io_glb.h new file mode 100644 index 000000000..2ac82f72b --- /dev/null +++ b/libs/mve/mesh_io_glb.h @@ -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 + +#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 */