499 lines
19 KiB
C++
499 lines
19 KiB
C++
#ifndef MODELLOADER_H
|
|
#define MODELLOADER_H
|
|
|
|
// CPUMesh loader with tinygltf
|
|
// - Loads first scene (or default scene), iterates nodes, extracts mesh primitives.
|
|
// - Handles POSITION/NORMAL/TANGENT/TEXCOORD_0 and indices.
|
|
// - Computes minPos/maxPos.
|
|
// - Can return per-primitive meshes, or merged-per-gltf-mesh meshes.
|
|
// - IMPORTANT FIX: Applies node transforms (TRS / matrix) so models don't appear flipped/rotated.
|
|
//
|
|
// NOTE: Defining TINYGLTF_IMPLEMENTATION in a header can cause ODR / multiple-definition issues
|
|
// if included in more than one translation unit. Best practice: put the TINYGLTF_IMPLEMENTATION
|
|
// define in exactly one .cpp. I keep it here because your original file did, but you may want
|
|
// to move it.
|
|
|
|
#define TINYGLTF_IMPLEMENTATION
|
|
#define TINYGLTF_NO_STB_IMAGE_WRITE
|
|
#include <tiny_gltf.h>
|
|
|
|
#include <glm/glm.hpp>
|
|
#include <glm/gtc/matrix_transform.hpp> // translate/scale
|
|
#include <glm/gtc/quaternion.hpp> // quat
|
|
#include <glm/gtx/quaternion.hpp> // mat4_cast
|
|
|
|
#include <destrum/Graphics/Resources/Mesh.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <stdexcept>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
|
|
namespace ModelLoader {
|
|
|
|
// -------------------- helpers --------------------
|
|
|
|
static size_t ComponentSizeInBytes(int componentType) {
|
|
switch (componentType) {
|
|
case TINYGLTF_COMPONENT_TYPE_BYTE: return 1;
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: return 1;
|
|
case TINYGLTF_COMPONENT_TYPE_SHORT: return 2;
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: return 2;
|
|
case TINYGLTF_COMPONENT_TYPE_INT: return 4;
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: return 4;
|
|
case TINYGLTF_COMPONENT_TYPE_FLOAT: return 4;
|
|
case TINYGLTF_COMPONENT_TYPE_DOUBLE: return 8;
|
|
default: throw std::runtime_error("Unknown glTF component type");
|
|
}
|
|
}
|
|
|
|
static int TypeNumComponents(int type) {
|
|
switch (type) {
|
|
case TINYGLTF_TYPE_SCALAR: return 1;
|
|
case TINYGLTF_TYPE_VEC2: return 2;
|
|
case TINYGLTF_TYPE_VEC3: return 3;
|
|
case TINYGLTF_TYPE_VEC4: return 4;
|
|
case TINYGLTF_TYPE_MAT2: return 4;
|
|
case TINYGLTF_TYPE_MAT3: return 9;
|
|
case TINYGLTF_TYPE_MAT4: return 16;
|
|
default: throw std::runtime_error("Unknown glTF type");
|
|
}
|
|
}
|
|
|
|
// Returns pointer to the first element, plus stride in bytes.
|
|
static std::pair<const std::uint8_t*, size_t>
|
|
GetAccessorDataPtrAndStride(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
|
|
if (accessor.bufferView < 0) {
|
|
throw std::runtime_error("Accessor has no bufferView");
|
|
}
|
|
const tinygltf::BufferView& bv = model.bufferViews.at(accessor.bufferView);
|
|
const tinygltf::Buffer& buf = model.buffers.at(bv.buffer);
|
|
|
|
const size_t componentSize = ComponentSizeInBytes(accessor.componentType);
|
|
const int numComps = TypeNumComponents(accessor.type);
|
|
const size_t packedStride = componentSize * size_t(numComps);
|
|
|
|
size_t stride = bv.byteStride ? size_t(bv.byteStride) : packedStride;
|
|
|
|
const size_t start = size_t(bv.byteOffset) + size_t(accessor.byteOffset);
|
|
// Conservative bounds check; don't hard-fail on weird stride but catches obvious issues.
|
|
if (start > buf.data.size()) {
|
|
throw std::runtime_error("Accessor start is out of buffer bounds");
|
|
}
|
|
|
|
const std::uint8_t* ptr = buf.data.data() + start;
|
|
return {ptr, stride};
|
|
}
|
|
|
|
template <typename T>
|
|
static T ReadAs(const std::uint8_t* p) {
|
|
T v{};
|
|
std::memcpy(&v, p, sizeof(T));
|
|
return v;
|
|
}
|
|
|
|
static glm::vec3 ReadVec3Float(const std::uint8_t* base, size_t stride, size_t i) {
|
|
const std::uint8_t* p = base + i * stride;
|
|
const float x = ReadAs<float>(p + 0);
|
|
const float y = ReadAs<float>(p + 4);
|
|
const float z = ReadAs<float>(p + 8);
|
|
return glm::vec3{x, y, z};
|
|
}
|
|
|
|
static glm::vec2 ReadVec2Float(const std::uint8_t* base, size_t stride, size_t i) {
|
|
const std::uint8_t* p = base + i * stride;
|
|
const float x = ReadAs<float>(p + 0);
|
|
const float y = ReadAs<float>(p + 4);
|
|
return glm::vec2{x, y};
|
|
}
|
|
|
|
static glm::vec4 ReadVec4Float(const std::uint8_t* base, size_t stride, size_t i) {
|
|
const std::uint8_t* p = base + i * stride;
|
|
const float x = ReadAs<float>(p + 0);
|
|
const float y = ReadAs<float>(p + 4);
|
|
const float z = ReadAs<float>(p + 8);
|
|
const float w = ReadAs<float>(p + 12);
|
|
return glm::vec4{x, y, z, w};
|
|
}
|
|
|
|
static std::uint32_t ReadIndexAsU32(const tinygltf::Model& model, const tinygltf::Accessor& accessor, size_t i) {
|
|
auto [base, stride] = GetAccessorDataPtrAndStride(model, accessor);
|
|
const std::uint8_t* p = base + i * stride;
|
|
|
|
switch (accessor.componentType) {
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
|
|
return static_cast<std::uint32_t>(ReadAs<std::uint8_t>(p));
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
|
|
return static_cast<std::uint32_t>(ReadAs<std::uint16_t>(p));
|
|
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
|
|
return static_cast<std::uint32_t>(ReadAs<std::uint32_t>(p));
|
|
default:
|
|
throw std::runtime_error("Unsupported index componentType (expected u8/u16/u32)");
|
|
}
|
|
}
|
|
|
|
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
|
|
mn.x = std::min(mn.x, p.x);
|
|
mn.y = std::min(mn.y, p.y);
|
|
mn.z = std::min(mn.z, p.z);
|
|
mx.x = std::max(mx.x, p.x);
|
|
mx.y = std::max(mx.y, p.y);
|
|
mx.z = std::max(mx.z, p.z);
|
|
}
|
|
|
|
// Build a node local matrix from either node.matrix or TRS.
|
|
// glTF stores rotation as quaternion [x,y,z,w].
|
|
static glm::mat4 NodeLocalMatrix(const tinygltf::Node& node) {
|
|
if (node.matrix.size() == 16) {
|
|
glm::mat4 m(1.0f);
|
|
// glTF is column-major; GLM is column-major -> fill columns.
|
|
for (int c = 0; c < 4; ++c)
|
|
for (int r = 0; r < 4; ++r)
|
|
m[c][r] = static_cast<float>(node.matrix[c * 4 + r]);
|
|
return m;
|
|
}
|
|
|
|
glm::vec3 t(0.0f);
|
|
if (node.translation.size() == 3) {
|
|
t = glm::vec3(
|
|
static_cast<float>(node.translation[0]),
|
|
static_cast<float>(node.translation[1]),
|
|
static_cast<float>(node.translation[2])
|
|
);
|
|
}
|
|
|
|
glm::quat q(1.0f, 0.0f, 0.0f, 0.0f); // w,x,y,z
|
|
if (node.rotation.size() == 4) {
|
|
q = glm::quat(
|
|
static_cast<float>(node.rotation[3]), // w
|
|
static_cast<float>(node.rotation[0]), // x
|
|
static_cast<float>(node.rotation[1]), // y
|
|
static_cast<float>(node.rotation[2]) // z
|
|
);
|
|
}
|
|
|
|
glm::vec3 s(1.0f);
|
|
if (node.scale.size() == 3) {
|
|
s = glm::vec3(
|
|
static_cast<float>(node.scale[0]),
|
|
static_cast<float>(node.scale[1]),
|
|
static_cast<float>(node.scale[2])
|
|
);
|
|
}
|
|
|
|
const glm::mat4 T = glm::translate(glm::mat4(1.0f), t);
|
|
const glm::mat4 R = glm::toMat4(q);
|
|
const glm::mat4 S = glm::scale(glm::mat4(1.0f), s);
|
|
return T * R * S;
|
|
}
|
|
|
|
// -------------------- primitive extraction --------------------
|
|
|
|
static CPUMesh LoadPrimitiveIntoCPUMesh(const tinygltf::Model& model,
|
|
const tinygltf::Primitive& prim,
|
|
const std::string& nameForMesh,
|
|
const glm::mat4& world) {
|
|
CPUMesh out{};
|
|
out.name = nameForMesh;
|
|
|
|
// POSITION is required
|
|
auto itPos = prim.attributes.find("POSITION");
|
|
if (itPos == prim.attributes.end()) {
|
|
throw std::runtime_error("Primitive has no POSITION attribute");
|
|
}
|
|
|
|
const tinygltf::Accessor& accPos = model.accessors.at(itPos->second);
|
|
if (accPos.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accPos.type != TINYGLTF_TYPE_VEC3) {
|
|
throw std::runtime_error("POSITION must be VEC3 float for this loader");
|
|
}
|
|
|
|
const size_t vertexCount = size_t(accPos.count);
|
|
|
|
// Optional attributes
|
|
const tinygltf::Accessor* accNormal = nullptr;
|
|
const tinygltf::Accessor* accTangent = nullptr;
|
|
const tinygltf::Accessor* accUV0 = nullptr;
|
|
|
|
if (auto it = prim.attributes.find("NORMAL"); it != prim.attributes.end()) {
|
|
accNormal = &model.accessors.at(it->second);
|
|
if (accNormal->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accNormal->type != TINYGLTF_TYPE_VEC3)
|
|
accNormal = nullptr;
|
|
}
|
|
if (auto it = prim.attributes.find("TANGENT"); it != prim.attributes.end()) {
|
|
accTangent = &model.accessors.at(it->second);
|
|
if (accTangent->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accTangent->type != TINYGLTF_TYPE_VEC4)
|
|
accTangent = nullptr;
|
|
}
|
|
if (auto it = prim.attributes.find("TEXCOORD_0"); it != prim.attributes.end()) {
|
|
accUV0 = &model.accessors.at(it->second);
|
|
if (accUV0->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accUV0->type != TINYGLTF_TYPE_VEC2)
|
|
accUV0 = nullptr;
|
|
}
|
|
|
|
// Prepare pointers/strides
|
|
auto [posBase, posStride] = GetAccessorDataPtrAndStride(model, accPos);
|
|
|
|
const std::uint8_t* nrmBase = nullptr;
|
|
size_t nrmStride = 0;
|
|
const std::uint8_t* tanBase = nullptr;
|
|
size_t tanStride = 0;
|
|
const std::uint8_t* uvBase = nullptr;
|
|
size_t uvStride = 0;
|
|
|
|
if (accNormal) {
|
|
auto p = GetAccessorDataPtrAndStride(model, *accNormal);
|
|
nrmBase = p.first;
|
|
nrmStride = p.second;
|
|
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr;
|
|
}
|
|
if (accTangent) {
|
|
auto p = GetAccessorDataPtrAndStride(model, *accTangent);
|
|
tanBase = p.first;
|
|
tanStride = p.second;
|
|
if (size_t(accTangent->count) != vertexCount) accTangent = nullptr;
|
|
}
|
|
if (accUV0) {
|
|
auto p = GetAccessorDataPtrAndStride(model, *accUV0);
|
|
uvBase = p.first;
|
|
uvStride = p.second;
|
|
if (size_t(accUV0->count) != vertexCount) accUV0 = nullptr;
|
|
}
|
|
|
|
// Allocate vertices
|
|
out.vertices.resize(vertexCount);
|
|
|
|
// Bounds init (in world space, because we transform positions)
|
|
glm::vec3 mn{std::numeric_limits<float>::infinity()};
|
|
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
|
|
|
// Normal matrix
|
|
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
|
const glm::mat3 tanMat = glm::mat3(world);
|
|
|
|
for (size_t i = 0; i < vertexCount; ++i) {
|
|
CPUMesh::Vertex v{};
|
|
|
|
// Position
|
|
const glm::vec3 pLocal = ReadVec3Float(posBase, posStride, i);
|
|
v.position = glm::vec3(world * glm::vec4(pLocal, 1.0f));
|
|
UpdateBounds(mn, mx, v.position);
|
|
|
|
// Normal
|
|
if (accNormal) {
|
|
const glm::vec3 nLocal = ReadVec3Float(nrmBase, nrmStride, i);
|
|
v.normal = glm::normalize(nrmMat * nLocal);
|
|
} else {
|
|
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
|
}
|
|
|
|
// UV
|
|
if (accUV0) {
|
|
glm::vec2 uv = ReadVec2Float(uvBase, uvStride, i);
|
|
v.uv_x = uv.x;
|
|
v.uv_y = uv.y;
|
|
} else {
|
|
v.uv_x = 0.0f;
|
|
v.uv_y = 0.0f;
|
|
}
|
|
|
|
// Tangent
|
|
if (accTangent) {
|
|
glm::vec4 t = ReadVec4Float(tanBase, tanStride, i);
|
|
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(t));
|
|
v.tangent = glm::vec4(t3, t.w); // keep handedness in w
|
|
} else {
|
|
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
|
}
|
|
|
|
out.vertices[i] = v;
|
|
}
|
|
|
|
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
|
|
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
|
|
|
|
// Indices
|
|
if (prim.indices >= 0) {
|
|
const tinygltf::Accessor& accIdx = model.accessors.at(prim.indices);
|
|
if (accIdx.type != TINYGLTF_TYPE_SCALAR) {
|
|
throw std::runtime_error("Indices accessor must be SCALAR");
|
|
}
|
|
|
|
out.indices.resize(size_t(accIdx.count));
|
|
for (size_t i = 0; i < size_t(accIdx.count); ++i) {
|
|
out.indices[i] = ReadIndexAsU32(model, accIdx, i);
|
|
}
|
|
} else {
|
|
out.indices.resize(vertexCount);
|
|
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
|
|
}
|
|
|
|
// If your renderer expects opposite winding vs glTF, you can flip triangle order.
|
|
// Keep what you had:
|
|
// for (size_t i = 0; i + 2 < out.indices.size(); i += 3) {
|
|
// std::swap(out.indices[i + 1], out.indices[i + 2]);
|
|
// }
|
|
|
|
return out;
|
|
}
|
|
|
|
// Merge "src" into "dst" (offset indices)
|
|
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
|
|
const std::uint32_t baseVertex = static_cast<std::uint32_t>(dst.vertices.size());
|
|
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
|
|
|
|
dst.indices.reserve(dst.indices.size() + src.indices.size());
|
|
for (std::uint32_t idx : src.indices) {
|
|
dst.indices.push_back(baseVertex + idx);
|
|
}
|
|
|
|
if (dst.vertices.size() == src.vertices.size()) {
|
|
dst.minPos = src.minPos;
|
|
dst.maxPos = src.maxPos;
|
|
} else {
|
|
dst.minPos = glm::vec3(
|
|
std::min(dst.minPos.x, src.minPos.x),
|
|
std::min(dst.minPos.y, src.minPos.y),
|
|
std::min(dst.minPos.z, src.minPos.z)
|
|
);
|
|
dst.maxPos = glm::vec3(
|
|
std::max(dst.maxPos.x, src.maxPos.x),
|
|
std::max(dst.maxPos.y, src.maxPos.y),
|
|
std::max(dst.maxPos.z, src.maxPos.z)
|
|
);
|
|
}
|
|
}
|
|
|
|
// -------------------- public API --------------------
|
|
|
|
struct NodeStackItem {
|
|
int nodeIdx;
|
|
glm::mat4 parentWorld;
|
|
};
|
|
|
|
// Option A: return one CPUMesh per *primitive* encountered in the scene
|
|
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
|
tinygltf::TinyGLTF loader;
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
|
|
bool ok = false;
|
|
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
|
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
|
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
|
|
|
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
|
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
|
|
|
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
|
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
|
throw std::runtime_error("glTF has no valid scene");
|
|
}
|
|
|
|
std::vector<CPUMesh> result;
|
|
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
|
|
|
std::vector<NodeStackItem> stack;
|
|
stack.reserve(scene.nodes.size());
|
|
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
|
|
|
while (!stack.empty()) {
|
|
NodeStackItem it = stack.back();
|
|
stack.pop_back();
|
|
|
|
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
|
const glm::mat4 local = NodeLocalMatrix(node);
|
|
const glm::mat4 world = it.parentWorld * local;
|
|
|
|
for (int child : node.children) stack.push_back({child, world});
|
|
|
|
if (node.mesh < 0) continue;
|
|
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
|
|
|
for (size_t p = 0; p < mesh.primitives.size(); ++p) {
|
|
const tinygltf::Primitive& prim = mesh.primitives[p];
|
|
CPUMesh cpu = LoadPrimitiveIntoCPUMesh(
|
|
model,
|
|
prim,
|
|
mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name,
|
|
world
|
|
);
|
|
cpu.name += "_prim" + std::to_string(p);
|
|
result.push_back(std::move(cpu));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Option B: return one CPUMesh per *glTF mesh instance*, merging all primitives of that mesh into one CPUMesh.
|
|
// Note: If the same glTF mesh is instanced by multiple nodes, you'll get one merged CPUMesh PER NODE instance.
|
|
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
|
|
tinygltf::TinyGLTF loader;
|
|
tinygltf::Model model;
|
|
std::string err, warn;
|
|
|
|
bool ok = false;
|
|
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
|
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
|
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
|
|
|
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
|
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
|
|
|
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
|
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
|
throw std::runtime_error("glTF has no valid scene");
|
|
}
|
|
|
|
std::vector<CPUMesh> result;
|
|
|
|
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
|
std::vector<NodeStackItem> stack;
|
|
stack.reserve(scene.nodes.size());
|
|
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
|
|
|
while (!stack.empty()) {
|
|
NodeStackItem it = stack.back();
|
|
stack.pop_back();
|
|
|
|
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
|
const glm::mat4 local = NodeLocalMatrix(node);
|
|
const glm::mat4 world = it.parentWorld * local;
|
|
|
|
for (int child : node.children) stack.push_back({child, world});
|
|
|
|
if (node.mesh < 0) continue;
|
|
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
|
|
|
CPUMesh merged{};
|
|
merged.name = mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name;
|
|
merged.minPos = glm::vec3(std::numeric_limits<float>::infinity());
|
|
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
|
|
|
bool any = false;
|
|
for (const tinygltf::Primitive& prim : mesh.primitives) {
|
|
CPUMesh part = LoadPrimitiveIntoCPUMesh(model, prim, merged.name, world);
|
|
if (!any) {
|
|
merged = std::move(part);
|
|
any = true;
|
|
} else {
|
|
AppendMesh(merged, part);
|
|
}
|
|
}
|
|
|
|
if (any) result.push_back(std::move(merged));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace ModelLoader
|
|
|
|
#endif // MODELLOADER_H
|