diff --git a/.gitmodules b/.gitmodules index b518897..7f12414 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,7 @@ [submodule "destrum/third_party/spdlog"] path = destrum/third_party/spdlog url = https://github.com/gabime/spdlog.git +[submodule "destrum/third_party/tinygltf"] + path = destrum/third_party/tinygltf + url = https://github.com/syoyo/tinygltf.git + diff --git a/CMakeLists.txt b/CMakeLists.txt index 67a5841..b92082c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,3 +10,4 @@ endif() add_subdirectory(destrum) add_subdirectory(lightkeeper) +add_subdirectory(TheChef) \ No newline at end of file diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index a29070e..e525605 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -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 +) diff --git a/destrum/include/destrum/Util/ModelLoader.h b/destrum/include/destrum/Util/ModelLoader.h new file mode 100644 index 0000000..a8ead79 --- /dev/null +++ b/destrum/include/destrum/Util/ModelLoader.h @@ -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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + struct CPUMesh { + std::vector indices; + + struct Vertex { + glm::vec3 position; + float uv_x{}; + glm::vec3 normal; + float uv_y{}; + glm::vec4 tangent; + }; + + std::vector 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 + 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 + 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(p + 0); + const float y = ReadAs(p + 4); + const float z = ReadAs(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(p + 0); + const float y = ReadAs(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(p + 0); + const float y = ReadAs(p + 4); + const float z = ReadAs(p + 8); + const float w = ReadAs(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(ReadAs(p)); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return static_cast(ReadAs(p)); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return static_cast(ReadAs(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::infinity()}; + glm::vec3 mx{-std::numeric_limits::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(i); + } + + return out; + } + + // Merge "src" into "dst" (offset indices) + static void AppendMesh(CPUMesh& dst, const CPUMesh& src) { + const std::uint32_t baseVertex = static_cast(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 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 result; + const tinygltf::Scene& scene = model.scenes.at(sceneIndex); + + // Traverse nodes and pull mesh primitives (ignoring node transforms in this basic example) + std::vector 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 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 result; + + const tinygltf::Scene& scene = model.scenes.at(sceneIndex); + std::vector 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::infinity()); + merged.maxPos = glm::vec3(-std::numeric_limits::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 diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 267c3aa..563d2f6 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -146,6 +146,7 @@ void App::run() { std::chrono::duration(dt) ); std::this_thread::sleep_until(targetEnd); + } } gfxDevice.waitIdle(); diff --git a/destrum/third_party/CMakeLists.txt b/destrum/third_party/CMakeLists.txt index 5d76599..2fc3045 100644 --- a/destrum/third_party/CMakeLists.txt +++ b/destrum/third_party/CMakeLists.txt @@ -51,3 +51,5 @@ option(JSON_Install "Install CMake targets during install step." OFF) add_subdirectory(json) add_subdirectory(spdlog) + +add_subdirectory(tinygltf) \ No newline at end of file diff --git a/destrum/third_party/tinygltf b/destrum/third_party/tinygltf new file mode 160000 index 0000000..81bd50c --- /dev/null +++ b/destrum/third_party/tinygltf @@ -0,0 +1 @@ +Subproject commit 81bd50c1062fdb956e878efa2a9234b2b9ec91ec diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index ab0c8fc..dd52110 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -51,7 +51,7 @@ void LightKeeper::customInit() { meshComp->SetMeshID(testMeshID); meshComp->SetMaterialID(testMaterialID); - testCube->AddComponent(10, 5); + // testCube->AddComponent(10, 5); scene.Add(testCube);