Add ModelLoader (not tested) add TheChef for cooking
This commit is contained in:
@@ -96,20 +96,19 @@ target_compile_definitions(destrum
|
||||
GLM_ENABLE_EXPERIMENTAL
|
||||
)
|
||||
|
||||
set(DESTRUM_SHADER_SRC "${CMAKE_CURRENT_LIST_DIR}/assets_src/shaders")
|
||||
set(DESTRUM_SHADER_OUT "${CMAKE_CURRENT_LIST_DIR}/assets_runtime/shaders")
|
||||
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
||||
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
||||
|
||||
include(../cmake/compile_shaders.cmake)
|
||||
add_custom_target(cook_assets_clean
|
||||
COMMAND TheChef
|
||||
--input "${ASSETS_SRC_DIR}"
|
||||
--output "${ASSETS_RUNTIME_DIR}"
|
||||
--clean
|
||||
)
|
||||
|
||||
compile_glsl_to_spv(destrum "${DESTRUM_SHADER_SRC}" "${DESTRUM_SHADER_OUT}" DESTRUM_SPV)
|
||||
add_dependencies(destrum destrum_shaders)
|
||||
#
|
||||
#include(../cmake/compile_slang_shaders.cmake)
|
||||
#
|
||||
#compile_slang_shaders(
|
||||
# TARGET compile_shaders
|
||||
# SRC_DIR "${CMAKE_SOURCE_DIR}/destrum/assets_src/shaders"
|
||||
# OUT_DIR "${CMAKE_SOURCE_DIR}/destrum/assets_runtime/shaders"
|
||||
# EXTRA_ARGS -O3
|
||||
# ALL
|
||||
#)
|
||||
add_custom_target(cook_assets ALL
|
||||
COMMAND TheChef
|
||||
--input "${ASSETS_SRC_DIR}"
|
||||
--output "${ASSETS_RUNTIME_DIR}"
|
||||
DEPENDS TheChef
|
||||
)
|
||||
|
||||
423
destrum/include/destrum/Util/ModelLoader.h
Normal file
423
destrum/include/destrum/Util/ModelLoader.h
Normal 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
|
||||
@@ -146,6 +146,7 @@ void App::run() {
|
||||
std::chrono::duration<float>(dt)
|
||||
);
|
||||
std::this_thread::sleep_until(targetEnd);
|
||||
|
||||
}
|
||||
}
|
||||
gfxDevice.waitIdle();
|
||||
|
||||
2
destrum/third_party/CMakeLists.txt
vendored
2
destrum/third_party/CMakeLists.txt
vendored
@@ -51,3 +51,5 @@ option(JSON_Install "Install CMake targets during install step." OFF)
|
||||
add_subdirectory(json)
|
||||
|
||||
add_subdirectory(spdlog)
|
||||
|
||||
add_subdirectory(tinygltf)
|
||||
1
destrum/third_party/tinygltf
vendored
Submodule
1
destrum/third_party/tinygltf
vendored
Submodule
Submodule destrum/third_party/tinygltf added at 81bd50c106
Reference in New Issue
Block a user