diff --git a/TheChef b/TheChef index df0da96..14f7dd9 160000 --- a/TheChef +++ b/TheChef @@ -1 +1 @@ -Subproject commit df0da96c38fb198869ad71ba15dd5e5af24661d2 +Subproject commit 14f7dd9423de2bf36643837b190347be197147bf diff --git a/destrum/assets_src/char2.fbx b/destrum/assets_src/char2.fbx index c9f094d..1f57ada 100644 Binary files a/destrum/assets_src/char2.fbx and b/destrum/assets_src/char2.fbx differ diff --git a/destrum/assets_src/cotw-capybara-male/source/capybara.fbx b/destrum/assets_src/cotw-capybara-male/source/capybara.fbx new file mode 100644 index 0000000..e66cf41 Binary files /dev/null and b/destrum/assets_src/cotw-capybara-male/source/capybara.fbx differ diff --git a/destrum/assets_src/cotw-capybara-male/textures/capybara_hair_light_brown_alpha_dif.ddsc.D.png b/destrum/assets_src/cotw-capybara-male/textures/capybara_hair_light_brown_alpha_dif.ddsc.D.png new file mode 100644 index 0000000..5f4cf91 Binary files /dev/null and b/destrum/assets_src/cotw-capybara-male/textures/capybara_hair_light_brown_alpha_dif.ddsc.D.png differ diff --git a/destrum/assets_src/cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.png b/destrum/assets_src/cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.png new file mode 100644 index 0000000..e90caa0 Binary files /dev/null and b/destrum/assets_src/cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.png differ diff --git a/destrum/assets_src/shaders/skinning.comp b/destrum/assets_src/shaders/skinning.comp index 5968f1b..de52b67 100644 --- a/destrum/assets_src/shaders/skinning.comp +++ b/destrum/assets_src/shaders/skinning.comp @@ -10,12 +10,14 @@ struct SkinningDataType { vec4 weights; }; -layout (buffer_reference, std430) readonly buffer SkinningData { - SkinningDataType data[]; +// Keep buffer-reference pointer alignment explicit so the C++ push-constant +// struct can safely use 64-bit VkDeviceAddress fields. +layout (buffer_reference, std430, buffer_reference_align = 8) readonly buffer SkinningData { + SkinningDataType data[]; }; -layout (buffer_reference, std430) readonly buffer JointMatrices { - mat4 matrices[]; +layout (buffer_reference, std430, buffer_reference_align = 8) readonly buffer JointMatrices { + mat4 matrices[]; }; layout (push_constant) uniform constants @@ -23,16 +25,45 @@ layout (push_constant) uniform constants JointMatrices jointMatrices; uint jointMatricesStartIndex; uint numVertices; - VertexBuffer inputBuffer; + VertexBuffer inputBuffer; SkinningData skinningData; - VertexBuffer outputBuffer; + 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]; + if (jointId < 0) { + return mat4(1.0); + } + + return pcs.jointMatrices.matrices[pcs.jointMatricesStartIndex + uint(jointId)]; +} + +vec3 safeNormalize(vec3 v, vec3 fallback) { + float len2 = dot(v, v); + if (len2 <= 1e-20) { + return fallback; + } + return v * inversesqrt(len2); +} + +mat4 buildSkinMatrix(SkinningDataType sd) { + // If imported weights are slightly imperfect, normalize them here as a + // second line of defense. If a vertex has no weights at all, pass it + // through unchanged instead of collapsing it to vec3(0). + float weightSum = sd.weights.x + sd.weights.y + sd.weights.z + sd.weights.w; + if (weightSum <= 1e-8) { + return mat4(1.0); + } + + vec4 w = sd.weights / weightSum; + + return + w.x * getJointMatrix(sd.jointIds.x) + + w.y * getJointMatrix(sd.jointIds.y) + + w.z * getJointMatrix(sd.jointIds.z) + + w.w * getJointMatrix(sd.jointIds.w); } void main() @@ -42,19 +73,18 @@ void main() 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]; + SkinningDataType sd = pcs.skinningData.data[index]; + mat4 skinMatrix = buildSkinMatrix(sd); + v.position = vec3(skinMatrix * vec4(v.position, 1.0)); + // For rigid joint matrices this is correct. If you allow non-uniform scale + // in bones, replace this with a proper inverse-transpose normal matrix path. mat3 skinMat3 = mat3(skinMatrix); - v.normal = skinMat3 * v.normal; - v.tangent.xyz = skinMat3 * v.tangent.xyz; // don't transform tangent.w + v.normal = safeNormalize(skinMat3 * v.normal, v.normal); + v.tangent.xyz = safeNormalize(skinMat3 * v.tangent.xyz, v.tangent.xyz); + // v.tangent.w is handedness; do not transform it. pcs.outputBuffer.vertices[index] = v; } diff --git a/destrum/include/destrum/Util/ModelLoader.h b/destrum/include/destrum/Util/ModelLoader.h index b959747..d3b6159 100644 --- a/destrum/include/destrum/Util/ModelLoader.h +++ b/destrum/include/destrum/Util/ModelLoader.h @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -493,7 +494,20 @@ static double GetMaxAnimationKeyTick(const aiAnimation* aiAnim) { static std::vector LoadAnimations(const aiScene* scene, const Skeleton& skeleton) { std::vector result; - if (!scene->HasAnimations() || skeleton.joints.empty()) return result; + + if (!scene->HasAnimations()) { + std::cout << "[ModelLoader] No animations found in scene.\n"; + return result; + } + + if (skeleton.joints.empty()) { + std::cout << "[ModelLoader] Scene has " << scene->mNumAnimations + << " animation(s), but no skeleton joints were loaded.\n"; + return result; + } + + std::cout << "[ModelLoader] Found " << scene->mNumAnimations + << " animation(s). Loading...\n"; const auto jointIndexByName = BuildJointIndexMap(skeleton); @@ -584,9 +598,19 @@ static std::vector LoadAnimations(const aiScene* scene, anim.tracks.push_back(std::move(track)); } + std::cout << "[ModelLoader] Loaded animation [" << a << "]: " + << anim.name << "" + << " | duration: " << anim.duration << "s" + << " | tracks: " << anim.tracks.size() + << " | source channels: " << aiAnim->mNumChannels + << "\n"; + result.push_back(std::move(anim)); } + std::cout << "[ModelLoader] Finished loading " << result.size() + << " skeletal animation(s).\n"; + return result; } @@ -702,6 +726,9 @@ static SkinnedModel LoadSkinnedModel(const std::string& path) { SkinnedModel model; model.skeleton = LoadSkeleton(scene); + std::cout << "[ModelLoader] Loaded skeleton with " << model.skeleton.joints.size() + << " joint(s).\n"; + model.animations = LoadAnimations(scene, model.skeleton); struct StackItem { const aiNode* node; glm::mat4 parentWorld; }; diff --git a/destrum/src/Components/Animator.cpp b/destrum/src/Components/Animator.cpp index 2e70dd0..2dabb17 100644 --- a/destrum/src/Components/Animator.cpp +++ b/destrum/src/Components/Animator.cpp @@ -62,7 +62,17 @@ void Animator::addClip(std::shared_ptr clip) { void Animator::play(const std::string& name, float blendTime) { auto it = m_clips.find(name); - if (it == m_clips.end()) return; + if (it == m_clips.end()) { + spdlog::warn("Animator::play failed. Clip '{}' not found.", name); + + spdlog::info("Available clips:"); + for (const auto& [clipName, _] : m_clips) + spdlog::info(" - '{}'", clipName); + + return; + } + + spdlog::info("Animator playing clip '{}'", name); if (m_current.clip && blendTime > 0.f) { m_previous = m_current; @@ -73,7 +83,7 @@ void Animator::play(const std::string& name, float blendTime) { m_blendT = 1.f; } - m_current = { it->second.get(), 0.f, 1.f }; + m_current = { it->second.get(), 0.f, 1.f }; m_currentClipName = name; } diff --git a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp index 297a253..7305639 100644 --- a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp @@ -4,6 +4,12 @@ #include "destrum/Graphics/MeshCache.h" #include "destrum/Graphics/MeshDrawCommand.h" +#include +#include +#include +#include +#include + void SkinningPipeline::init(GfxDevice& gfxDevice) { const auto& device = gfxDevice.getDevice(); @@ -17,15 +23,13 @@ void SkinningPipeline::init(GfxDevice& gfxDevice) { 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"); + if (vkCreatePipelineLayout(device.device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("Could not make pipeline layout"); } - ComputePipelineConfigInfo pipelineConfig{}; pipelineConfig.name = "skinning compute pipeline"; pipelineConfig.pipelineLayout = m_pipelineLayout; @@ -38,7 +42,6 @@ void SkinningPipeline::init(GfxDevice& gfxDevice) { pipelineConfig ); - for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { auto& jointMatricesBuffer = framesData[i].jointMatricesBuffer; jointMatricesBuffer.capacity = MAX_JOINT_MATRICES; @@ -52,12 +55,15 @@ 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) { - +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); @@ -66,27 +72,55 @@ void SkinningPipeline::doSkinning(VkCommandBuffer cmd, std::size_t frameIndex, c const auto cs = PushConstants{ .jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer.address, - .jointMatricesStartIndex = static_cast(dc.jointMatricesStartIndex), // explicit cast + .jointMatricesStartIndex = static_cast(dc.jointMatricesStartIndex), .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)); + vkCmdPushConstants(cmd, + m_pipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, + 0, + sizeof(PushConstants), + &cs); + + constexpr std::uint32_t workgroupSize = 256; + const auto groupSizeX = static_cast((mesh.numVertices + workgroupSize - 1) / workgroupSize); + if (groupSizeX == 0) { + return; + } + vkCmdDispatch(cmd, groupSizeX, 1, 1); + + // Required before the graphics pass reads skinnedVertexBuffer as a vertex buffer. + // Without this, the draw can see stale/partial data from before the compute dispatch. + VkBufferMemoryBarrier skinnedVertexBarrier{}; + skinnedVertexBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + skinnedVertexBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + skinnedVertexBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT; + skinnedVertexBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + skinnedVertexBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + skinnedVertexBarrier.buffer = dc.skinnedMesh->skinnedVertexBuffer.buffer; + skinnedVertexBarrier.offset = 0; + skinnedVertexBarrier.size = VK_WHOLE_SIZE; + + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, + 0, + 0, nullptr, + 1, &skinnedVertexBarrier, + 0, nullptr); } void SkinningPipeline::beginDrawing(std::size_t frameIndex) { getCurrentFrameData(frameIndex).jointMatricesBuffer.clear(); } -std::size_t SkinningPipeline::appendJointMatrices(std::span jointMatrices, std::size_t frameIndex) { - +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); diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 33fd4cb..e56ee3d 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -122,12 +122,13 @@ void LightKeeper::customInit() { const auto CharObj = std::make_shared("Character"); auto charModel = ModelLoader::LoadSkinnedModel( - AssetFS::GetInstance().GetFullPath("engine://char2.fbx").generic_string() + AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string() ); const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); - const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://textures/T_T.png")); + const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.png")); + // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); const auto charMaterialID = materialCache.addMaterial(gfxDevice, { .baseColor = glm::vec3(1.f), .diffuseTexture = charTextureID, @@ -141,18 +142,24 @@ void LightKeeper::customInit() { const auto animator = CharObj->AddComponent(); animator->setSkeleton(std::move(charModel.skeleton)); + + std::string firstAnimationName; + for (auto& clip : charModel.animations) { + spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + + if (firstAnimationName.empty()) + firstAnimationName = clip.name; + 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 (!firstAnimationName.empty()) { + spdlog::info("Playing animation: '{}'", firstAnimationName); + animator->play(firstAnimationName); + } - if (!charModel.animations.empty()) - // animator->play("Armature|Armature|mixamo.com"); - // // animator->play(charModel.animations[0].name); - animator->play("Armature|Armature|Armature|main"); - // or: animator->play("Run", 0.2f); // 0.2s cross-fade + animator->play("capybara_canter_fwd_01|capybara_walk_fwd_01"); CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);