Add ModelLoader (not tested) add TheChef for cooking

This commit is contained in:
2026-01-13 17:25:18 +01:00
parent 0bfc5e0705
commit 87dcbb50ec
8 changed files with 447 additions and 16 deletions

View File

@@ -0,0 +1,423 @@
#ifndef MODELLOADER_H
#define MODELLOADER_H
namespace ModelLoader {
// 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.
// - Optionally merges primitives into a single CPUMesh per glTF mesh.
#include <tiny_gltf.h>
#include <glm/glm.hpp>
#include <cstdint>
#include <cstring>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <stdexcept>
#include <iostream>
struct CPUMesh {
std::vector<std::uint32_t> indices;
struct Vertex {
glm::vec3 position;
float uv_x{};
glm::vec3 normal;
float uv_y{};
glm::vec4 tangent;
};
std::vector<Vertex> vertices;
std::string name;
glm::vec3 minPos;
glm::vec3 maxPos;
};
// -------------------- 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);
if (start + accessor.count * stride > buf.data.size() + 0ull) {
// Note: This check is conservative; stride can be larger than packed.
// Still useful to catch obvious corrupt files.
// If you want strict validation, check per-element.
}
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;
}
// Reads VEC3 float accessor element i into glm::vec3.
// Allows input component type float only for simplicity (common in glTF).
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);
}
// -------------------- primitive extraction --------------------
static CPUMesh LoadPrimitiveIntoCPUMesh(const tinygltf::Model& model,
const tinygltf::Primitive& prim,
const std::string& nameForMesh) {
CPUMesh out{};
out.name = nameForMesh;
// POSITION is required for our vertex buffer
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; // ignore unsupported
}
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; // mismatch -> ignore
}
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
glm::vec3 mn{std::numeric_limits<float>::infinity()};
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
// Fill vertices
for (size_t i = 0; i < vertexCount; ++i) {
CPUMesh::Vertex v{};
v.position = ReadVec3Float(posBase, posStride, i);
UpdateBounds(mn, mx, v.position);
if (accNormal) {
v.normal = ReadVec3Float(nrmBase, nrmStride, i);
} else {
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
}
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;
}
if (accTangent) {
v.tangent = ReadVec4Float(tanBase, tanStride, i);
} 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 (optional; if absent, build linear 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 {
// Non-indexed primitive: make it indexed
out.indices.resize(vertexCount);
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
}
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 {
// update bounds using src bounds (cheap)
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 --------------------
// 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);
// Traverse nodes and pull mesh primitives (ignoring node transforms in this basic example)
std::vector<int> stack(scene.nodes.begin(), scene.nodes.end());
while (!stack.empty()) {
int nodeIdx = stack.back();
stack.pop_back();
const tinygltf::Node& node = model.nodes.at(nodeIdx);
for (int child: node.children) stack.push_back(child);
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);
// Optionally make name unique per primitive
cpu.name += "_prim" + std::to_string(p);
result.push_back(std::move(cpu));
}
}
return result;
}
// Option B: return one CPUMesh per *glTF mesh*, merging all primitives of that mesh into one CPUMesh
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<int> stack(scene.nodes.begin(), scene.nodes.end());
while (!stack.empty()) {
int nodeIdx = stack.back();
stack.pop_back();
const tinygltf::Node& node = model.nodes.at(nodeIdx);
for (int child: node.children) stack.push_back(child);
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);
if (!any) {
merged = part;
any = true;
} else {
AppendMesh(merged, part);
}
}
if (any) result.push_back(std::move(merged));
}
return result;
}
}
#endif //MODELLOADER_H