Files
ViewDesignEngine/src/foundation/io_gltf.cpp
T
茂之钳 600c7e6158
CI / Build & Test (push) Failing after 1m33s
CI / Release Build (push) Failing after 31s
fix: use [0,1]x[0,1] NURBS domain
2026-07-24 09:13:43 +00:00

346 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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