MODEL LOADING BABY WORKS

This commit is contained in:
2026-01-20 00:13:54 +01:00
parent 87dcbb50ec
commit b9878f2a06
31 changed files with 755 additions and 252 deletions

View File

@@ -0,0 +1,48 @@
#ifndef ORBITANDSPIN_H
#define ORBITANDSPIN_H
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
class OrbitAndSpin final : public Component {
public:
OrbitAndSpin(
GameObject& parent,
float radius,
glm::vec3 center = glm::vec3(0.0f)
)
: Component(parent, "OrbitAndSpin")
, m_Radius(radius)
, m_Center(center)
{}
// Call after constructing if you want deterministic randomness per object
void Randomize(uint32_t seed);
void Update() override;
// optional setters
void SetRadius(float r) { m_Radius = r; }
void SetCenter(glm::vec3 c) { m_Center = c; }
private:
void BuildOrbitBasis(); // builds m_U/m_V from m_OrbitAxis
float m_Radius = 5.0f;
glm::vec3 m_Center{0.0f};
// orbit
glm::vec3 m_OrbitAxis{0,1,0}; // normal of the orbit plane
float m_OrbitSpeed = 1.0f; // rad/sec
float m_OrbitAngle = 0.0f; // current angle
float m_OrbitPhase = 0.0f; // starting offset
glm::vec3 m_U{1,0,0}; // orbit basis axis 1
glm::vec3 m_V{0,0,1}; // orbit basis axis 2
// self spin
glm::vec3 m_SpinAxis{0,1,0};
float m_SpinSpeed = 2.0f; // rad/sec
};
#endif //ORBITANDSPIN_H

View File

@@ -4,6 +4,8 @@
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/Transform.h>
#include <glm/glm.hpp>
class Rotator final : public Component {
public:
explicit Rotator(GameObject& parent, float distance, float speed);
@@ -15,15 +17,29 @@ public:
void Update() override;
void SetDistance(float distance) { m_Distance = distance; }
void SetDistance(float distance);
void SetSpeed(float speed) { m_Speed = speed; }
void SetRotatePosition(const glm::vec3& position) { GetTransform().SetLocalPosition(position); }
// Pivot (the point you orbit around) in LOCAL space
void SetPivotPosition(const glm::vec3& pivot);
// Axis to orbit around (in LOCAL space)
void SetAxis(const glm::vec3& axis);
// Backwards-compatible name if you were using this already
void SetRotatePosition(const glm::vec3& pivot) { SetPivotPosition(pivot); }
private:
static glm::vec3 MakePerpendicularUnitVector(const glm::vec3& axis);
private:
float m_Distance{};
float m_Speed{};
float m_CurrentAngle{};
glm::vec3 m_OriginalPosition{};
glm::vec3 m_Pivot{}; // center of rotation (local)
glm::vec3 m_Axis{0, 0, 1}; // rotation axis (local, normalized)
glm::vec3 m_InitialOffset{}; // from pivot -> object at angle 0 (local)
};
#endif //ROTATOR_H
#endif // ROTATOR_H

View File

@@ -0,0 +1,25 @@
#ifndef SPINNER_H
#define SPINNER_H
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
class Spinner final : public Component {
public:
explicit Spinner(GameObject& parent, glm::vec3 axis, float speedRadPerSec)
: Component(parent, "Spinner")
, m_Axis(glm::normalize(axis))
, m_Speed(speedRadPerSec) {}
void Update() override;
void SetAxis(const glm::vec3& axis) { m_Axis = glm::normalize(axis); }
void SetSpeed(float speedRadPerSec) { m_Speed = speedRadPerSec; }
private:
glm::vec3 m_Axis{0,1,0};
float m_Speed = 1.0f;
float m_Angle = 0.0f;
};
#endif //SPINNER_H

View File

@@ -7,9 +7,14 @@
#include <destrum/Singleton.h>
#include <destrum/FS/Manifest.h>
struct FSMount {
std::string scheme; // "engine", "game"
std::filesystem::path root;
std::optional<AssetManifest> manifest;
};
class AssetFS final: public Singleton<AssetFS> {
@@ -21,6 +26,8 @@ public:
[[nodiscard]] std::filesystem::path GetFullPath(std::string_view vpath) const;
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
private:
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);

View File

@@ -0,0 +1,32 @@
#ifndef MANIFEST_H
#define MANIFEST_H
#include <string>
#include <optional>
#include <unordered_map>
struct ManifestAsset {
std::string src; // "shaders/mesh.frag"
std::optional<std::string> out; // "shaders/mesh.frag.spv" or null
std::string type; // "shader_spirv", "texture", etc
int64_t mtime_epoch_ns = 0;
uint64_t size_bytes = 0;
};
struct AssetManifest {
uint32_t version = 0;
// Fast lookup by src path
std::unordered_map<std::string, ManifestAsset> assetsBySrc;
const ManifestAsset* FindBySrc(std::string_view src) const {
auto it = assetsBySrc.find(std::string(src));
return it != assetsBySrc.end() ? &it->second : nullptr;
}
};
namespace FS {
AssetManifest LoadAssetManifest(const std::filesystem::path& manifestPath);
}
#endif //MANIFEST_H

View File

@@ -94,12 +94,23 @@ static std::vector<CPUMesh::Vertex> vertices = {
};
static std::vector<uint32_t> indices = {
0, 1, 2, 2, 3, 0, // Front (+Z)
4, 5, 6, 6, 7, 4, // Back (-Z) <-- fixed
8, 9,10, 10,11, 8, // Right (+X)
12,13,14, 14,15,12, // Left (-X)
16,17,18, 18,19,16, // Top (+Y)
20,21,22, 22,23,20 // Bottom(-Y)
// Front (+Z)
0, 2, 1, 0, 3, 2,
// Back (-Z)
4, 6, 5, 4, 7, 6,
// Right (+X)
8,10, 9, 8,11,10,
// Left (-X)
12,14,13, 12,15,14,
// Top (+Y)
16,18,17, 16,19,18,
// Bottom (-Y)
20,22,21, 20,23,22
};

View File

@@ -1,15 +1,28 @@
#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.
// 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>
@@ -20,25 +33,9 @@ namespace ModelLoader {
#include <vector>
#include <stdexcept>
#include <iostream>
#include <algorithm>
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;
};
namespace ModelLoader {
// -------------------- helpers --------------------
@@ -84,13 +81,10 @@ namespace ModelLoader {
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 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;
@@ -104,8 +98,6 @@ namespace ModelLoader {
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);
@@ -155,15 +147,62 @@ namespace ModelLoader {
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 tinygltf::Primitive& prim,
const std::string& nameForMesh,
const glm::mat4& world) {
CPUMesh out{};
out.name = nameForMesh;
// POSITION is required for our vertex buffer
// POSITION is required
auto itPos = prim.attributes.find("POSITION");
if (itPos == prim.attributes.end()) {
throw std::runtime_error("Primitive has no POSITION attribute");
@@ -184,7 +223,7 @@ namespace ModelLoader {
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
accNormal = nullptr;
}
if (auto it = prim.attributes.find("TANGENT"); it != prim.attributes.end()) {
accTangent = &model.accessors.at(it->second);
@@ -211,7 +250,7 @@ namespace ModelLoader {
auto p = GetAccessorDataPtrAndStride(model, *accNormal);
nrmBase = p.first;
nrmStride = p.second;
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr; // mismatch -> ignore
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr;
}
if (accTangent) {
auto p = GetAccessorDataPtrAndStride(model, *accTangent);
@@ -229,23 +268,31 @@ namespace ModelLoader {
// Allocate vertices
out.vertices.resize(vertexCount);
// Bounds init
// 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()};
// Fill vertices
// 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{};
v.position = ReadVec3Float(posBase, posStride, i);
// 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) {
v.normal = ReadVec3Float(nrmBase, nrmStride, i);
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;
@@ -255,8 +302,11 @@ namespace ModelLoader {
v.uv_y = 0.0f;
}
// Tangent
if (accTangent) {
v.tangent = ReadVec4Float(tanBase, tanStride, i);
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);
}
@@ -267,7 +317,7 @@ namespace ModelLoader {
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)
// Indices
if (prim.indices >= 0) {
const tinygltf::Accessor& accIdx = model.accessors.at(prim.indices);
if (accIdx.type != TINYGLTF_TYPE_SCALAR) {
@@ -279,11 +329,16 @@ namespace ModelLoader {
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);
}
// 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;
}
@@ -293,7 +348,7 @@ namespace ModelLoader {
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) {
for (std::uint32_t idx : src.indices) {
dst.indices.push_back(baseVertex + idx);
}
@@ -301,7 +356,6 @@ namespace ModelLoader {
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),
@@ -317,6 +371,11 @@ namespace ModelLoader {
// -------------------- 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;
@@ -339,22 +398,31 @@ namespace ModelLoader {
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);
std::vector<NodeStackItem> stack;
stack.reserve(scene.nodes.size());
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
for (int child: node.children) stack.push_back(child);
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);
// Optionally make name unique per primitive
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));
}
@@ -363,7 +431,8 @@ namespace ModelLoader {
return result;
}
// Option B: return one CPUMesh per *glTF mesh*, merging all primitives of that mesh into one CPUMesh
// 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;
@@ -385,14 +454,19 @@ namespace ModelLoader {
std::vector<CPUMesh> result;
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
std::vector<int> stack(scene.nodes.begin(), scene.nodes.end());
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()) {
int nodeIdx = stack.back();
NodeStackItem it = stack.back();
stack.pop_back();
const tinygltf::Node& node = model.nodes.at(nodeIdx);
for (int child: node.children) stack.push_back(child);
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);
@@ -403,10 +477,10 @@ namespace ModelLoader {
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);
for (const tinygltf::Primitive& prim : mesh.primitives) {
CPUMesh part = LoadPrimitiveIntoCPUMesh(model, prim, merged.name, world);
if (!any) {
merged = part;
merged = std::move(part);
any = true;
} else {
AppendMesh(merged, part);
@@ -418,6 +492,7 @@ namespace ModelLoader {
return result;
}
}
#endif //MODELLOADER_H
} // namespace ModelLoader
#endif // MODELLOADER_H