346 lines
13 KiB
C++
346 lines
13 KiB
C++
#include "vde/foundation/io_gltf.h"
|
||
#include "vde/core/aabb.h"
|
||
#include "vde/curves/tessellation.h"
|
||
#include <fstream>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <sstream>
|
||
#include <iomanip>
|
||
|
||
namespace vde::foundation {
|
||
|
||
// ── Helpers ──────────────────────────────────────────────────────────
|
||
|
||
static std::string vec3_json(double x, double y, double z) {
|
||
std::ostringstream ss;
|
||
ss << std::fixed << "[" << x << "," << y << "," << z << "]";
|
||
return ss.str();
|
||
}
|
||
|
||
static void compute_normals(const mesh::HalfedgeMesh& mesh,
|
||
std::vector<float>& normals) {
|
||
normals.resize(mesh.num_vertices() * 3, 0.0f);
|
||
// For each face, compute face normal and accumulate to vertices
|
||
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
|
||
auto vis = mesh.face_vertices(static_cast<int>(fi));
|
||
if (vis.size() < 3) continue;
|
||
const auto& v0 = mesh.vertex(vis[0]);
|
||
const auto& v1 = mesh.vertex(vis[1]);
|
||
const auto& v2 = mesh.vertex(vis[2]);
|
||
// Cross product for face normal
|
||
float nx = static_cast<float>((v1.y() - v0.y()) * (v2.z() - v0.z()) -
|
||
(v1.z() - v0.z()) * (v2.y() - v0.y()));
|
||
float ny = static_cast<float>((v1.z() - v0.z()) * (v2.x() - v0.x()) -
|
||
(v1.x() - v0.x()) * (v2.z() - v0.z()));
|
||
float nz = static_cast<float>((v1.x() - v0.x()) * (v2.y() - v0.y()) -
|
||
(v1.y() - v0.y()) * (v2.x() - v0.x()));
|
||
for (size_t j = 0; j < 3 && j < vis.size(); ++j) {
|
||
normals[vis[j] * 3 + 0] += nx;
|
||
normals[vis[j] * 3 + 1] += ny;
|
||
normals[vis[j] * 3 + 2] += nz;
|
||
}
|
||
}
|
||
// Normalize
|
||
for (size_t i = 0; i < mesh.num_vertices(); ++i) {
|
||
float len = std::sqrt(normals[i * 3] * normals[i * 3] +
|
||
normals[i * 3 + 1] * normals[i * 3 + 1] +
|
||
normals[i * 3 + 2] * normals[i * 3 + 2]);
|
||
if (len > 1e-10f) {
|
||
normals[i * 3] /= len;
|
||
normals[i * 3 + 1] /= len;
|
||
normals[i * 3 + 2] /= len;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void write_glb_header(std::ostream& out, uint32_t total_length) {
|
||
uint32_t magic = 0x46546C67; // "glTF"
|
||
uint32_t version = 2;
|
||
out.write(reinterpret_cast<const char*>(&magic), 4);
|
||
out.write(reinterpret_cast<const char*>(&version), 4);
|
||
out.write(reinterpret_cast<const char*>(&total_length), 4);
|
||
}
|
||
|
||
static void write_glb_chunk(std::ostream& out, uint32_t chunk_type,
|
||
const std::string& data, uint32_t unpadded_len) {
|
||
out.write(reinterpret_cast<const char*>(&unpadded_len), 4);
|
||
out.write(reinterpret_cast<const char*>(&chunk_type), 4);
|
||
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
||
}
|
||
|
||
// ── GLB binary buffer layout ─────────────────────────────────────────
|
||
|
||
struct GlbBufferLayout {
|
||
size_t positions_offset = 0;
|
||
size_t positions_byte_len = 0;
|
||
size_t normals_offset = 0;
|
||
size_t normals_byte_len = 0;
|
||
size_t indices_offset = 0;
|
||
size_t indices_byte_len = 0;
|
||
size_t total_bin_len = 0;
|
||
};
|
||
|
||
static GlbBufferLayout compute_layout(const mesh::HalfedgeMesh& mesh,
|
||
const GltfOptions& options) {
|
||
GlbBufferLayout layout;
|
||
size_t nv = mesh.num_vertices();
|
||
size_t nf = mesh.num_faces();
|
||
size_t index_count = nf * 3;
|
||
|
||
layout.positions_offset = 0;
|
||
layout.positions_byte_len = nv * 12; // 3 floats * 4 bytes
|
||
|
||
layout.normals_offset = layout.positions_byte_len;
|
||
layout.normals_byte_len = options.include_normals ? nv * 12 : 0;
|
||
|
||
layout.indices_offset = layout.normals_offset + layout.normals_byte_len;
|
||
layout.indices_byte_len = index_count * 4; // uint32 per index
|
||
|
||
layout.total_bin_len = layout.indices_offset + layout.indices_byte_len;
|
||
return layout;
|
||
}
|
||
|
||
static std::string generate_json(const mesh::HalfedgeMesh& mesh,
|
||
const GltfOptions& options,
|
||
const GlbBufferLayout& layout,
|
||
const std::string& bin_uri = "") {
|
||
size_t nv = mesh.num_vertices();
|
||
size_t nf = mesh.num_faces();
|
||
size_t index_count = nf * 3;
|
||
core::AABB3D bounds = mesh.bounds();
|
||
|
||
std::ostringstream ss;
|
||
ss << std::fixed << std::setprecision(6);
|
||
ss << "{\n";
|
||
ss << " \"asset\":{\"version\":\"2.0\",\"generator\":\"ViewDesignEngine\"},\n";
|
||
ss << " \"scene\":0,\n";
|
||
ss << " \"scenes\":[{\"nodes\":[0]}],\n";
|
||
ss << " \"nodes\":[{\"mesh\":0}],\n";
|
||
|
||
// Mesh primitive
|
||
ss << " \"meshes\":[{\"primitives\":[{\"attributes\":{";
|
||
ss << "\"POSITION\":0";
|
||
if (options.include_normals) ss << ",\"NORMAL\":1";
|
||
ss << "},\"indices\":" << (options.include_normals ? 2 : 1) << "}]}],\n";
|
||
|
||
// Buffer
|
||
ss << " \"buffers\":[{";
|
||
if (!bin_uri.empty()) ss << "\"uri\":\"" << bin_uri << "\",";
|
||
ss << "\"byteLength\":" << layout.total_bin_len << "}],\n";
|
||
|
||
// Buffer views
|
||
ss << " \"bufferViews\":[\n";
|
||
ss << " {\"buffer\":0,\"byteOffset\":" << layout.positions_offset
|
||
<< ",\"byteLength\":" << layout.positions_byte_len
|
||
<< ",\"target\":34962},\n";
|
||
if (options.include_normals) {
|
||
ss << " {\"buffer\":0,\"byteOffset\":" << layout.normals_offset
|
||
<< ",\"byteLength\":" << layout.normals_byte_len
|
||
<< ",\"target\":34962},\n";
|
||
}
|
||
ss << " {\"buffer\":0,\"byteOffset\":" << layout.indices_offset
|
||
<< ",\"byteLength\":" << layout.indices_byte_len
|
||
<< ",\"target\":34963}\n";
|
||
ss << " ],\n";
|
||
|
||
// Accessors
|
||
ss << " \"accessors\":[\n";
|
||
ss << " {\"bufferView\":0,\"componentType\":5126,\"count\":" << nv
|
||
<< ",\"type\":\"VEC3\",\"max\":"
|
||
<< vec3_json(bounds.max().x(), bounds.max().y(), bounds.max().z())
|
||
<< ",\"min\":"
|
||
<< vec3_json(bounds.min().x(), bounds.min().y(), bounds.min().z())
|
||
<< "},\n";
|
||
int acc_index = 1;
|
||
if (options.include_normals) {
|
||
ss << " {\"bufferView\":1,\"componentType\":5126,\"count\":" << nv
|
||
<< ",\"type\":\"VEC3\"},\n";
|
||
acc_index = 2;
|
||
}
|
||
ss << " {\"bufferView\":" << (options.include_normals ? 2 : 1)
|
||
<< ",\"componentType\":5125,\"count\":" << index_count
|
||
<< ",\"type\":\"SCALAR\"}\n";
|
||
ss << " ]\n";
|
||
ss << "}";
|
||
|
||
return ss.str();
|
||
}
|
||
|
||
// ── Write binary data ─────────────────────────────────────────────────
|
||
|
||
static void write_binary_data(std::ostream& bin,
|
||
const mesh::HalfedgeMesh& mesh,
|
||
const GltfOptions& options,
|
||
const GlbBufferLayout& layout) {
|
||
size_t nv = mesh.num_vertices();
|
||
size_t nf = mesh.num_faces();
|
||
|
||
// Positions
|
||
for (size_t i = 0; i < nv; ++i) {
|
||
const auto& v = mesh.vertex(i);
|
||
float fx = static_cast<float>(v.x());
|
||
float fy = static_cast<float>(v.y());
|
||
float fz = static_cast<float>(v.z());
|
||
bin.write(reinterpret_cast<const char*>(&fx), 4);
|
||
bin.write(reinterpret_cast<const char*>(&fy), 4);
|
||
bin.write(reinterpret_cast<const char*>(&fz), 4);
|
||
}
|
||
|
||
// Normals
|
||
if (options.include_normals) {
|
||
std::vector<float> normals;
|
||
compute_normals(mesh, normals);
|
||
bin.write(reinterpret_cast<const char*>(normals.data()),
|
||
static_cast<std::streamsize>(normals.size() * sizeof(float)));
|
||
}
|
||
|
||
// Indices
|
||
for (size_t i = 0; i < nf; ++i) {
|
||
auto vis = mesh.face_vertices(static_cast<int>(i));
|
||
for (size_t j = 0; j < std::min(vis.size(), size_t(3)); ++j) {
|
||
uint32_t idx = static_cast<uint32_t>(vis[j]);
|
||
bin.write(reinterpret_cast<const char*>(&idx), 4);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Public API ────────────────────────────────────────────────────────
|
||
|
||
bool write_gltf(const std::string& filepath, const mesh::HalfedgeMesh& mesh,
|
||
const GltfOptions& options) {
|
||
if (mesh.num_vertices() == 0 || mesh.num_faces() == 0) return false;
|
||
|
||
auto layout = compute_layout(mesh, options);
|
||
|
||
if (options.binary) {
|
||
std::string json = generate_json(mesh, options, layout);
|
||
// ── GLB format ──
|
||
uint32_t json_unpadded_len = static_cast<uint32_t>(json.size());
|
||
|
||
// Pad JSON chunk to 4-byte alignment with spaces
|
||
std::string json_padded = json;
|
||
while (json_padded.size() % 4 != 0) json_padded += ' ';
|
||
uint32_t json_padded_len = static_cast<uint32_t>(json_padded.size());
|
||
|
||
// Serialize binary data
|
||
std::string bin_data;
|
||
uint32_t bin_unpadded_len = static_cast<uint32_t>(layout.total_bin_len);
|
||
if (layout.total_bin_len > 0) {
|
||
std::ostringstream bin_stream(std::ios::binary);
|
||
write_binary_data(bin_stream, mesh, options, layout);
|
||
bin_data = bin_stream.str();
|
||
while (bin_data.size() % 4 != 0) bin_data.push_back('\0');
|
||
}
|
||
|
||
// Total = header(12) + json_chunk_header(8) + json_padded + [bin_chunk_header(8) + bin_padded]
|
||
uint32_t total = 12 + 8 + json_padded_len;
|
||
if (!bin_data.empty()) total += 8 + static_cast<uint32_t>(bin_data.size());
|
||
|
||
std::ofstream file(filepath, std::ios::binary);
|
||
if (!file) return false;
|
||
|
||
write_glb_header(file, total);
|
||
write_glb_chunk(file, 0x4E4F534A, json_padded, json_unpadded_len); // "JSON"
|
||
if (!bin_data.empty()) {
|
||
write_glb_chunk(file, 0x004E4942, bin_data, bin_unpadded_len); // "BIN\0"
|
||
}
|
||
return true;
|
||
} else {
|
||
// ── glTF JSON + separate .bin ──
|
||
// Determine bin filename from output path
|
||
std::string dot = ".";
|
||
size_t dot_pos = filepath.rfind(dot);
|
||
std::string binpath;
|
||
std::string bin_basename;
|
||
if (dot_pos != std::string::npos) {
|
||
binpath = filepath.substr(0, dot_pos) + ".bin";
|
||
// Extract just the filename
|
||
size_t slash_pos = binpath.rfind('/');
|
||
bin_basename = (slash_pos != std::string::npos)
|
||
? binpath.substr(slash_pos + 1) : binpath;
|
||
} else {
|
||
binpath = filepath + ".bin";
|
||
bin_basename = binpath;
|
||
}
|
||
|
||
std::string json = generate_json(mesh, options, layout, bin_basename);
|
||
|
||
std::ofstream file(filepath);
|
||
if (!file) return false;
|
||
file << json;
|
||
|
||
std::ofstream bin(binpath, std::ios::binary);
|
||
if (!bin) return false;
|
||
write_binary_data(bin, mesh, options, layout);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
bool write_brep_gltf(const std::string& filepath, const brep::BrepModel& model,
|
||
int tessellation_res) {
|
||
if (model.num_faces() == 0) return false;
|
||
|
||
// Collect all vertices and triangles from tessellated faces
|
||
std::vector<core::Point3D> all_verts;
|
||
std::vector<std::array<int, 3>> all_tris;
|
||
|
||
for (size_t fi = 0; fi < model.num_faces(); ++fi) {
|
||
const auto& face = model.face(static_cast<int>(fi));
|
||
if (face.loops.empty()) continue;
|
||
|
||
// Get the surface for this face
|
||
int surf_id = face.surface_id;
|
||
if (surf_id < 0 || surf_id >= static_cast<int>(model.num_surfaces()))
|
||
continue;
|
||
|
||
const auto& surf = model.surface(surf_id);
|
||
|
||
// Inline tessellation: sample surface uniformly, create quad mesh
|
||
int res = std::max(4, tessellation_res);
|
||
std::vector<core::Point3D> verts;
|
||
std::vector<std::array<int, 3>> tris;
|
||
|
||
// NURBS domain is [0,1]×[0,1]
|
||
for (int j = 0; j <= res; ++j) {
|
||
double v = static_cast<double>(j) / res;
|
||
for (int i = 0; i <= res; ++i) {
|
||
double u = static_cast<double>(i) / res;
|
||
verts.push_back(surf.evaluate(u, v));
|
||
}
|
||
}
|
||
|
||
for (int j = 0; j < res; ++j) {
|
||
for (int i = 0; i < res; ++i) {
|
||
int a = j * (res + 1) + i;
|
||
int b = a + 1;
|
||
int c = a + (res + 1);
|
||
int d = c + 1;
|
||
tris.push_back({a, b, d});
|
||
tris.push_back({a, d, c});
|
||
}
|
||
}
|
||
|
||
// Offset indices
|
||
int base = static_cast<int>(all_verts.size());
|
||
for (const auto& v : verts) all_verts.push_back(v);
|
||
for (const auto& t : tris) {
|
||
all_tris.push_back({t[0] + base, t[1] + base, t[2] + base});
|
||
}
|
||
}
|
||
|
||
if (all_verts.empty() || all_tris.empty()) return false;
|
||
|
||
// Build HalfedgeMesh from tessellated data
|
||
mesh::HalfedgeMesh mesh;
|
||
mesh.build_from_triangles(all_verts, all_tris);
|
||
|
||
// Export as GLB
|
||
GltfOptions opt;
|
||
opt.binary = true;
|
||
opt.include_normals = true;
|
||
return write_gltf(filepath, mesh, opt);
|
||
}
|
||
|
||
} // namespace vde::foundation
|