diff --git a/.gitmodules b/.gitmodules index 9e3085d..9bddb95 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,3 +38,6 @@ [submodule "destrum/third_party/tinyexr"] path = destrum/third_party/tinyexr url = https://github.com/syoyo/tinyexr.git +[submodule "destrum/third_party/assimp"] + path = destrum/third_party/assimp + url = https://github.com/assimp/assimp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f76378a..993ef2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,4 +22,4 @@ add_custom_target(CookAssets) add_dependencies(CookAssets _internal_cook_game_assets _internal_cook_engine_assets -) \ No newline at end of file +) diff --git a/TheChef b/TheChef index faaf7fa..3a7b137 160000 --- a/TheChef +++ b/TheChef @@ -1 +1 @@ -Subproject commit faaf7fa12094f4aac8cb8e1082565096c005f51c +Subproject commit 3a7b1371655214dd54a249df8ae6eb6e7c29f621 diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 039288a..15b371a 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRC_FILES "src/App.cpp" "src/Event.cpp" + "src/Components/Animator.cpp" "src/Components/MeshRendererComponent.cpp" "src/Components/Rotator.cpp" "src/Components/Spinner.cpp" @@ -11,6 +12,8 @@ set(SRC_FILES "src/Graphics/BindlessSetManager.cpp" "src/Graphics/Camera.cpp" + "src/Graphics/ComputePipeline.cpp" + "src/Graphics/Frustum.cpp" "src/Graphics/GfxDevice.cpp" "src/Graphics/ImageCache.cpp" "src/Graphics/ImageLoader.cpp" @@ -29,6 +32,7 @@ set(SRC_FILES "src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp" + "src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Input/InputManager.cpp" @@ -69,6 +73,7 @@ target_link_libraries(destrum spdlog::spdlog stb::image tinygltf + assimp PRIVATE freetype::freetype @@ -105,14 +110,14 @@ target_compile_definitions(destrum set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") -set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine") - -add_custom_command(TARGET destrum POST_BUILD - COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}" - COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets" - COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}" - VERBATIM -) +#set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine") +# +#add_custom_command(TARGET destrum POST_BUILD +# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}" +# COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets" +# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}" +# VERBATIM +#) add_custom_target(_internal_clean_engine_assets COMMAND TheChef @@ -127,3 +132,17 @@ add_custom_target(_internal_cook_engine_assets ALL --output "${ASSETS_RUNTIME_DIR}" DEPENDS TheChef ) + +function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR) + # This resolves to destrum's own directory at function DEFINITION time + set(_engine_src "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_src") + set(_engine_runtime "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_runtime") + set(_output_engine "${GAME_OUTPUT_DIR}/assets/engine") + + add_custom_command(TARGET ${GAME_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${GAME_OUTPUT_DIR}/assets" + COMMAND ${CMAKE_COMMAND} -E rm -rf "${_output_engine}" + COMMAND ${CMAKE_COMMAND} -E create_symlink "${_engine_runtime}" "${_output_engine}" + VERBATIM + ) +endfunction() diff --git a/destrum/assets_src/char.fbx b/destrum/assets_src/char.fbx new file mode 100644 index 0000000..5e9e9f4 Binary files /dev/null and b/destrum/assets_src/char.fbx differ diff --git a/destrum/assets_src/char.jpg b/destrum/assets_src/char.jpg new file mode 100644 index 0000000..689a26a Binary files /dev/null and b/destrum/assets_src/char.jpg differ diff --git a/destrum/assets_src/char2.fbx b/destrum/assets_src/char2.fbx new file mode 100644 index 0000000..fd236d4 Binary files /dev/null and b/destrum/assets_src/char2.fbx differ diff --git a/destrum/assets_src/shaders/bindless.glsl b/destrum/assets_src/shaders/bindless.glsl index 66b2e79..64db2b0 100644 --- a/destrum/assets_src/shaders/bindless.glsl +++ b/destrum/assets_src/shaders/bindless.glsl @@ -14,6 +14,10 @@ vec4 sampleTexture2DNearest(uint texID, vec2 uv) { return texture(nonuniformEXT(sampler2D(textures[texID], samplers[NEAREST_SAMPLER_ID])), uv); } +vec4 sampleTexture2D(uint texID, uint samplerID, vec2 uv) { + return texture(nonuniformEXT(sampler2D(textures[texID], samplers[samplerID])), uv); +} + vec4 sampleTexture2DMSNearest(uint texID, ivec2 p, int s) { return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s); } diff --git a/destrum/assets_src/shaders/materials.glsl b/destrum/assets_src/shaders/materials.glsl index 674f561..8ad827f 100644 --- a/destrum/assets_src/shaders/materials.glsl +++ b/destrum/assets_src/shaders/materials.glsl @@ -6,6 +6,7 @@ struct MaterialData { vec4 baseColor; vec4 metallicRoughnessEmissive; + uint textureFilteringMode; uint diffuseTex; uint normalTex; uint metallicRoughnessTex; diff --git a/destrum/assets_src/shaders/mesh.frag b/destrum/assets_src/shaders/mesh.frag index b7f93ff..62b5290 100644 --- a/destrum/assets_src/shaders/mesh.frag +++ b/destrum/assets_src/shaders/mesh.frag @@ -17,8 +17,9 @@ layout (location = 0) out vec4 outFragColor; void main() { MaterialData material = pcs.sceneData.materials.data[pcs.materialID]; + uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode; - vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV) * material.baseColor; + vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor; outFragColor = diffuse; diff --git a/destrum/assets_src/shaders/skinning.comp b/destrum/assets_src/shaders/skinning.comp new file mode 100644 index 0000000..5968f1b --- /dev/null +++ b/destrum/assets_src/shaders/skinning.comp @@ -0,0 +1,60 @@ +#version 460 + +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_buffer_reference : require + +#include "vertex.glsl" + +struct SkinningDataType { + ivec4 jointIds; + vec4 weights; +}; + +layout (buffer_reference, std430) readonly buffer SkinningData { + SkinningDataType data[]; +}; + +layout (buffer_reference, std430) readonly buffer JointMatrices { + mat4 matrices[]; +}; + +layout (push_constant) uniform constants +{ + JointMatrices jointMatrices; + uint jointMatricesStartIndex; + uint numVertices; + VertexBuffer inputBuffer; + SkinningData skinningData; + VertexBuffer outputBuffer; +} pcs; + +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +mat4 getJointMatrix(int jointId) { + if (jointId < 0) return mat4(1.0); + return pcs.jointMatrices.matrices[pcs.jointMatricesStartIndex + jointId]; +} + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= pcs.numVertices) { + return; + } + + SkinningDataType sd = pcs.skinningData.data[index]; + mat4 skinMatrix = + sd.weights.x * getJointMatrix(sd.jointIds.x) + + sd.weights.y * getJointMatrix(sd.jointIds.y) + + sd.weights.z * getJointMatrix(sd.jointIds.z) + + sd.weights.w * getJointMatrix(sd.jointIds.w); + + Vertex v = pcs.inputBuffer.vertices[index]; + v.position = vec3(skinMatrix * vec4(v.position, 1.0)); + + mat3 skinMat3 = mat3(skinMatrix); + v.normal = skinMat3 * v.normal; + v.tangent.xyz = skinMat3 * v.tangent.xyz; // don't transform tangent.w + + pcs.outputBuffer.vertices[index] = v; +} diff --git a/destrum/include/destrum/Components/Animator.h b/destrum/include/destrum/Components/Animator.h new file mode 100644 index 0000000..0cbab24 --- /dev/null +++ b/destrum/include/destrum/Components/Animator.h @@ -0,0 +1,70 @@ +#ifndef ANIMATOR_H +#define ANIMATOR_H + +#include +#include +#include +#include + +#include + +#include +#include +#include + +class SkinningPipeline; + +class Animator : public Component { +public: + explicit Animator(GameObject& parent); + + // Component interface + void Update() override; + void ImGuiInspector() override; + + // Animation control + void addClip(std::shared_ptr clip); + void play(const std::string& name, float blendTime = 0.f); + void stop(); + + bool isPlaying() const { return m_current.clip != nullptr; } + const std::string& currentClipName() const { return m_currentClipName; } + float currentTime() const { return m_current.time; } + + // Called during draw command building + std::size_t uploadJointMatrices(SkinningPipeline& pipeline, + const Skeleton& skeleton, + std::size_t frameIndex); + Skeleton* getSkeleton() { + return &m_skeleton; + } + + void setSkeleton(Skeleton skeleton) { + m_skeleton = std::move(skeleton); + } + +private: + struct PlaybackState { + SkeletalAnimation* clip = nullptr; + float time = 0.f; + float speed = 1.f; + }; + + Skeleton m_skeleton{}; + + PlaybackState m_current; + PlaybackState m_previous; + float m_blendT = 0.f; + float m_blendDuration = 0.f; + std::string m_currentClipName; + + std::unordered_map> m_clips; + + std::vector computeJointMatrices(const Skeleton& skeleton); + + glm::vec3 sampleTranslation(const SkeletalAnimation::Track& track, float t); + glm::quat sampleRotation (const SkeletalAnimation::Track& track, float t); + glm::vec3 sampleScale (const SkeletalAnimation::Track& track, float t); +}; + +#endif // ANIMATOR_H \ No newline at end of file diff --git a/destrum/include/destrum/Components/MeshRendererComponent.h b/destrum/include/destrum/Components/MeshRendererComponent.h index bf2e916..0890988 100644 --- a/destrum/include/destrum/Components/MeshRendererComponent.h +++ b/destrum/include/destrum/Components/MeshRendererComponent.h @@ -21,6 +21,8 @@ public: private: MeshID meshID{NULL_MESH_ID}; MaterialID materialID{NULL_MATERIAL_ID}; + + std::unique_ptr m_skinnedMesh; }; #endif //MESHRENDERERCOMPONENT_H diff --git a/destrum/include/destrum/Graphics/Camera.h b/destrum/include/destrum/Graphics/Camera.h index 94165f5..e4c77c0 100644 --- a/destrum/include/destrum/Graphics/Camera.h +++ b/destrum/include/destrum/Graphics/Camera.h @@ -96,6 +96,13 @@ public: this->CalculateProjectionMatrix(); } + float GetZNear() const { return m_zNear; } + void SetZNear(float zNear) { m_zNear = zNear; } + float GetZFar() const { return m_zFar; } + void SetZFar(float zFar) { m_zFar = zFar; } + float GetFOVY() const { return fovAngle; } + float GetAspectRatio() const { return m_aspectRatio; } + glm::vec3 m_position{2.f, 0, 0}; void Translate(const glm::vec3& translation) { diff --git a/destrum/include/destrum/Graphics/ComputePipeline.h b/destrum/include/destrum/Graphics/ComputePipeline.h index 1db350d..95872b5 100644 --- a/destrum/include/destrum/Graphics/ComputePipeline.h +++ b/destrum/include/destrum/Graphics/ComputePipeline.h @@ -1,4 +1,48 @@ #ifndef COMPUTEPIPELINE_H #define COMPUTEPIPELINE_H +#include +#include + +#include + +struct ComputePipelineConfigInfo { + std::string name; + + // Optional specialization constants (can be nullptr if unused) + VkSpecializationInfo* specializationInfo{nullptr}; + + VkPipelineLayout pipelineLayout{VK_NULL_HANDLE}; +}; + +class ComputePipeline { +public: + ComputePipeline(GfxDevice& device, + const std::string& compPath, + const ComputePipelineConfigInfo& configInfo); + ~ComputePipeline(); + + ComputePipeline(const ComputePipeline& other) = delete; + ComputePipeline(ComputePipeline&& other) noexcept = delete; + ComputePipeline& operator=(const ComputePipeline& other) = delete; + ComputePipeline& operator=(ComputePipeline&& other) noexcept = delete; + + void bind(VkCommandBuffer buffer) const; + + static void DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo); + +private: + static std::vector readFile(const std::string& filename); + + void CreateComputePipeline(const std::string& compPath, + const ComputePipelineConfigInfo& configInfo); + + void CreateShaderModule(const std::vector& code, VkShaderModule* shaderModule) const; + + GfxDevice& m_device; + + VkPipeline m_computePipeline{VK_NULL_HANDLE}; + VkShaderModule m_compShaderModule{VK_NULL_HANDLE}; +}; + #endif //COMPUTEPIPELINE_H diff --git a/destrum/include/destrum/Graphics/Frustum.h b/destrum/include/destrum/Graphics/Frustum.h index 850e557..16706c1 100644 --- a/destrum/include/destrum/Graphics/Frustum.h +++ b/destrum/include/destrum/Graphics/Frustum.h @@ -1,4 +1,80 @@ #ifndef FRUSTUM_H #define FRUSTUM_H +#include + +#include "Camera.h" + +struct Frustum { + struct Plane { + Plane() = default; + Plane(const glm::vec3& p1, const glm::vec3& norm) : + normal(glm::normalize(norm)), distance(glm::dot(normal, p1)) + {} + + glm::vec3 normal{0.f, 1.f, 0.f}; + + // distance from the origin to the nearest point in the plane + float distance{0.f}; + + float getSignedDistanceToPlane(const glm::vec3& point) const + { + return glm::dot(normal, point) - distance; + } + }; + + const Plane& getPlane(int i) const + { + switch (i) { + case 0: + return farFace; + case 1: + return nearFace; + case 2: + return leftFace; + case 3: + return rightFace; + case 4: + return topFace; + case 5: + return bottomFace; + default: + assert(false); + return nearFace; + } + } + + Plane farFace; + Plane nearFace; + + Plane leftFace; + Plane rightFace; + + Plane topFace; + Plane bottomFace; +}; + +struct Sphere { + glm::vec3 center{}; + float radius{}; +}; + +struct AABB { + glm::vec3 min; + glm::vec3 max; + + glm::vec3 calculateSize() const { return glm::abs(max - min); } +}; + +namespace edge +{ + std::array calculateFrustumCornersWorldSpace(const Camera& camera); + + Frustum createFrustumFromCamera(const Camera& camera); + bool isInFrustum(const Frustum& frustum, const Sphere& s); + bool isInFrustum(const Frustum& frustum, const AABB& aabb); + Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton); +} + + #endif //FRUSTUM_H diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index 1052259..b2996f4 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.h @@ -26,6 +26,8 @@ #include "Util.h" +class MeshCache; + namespace { using ImmediateExecuteFunction = std::function; } diff --git a/destrum/include/destrum/Graphics/Material.h b/destrum/include/destrum/Graphics/Material.h index a6de891..890e400 100644 --- a/destrum/include/destrum/Graphics/Material.h +++ b/destrum/include/destrum/Graphics/Material.h @@ -4,13 +4,21 @@ #include #include -struct MaterialData { - glm::vec4 baseColor; - glm::vec4 metalRoughnessEmissive; - std::uint32_t diffuseTex; - std::uint32_t normalTex; - std::uint32_t metallicRoughnessTex; - std::uint32_t emissiveTex; +struct alignas(16) MaterialData { + alignas(16) glm::vec4 baseColor; + alignas(16) glm::vec4 metalRoughnessEmissive; + uint32_t textureFilteringMode; + uint32_t diffuseTex; + uint32_t normalTex; + uint32_t metallicRoughnessTex; + uint32_t emissiveTex; + uint32_t _pad0, _pad1, _pad2; // explicit to 64 bytes +}; + +enum class TextureFilteringMode : std::uint32_t { + Nearest, + Linear, + Anisotropic, }; struct Material { @@ -19,6 +27,8 @@ struct Material { float roughnessFactor{0.7f}; float emissiveFactor{0.f}; + TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear}; + ImageID diffuseTexture{NULL_IMAGE_ID}; // ImageId normalMapTexture{NULL_IMAGE_ID}; // ImageId metallicRoughnessTexture{NULL_IMAGE_ID}; diff --git a/destrum/include/destrum/Graphics/MaterialCache.h b/destrum/include/destrum/Graphics/MaterialCache.h index 6ca08f1..b1b8d34 100644 --- a/destrum/include/destrum/Graphics/MaterialCache.h +++ b/destrum/include/destrum/Graphics/MaterialCache.h @@ -18,13 +18,14 @@ public: void cleanup(GfxDevice& gfxDevice); MaterialID addMaterial(GfxDevice& gfxDevice, Material material); - const Material& getMaterial(MaterialID id) const; + MaterialID addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID); + [[nodiscard]] const Material& getMaterial(MaterialID id) const; - MaterialID getFreeMaterialId() const; - MaterialID getPlaceholderMaterialId() const; + [[nodiscard]] MaterialID getFreeMaterialId() const; + [[nodiscard]] MaterialID getPlaceholderMaterialId() const; - const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; } - VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; } + [[nodiscard]] const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; } + [[nodiscard]] VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; } Material& getMaterialMutable(MaterialID id); @@ -33,7 +34,7 @@ public: private: std::vector materials; - static const auto MAX_MATERIALS = 1000; + static constexpr auto MAX_MATERIALS = 1000; GPUBuffer materialDataBuffer; // material which is used for meshes without materials diff --git a/destrum/include/destrum/Graphics/MeshCache.h b/destrum/include/destrum/Graphics/MeshCache.h index 3da405b..7789b1a 100644 --- a/destrum/include/destrum/Graphics/MeshCache.h +++ b/destrum/include/destrum/Graphics/MeshCache.h @@ -13,11 +13,13 @@ public: MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh); const GPUMesh& getMesh(MeshID id) const; + const CPUMesh& getCPUMesh(MeshID id) const; private: void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const; std::vector meshes; + std::vector cpuMeshes; }; #endif //MESHCACHE_H diff --git a/destrum/include/destrum/Graphics/MeshDrawCommand.h b/destrum/include/destrum/Graphics/MeshDrawCommand.h index 28bafe0..6c61ec2 100644 --- a/destrum/include/destrum/Graphics/MeshDrawCommand.h +++ b/destrum/include/destrum/Graphics/MeshDrawCommand.h @@ -5,13 +5,15 @@ #include +#include + struct MeshDrawCommand { MeshID meshId; glm::mat4 transformMatrix; // for frustum culling - // math::Sphere worldBoundingSphere; + Sphere worldBoundingSphere; // If set - mesh will be drawn with overrideMaterialId // instead of whatever material the mesh has @@ -20,8 +22,8 @@ struct MeshDrawCommand { bool castShadow{true}; // skinned meshes only - // const SkinnedMesh* skinnedMesh{nullptr}; - // std::uint32_t jointMatricesStartIndex; + const SkinnedMesh* skinnedMesh{nullptr}; + std::uint32_t jointMatricesStartIndex; }; #endif //MESHDRAWCOMMAND_H diff --git a/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h b/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h index 8763f9e..93e83c2 100644 --- a/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h @@ -1,4 +1,58 @@ #ifndef SKINNINGPIPELINE_H #define SKINNINGPIPELINE_H +#include +#include +#include +#include + +#include +#include + +#include "destrum/Graphics/Resources/NBuffer.h" + +struct MeshDrawCommand; +class MeshCache; +class GfxDevice; + +class SkinningPipeline final { +public: + void init(GfxDevice& gfxDevice); + void cleanup(GfxDevice& gfxDevice); + + void doSkinning( + VkCommandBuffer cmd, + std::size_t frameIndex, + const MeshCache& meshCache, + const MeshDrawCommand& dc); + + void beginDrawing(std::size_t frameIndex); + std::size_t appendJointMatrices( + std::span jointMatrices, + std::size_t frameIndex); + +private: + VkPipelineLayout m_pipelineLayout; + std::unique_ptr skinningPipeline; + struct PushConstants { + VkDeviceAddress jointMatricesBuffer; + std::uint32_t jointMatricesStartIndex; + std::uint32_t numVertices; + VkDeviceAddress inputBuffer; + VkDeviceAddress skinningData; + VkDeviceAddress outputBuffer; + }; + static constexpr std::size_t MAX_JOINT_MATRICES = 5000; + + struct PerFrameData { + AppendableBuffer jointMatricesBuffer; + }; + + std::array framesData; + // NBuffer framesData; + + PerFrameData& getCurrentFrameData(std::size_t frameIndex); +}; + + #endif //SKINNINGPIPELINE_H diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 0f37c37..08907d3 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -13,6 +13,7 @@ #include +#include #include "Pipelines/SkyboxPipeline.h" class GameRenderer { @@ -35,6 +36,11 @@ public: void cleanup(VkDevice device); void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); + void drawSkinnedMesh(MeshID id, + const glm::mat4& transform, + MaterialID materialId, + SkinnedMesh* skinnedMesh, + std::size_t jointMatricesStartIndex); const GPUImage& getDrawImage(const GfxDevice& gfx_device) const; void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { @@ -47,6 +53,13 @@ public: void setSkyboxTexture(ImageID skyboxImageId); void flushMaterialUpdates(GfxDevice& gfxDevice); + [[nodiscard]] MeshCache& GetMeshCache() const { + return meshCache; + } + + [[nodiscard]] SkinningPipeline& getSkinningPipeline() const { + return *skinningPipeline; + } private: void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); @@ -91,6 +104,8 @@ private: std::unique_ptr meshPipeline; std::unique_ptr skyboxPipeline; + + std::unique_ptr skinningPipeline; }; #endif //RENDERER_H diff --git a/destrum/include/destrum/Graphics/Resources/AnimationClip.h b/destrum/include/destrum/Graphics/Resources/AnimationClip.h new file mode 100644 index 0000000..de98a47 --- /dev/null +++ b/destrum/include/destrum/Graphics/Resources/AnimationClip.h @@ -0,0 +1,30 @@ +#ifndef ANIMATIONCLIP_H +#define ANIMATIONCLIP_H + +#include +#include +#include + +#include + +struct JointKeyframes { + std::uint32_t jointIndex; // matches your skeleton joint order + + std::vector positionTimes; + std::vector positions; + + std::vector rotationTimes; + std::vector rotations; + + std::vector scaleTimes; + std::vector scales; +}; + +struct AnimationClip { + std::string name; + float duration; // seconds + float ticksPerSecond; + std::vector channels; // one per animated joint +}; + +#endif //ANIMATIONCLIP_H diff --git a/destrum/include/destrum/Graphics/Resources/AppendableBuffer.h b/destrum/include/destrum/Graphics/Resources/AppendableBuffer.h index ab5c323..edf9199 100644 --- a/destrum/include/destrum/Graphics/Resources/AppendableBuffer.h +++ b/destrum/include/destrum/Graphics/Resources/AppendableBuffer.h @@ -1,4 +1,40 @@ #ifndef APPENDABLEBUFFER_H #define APPENDABLEBUFFER_H +#include +#include // memcpy +#include + +#include + +#include + +template +struct AppendableBuffer { + void append(std::span elements) + { + assert(size + elements.size() <= capacity); + auto arr = (T*)buffer.info.pMappedData; + std::memcpy((void*)&arr[size], elements.data(), elements.size() * sizeof(T)); + size += elements.size(); + } + + void append(const T& element) + { + assert(size + 1 <= capacity); + auto arr = (T*)buffer.info.pMappedData; + std::memcpy((void*)&arr[size], &element, sizeof(T)); + ++size; + } + + void clear() { size = 0; } + + VkBuffer getVkBuffer() const { return buffer.buffer; } + + GPUBuffer buffer; + std::size_t capacity{}; + std::size_t size{0}; +}; + + #endif //APPENDABLEBUFFER_H diff --git a/destrum/include/destrum/Graphics/Resources/Mesh.h b/destrum/include/destrum/Graphics/Resources/Mesh.h index b8b5684..71b06cd 100644 --- a/destrum/include/destrum/Graphics/Resources/Mesh.h +++ b/destrum/include/destrum/Graphics/Resources/Mesh.h @@ -2,33 +2,48 @@ #define MESH_H #include +#include #include +#include #include #include #include #include +#include #include +#include +#include +// ─── CPU Mesh ───────────────────────────────────────────────────────────────── struct CPUMesh { - std::vector indices; - struct Vertex { glm::vec3 position; - float uv_x{}; + float uv_x{}; glm::vec3 normal; - float uv_y{}; + float uv_y{}; glm::vec4 tangent; }; - std::vector vertices; + + struct SkinningData { + glm::vec<4, std::uint32_t> jointIds; + glm::vec4 weights; + }; + + std::vector indices; + std::vector vertices; + std::vector skinningData; // empty if no skeleton std::string name; glm::vec3 minPos; glm::vec3 maxPos; + + std::optional skeleton; // present only for skinned meshes }; +// ─── GPU Mesh ───────────────────────────────────────────────────────────────── struct GPUMesh { GPUBuffer vertexBuffer; @@ -40,78 +55,64 @@ struct GPUMesh { // AABB glm::vec3 minPos; glm::vec3 maxPos; - // math::Sphere boundingSphere; + Sphere boundingSphere; + + bool hasSkeleton{false}; + GPUBuffer skinningDataBuffer; // valid only when hasSkeleton == true }; -static std::vector vertices = { - // ======================= +// ─── Skinned output buffer (one per entity, not per mesh asset) ─────────────── + +struct SkinnedMesh { + GPUBuffer skinnedVertexBuffer; +}; + +// ─── Cube geometry (dev / test asset) ───────────────────────────────────────── + +namespace CubeMesh { + +inline std::vector vertices = { // +Z (Front) - // ======================= {{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}}, - - // ======================= // -Z (Back) - // ======================= {{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}}, {{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}}, {{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}}, {{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}}, - - // ======================= // +X (Right) - // ======================= {{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}}, {{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}}, {{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}}, {{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}}, - - // ======================= // -X (Left) - // ======================= {{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}}, {{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}}, {{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}}, {{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}}, - - // ======================= // +Y (Top) - // ======================= {{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}}, - - // ======================= // -Y (Bottom) - // ======================= {{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}}, }; -static std::vector indices = { - // 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 +inline std::vector indices = { + 0, 2, 1, 0, 3, 2, // Front + 4, 6, 5, 4, 7, 6, // Back + 8,10, 9, 8,11,10, // Right + 12,14,13, 12,15,14, // Left + 16,18,17, 16,19,18, // Top + 20,22,21, 20,23,22, // Bottom }; +} // namespace CubeMesh -#endif //MESH_H +#endif // MESH_H \ No newline at end of file diff --git a/destrum/include/destrum/Graphics/SkeletalAnimation.h b/destrum/include/destrum/Graphics/SkeletalAnimation.h new file mode 100644 index 0000000..67feed7 --- /dev/null +++ b/destrum/include/destrum/Graphics/SkeletalAnimation.h @@ -0,0 +1,35 @@ +#ifndef SKELETALANIMATION_H +#define SKELETALANIMATION_H + +#include +#include +#include + +#include +#include + +struct SkeletalAnimation { + struct Keyframe { + float time; + glm::vec3 translation; + glm::quat rotation; + glm::vec3 scale; + }; + + struct Track { + std::uint32_t jointIndex; + std::vector keyframes; // sorted by time + }; + + std::vector tracks; // one per animated joint (not all joints need a track) + float duration{0.f}; + bool looped{true}; + std::string name; + + int startFrame{0}; + std::map> events; + + const std::vector& getEventsForFrame(int frame) const; +}; + +#endif // SKELETALANIMATION_H \ No newline at end of file diff --git a/destrum/include/destrum/Graphics/Skeleton.h b/destrum/include/destrum/Graphics/Skeleton.h new file mode 100644 index 0000000..ce35083 --- /dev/null +++ b/destrum/include/destrum/Graphics/Skeleton.h @@ -0,0 +1,55 @@ +#ifndef SKELETON_H +#define SKELETON_H + +#include +#include + +#include +#include +#include + +#include + +struct Joint { + JointId id{NULL_JOINT_ID}; + glm::vec3 localTranslation{0.f, 0.f, 0.f}; + glm::quat localRotation{glm::identity()}; + glm::vec3 localScale{1.f, 1.f, 1.f}; +}; + +struct Skeleton { + struct JointNode { + JointId id{NULL_JOINT_ID}; + std::vector children; + }; + + std::vector hierarchy; + std::vector inverseBindMatrices; + std::vector joints; + std::vector jointNames; + std::vector parentIndex; // -1 = root, built once after loading +}; + +inline void buildParentIndex(Skeleton& skeleton) { + const std::size_t numJoints = skeleton.joints.size(); + skeleton.parentIndex.assign(numJoints, -1); + + // Build id -> slot map so we don't assume hierarchy[i] == joints[i] + std::unordered_map idToSlot; + idToSlot.reserve(numJoints); + for (std::size_t j = 0; j < numJoints; ++j) + idToSlot[skeleton.joints[j].id] = j; + + for (const auto& node : skeleton.hierarchy) { + auto parentIt = idToSlot.find(node.id); + if (parentIt == idToSlot.end()) continue; + + for (const JointId childId : node.children) { + auto childIt = idToSlot.find(childId); + if (childIt != idToSlot.end()) + skeleton.parentIndex[childIt->second] = static_cast(parentIt->second); + } + } +} + +#endif // SKELETON_H \ No newline at end of file diff --git a/destrum/include/destrum/Graphics/ids.h b/destrum/include/destrum/Graphics/ids.h index 0a26d11..0a35088 100644 --- a/destrum/include/destrum/Graphics/ids.h +++ b/destrum/include/destrum/Graphics/ids.h @@ -16,4 +16,8 @@ constexpr MaterialID NULL_MATERIAL_ID = std::numeric_limits::max( using BindlessID = std::uint32_t; constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits::max(); +using JointId = std::uint16_t; +static const JointId NULL_JOINT_ID = std::numeric_limits::max(); +static const JointId ROOT_JOINT_ID = 0; + #endif //IDS_H diff --git a/destrum/include/destrum/Util/MathUtils.h b/destrum/include/destrum/Util/MathUtils.h index fde5f82..eb6bb07 100644 --- a/destrum/include/destrum/Util/MathUtils.h +++ b/destrum/include/destrum/Util/MathUtils.h @@ -1,4 +1,86 @@ #ifndef MATHUTILS_H #define MATHUTILS_H +#include +#include +#include +#include +#include + +#include "glm/gtx/norm.hpp" + +inline Sphere calculateBoundingSphere(std::span positions) +{ + assert(!positions.empty()); + auto calculateInitialSphere = [](const std::span& positions) -> Sphere { + constexpr int dirCount = 13; + static const std::array direction = {{ + {1.f, 0.f, 0.f}, + {0.f, 1.f, 0.f}, + {0.f, 0.f, 1.f}, + {1.f, 1.f, 0.f}, + {1.f, 0.f, 1.f}, + {0.f, 1.f, 1.f}, + {1.f, -1.f, 0.f}, + {1.f, 0.f, -1.f}, + {0.f, 1.f, -1.f}, + {1.f, 1.f, 1.f}, + {1.f, -1.f, 1.f}, + {1.f, 1.f, -1.f}, + {1.f, -1.f, -1.f}, + }}; + + std::array dmin{}; + std::array dmax{}; + std::array imin{}; + std::array imax{}; + + // Find min and max dot products for each direction and record vertex indices. + for (int j = 0; j < dirCount; ++j) { + const auto& u = direction[j]; + dmin[j] = glm::dot(u, positions[0]); + dmax[j] = dmin[j]; + for (std::size_t i = 1; i < positions.size(); ++i) { + const auto d = glm::dot(u, positions[i]); + if (d < dmin[j]) { + dmin[j] = d; + imin[j] = i; + } else if (d > dmax[j]) { + dmax[j] = d; + imax[j] = i; + } + }; + } + + // Find direction for which vertices at min and max extents are furthest apart. + float d2 = glm::length2(positions[imax[0]] - positions[imin[0]]); + int k = 0; + for (int j = 1; j < dirCount; j++) { + const auto m2 = glm::length2(positions[imax[j]] - positions[imin[j]]); + if (m2 > d2) { + d2 = m2; + k = j; + } + } + + const auto center = (positions[imin[k]] + positions[imax[k]]) * 0.5f; + float radius = sqrt(d2) * 0.5f; + return {center, radius}; + }; + + // Determine initial center and radius. + auto s = calculateInitialSphere(positions); + // Make pass through vertices and adjust sphere as necessary. + for (std::size_t i = 0; i < positions.size(); i++) { + const auto pv = positions[i] - s.center; + float m2 = glm::length2(pv); + if (m2 > s.radius * s.radius) { + auto q = s.center - (pv * (s.radius / std::sqrt(m2))); + s.center = (q + positions[i]) * 0.5f; + s.radius = glm::length(q - s.center); + } + } + return s; +} + #endif //MATHUTILS_H diff --git a/destrum/include/destrum/Util/ModelLoader.h b/destrum/include/destrum/Util/ModelLoader.h index 0a83040..40ffaf7 100644 --- a/destrum/include/destrum/Util/ModelLoader.h +++ b/destrum/include/destrum/Util/ModelLoader.h @@ -1,498 +1,521 @@ #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. +// CPUMesh loader using Assimp +// - Loads first scene, iterates nodes recursively, extracts mesh primitives. +// - Handles POSITION / NORMAL / TANGENT / TEXCOORD_0 and indices. +// - Handles bones/skinning data and skeletal animations. +// - Computes minPos / maxPos (in world space). +// - Can return per-primitive meshes (PerPrimitive) or one merged mesh per +// Assimp mesh-node (MergedPerMesh). +// - LoadSkinnedModel loads meshes + skeleton + animations in one call. +// - For skinned meshes, vertex positions are kept in local/bind-pose space +// (identity world transform) so the skinning shader can apply joint matrices +// correctly at runtime. // -// 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. +// Link against: assimp (e.g. -lassimp) +// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …). -#define TINYGLTF_IMPLEMENTATION -#define TINYGLTF_NO_STB_IMAGE_WRITE -#include +#include +#include +#include #include -#include // translate/scale -#include // quat -#include // mat4_cast +#include +#include +#include #include +#include +#include +#include #include -#include #include -#include +#include #include #include #include -#include -#include -#include namespace ModelLoader { - // -------------------- helpers -------------------- +// ─── 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"); - } - } +// Convert an Assimp row-major 4×4 matrix to GLM column-major. +static glm::mat4 ToGLM(const aiMatrix4x4& m) { + return glm::transpose(glm::make_mat4(&m.a1)); +} - 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"); - } - } +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); +} - // 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); +// ─── Post-process flags ─────────────────────────────────────────────────────── - const size_t componentSize = ComponentSizeInBytes(accessor.componentType); - const int numComps = TypeNumComponents(accessor.type); - const size_t packedStride = componentSize * size_t(numComps); +static constexpr unsigned int kImportFlags = + aiProcess_Triangulate | + aiProcess_CalcTangentSpace | + aiProcess_JoinIdenticalVertices | + aiProcess_GenSmoothNormals | + aiProcess_FlipUVs | + aiProcess_LimitBoneWeights; // guarantees <= 4 weights per vertex - size_t stride = bv.byteStride ? size_t(bv.byteStride) : packedStride; +static const aiScene* LoadScene(Assimp::Importer& importer, const std::string& path) { + const aiScene* scene = importer.ReadFile(path, kImportFlags); + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) + throw std::runtime_error(std::string("Assimp failed to load: ") + importer.GetErrorString()); + return scene; +} - 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"); - } +// ─── Primitive extraction ───────────────────────────────────────────────────── - const std::uint8_t* ptr = buf.data.data() + start; - return {ptr, stride}; - } +// Build one CPUMesh from a single aiMesh, applying the given world transform. +// For skinned meshes, pass glm::mat4{1.f} so vertices stay in bind-pose space. +// Skinning data is NOT populated here — call LoadSkinningData afterwards. +static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh, + const std::string& name, + const glm::mat4& world) { + CPUMesh out{}; + out.name = name; - template - static T ReadAs(const std::uint8_t* p) { - T v{}; - std::memcpy(&v, p, sizeof(T)); - return v; - } + const size_t vertexCount = mesh->mNumVertices; + out.vertices.resize(vertexCount); - 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}; - } + const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world))); + const glm::mat3 tanMat = glm::mat3(world); - 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}; - } + glm::vec3 mn{ std::numeric_limits::infinity()}; + glm::vec3 mx{-std::numeric_limits::infinity()}; - 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}; - } + for (size_t i = 0; i < vertexCount; ++i) { + CPUMesh::Vertex v{}; - 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; + // Position (required) + const aiVector3D& ap = mesh->mVertices[i]; + v.position = glm::vec3(world * glm::vec4(ap.x, ap.y, ap.z, 1.0f)); + UpdateBounds(mn, mx, v.position); - 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); - } - - // 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(node.matrix[c * 4 + r]); - return m; - } - - glm::vec3 t(0.0f); - if (node.translation.size() == 3) { - t = glm::vec3( - static_cast(node.translation[0]), - static_cast(node.translation[1]), - static_cast(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(node.rotation[3]), // w - static_cast(node.rotation[0]), // x - static_cast(node.rotation[1]), // y - static_cast(node.rotation[2]) // z - ); - } - - glm::vec3 s(1.0f); - if (node.scale.size() == 3) { - s = glm::vec3( - static_cast(node.scale[0]), - static_cast(node.scale[1]), - static_cast(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::infinity()}; - glm::vec3 mx{-std::numeric_limits::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); - } + // Normal + if (mesh->HasNormals()) { + const aiVector3D& an = mesh->mNormals[i]; + v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z)); } else { - out.indices.resize(vertexCount); - for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast(i); + v.normal = glm::vec3(0.0f, 1.0f, 0.0f); } - // 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(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; + // UV channel 0 + if (mesh->HasTextureCoords(0)) { + v.uv_x = mesh->mTextureCoords[0][i].x; + v.uv_y = mesh->mTextureCoords[0][i].y; } 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) - ); + v.uv_x = 0.0f; + v.uv_y = 0.0f; + } + + // Tangent + bitangent sign (stored in w as +1/-1 handedness) + if (mesh->HasTangentsAndBitangents()) { + const aiVector3D& at = mesh->mTangents[i]; + const aiVector3D& ab = mesh->mBitangents[i]; + glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(at.x, at.y, at.z)); + glm::vec3 b3 = glm::normalize(tanMat * glm::vec3(ab.x, ab.y, ab.z)); + glm::vec3 n3 = v.normal; + float sign = (glm::dot(glm::cross(n3, t3), b3) < 0.0f) ? -1.0f : 1.0f; + v.tangent = glm::vec4(t3, sign); + } 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 (mesh->HasFaces()) { + out.indices.reserve(size_t(mesh->mNumFaces) * 3); + for (unsigned int f = 0; f < mesh->mNumFaces; ++f) { + const aiFace& face = mesh->mFaces[f]; + for (unsigned int k = 0; k < face.mNumIndices; ++k) + out.indices.push_back(static_cast(face.mIndices[k])); + } + } else { + out.indices.resize(vertexCount); + for (size_t i = 0; i < vertexCount; ++i) + out.indices[i] = static_cast(i); + } + + return out; +} + +// Merge src into dst, offsetting src indices by the current dst vertex count. +static void AppendMesh(CPUMesh& dst, const CPUMesh& src) { + const std::uint32_t base = 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(base + idx); + + 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) + ); +} + +// ─── Skeleton loading ───────────────────────────────────────────────────────── + +static Skeleton LoadSkeleton(const aiScene* scene) { + Skeleton skeleton; + + // Pass 1: collect all bone names and their inverse bind matrices + // from every mesh in the scene. + std::unordered_map boneOffsets; + for (unsigned int m = 0; m < scene->mNumMeshes; ++m) { + const aiMesh* mesh = scene->mMeshes[m]; + for (unsigned int b = 0; b < mesh->mNumBones; ++b) { + const aiBone* bone = mesh->mBones[b]; + std::string boneName(bone->mName.C_Str()); + if (!boneOffsets.count(boneName)) + boneOffsets[boneName] = ToGLM(bone->mOffsetMatrix); } } - // -------------------- public API -------------------- + if (boneOffsets.empty()) return skeleton; // static mesh, no skeleton - struct NodeStackItem { - int nodeIdx; - glm::mat4 parentWorld; - }; + // Pass 2: walk the node tree in depth-first order (parent before child), + // registering only nodes that correspond to actual bones. + // This guarantees joints are ordered parent-before-child, which is required + // by Animator::computeJointMatrices. + JointId nextId = 0; - // 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; + struct StackItem { const aiNode* node; int parentIdx; }; + std::vector stack{{ scene->mRootNode, -1 }}; - 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); + while (!stack.empty()) { + auto [node, parentIdx] = stack.back(); + stack.pop_back(); - if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n"; - if (!ok) throw std::runtime_error("Failed to load glTF: " + err); + std::string name(node->mName.C_Str()); + int myIdx = parentIdx; // default: pass parent through to children - 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"); + if (boneOffsets.count(name)) { + const JointId id = nextId++; + myIdx = static_cast(id); + + Joint joint{}; + joint.id = id; + skeleton.joints.push_back(joint); + skeleton.jointNames.push_back(name); + skeleton.inverseBindMatrices.push_back(boneOffsets[name]); + + // Grow hierarchy to accommodate this id + while (skeleton.hierarchy.size() <= id) + skeleton.hierarchy.push_back({}); + + skeleton.hierarchy[id].id = id; // record the node's own id + + if (parentIdx >= 0) + skeleton.hierarchy[parentIdx].children.push_back(id); } - std::vector result; - const tinygltf::Scene& scene = model.scenes.at(sceneIndex); + // Push children in reverse so left-most child is processed first + for (int c = static_cast(node->mNumChildren) - 1; c >= 0; --c) + stack.push_back({ node->mChildren[c], myIdx }); + } - std::vector stack; - stack.reserve(scene.nodes.size()); - for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)}); + buildParentIndex(skeleton); + return skeleton; +} - while (!stack.empty()) { - NodeStackItem it = stack.back(); - stack.pop_back(); +// ─── Skinning data ──────────────────────────────────────────────────────────── - const tinygltf::Node& node = model.nodes.at(it.nodeIdx); - const glm::mat4 local = NodeLocalMatrix(node); - const glm::mat4 world = it.parentWorld * local; +// Populate cpuMesh.skinningData from the matching aiMesh. +// Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized. +static void LoadSkinningData(CPUMesh& cpuMesh, + const aiMesh* aiMesh, + const Skeleton& skeleton) { + if (!aiMesh->HasBones() || skeleton.joints.empty()) return; - for (int child : node.children) stack.push_back({child, world}); + cpuMesh.skinningData.assign(cpuMesh.vertices.size(), + CPUMesh::SkinningData{ {0, 0, 0, 0}, {0.f, 0.f, 0.f, 0.f} }); - if (node.mesh < 0) continue; - const tinygltf::Mesh& mesh = model.meshes.at(node.mesh); + std::vector weightCount(cpuMesh.vertices.size(), 0); - 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)); + for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) { + const aiBone* bone = aiMesh->mBones[b]; + std::string boneName(bone->mName.C_Str()); + + // Find which joint index this bone maps to + std::uint32_t jointIdx = 0; + bool found = false; + for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) { + if (skeleton.jointNames[j] == boneName) { + jointIdx = static_cast(j); + found = true; + break; } } + if (!found) continue; - return result; - } + for (unsigned int w = 0; w < bone->mNumWeights; ++w) { + const unsigned int vertIdx = bone->mWeights[w].mVertexId; + const float weight = bone->mWeights[w].mWeight; + const int slot = weightCount[vertIdx]; - // 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 LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) { - tinygltf::TinyGLTF loader; - tinygltf::Model model; - std::string err, warn; + if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this - 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"); + cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx; + cpuMesh.skinningData[vertIdx].weights[slot] = weight; + ++weightCount[vertIdx]; } + } +} - std::vector result; +// ─── Animation loading ──────────────────────────────────────────────────────── - const tinygltf::Scene& scene = model.scenes.at(sceneIndex); - std::vector stack; - stack.reserve(scene.nodes.size()); - for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)}); +static std::vector LoadAnimations(const aiScene* scene, + const Skeleton& skeleton) { + std::vector result; + if (!scene->HasAnimations()) return result; - while (!stack.empty()) { - NodeStackItem it = stack.back(); - stack.pop_back(); + for (unsigned int a = 0; a < scene->mNumAnimations; ++a) { + const aiAnimation* aiAnim = scene->mAnimations[a]; + const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0; - const tinygltf::Node& node = model.nodes.at(it.nodeIdx); - const glm::mat4 local = NodeLocalMatrix(node); - const glm::mat4 world = it.parentWorld * local; + SkeletalAnimation anim; + anim.name = aiAnim->mName.C_Str(); + anim.duration = static_cast(aiAnim->mDuration / tps); + anim.looped = true; - for (int child : node.children) stack.push_back({child, world}); + for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) { + const aiNodeAnim* ch = aiAnim->mChannels[c]; + std::string boneName(ch->mNodeName.C_Str()); - 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, world); - if (!any) { - merged = std::move(part); - any = true; - } else { - AppendMesh(merged, part); + // Map bone name to joint index + std::uint32_t jointIdx = 0; + bool found = false; + for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) { + if (skeleton.jointNames[j] == boneName) { + jointIdx = static_cast(j); + found = true; + break; } } + if (!found) continue; - if (any) result.push_back(std::move(merged)); + SkeletalAnimation::Track track; + track.jointIndex = jointIdx; + + // FIX: position, rotation and scale channels can have different + // keyframe counts and independent time axes. Build the track from + // the position channel's timeline and pick the nearest rotation/ + // scale key for each sample rather than assuming index alignment. + // This avoids corrupted keyframes when counts differ. + const unsigned int numPos = ch->mNumPositionKeys; + const unsigned int numRot = ch->mNumRotationKeys; + const unsigned int numSca = ch->mNumScalingKeys; + + // Use position keys to drive the timeline (most common case). + // If there are no position keys fall back to rotation keys. + const unsigned int numKeys = numPos > 0 ? numPos : numRot; + + for (unsigned int k = 0; k < numKeys; ++k) { + SkeletalAnimation::Keyframe kf; + + // Time comes from whichever channel drives this loop + if (numPos > 0) { + kf.time = static_cast(ch->mPositionKeys[k].mTime / tps); + const auto& p = ch->mPositionKeys[k].mValue; + kf.translation = { p.x, p.y, p.z }; + } else { + kf.time = static_cast(ch->mRotationKeys[k].mTime / tps); + kf.translation = { 0.f, 0.f, 0.f }; + } + + // Nearest rotation key at this index + { + const unsigned int ri = std::min(k, numRot - 1); + const auto& r = ch->mRotationKeys[ri].mValue; + kf.rotation = glm::quat(r.w, r.x, r.y, r.z); // glm: (w,x,y,z) + } + + // Nearest scale key at this index + { + const unsigned int si = std::min(k, numSca - 1); + const auto& s = ch->mScalingKeys[si].mValue; + kf.scale = { s.x, s.y, s.z }; + } + + track.keyframes.push_back(kf); + } + + anim.tracks.push_back(std::move(track)); } - return result; + result.push_back(std::move(anim)); } + return result; +} + +// ─── Public API: static meshes ──────────────────────────────────────────────── + +// Option A: one CPUMesh per aiMesh reference encountered in the node tree. +static std::vector LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) { + Assimp::Importer importer; + const aiScene* scene = LoadScene(importer, path); + + std::vector result; + + struct StackItem { const aiNode* node; glm::mat4 parentWorld; }; + std::vector stack{{ scene->mRootNode, glm::mat4(1.0f) }}; + + while (!stack.empty()) { + auto [node, parentWorld] = stack.back(); + stack.pop_back(); + + const glm::mat4 world = parentWorld * ToGLM(node->mTransformation); + + for (unsigned int c = 0; c < node->mNumChildren; ++c) + stack.push_back({ node->mChildren[c], world }); + + for (unsigned int m = 0; m < node->mNumMeshes; ++m) { + const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]]; + std::string name = mesh->mName.length > 0 + ? std::string(mesh->mName.C_Str()) + : ("mesh_" + std::to_string(node->mMeshes[m])); + name += "_prim" + std::to_string(m); + + result.push_back(LoadAiMeshIntoCPUMesh(mesh, name, world)); + } + } + + return result; +} + +// Option B: one CPUMesh per node, merging all aiMesh references of that node. +static std::vector LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) { + Assimp::Importer importer; + const aiScene* scene = LoadScene(importer, path); + + std::vector result; + + struct StackItem { const aiNode* node; glm::mat4 parentWorld; }; + std::vector stack{{ scene->mRootNode, glm::mat4(1.0f) }}; + + while (!stack.empty()) { + auto [node, parentWorld] = stack.back(); + stack.pop_back(); + + const glm::mat4 world = parentWorld * ToGLM(node->mTransformation); + + for (unsigned int c = 0; c < node->mNumChildren; ++c) + stack.push_back({ node->mChildren[c], world }); + + if (node->mNumMeshes == 0) continue; + + std::string mergedName = node->mName.length > 0 + ? std::string(node->mName.C_Str()) + : std::string(scene->mMeshes[node->mMeshes[0]]->mName.C_Str()); + + CPUMesh merged{}; + merged.name = mergedName; + merged.minPos = glm::vec3( std::numeric_limits::infinity()); + merged.maxPos = glm::vec3(-std::numeric_limits::infinity()); + + bool any = false; + for (unsigned int m = 0; m < node->mNumMeshes; ++m) { + const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]]; + CPUMesh part = LoadAiMeshIntoCPUMesh(mesh, mergedName, world); + if (!any) { merged = std::move(part); any = true; } + else AppendMesh(merged, part); + } + + if (any) result.push_back(std::move(merged)); + } + + return result; +} + +// ─── Public API: skinned model ──────────────────────────────────────────────── + +struct SkinnedModel { + std::vector meshes; + Skeleton skeleton; + std::vector animations; +}; + +// Loads meshes + skeleton + animations in a single Assimp pass. +// Each CPUMesh has skinningData and skeleton populated. +// +// IMPORTANT: skinned mesh vertices are loaded with an identity world transform +// so they remain in bind-pose space. The skinning shader applies joint matrices +// at runtime — baking the node transform into positions would break that. +// +// Usage: +// auto model = ModelLoader::LoadSkinnedModel("character.glb"); +// MeshId id = meshCache.uploadMesh(model.meshes[0]); +// auto* anim = go.AddComponent(); +// for (auto& clip : model.animations) +// anim->addClip(std::make_shared(std::move(clip))); +// anim->play("Walk"); +static SkinnedModel LoadSkinnedModel(const std::string& path) { + Assimp::Importer importer; + const aiScene* scene = LoadScene(importer, path); + + SkinnedModel model; + model.skeleton = LoadSkeleton(scene); + model.animations = LoadAnimations(scene, model.skeleton); + + struct StackItem { const aiNode* node; glm::mat4 parentWorld; }; + std::vector stack{{ scene->mRootNode, glm::mat4(1.f) }}; + + while (!stack.empty()) { + auto [node, parentWorld] = stack.back(); + stack.pop_back(); + + // We still traverse with world transforms so non-skinned siblings + // work correctly, but skinned meshes are loaded with identity below. + const glm::mat4 world = parentWorld * ToGLM(node->mTransformation); + + for (unsigned int c = 0; c < node->mNumChildren; ++c) + stack.push_back({ node->mChildren[c], world }); + + for (unsigned int m = 0; m < node->mNumMeshes; ++m) { + const aiMesh* aiMesh = scene->mMeshes[node->mMeshes[m]]; + std::string name = aiMesh->mName.length > 0 + ? std::string(aiMesh->mName.C_Str()) + : ("mesh_" + std::to_string(node->mMeshes[m])); + + // FIX: pass identity matrix for skinned meshes so vertices stay in + // bind-pose space. The inverse bind matrices and joint transforms + // are already expressed in that space — baking the node world + // transform into positions would offset everything incorrectly. + const bool isSkinned = aiMesh->HasBones() && !model.skeleton.joints.empty(); + const glm::mat4 meshWorld = isSkinned ? glm::mat4{1.f} : world; + + CPUMesh cpu = LoadAiMeshIntoCPUMesh(aiMesh, name, meshWorld); + LoadSkinningData(cpu, aiMesh, model.skeleton); + cpu.skeleton = model.skeleton; + model.meshes.push_back(std::move(cpu)); + } + } + + return model; +} + } // namespace ModelLoader -#endif // MODELLOADER_H +#endif // MODELLOADER_H \ No newline at end of file diff --git a/destrum/src/Components/Animator.cpp b/destrum/src/Components/Animator.cpp new file mode 100644 index 0000000..3caf55c --- /dev/null +++ b/destrum/src/Components/Animator.cpp @@ -0,0 +1,189 @@ +#include +#include +#include + +#include +#include + +#include "spdlog/spdlog.h" + +Animator::Animator(GameObject& parent) + : Component(parent, "Animator") {} + +// ─── Component interface ────────────────────────────────────────────────────── + +void Animator::Update() { + if (!m_current.clip) return; + + // Time delta comes from your engine's time system + // const float dt = Time::GetDeltaTime(); + + const float dt = 0.016f; // ~60 FPS + + m_current.time += dt * m_current.speed; + if (m_current.clip->looped) + m_current.time = std::fmod(m_current.time, m_current.clip->duration); + else + m_current.time = std::min(m_current.time, m_current.clip->duration); + + if (m_previous.clip) { + m_previous.time += dt * m_previous.speed; + if (m_previous.clip->looped) + m_previous.time = std::fmod(m_previous.time, m_previous.clip->duration); + + m_blendT += dt / m_blendDuration; + if (m_blendT >= 1.f) { + m_blendT = 1.f; + m_previous = {}; + } + } + + spdlog::info("Playing '{}': time = {:.2f}s, blend = {:.2f}", + m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str(), + m_current.time, + m_blendT); +} + +void Animator::ImGuiInspector() { + // ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str()); + // ImGui::Text("Time: %.2f", m_current.time); + // ImGui::Text("Blend: %.2f", m_blendT); + // + // if (!isPlaying()) { + // ImGui::BeginDisabled(); + // ImGui::Button("Stop"); + // ImGui::EndDisabled(); + // } else if (ImGui::Button("Stop")) { + // stop(); + // } + // + // ImGui::Separator(); + // ImGui::Text("Clips:"); + // for (auto& [name, _] : m_clips) { + // if (ImGui::Selectable(name.c_str(), name == m_currentClipName)) + // play(name); + // } +} + +// ─── Animation control ──────────────────────────────────────────────────────── + +void Animator::addClip(std::shared_ptr clip) { + m_clips[clip->name] = std::move(clip); +} + +void Animator::play(const std::string& name, float blendTime) { + auto it = m_clips.find(name); + if (it == m_clips.end()) return; + + if (m_current.clip && blendTime > 0.f) { + m_previous = m_current; + m_blendT = 0.f; + m_blendDuration = blendTime; + } else { + m_previous = {}; + m_blendT = 1.f; + } + + m_current = { it->second.get(), 0.f, 1.f }; + m_currentClipName = name; +} + +void Animator::stop() { + m_current = {}; + m_previous = {}; + m_currentClipName = {}; +} + +// ─── Joint matrix upload ────────────────────────────────────────────────────── + +std::size_t Animator::uploadJointMatrices(SkinningPipeline& pipeline, + const Skeleton& skeleton, + std::size_t frameIndex) { + auto matrices = computeJointMatrices(skeleton); + return pipeline.appendJointMatrices(matrices, frameIndex); +} + +// ─── Private: pose evaluation ───────────────────────────────────────────────── + +std::vector Animator::computeJointMatrices(const Skeleton& skeleton) { + const std::size_t numJoints = skeleton.joints.size(); + std::vector global(numJoints, glm::mat4{1.f}); + std::vector result(numJoints, glm::mat4{1.f}); + + auto findTrack = [](const SkeletalAnimation* clip, + std::uint32_t idx) -> const SkeletalAnimation::Track* { + if (!clip) return nullptr; + for (auto& t : clip->tracks) + if (t.jointIndex == idx) return &t; + return nullptr; + }; + + for (std::uint32_t i = 0; i < numJoints; ++i) { + glm::vec3 tr = {0.f, 0.f, 0.f}; + glm::quat rot = glm::identity(); + glm::vec3 sc = {1.f, 1.f, 1.f}; + + if (const auto* track = findTrack(m_current.clip, i)) { + tr = sampleTranslation(*track, m_current.time); + rot = sampleRotation (*track, m_current.time); + sc = sampleScale (*track, m_current.time); + } + + if (m_previous.clip && m_blendT < 1.f) { + if (const auto* prev = findTrack(m_previous.clip, i)) { + tr = glm::mix (sampleTranslation(*prev, m_previous.time), tr, m_blendT); + rot = glm::slerp(sampleRotation (*prev, m_previous.time), rot, m_blendT); + sc = glm::mix (sampleScale (*prev, m_previous.time), sc, m_blendT); + } + } + + glm::mat4 local = glm::translate(glm::mat4{1.f}, tr) + * glm::mat4_cast(rot) + * glm::scale(glm::mat4{1.f}, sc); + + const int parent = skeleton.parentIndex[i]; + global[i] = (parent < 0) ? local : global[parent] * local; + + result[i] = global[i] * skeleton.inverseBindMatrices[i]; + } + + return result; +} + +// ─── Keyframe sampling ──────────────────────────────────────────────────────── + +glm::vec3 Animator::sampleTranslation(const SkeletalAnimation::Track& track, float t) { + const auto& kf = track.keyframes; + if (kf.size() == 1) return kf[0].translation; + for (std::size_t i = 0; i + 1 < kf.size(); ++i) { + if (t <= kf[i + 1].time) { + float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time); + return glm::mix(kf[i].translation, kf[i + 1].translation, f); + } + } + return kf.back().translation; +} + +glm::quat Animator::sampleRotation(const SkeletalAnimation::Track& track, float t) { + const auto& kf = track.keyframes; + if (kf.size() == 1) return kf[0].rotation; + for (std::size_t i = 0; i + 1 < kf.size(); ++i) { + if (t <= kf[i + 1].time) { + float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time); + return glm::slerp(kf[i].rotation, kf[i + 1].rotation, f); + } + } + return kf.back().rotation; +} + +glm::vec3 Animator::sampleScale(const SkeletalAnimation::Track& track, float t) { + const auto& kf = track.keyframes; + if (kf.size() == 1) return kf[0].scale; + for (std::size_t i = 0; i + 1 < kf.size(); ++i) { + if (t <= kf[i + 1].time) { + float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time); + return glm::mix(kf[i].scale, kf[i + 1].scale, f); + } + } + return kf.back().scale; +} \ No newline at end of file diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index 85130aa..95f6f67 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -1,19 +1,56 @@ #include #include +#include "destrum/Components/Animator.h" +#include "destrum/ObjectModel/GameObject.h" +#include "destrum/Util/GameState.h" + MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") { + } void MeshRendererComponent::Start() { Component::Start(); + if (auto* animator = GetGameObject()->GetComponent()) { + const auto& gfxDevice = GameState::GetInstance().Gfx(); + const auto& mesh = GameState::GetInstance().Renderer().GetMeshCache().getMesh(meshID); + + m_skinnedMesh = std::make_unique(); + m_skinnedMesh->skinnedVertexBuffer = gfxDevice.createBuffer( + mesh.numVertices * sizeof(CPUMesh::Vertex), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT + ); + } } void MeshRendererComponent::Update() { } void MeshRendererComponent::Render(const RenderContext& ctx) { - if (meshID != NULL_MESH_ID && materialID != NULL_MATERIAL_ID) { + if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return; + + if (auto* animator = GetGameObject()->GetComponent(); animator && m_skinnedMesh) { + const auto& mesh = ctx.renderer.GetMeshCache().getCPUMesh(meshID); + const auto skeleton = GetGameObject()->GetComponent()->getSkeleton(); + std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex(); + + const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices( + ctx.renderer.getSkinningPipeline(), + *skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh) + frameIdx + ); + + ctx.renderer.drawSkinnedMesh( + meshID, + GetTransform().GetWorldMatrix(), + materialID, + m_skinnedMesh.get(), + jointMatricesStartIndex + ); + } else { ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID); } } diff --git a/destrum/src/Components/OrbitAndSpin.cpp b/destrum/src/Components/OrbitAndSpin.cpp index 1eed17e..7eba3e6 100644 --- a/destrum/src/Components/OrbitAndSpin.cpp +++ b/destrum/src/Components/OrbitAndSpin.cpp @@ -98,13 +98,13 @@ void OrbitAndSpin::Update() // GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase))); // material color - auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID); - mat.baseColor = glm::vec3( - 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f), - 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f), - 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f) - ); - GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID); + // auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID); + // mat.baseColor = glm::vec3( + // 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f), + // 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f), + // 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f) + // ); + // GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID); } void OrbitAndSpin::Start() { diff --git a/destrum/src/Graphics/BindlessSetManager.cpp b/destrum/src/Graphics/BindlessSetManager.cpp index 3d3f78c..ff90dc7 100644 --- a/destrum/src/Graphics/BindlessSetManager.cpp +++ b/destrum/src/Graphics/BindlessSetManager.cpp @@ -7,11 +7,11 @@ namespace { -static const std::uint32_t maxBindlessResources = 16536; -static const std::uint32_t maxSamplers = 32; + constexpr std::uint32_t maxBindlessResources = 16536; + constexpr std::uint32_t maxSamplers = 32; -static const std::uint32_t texturesBinding = 0; -static const std::uint32_t samplersBinding = 1; + constexpr std::uint32_t texturesBinding = 0; + constexpr std::uint32_t samplersBinding = 1; } void BindlessSetManager::init(VkDevice device, float maxAnisotropy) @@ -26,7 +26,7 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy) .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT, .maxSets = 10, - .poolSizeCount = (std::uint32_t)poolSizesBindless.size(), + .poolSizeCount = static_cast(poolSizesBindless.size()), .pPoolSizes = poolSizesBindless.data(), }; diff --git a/destrum/src/Graphics/ComputePipeline.cpp b/destrum/src/Graphics/ComputePipeline.cpp index e69de29..1e6c941 100644 --- a/destrum/src/Graphics/ComputePipeline.cpp +++ b/destrum/src/Graphics/ComputePipeline.cpp @@ -0,0 +1,105 @@ +#include + +#include +#include +#include +#include + +#include "destrum/Util/GameState.h" +#include "spdlog/spdlog.h" + +ComputePipeline::ComputePipeline(GfxDevice& device, + const std::string& compPath, + const ComputePipelineConfigInfo& configInfo) + : m_device(device) { + CreateComputePipeline(compPath, configInfo); +} + +ComputePipeline::~ComputePipeline() { + if (m_compShaderModule != VK_NULL_HANDLE) { + vkDestroyShaderModule(m_device.getDevice(), m_compShaderModule, nullptr); + } + if (m_computePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_device.getDevice(), m_computePipeline, nullptr); + } +} + +void ComputePipeline::bind(VkCommandBuffer buffer) const { + vkCmdBindPipeline(buffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_computePipeline); +} + +void ComputePipeline::DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo) { + configInfo.name = "DefaultComputePipelineConfigInfo"; + configInfo.specializationInfo = nullptr; + configInfo.pipelineLayout = VK_NULL_HANDLE; +} + +std::vector ComputePipeline::readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + const size_t fileSize = static_cast(file.tellg()); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), static_cast(fileSize)); + file.close(); + + return buffer; +} + +void ComputePipeline::CreateComputePipeline(const std::string& compPath, + const ComputePipelineConfigInfo& configInfo) { + assert(configInfo.pipelineLayout != VK_NULL_HANDLE && "no pipelineLayout provided in configInfo"); + assert(!compPath.empty() && "Compute shader path is empty"); + + const std::string compFileName = std::filesystem::path(compPath).filename().string(); + auto compCode = readFile(compPath); + + spdlog::debug("Compute shader code size: {}", compCode.size()); + spdlog::debug("Compute shader file: {}", compFileName); + + CreateShaderModule(compCode, &m_compShaderModule); + + VkPipelineShaderStageCreateInfo compStageInfo{}; + compStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + compStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; + compStageInfo.module = m_compShaderModule; + compStageInfo.pName = "main"; + compStageInfo.pSpecializationInfo = configInfo.specializationInfo; + + VkComputePipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipelineInfo.stage = compStageInfo; + pipelineInfo.layout = configInfo.pipelineLayout; + + pipelineInfo.basePipelineIndex = -1; + pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; + + if (vkCreateComputePipelines(m_device.getDevice(), + VK_NULL_HANDLE, + 1, + &pipelineInfo, + nullptr, + &m_computePipeline) != VK_SUCCESS) { + throw std::runtime_error("Can't make compute pipeline!"); + } + + if (!configInfo.name.empty()) { + vkutil::addDebugLabel(GameState::GetInstance().Gfx().getDevice(), m_computePipeline, configInfo.name.c_str()); + } +} + +void ComputePipeline::CreateShaderModule(const std::vector& code, VkShaderModule* shaderModule) const { + VkShaderModuleCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + createInfo.codeSize = code.size(); + createInfo.pCode = reinterpret_cast(code.data()); + + if (vkCreateShaderModule(m_device.getDevice(), &createInfo, nullptr, shaderModule) != VK_SUCCESS) { + throw std::runtime_error("Failed to create shader module!"); + } +} diff --git a/destrum/src/Graphics/Frustum.cpp b/destrum/src/Graphics/Frustum.cpp index e69de29..209a41c 100644 --- a/destrum/src/Graphics/Frustum.cpp +++ b/destrum/src/Graphics/Frustum.cpp @@ -0,0 +1,128 @@ +#include + +Frustum edge::createFrustumFromCamera(const Camera& camera) { +// TODO: write a non-horrible version of this, lol + Frustum frustum; +// if (camera.isOrthographic()) { +// const auto points = calculateFrustumCornersWorldSpace(camera); +// +// /* +// 5────────6 +// ╱┊ ╱│ +// ╱ ┊ ╱ │ +// 1──┼─────2 │ +// │ ┊ (C) │ │ Y ╿ . Z +// │ 4┈┈┈┈┈│┈┈7 │ ╱ +// │ ╱ │ ╱ X │ ╱ +// │╱ │╱ ╾────┼ +// 0--------3 +// +// */ +// // from bottom-left and moving CW... +// static const std::array near{0, 1, 2, 3}; +// static const std::array far{7, 6, 5, 4}; +// static const std::array left{4, 5, 1, 0}; +// static const std::array right{3, 2, 6, 7}; +// static const std::array bottom{4, 0, 3, 7}; +// static const std::array top{5, 6, 2, 1}; +// +// frustum.nearFace = {findCenter(points, near), findNormal(points, near)}; +// frustum.farFace = {findCenter(points, far), findNormal(points, far)}; +// frustum.leftFace = {findCenter(points, left), findNormal(points, left)}; +// frustum.rightFace = {findCenter(points, right), findNormal(points, right)}; +// frustum.bottomFace = {findCenter(points, bottom), findNormal(points, bottom)}; +// frustum.topFace = {findCenter(points, top), findNormal(points, top)}; +// } else { + const auto camPos = camera.GetPosition(); + const auto camFront = camera.GetForward(); + const auto camUp = camera.GetUp(); + const auto camRight = camera.GetRight(); + + const auto zNear = camera.GetZNear(); + const auto zFar = camera.GetZFar(); + const auto halfVSide = zFar * tanf(camera.GetFOVY() * .5f); + const auto halfHSide = halfVSide * camera.GetAspectRatio(); + const auto frontMultFar = zFar * camFront; + + frustum.nearFace = {camPos + zNear * camFront, camFront}; + frustum.farFace = {camPos + frontMultFar, -camFront}; + frustum.leftFace = {camPos, glm::cross(camUp, frontMultFar + camRight * halfHSide)}; + frustum.rightFace = {camPos, glm::cross(frontMultFar - camRight * halfHSide, camUp)}; + frustum.bottomFace = {camPos, glm::cross(frontMultFar + camUp * halfVSide, camRight)}; + frustum.topFace = {camPos, glm::cross(camRight, frontMultFar - camUp * halfVSide)}; + // } + + return frustum; +} + +bool isOnOrForwardPlane(const Frustum::Plane& plane, const Sphere& sphere) +{ + return plane.getSignedDistanceToPlane(sphere.center) > -sphere.radius; +} + +bool edge::isInFrustum(const Frustum& frustum, const Sphere& s) { + return ( + isOnOrForwardPlane(frustum.farFace, s) && isOnOrForwardPlane(frustum.nearFace, s) && + isOnOrForwardPlane(frustum.leftFace, s) && isOnOrForwardPlane(frustum.rightFace, s) && + isOnOrForwardPlane(frustum.topFace, s) && isOnOrForwardPlane(frustum.bottomFace, s)); +} + +bool edge::isInFrustum(const Frustum& frustum, const AABB& aabb) { + glm::vec3 vmin, vmax; + bool ret = true; + for (int i = 0; i < 6; ++i) { + const auto& plane = frustum.getPlane(i); + // X axis + if (plane.normal.x < 0) { + vmin.x = aabb.min.x; + vmax.x = aabb.max.x; + } else { + vmin.x = aabb.max.x; + vmax.x = aabb.min.x; + } + // Y axis + if (plane.normal.y < 0) { + vmin.y = aabb.min.y; + vmax.y = aabb.max.y; + } else { + vmin.y = aabb.max.y; + vmax.y = aabb.min.y; + } + // Z axis + if (plane.normal.z < 0) { + vmin.z = aabb.min.z; + vmax.z = aabb.max.z; + } else { + vmin.z = aabb.max.z; + vmax.z = aabb.min.z; + } + if (plane.getSignedDistanceToPlane(vmin) < 0) { + return false; + } + if (plane.getSignedDistanceToPlane(vmax) <= 0) { + ret = true; + } + } + return ret; +} + +glm::vec3 getTransformScale(const glm::mat4& transform) +{ + float sx = glm::length(glm::vec3{transform[0][0], transform[0][1], transform[0][2]}); + float sy = glm::length(glm::vec3{transform[1][0], transform[1][1], transform[1][2]}); + float sz = glm::length(glm::vec3{transform[2][0], transform[2][1], transform[2][2]}); + return {sx, sy, sz}; +} + +Sphere edge::calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton) { + const auto scale = getTransformScale(transform); + float maxScale = std::max({scale.x, scale.y, scale.z}); + if (hasSkeleton) { + maxScale = 5.f; // ignore scale for skeleton meshes (TODO: fix) + // setting scale to 1.f causes prolems with frustum culling + } + auto sphereWorld = s; + sphereWorld.radius *= maxScale; + sphereWorld.center = glm::vec3(transform * glm::vec4(sphereWorld.center, 1.f)); + return sphereWorld; +} diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index f452db9..be3fc3f 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -129,18 +129,36 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) auto& mainCommandBuffer = frames[i].commandBuffer; VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); } - // - // { // create white texture - // std::uint32_t pixel = 0xFFFFFFFF; - // whiteImageId = createImage( - // { - // .format = VK_FORMAT_R8G8B8A8_UNORM, - // .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - // .extent = VkExtent3D{1, 1, 1}, - // }, - // "white texture", - // &pixel); - // } + + { // create white texture + std::uint32_t pixel = 0xFFFFFFFF; + whiteImageId = createImage( + { + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .extent = VkExtent3D{1, 1, 1}, + }, + "white texture", + &pixel); + } + + { // create error texture (black/magenta checker) + constexpr auto black = 0xFF000000; + constexpr auto magenta = 0xFFFF00FF; + + std::array pixels{black, magenta, magenta, black}; + errorImageId = createImage( + { + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .extent = VkExtent3D{2, 2, 1}, + }, + "error texture", + pixels.data()); + imageCache.setErrorImageId(errorImageId); + } + GameState::GetInstance().SetGfxDevice(this); } @@ -495,25 +513,14 @@ GPUImage GfxDevice::createImageRaw( return image; } -GPUImage GfxDevice::loadImageFromFileRaw( - const std::filesystem::path& path, - VkImageUsageFlags usage, - bool mipMap, - TextureIntent intent) const { - // 1) Decode file to CPU pixels (stb for png/jpg/hdr, tinyexr for exr) - // IMPORTANT: util::loadImage should fill: - // - width/height/channels(=4) - // - hdr + (pixels OR hdrPixels) - // - vkFormat (RGBA8_SRGB or RGBA8_UNORM or RGBA32F) - // - byteSize (exact bytes to upload) +GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const { const auto data = util::loadImage(path, intent); - if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || - (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) { + + if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) { spdlog::error("Failed to load image '{}'", path.string()); return getImage(errorImageId); } - // 2) Create GPU image using the format the loader chose (matches CPU memory layout) auto image = createImageRaw({ .format = data.vkFormat, .usage = usage | @@ -527,15 +534,10 @@ GPUImage GfxDevice::loadImageFromFileRaw( .mipMap = mipMap, }); - // 3) Upload *exactly* byteSize bytes from the correct pointer - const void* src = data.hdr - ? static_cast(data.hdrPixels) - : static_cast(data.pixels); + const void* src = data.hdr ? static_cast(data.hdrPixels) : static_cast(data.pixels); - // Use the "sized" upload to avoid BytesPerTexel mismatches uploadImageDataSized(image, src, data.byteSize, 0); - // 4) Debug label image.debugName = path.string(); vkutil::addDebugLabel(device, image.image, path.string().c_str()); diff --git a/destrum/src/Graphics/ImageCache.cpp b/destrum/src/Graphics/ImageCache.cpp index 1dd2d56..d990e6b 100644 --- a/destrum/src/Graphics/ImageCache.cpp +++ b/destrum/src/Graphics/ImageCache.cpp @@ -2,21 +2,12 @@ #include -ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) -{} +ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) { +} -ImageID ImageCache::loadImageFromFile( - const std::filesystem::path& path, - VkImageUsageFlags usage, - bool mipMap, - TextureIntent intent) -{ - for (const auto& [id, info] : loadedImagesInfo) { - if (info.path == path && - info.intent == intent && - info.usage == usage && - info.mipMap == mipMap) - { +ImageID ImageCache::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) { + for (const auto& [id, info]: loadedImagesInfo) { + if (info.path == path && info.intent == intent && info.usage == usage && info.mipMap == mipMap) { return id; } } @@ -41,13 +32,11 @@ ImageID ImageCache::loadImageFromFile( return id; } -ImageID ImageCache::addImage(GPUImage image) -{ +ImageID ImageCache::addImage(GPUImage image) { return addImage(getFreeImageId(), std::move(image)); } -ImageID ImageCache::addImage(ImageID id, GPUImage image) -{ +ImageID ImageCache::addImage(ImageID id, GPUImage image) { image.setBindlessId(static_cast(id)); if (id != images.size()) { images[id] = std::move(image); // replacing existing image @@ -59,20 +48,17 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image) return id; } -const GPUImage& ImageCache::getImage(ImageID id) const -{ +const GPUImage& ImageCache::getImage(ImageID id) const { assert(id != NULL_IMAGE_ID && id < images.size()); return images.at(id); } -ImageID ImageCache::getFreeImageId() const -{ +ImageID ImageCache::getFreeImageId() const { return images.size(); } -void ImageCache::destroyImages() -{ - for (const auto& image : images) { +void ImageCache::destroyImages() { + for (const auto& image: images) { gfxDevice.destroyImage(image); } images.clear(); diff --git a/destrum/src/Graphics/ImageLoader.cpp b/destrum/src/Graphics/ImageLoader.cpp index d317f54..27a628e 100644 --- a/destrum/src/Graphics/ImageLoader.cpp +++ b/destrum/src/Graphics/ImageLoader.cpp @@ -37,6 +37,8 @@ namespace util { ImageData data; + + // ---------- EXR ---------- if (isExrExt(p)) { float* out = nullptr; diff --git a/destrum/src/Graphics/MaterialCache.cpp b/destrum/src/Graphics/MaterialCache.cpp index 98a09fa..97b4a3d 100644 --- a/destrum/src/Graphics/MaterialCache.cpp +++ b/destrum/src/Graphics/MaterialCache.cpp @@ -24,7 +24,7 @@ void MaterialCache::init(GfxDevice& gfxDevice) &normal); } - Material placeholderMaterial{.name = "PLACEHOLDER_MATERIAL"}; + Material placeholderMaterial{.diffuseTexture = defaultNormalMapTextureID, .name = "PLACEHOLDER_MATERIAL"}; placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial); } @@ -41,13 +41,14 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material) }; // store on GPU - MaterialData* data = (MaterialData*)materialDataBuffer.info.pMappedData; - auto whiteTextureID = gfxDevice.getWhiteTextureID(); - auto id = getFreeMaterialId(); + MaterialData* data = static_cast(materialDataBuffer.info.pMappedData); + const auto whiteTextureID = gfxDevice.getWhiteTextureID(); + const auto id = getFreeMaterialId(); assert(id < MAX_MATERIALS); data[id] = MaterialData{ .baseColor = glm::vec4(material.baseColor, 1.0f), .metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f}, + .textureFilteringMode = static_cast(material.textureFilteringMode), .diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID), .normalTex = whiteTextureID, .metallicRoughnessTex = whiteTextureID, @@ -60,6 +61,15 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material) return id; } +MaterialID MaterialCache::addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID) { + Material material{}; + material.name = "idk"; + material.diffuseTexture = textureID; + material.metallicFactor = 0.0f; + material.roughnessFactor = 1.0f; + return addMaterial(gfxDevice, material); +} + const Material& MaterialCache::getMaterial(MaterialID id) const { return materials.at(id); @@ -100,7 +110,7 @@ void MaterialCache::updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id) .baseColor = glm::vec4(material.baseColor, 1.0f), .metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f), .diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID), - .normalTex = whiteTextureID, // if you have this field + .normalTex = whiteTextureID, .metallicRoughnessTex = whiteTextureID, .emissiveTex = whiteTextureID, }; diff --git a/destrum/src/Graphics/MeshCache.cpp b/destrum/src/Graphics/MeshCache.cpp index 0431856..02e4215 100644 --- a/destrum/src/Graphics/MeshCache.cpp +++ b/destrum/src/Graphics/MeshCache.cpp @@ -3,25 +3,30 @@ #include #include #include + +#include // #include MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh) { auto gpuMesh = GPUMesh{ .numVertices = (std::uint32_t)cpuMesh.vertices.size(), - .numIndices = (std::uint32_t)cpuMesh.indices.size(), - .minPos = cpuMesh.minPos, - .maxPos = cpuMesh.maxPos, + .numIndices = (std::uint32_t)cpuMesh.indices.size(), + .minPos = cpuMesh.minPos, + .maxPos = cpuMesh.maxPos, + .hasSkeleton = cpuMesh.skeleton.has_value(), }; std::vector positions(cpuMesh.vertices.size()); - for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i) { + for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i) positions[i] = cpuMesh.vertices[i].position; - } + gpuMesh.boundingSphere = calculateBoundingSphere(positions); uploadMesh(gfxDevice, cpuMesh, gpuMesh); + const auto id = meshes.size(); meshes.push_back(std::move(gpuMesh)); + cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh return id; } @@ -70,6 +75,33 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh const auto idxBufferName = cpuMesh.name + " (idx)"; vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str()); vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str()); + + if (gpuMesh.hasSkeleton) { + // create skinning data buffer + const auto skinningDataSize = cpuMesh.vertices.size() * sizeof(CPUMesh::SkinningData); + gpuMesh.skinningDataBuffer = gfxDevice.createBuffer( + skinningDataSize, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + + const auto staging = + gfxDevice.createBuffer(skinningDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + + // copy data + void* data = staging.info.pMappedData; + memcpy(data, cpuMesh.skinningData.data(), skinningDataSize); + + gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) { + const auto vertexCopy = VkBufferCopy{ + .srcOffset = 0, + .dstOffset = 0, + .size = skinningDataSize, + }; + vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy); + }); + + gfxDevice.destroyBuffer(staging); + } } const GPUMesh& MeshCache::getMesh(MeshID id) const @@ -77,6 +109,11 @@ const GPUMesh& MeshCache::getMesh(MeshID id) const return meshes.at(id); } +const CPUMesh& MeshCache::getCPUMesh(MeshID id) const +{ + return cpuMeshes.at(id); +} + void MeshCache::cleanup(const GfxDevice& gfxDevice) { for (const auto& mesh : meshes) { diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index 293e5af..ca8c552 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -1,6 +1,9 @@ #include #include +#include "destrum/Graphics/Frustum.h" +#include "spdlog/spdlog.h" + MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} { } @@ -68,6 +71,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd, m_pipeline->bind(cmd); gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout); + int ActualDrawCalls = 0; + const auto viewport = VkViewport{ .x = 0, .y = 0, @@ -86,16 +91,17 @@ void MeshPipeline::draw(VkCommandBuffer cmd, auto prevMeshId = NULL_MESH_ID; - // const auto frustum = edge::createFrustumFromCamera(camera); + const auto frustum = edge::createFrustumFromCamera(camera); for (const auto& dcIdx : drawCommands) { - // const auto& dc = drawCommands[dcIdx]; const auto& dc = dcIdx; // if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { - // continue; + // continue; // } + ActualDrawCalls++; + const auto& mesh = meshCache.getMesh(dc.meshId); if (dc.meshId != prevMeshId) { prevMeshId = dc.meshId; @@ -106,7 +112,7 @@ void MeshPipeline::draw(VkCommandBuffer cmd, const auto pushConstants = PushConstants{ .transform = dc.transformMatrix, .sceneDataBuffer = sceneDataBuffer.address, - .vertexBuffer = mesh.vertexBuffer.address, + .vertexBuffer = dc.skinnedMesh != nullptr ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address, .materialId = dc.materialId, }; vkCmdPushConstants( diff --git a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp index e69de29..297a253 100644 --- a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp @@ -0,0 +1,98 @@ +#include + +#include "destrum/FS/AssetFS.h" +#include "destrum/Graphics/MeshCache.h" +#include "destrum/Graphics/MeshDrawCommand.h" + +void SkinningPipeline::init(GfxDevice& gfxDevice) { + const auto& device = gfxDevice.getDevice(); + + const auto pushConstant = VkPushConstantRange{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + + const auto pushConstants = std::array{pushConstant}; + + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = pushConstants.data(); + + if (vkCreatePipelineLayout(gfxDevice.getDevice().device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("Could not make pipleine layout"); + } + + + ComputePipelineConfigInfo pipelineConfig{}; + pipelineConfig.name = "skinning compute pipeline"; + pipelineConfig.pipelineLayout = m_pipelineLayout; + + const auto SkinningShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/skinning.comp"); + + skinningPipeline = std::make_unique( + gfxDevice, + SkinningShaderPath.generic_string(), + pipelineConfig + ); + + + for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + auto& jointMatricesBuffer = framesData[i].jointMatricesBuffer; + jointMatricesBuffer.capacity = MAX_JOINT_MATRICES; + jointMatricesBuffer.buffer = gfxDevice.createBuffer( + MAX_JOINT_MATRICES * sizeof(glm::mat4), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + } +} + +void SkinningPipeline::cleanup(GfxDevice& gfxDevice) { + for (auto& frame : framesData) { + gfxDevice.destroyBuffer(frame.jointMatricesBuffer.buffer); + } + vkDestroyPipelineLayout(gfxDevice.getDevice().device, m_pipelineLayout, nullptr); + skinningPipeline.reset(); +} + +void SkinningPipeline::doSkinning(VkCommandBuffer cmd, std::size_t frameIndex, const MeshCache& meshCache, const MeshDrawCommand& dc) { + + skinningPipeline->bind(cmd); + + const auto& mesh = meshCache.getMesh(dc.meshId); + assert(mesh.hasSkeleton); + assert(dc.skinnedMesh); + + const auto cs = PushConstants{ + .jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer.address, + .jointMatricesStartIndex = static_cast(dc.jointMatricesStartIndex), // explicit cast + .numVertices = mesh.numVertices, + .inputBuffer = mesh.vertexBuffer.address, + .skinningData = mesh.skinningDataBuffer.address, + .outputBuffer = dc.skinnedMesh->skinnedVertexBuffer.address, + }; + vkCmdPushConstants(cmd, m_pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), &cs); + + static const auto workgroupSize = 256; + // const auto groupSizeX = (std::uint32_t)std::ceil(mesh.numVertices / (float)workgroupSize); + const auto groupSizeX = static_cast( + std::ceil(mesh.numVertices / (float)workgroupSize)); + vkCmdDispatch(cmd, groupSizeX, 1, 1); +} + +void SkinningPipeline::beginDrawing(std::size_t frameIndex) { + getCurrentFrameData(frameIndex).jointMatricesBuffer.clear(); +} + +std::size_t SkinningPipeline::appendJointMatrices(std::span jointMatrices, std::size_t frameIndex) { + + auto& jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer; + const auto startIndex = jointMatricesBuffer.size; + jointMatricesBuffer.append(jointMatrices); + return startIndex; +} + +SkinningPipeline::PerFrameData& SkinningPipeline::getCurrentFrameData(std::size_t frameIndex) { + return framesData[frameIndex % FRAMES_IN_FLIGHT]; +} diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 7bc7612..17a7ea5 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -23,6 +23,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) { skyboxPipeline = std::make_unique(); skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat); + skinningPipeline = std::make_unique(); + skinningPipeline->init(gfxDevice); + GameState::GetInstance().SetRenderer(this); } @@ -30,6 +33,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) { void GameRenderer::beginDrawing(GfxDevice& gfxDevice) { flushMaterialUpdates(gfxDevice); meshDrawCommands.clear(); + skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); } void GameRenderer::endDrawing() { @@ -37,6 +41,13 @@ void GameRenderer::endDrawing() { } void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) { + + for (const auto& dc : meshDrawCommands) { + if (!dc.skinnedMesh) { + continue; + } + skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc); + } const auto gpuSceneData = GPUSceneData{ .view = sceneData.camera.GetViewMatrix(), .proj = sceneData.camera.GetProjectionMatrix(), @@ -107,7 +118,7 @@ void GameRenderer::cleanup(VkDevice device) { void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) { const auto& mesh = meshCache.getMesh(id); - // const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); + const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); assert(materialId != NULL_MATERIAL_ID); meshDrawCommands.push_back(MeshDrawCommand{ @@ -117,6 +128,25 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma }); } +void GameRenderer::drawSkinnedMesh(MeshID id, + const glm::mat4& transform, + MaterialID materialId, + SkinnedMesh* skinnedMesh, + std::size_t jointMatricesStartIndex) { + const auto& mesh = meshCache.getMesh(id); + const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); + assert(materialId != NULL_MATERIAL_ID); + assert(skinnedMesh != nullptr); + + meshDrawCommands.push_back(MeshDrawCommand{ + .meshId = id, + .transformMatrix = transform, + .materialId = materialId, + .skinnedMesh = skinnedMesh, + .jointMatricesStartIndex = static_cast(jointMatricesStartIndex), + }); +} + const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const { return gfx_device.getImage(drawImageId); } diff --git a/destrum/src/Graphics/Util.cpp b/destrum/src/Graphics/Util.cpp index 67df28c..b24a734 100644 --- a/destrum/src/Graphics/Util.cpp +++ b/destrum/src/Graphics/Util.cpp @@ -122,6 +122,16 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } +void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) { + const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, + .objectType = VK_OBJECT_TYPE_PIPELINE, + .objectHandle = (std::uint64_t)pipeline, + .pObjectName = label, + }; + vkSetDebugUtilsObjectNameEXT(device, &nameInfo); +} + void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) { const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, diff --git a/destrum/third_party/CMakeLists.txt b/destrum/third_party/CMakeLists.txt index 137acb1..0c6bed2 100644 --- a/destrum/third_party/CMakeLists.txt +++ b/destrum/third_party/CMakeLists.txt @@ -1,4 +1,5 @@ # glm +set(BUILD_SHARED_LIBS OFF) add_subdirectory(glm) # stb @@ -56,3 +57,10 @@ add_subdirectory(tinygltf) add_subdirectory(tinyexr) target_include_directories(tinyexr PUBLIC "${CMAKE_CURRENT_LIST_DIR}/tinyexr") + + +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE) +set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory(assimp) diff --git a/destrum/third_party/assimp b/destrum/third_party/assimp new file mode 160000 index 0000000..e13e0b5 --- /dev/null +++ b/destrum/third_party/assimp @@ -0,0 +1 @@ +Subproject commit e13e0b5b7da1d6d80b2ee12b043f1253a34d2ff9 diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 3d73dfe..0023acd 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -22,14 +22,26 @@ target_include_directories(lightkeeper PRIVATE "${CMAKE_CURRENT_LIST_DIR}/includ target_link_libraries(lightkeeper PRIVATE destrum::destrum) -set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") -set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") -set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game") +#set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") +#set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") +#set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game") +# +#add_custom_command(TARGET lightkeeper POST_BUILD +# COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}" +# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}" +# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}" +# VERBATIM +#) + +set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") +set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") +set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets/game") add_custom_command(TARGET lightkeeper POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}" - COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}" - COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/assets" + + COMMAND ${CMAKE_COMMAND} -E rm -rf "${CMAKE_CURRENT_BINARY_DIR}/assets/game" + COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/assets/game" VERBATIM ) @@ -47,5 +59,4 @@ add_custom_target(_internal_cook_game_assets ALL DEPENDS TheChef ) - - +destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}") \ No newline at end of file diff --git a/lightkeeper/assets_src/plane.glb b/lightkeeper/assets_src/plane.glb new file mode 100644 index 0000000..1feddc2 Binary files /dev/null and b/lightkeeper/assets_src/plane.glb differ diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index aa5b427..ed90656 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -11,6 +11,7 @@ #include "destrum/Components/OrbitAndSpin.h" #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/ModelLoader.h" +#include "destrum/Components/Animator.h" LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) { } @@ -52,19 +53,19 @@ void LightKeeper::customInit() { const float orbitRadius = 5.0f; for (int i = 0; i < count; ++i) { - auto childCube = std::make_shared(fmt::format("ChildCube{}", i)); - - auto childMeshComp = childCube->AddComponent(); - childMeshComp->SetMeshID(testMeshID); - childMeshComp->SetMaterialID(testMaterialID); - - childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); - - // Add orbit + self spin - auto orbit = childCube->AddComponent(orbitRadius, glm::vec3(0.0f)); - orbit->Randomize(1337u + (uint32_t)i); // stable random per index - - scene.Add(childCube); + // auto childCube = std::make_shared(fmt::format("ChildCube{}", i)); + // + // auto childMeshComp = childCube->AddComponent(); + // childMeshComp->SetMeshID(testMeshID); + // childMeshComp->SetMaterialID(testMaterialID); + // + // childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); + // + // // Add orbit + self spin + // auto orbit = childCube->AddComponent(orbitRadius, glm::vec3(0.0f)); + // orbit->Randomize(1337u + (uint32_t)i); // stable random per index + // + // scene.Add(childCube); } testCube->AddComponent(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec //rotate 180 around X axis @@ -94,6 +95,68 @@ void LightKeeper::customInit() { skyboxCubemap->CreateCubeMap(); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); + + + const auto planeObj = std::make_shared("GroundPlane"); + const auto planeMeshComp = planeObj->AddComponent(); + const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string()); + const auto planeMeshID = meshCache.addMesh(gfxDevice, planeModel[0]); + + const auto planeTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://grass.png")); + const auto planeMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .textureFilteringMode = TextureFilteringMode::Nearest, + .diffuseTexture = planeTextureID, + .name = "GroundPlaneMaterial", + }); + planeMeshComp->SetMeshID(planeMeshID); + planeMeshComp->SetMaterialID(planeMaterialID); + planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); + planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); + scene.Add(planeObj); + + + // At the bottom of customInit(), replace the incomplete CharObj block: + + const auto CharObj = std::make_shared("Character"); + + auto charModel = ModelLoader::LoadSkinnedModel( + AssetFS::GetInstance().GetFullPath("engine://char.fbx").generic_string() + ); + + const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); + + const auto charTextureID = gfxDevice.loadImageFromFile( + AssetFS::GetInstance().GetFullPath("engine://char.jpg")); + const auto charMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .diffuseTexture = charTextureID, + .name = "CharacterMaterial", + }); + + const auto charMeshComp = CharObj->AddComponent(); + charMeshComp->SetMeshID(charMeshID); + charMeshComp->SetMaterialID(charMaterialID); + + + const auto animator = CharObj->AddComponent(); + animator->setSkeleton(std::move(charModel.skeleton)); + for (auto& clip : charModel.animations) { + animator->addClip(std::make_shared(std::move(clip))); + } + + for (const auto& clip : charModel.animations) + spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + + if (!charModel.animations.empty()) + // animator->play(charModel.animations[0].name); + // animator->play("Armature|main"); + animator->play("Armature|mixamo.com"); + // or: animator->play("Run", 0.2f); // 0.2s cross-fade + + CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); + CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); + scene.Add(CharObj); } void LightKeeper::customUpdate(float dt) {