From 4a4fbb8aa902eb4e3b8585540c064cca13c641bb Mon Sep 17 00:00:00 2001 From: Gopmyc <74107065+Gopmyc@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:34:27 +0200 Subject: [PATCH 01/18] Implement skinned mesh animation pipeline with Lua bindings --- Resources/Editor/Shaders/OutlineFallback.ovfx | 14 +- Resources/Editor/Shaders/PickingFallback.ovfx | 14 +- .../Shaders/Common/Buffers/SkinningSSBO.ovfxh | 4 + .../Engine/Shaders/Common/Skinning.ovfxh | 47 + Resources/Engine/Shaders/ShadowFallback.ovfx | 13 +- Resources/Engine/Shaders/Standard.ovfx | 17 +- Resources/Engine/Shaders/Unlit.ovfx | 13 +- .../ECS/Components/CSkinnedMeshRenderer.h | 245 +++++ .../Rendering/SkinningDrawableDescriptor.h | 24 + .../OvCore/Rendering/SkinningRenderFeature.h | 76 ++ .../include/OvCore/Rendering/SkinningUtils.h | 53 ++ Sources/OvCore/src/OvCore/ECS/Actor.cpp | 2 + .../OvCore/ECS/Components/CModelRenderer.cpp | 14 +- .../ECS/Components/CSkinnedMeshRenderer.cpp | 868 ++++++++++++++++++ .../src/OvCore/Rendering/SceneRenderer.cpp | 33 +- .../src/OvCore/Rendering/ShadowRenderPass.cpp | 29 +- .../Rendering/SkinningRenderFeature.cpp | 153 +++ .../src/OvCore/Rendering/SkinningUtils.cpp | 54 ++ .../Lua/Bindings/LuaActorBindings.cpp | 4 + .../Lua/Bindings/LuaComponentsBindings.cpp | 28 + .../OvEditor/Rendering/OutlineRenderFeature.h | 20 +- .../src/OvEditor/Core/EditorResources.cpp | 34 +- .../src/OvEditor/Panels/Inspector.cpp | 2 + .../Rendering/OutlineRenderFeature.cpp | 109 ++- .../OvEditor/Rendering/PickingRenderPass.cpp | 49 +- .../OvRendering/Animation/SkeletalData.h | 117 +++ .../include/OvRendering/Geometry/Vertex.h | 6 +- .../include/OvRendering/Resources/Mesh.h | 11 +- .../include/OvRendering/Resources/Model.h | 27 +- .../Resources/Parsers/AssimpParser.h | 25 +- .../Resources/Parsers/IModelParser.h | 6 +- .../Resources/Loaders/ModelLoader.cpp | 15 +- .../src/OvRendering/Resources/Mesh.cpp | 111 ++- .../src/OvRendering/Resources/Model.cpp | 32 +- .../Resources/Parsers/AssimpParser.cpp | 390 +++++++- 35 files changed, 2530 insertions(+), 129 deletions(-) create mode 100644 Resources/Engine/Shaders/Common/Buffers/SkinningSSBO.ovfxh create mode 100644 Resources/Engine/Shaders/Common/Skinning.ovfxh create mode 100644 Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h create mode 100644 Sources/OvCore/include/OvCore/Rendering/SkinningDrawableDescriptor.h create mode 100644 Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h create mode 100644 Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h create mode 100644 Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp create mode 100644 Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp create mode 100644 Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp create mode 100644 Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h diff --git a/Resources/Editor/Shaders/OutlineFallback.ovfx b/Resources/Editor/Shaders/OutlineFallback.ovfx index eb19c8396..0e8793bc9 100644 --- a/Resources/Editor/Shaders/OutlineFallback.ovfx +++ b/Resources/Editor/Shaders/OutlineFallback.ovfx @@ -1,7 +1,14 @@ +#pass OUTLINE_PASS +#feature SKINNING + #shader vertex #version 450 core layout(location = 0) in vec3 geo_Pos; +layout(location = 5) in vec4 geo_BoneIDs; +layout(location = 6) in vec4 geo_BoneWeights; + +#include ":Shaders/Common/Skinning.ovfxh" layout(std140) uniform EngineUBO { @@ -14,7 +21,12 @@ layout(std140) uniform EngineUBO void main() { - gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0); + mat4 skinningMatrix = mat4(1.0); +#if defined(SKINNING) + skinningMatrix = ComputeSkinningMatrix(geo_BoneIDs, geo_BoneWeights); +#endif + + gl_Position = ubo_Projection * ubo_View * ubo_Model * skinningMatrix * vec4(geo_Pos, 1.0); } #shader fragment diff --git a/Resources/Editor/Shaders/PickingFallback.ovfx b/Resources/Editor/Shaders/PickingFallback.ovfx index 4674e5535..373ba1d88 100644 --- a/Resources/Editor/Shaders/PickingFallback.ovfx +++ b/Resources/Editor/Shaders/PickingFallback.ovfx @@ -1,7 +1,12 @@ +#pass PICKING_PASS +#feature SKINNING + #shader vertex #version 450 core layout (location = 0) in vec3 geo_Pos; +layout (location = 5) in vec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; layout (std140) uniform EngineUBO { @@ -12,9 +17,16 @@ layout (std140) uniform EngineUBO float ubo_Time; }; +#include ":Shaders/Common/Skinning.ovfxh" + void main() { - gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0); + mat4 skinningMatrix = mat4(1.0); +#if defined(SKINNING) + skinningMatrix = ComputeSkinningMatrix(geo_BoneIDs, geo_BoneWeights); +#endif + + gl_Position = ubo_Projection * ubo_View * ubo_Model * skinningMatrix * vec4(geo_Pos, 1.0); } #shader fragment diff --git a/Resources/Engine/Shaders/Common/Buffers/SkinningSSBO.ovfxh b/Resources/Engine/Shaders/Common/Buffers/SkinningSSBO.ovfxh new file mode 100644 index 000000000..69fcc7ff0 --- /dev/null +++ b/Resources/Engine/Shaders/Common/Buffers/SkinningSSBO.ovfxh @@ -0,0 +1,4 @@ +layout(std430, binding = 1) buffer SkinningSSBO +{ + mat4 ssbo_Bones[]; +}; diff --git a/Resources/Engine/Shaders/Common/Skinning.ovfxh b/Resources/Engine/Shaders/Common/Skinning.ovfxh new file mode 100644 index 000000000..ad0d90b64 --- /dev/null +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -0,0 +1,47 @@ +#include ":Shaders/Common/Buffers/SkinningSSBO.ovfxh" + +void ApplyBone( + float p_boneID, + float p_boneWeight, + int p_boneCount, + inout mat4 p_skinning, + inout float p_appliedWeight +) +{ + if (p_boneWeight > 0.0) + { + const int index = int(floor(p_boneID + 0.5)); + if (index >= 0 && index < p_boneCount) + { + p_skinning += ssbo_Bones[index] * p_boneWeight; + p_appliedWeight += p_boneWeight; + } + } +} + +mat4 ComputeSkinningMatrix(vec4 p_boneIDs, vec4 p_boneWeights) +{ + const int boneCount = ssbo_Bones.length(); + + mat4 skinning = mat4(0.0); + float appliedWeight = 0.0; + + for (int i = 0; i < 4; ++i) + { + ApplyBone(p_boneIDs[i], p_boneWeights[i], boneCount, skinning, appliedWeight); + } + + if (appliedWeight <= 0.0) + { + return mat4(1.0); + } + + if (appliedWeight == 1.0) + { + return skinning; + } + + return appliedWeight < 1.0 ? + skinning + mat4(1.0 - appliedWeight) : + skinning / appliedWeight; +} diff --git a/Resources/Engine/Shaders/ShadowFallback.ovfx b/Resources/Engine/Shaders/ShadowFallback.ovfx index ff1f3dc07..ebbd6804d 100644 --- a/Resources/Engine/Shaders/ShadowFallback.ovfx +++ b/Resources/Engine/Shaders/ShadowFallback.ovfx @@ -1,13 +1,24 @@ +#pass SHADOW_PASS +#feature SKINNING + #shader vertex #version 450 core layout (location = 0) in vec3 geo_Pos; +layout (location = 5) in vec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; #include ":Shaders/Common/Buffers/EngineUBO.ovfxh" +#include ":Shaders/Common/Skinning.ovfxh" void main() { - gl_Position = ubo_Projection * ubo_View * ubo_Model * vec4(geo_Pos, 1.0); + mat4 skinningMatrix = mat4(1.0); +#if defined(SKINNING) + skinningMatrix = ComputeSkinningMatrix(geo_BoneIDs, geo_BoneWeights); +#endif + + gl_Position = ubo_Projection * ubo_View * ubo_Model * skinningMatrix * vec4(geo_Pos, 1.0); } #shader fragment diff --git a/Resources/Engine/Shaders/Standard.ovfx b/Resources/Engine/Shaders/Standard.ovfx index 5092a0771..7de497d90 100644 --- a/Resources/Engine/Shaders/Standard.ovfx +++ b/Resources/Engine/Shaders/Standard.ovfx @@ -5,11 +5,13 @@ #feature NORMAL_MAPPING #feature DISTANCE_FADE #feature SPECULAR_WORKFLOW +#feature SKINNING #shader vertex #version 450 core #include ":Shaders/Common/Buffers/EngineUBO.ovfxh" +#include ":Shaders/Common/Skinning.ovfxh" #include ":Shaders/Common/Utils.ovfxh" layout (location = 0) in vec3 geo_Pos; @@ -17,6 +19,8 @@ layout (location = 1) in vec2 geo_TexCoords; layout (location = 2) in vec3 geo_Normal; layout (location = 3) in vec3 geo_Tangent; layout (location = 4) in vec3 geo_Bitangent; +layout (location = 5) in vec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; out VS_OUT { @@ -32,10 +36,17 @@ out VS_OUT void main() { - vs_out.FragPos = vec3(ubo_Model * vec4(geo_Pos, 1.0)); + mat4 skinningMatrix = mat4(1.0); +#if defined(SKINNING) + skinningMatrix = ComputeSkinningMatrix(geo_BoneIDs, geo_BoneWeights); +#endif + + const mat4 modelMatrix = ubo_Model * skinningMatrix; + + vs_out.FragPos = vec3(modelMatrix * vec4(geo_Pos, 1.0)); vs_out.TexCoords = geo_TexCoords; - vs_out.Normal = normalize(mat3(transpose(inverse(ubo_Model))) * geo_Normal); - vs_out.TBN = ConstructTBN(ubo_Model, geo_Normal, geo_Tangent, geo_Bitangent); + vs_out.Normal = normalize(mat3(transpose(inverse(modelMatrix))) * geo_Normal); + vs_out.TBN = ConstructTBN(modelMatrix, geo_Normal, geo_Tangent, geo_Bitangent); #if defined(PARALLAX_MAPPING) const mat3 TBNi = transpose(vs_out.TBN); diff --git a/Resources/Engine/Shaders/Unlit.ovfx b/Resources/Engine/Shaders/Unlit.ovfx index 40b8d7f85..8feb4d651 100644 --- a/Resources/Engine/Shaders/Unlit.ovfx +++ b/Resources/Engine/Shaders/Unlit.ovfx @@ -1,15 +1,19 @@ #pass SHADOW_PASS #feature ALPHA_CLIPPING +#feature SKINNING #shader vertex #version 450 core #include ":Shaders/Common/Buffers/EngineUBO.ovfxh" +#include ":Shaders/Common/Skinning.ovfxh" #include ":Shaders/Common/Utils.ovfxh" layout (location = 0) in vec3 geo_Pos; layout (location = 1) in vec2 geo_TexCoords; +layout (location = 5) in vec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; out VS_OUT { @@ -19,7 +23,14 @@ out VS_OUT void main() { - vs_out.FragPos = vec3(ubo_Model * vec4(geo_Pos, 1.0)); + mat4 skinningMatrix = mat4(1.0); +#if defined(SKINNING) + skinningMatrix = ComputeSkinningMatrix(geo_BoneIDs, geo_BoneWeights); +#endif + + const mat4 modelMatrix = ubo_Model * skinningMatrix; + + vs_out.FragPos = vec3(modelMatrix * vec4(geo_Pos, 1.0)); vs_out.TexCoords = geo_TexCoords; gl_Position = ubo_Projection * ubo_View * vec4(vs_out.FragPos, 1.0); diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h new file mode 100644 index 000000000..b82d53404 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -0,0 +1,245 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace OvCore::ECS { class Actor; } +namespace OvRendering::Resources { class Model; } + +namespace OvCore::ECS::Components +{ + /** + * Component responsible for skeletal animation playback and skinning data generation. + */ + class CSkinnedMeshRenderer : public AComponent + { + public: + /** + * Constructor + * @param p_owner + */ + CSkinnedMeshRenderer(ECS::Actor& p_owner); + + /** + * Returns the name of the component + */ + std::string GetName() override; + + /** + * Returns the type name of the component + */ + virtual std::string GetTypeName() override; + + /** + * Called by the model renderer when its model changes + */ + void NotifyModelChanged(); + + /** + * Returns true if a valid skinned model is currently attached + */ + bool HasCompatibleModel() const; + + /** + * Returns true if the component has a valid skinning palette ready for rendering + */ + bool HasSkinningData() const; + + /** + * Enable / disable this component + * @param p_value + */ + void SetEnabled(bool p_value); + + /** + * Returns true if the component is enabled + */ + bool IsEnabled() const; + + /** + * Start animation playback + */ + void Play(); + + /** + * Pause animation playback + */ + void Pause(); + + /** + * Stop animation playback and reset time to 0 + */ + void Stop(); + + /** + * Returns true if playback is active + */ + bool IsPlaying() const; + + /** + * Sets loop mode + * @param p_value + */ + void SetLooping(bool p_value); + + /** + * Returns true if loop mode is enabled + */ + bool IsLooping() const; + + /** + * Set playback speed + * @param p_value + */ + void SetPlaybackSpeed(float p_value); + + /** + * Get playback speed + */ + float GetPlaybackSpeed() const; + + /** + * Sets the current playback time in seconds + * @param p_timeSeconds + */ + void SetTime(float p_timeSeconds); + + /** + * Returns the current playback time in seconds + */ + float GetTime() const; + + /** + * Returns the number of available animations + */ + uint32_t GetAnimationCount() const; + + /** + * Returns the animation name at index (std::nullopt if index is invalid) + * @param p_index + */ + std::optional GetAnimationName(uint32_t p_index) const; + + /** + * Sets the active animation by index + * @param p_index + */ + bool SetAnimation(uint32_t p_index); + + /** + * Sets the active animation by name + * @param p_name + */ + bool SetAnimation(const std::string& p_name); + + /** + * Returns the active animation index (-1 if none) + */ + int32_t GetAnimationIndex() const; + + /** + * Returns the active animation name (empty if none) + */ + std::string GetAnimation() const; + + /** + * Returns the current skinning matrix palette + */ + const std::vector& GetBoneMatrices() const; + + /** + * Returns the transposed skinning matrix palette ready for GPU upload + */ + const std::vector& GetBoneMatricesTransposed() const; + + /** + * Returns an incrementing pose version, updated whenever bone matrices change + */ + uint64_t GetPoseVersion() const; + + /** + * Called each frame by the actor + * @param p_deltaTime + */ + void OnUpdate(float p_deltaTime) override; + + /** + * Serialize the component + * @param p_doc + * @param p_node + */ + virtual void OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Deserialize the component + * @param p_doc + * @param p_node + */ + virtual void OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) override; + + /** + * Draw component inspector widgets + * @param p_root + */ + virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; + + private: + struct TrackSamplingCursor + { + size_t positionKeyIndex = 0; + size_t rotationKeyIndex = 0; + size_t scaleKeyIndex = 0; + }; + + void SyncWithModel(); + void RebuildRuntimeData(); + void EvaluatePose(); + float GetAnimationDurationSeconds() const; + void UpdatePlayback(float p_deltaTime); + + private: + const OvRendering::Resources::Model* m_model = nullptr; + + bool m_enabled = true; + bool m_playing = true; + bool m_looping = true; + float m_playbackSpeed = 1.0f; + float m_poseEvaluationRate = 60.0f; + float m_poseEvaluationAccumulator = 0.0f; + + float m_currentTimeTicks = 0.0f; + std::optional m_animationIndex = std::nullopt; + std::string m_deserializedAnimationName; + + uint64_t m_poseVersion = 0; + + std::vector m_animationNames; + std::vector m_bindPoseTransforms; + std::vector m_localPose; + std::vector m_globalPose; + std::vector m_boneMatrices; + std::vector m_boneMatricesTransposed; + std::vector m_trackSamplingCursors; + float m_lastSampleTimeTicks = 0.0f; + std::optional m_lastSampledAnimationIndex = std::nullopt; + }; + + template<> + struct ComponentTraits + { + static constexpr std::string_view Name = "class OvCore::ECS::Components::CSkinnedMeshRenderer"; + }; +} diff --git a/Sources/OvCore/include/OvCore/Rendering/SkinningDrawableDescriptor.h b/Sources/OvCore/include/OvCore/Rendering/SkinningDrawableDescriptor.h new file mode 100644 index 000000000..d34c139af --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/SkinningDrawableDescriptor.h @@ -0,0 +1,24 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include + +namespace OvCore::Rendering +{ + /** + * Descriptor attached to drawables that require skinning. + */ + struct SkinningDrawableDescriptor + { + const OvMaths::FMatrix4* matrices = nullptr; + uint32_t count = 0; + uint64_t poseVersion = 0; + }; +} diff --git a/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h b/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h new file mode 100644 index 000000000..534c40bdb --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h @@ -0,0 +1,76 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace OvCore::Rendering +{ + /** + * Render feature responsible for uploading and binding skinning matrices. + */ + class SkinningRenderFeature : public OvRendering::Features::ARenderFeature + { + public: + static constexpr uint32_t kDefaultBufferBindingPoint = 1; + + /** + * Constructor + * @param p_renderer + * @param p_executionPolicy + * @param p_bufferBindingPoint + */ + SkinningRenderFeature( + OvRendering::Core::CompositeRenderer& p_renderer, + OvRendering::Features::EFeatureExecutionPolicy p_executionPolicy, + uint32_t p_bufferBindingPoint = kDefaultBufferBindingPoint + ); + + /** + * Returns the skinning buffer binding point + */ + uint32_t GetBufferBindingPoint() const; + + protected: + virtual void OnBeginFrame(const OvRendering::Data::FrameDescriptor& p_frameDescriptor) override; + virtual void OnEndFrame() override; + virtual void OnBeforeDraw(OvRendering::Data::PipelineState& p_pso, const OvRendering::Entities::Drawable& p_drawable) override; + + private: + void BindIdentityPalette() const; + OvRendering::HAL::ShaderStorageBuffer& GetCurrentSkinningBuffer() const; + + private: + static constexpr uint32_t kSkinningBufferRingSize = 3; + + enum class EBoundPalette + { + NONE, + IDENTITY, + SKINNING + }; + + uint32_t m_bufferBindingPoint; + mutable uint32_t m_skinningBufferIndex = kSkinningBufferRingSize - 1; + std::array, kSkinningBufferRingSize> m_skinningBuffers; + std::unique_ptr m_identityBuffer; + + const OvMaths::FMatrix4* m_lastPalettePtr = nullptr; + uint32_t m_lastPaletteCount = 0; + uint64_t m_lastPoseVersion = 0; + mutable EBoundPalette m_boundPalette = EBoundPalette::NONE; + mutable const OvMaths::FMatrix4* m_boundPalettePtr = nullptr; + mutable uint32_t m_boundPaletteCount = 0; + mutable uint64_t m_boundPoseVersion = 0; + }; +} diff --git a/Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h b/Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h new file mode 100644 index 000000000..b5976a4a7 --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h @@ -0,0 +1,53 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include + +#include + +namespace OvCore::ECS::Components { class CSkinnedMeshRenderer; } +namespace OvRendering::Entities { struct Drawable; } + +namespace OvCore::Rendering::SkinningUtils +{ + inline constexpr std::string_view kFeatureName = "SKINNING"; + + /** + * Returns true when a skinned renderer can provide valid skinning data. + * @param p_renderer + */ + bool IsSkinningActive(const OvCore::ECS::Components::CSkinnedMeshRenderer* p_renderer); + + /** + * Builds a feature set containing the skinning feature. + * @param p_baseFeatures + */ + OvRendering::Data::FeatureSet BuildFeatureSet(const OvRendering::Data::FeatureSet* p_baseFeatures = nullptr); + + /** + * Adds the skinning descriptor to the target drawable. + * @param p_drawable + * @param p_renderer + */ + void ApplyDescriptor( + OvRendering::Entities::Drawable& p_drawable, + const OvCore::ECS::Components::CSkinnedMeshRenderer& p_renderer + ); + + /** + * Applies both feature override and skinning descriptor to a drawable. + * @param p_drawable + * @param p_renderer + * @param p_baseFeatures + */ + void ApplyToDrawable( + OvRendering::Entities::Drawable& p_drawable, + const OvCore::ECS::Components::CSkinnedMeshRenderer& p_renderer, + const OvRendering::Data::FeatureSet* p_baseFeatures = nullptr + ); +} diff --git a/Sources/OvCore/src/OvCore/ECS/Actor.cpp b/Sources/OvCore/src/OvCore/ECS/Actor.cpp index 2ab0ae9b8..7c009a499 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -480,6 +481,7 @@ void OvCore::ECS::Actor::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2:: else if (IsType(componentType)) component = &AddComponent(); else if (IsType(componentType)) component = &AddComponent(); else if (IsType(componentType)) component = &AddComponent(); + else if (IsType(componentType)) component = &AddComponent(); if (component) { diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp index 0c33fe22c..de0a52b87 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,15 @@ void OvCore::ECS::Components::CModelRenderer::SetModel(OvRendering::Resources::M { m_model = p_model; m_modelChangedEvent.Invoke(); + + if (m_model && m_model->IsSkinned()) + { + owner.AddComponent().NotifyModelChanged(); + } + else if (auto skinnedRenderer = owner.GetComponent()) + { + skinnedRenderer->NotifyModelChanged(); + } } OvRendering::Resources::Model * OvCore::ECS::Components::CModelRenderer::GetModel() const @@ -80,7 +90,9 @@ void OvCore::ECS::Components::CModelRenderer::OnSerialize(tinyxml2::XMLDocument void OvCore::ECS::Components::CModelRenderer::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode* p_node) { - OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", m_model); + OvRendering::Resources::Model* deserializedModel = nullptr; + OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", deserializedModel); + SetModel(deserializedModel); OvCore::Helpers::Serializer::DeserializeInt(p_doc, p_node, "frustum_behaviour", reinterpret_cast(m_frustumBehaviour)); OvCore::Helpers::Serializer::DeserializeVec3(p_doc, p_node, "custom_bounding_sphere_position", m_customBoundingSphere.position); OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "custom_bounding_sphere_radius", m_customBoundingSphere.radius); diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp new file mode 100644 index 000000000..44a6a7175 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -0,0 +1,868 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr float kDefaultTicksPerSecond = 25.0f; + + OvMaths::FTransform DecomposeTransform(const OvMaths::FMatrix4& p_matrix) + { + constexpr float kEpsilon = 1e-8f; + OvMaths::FVector3 position = OvMaths::FVector3::Zero; + OvMaths::FQuaternion rotation = OvMaths::FQuaternion::Identity; + OvMaths::FVector3 scale = OvMaths::FVector3::One; + + position = { + p_matrix.data[3], + p_matrix.data[7], + p_matrix.data[11] + }; + + OvMaths::FVector3 columns[3] = { + { p_matrix.data[0], p_matrix.data[4], p_matrix.data[8] }, + { p_matrix.data[1], p_matrix.data[5], p_matrix.data[9] }, + { p_matrix.data[2], p_matrix.data[6], p_matrix.data[10] } + }; + + scale.x = OvMaths::FVector3::Length(columns[0]); + scale.y = OvMaths::FVector3::Length(columns[1]); + scale.z = OvMaths::FVector3::Length(columns[2]); + + if (scale.x > kEpsilon) columns[0] /= scale.x; + if (scale.y > kEpsilon) columns[1] /= scale.y; + if (scale.z > kEpsilon) columns[2] /= scale.z; + + const float basisDeterminant = OvMaths::FVector3::Dot( + OvMaths::FVector3::Cross(columns[0], columns[1]), + columns[2] + ); + + if (basisDeterminant < 0.0f) + { + if (scale.x >= scale.y && scale.x >= scale.z) + { + scale.x = -scale.x; + columns[0] = -columns[0]; + } + else if (scale.y >= scale.x && scale.y >= scale.z) + { + scale.y = -scale.y; + columns[1] = -columns[1]; + } + else + { + scale.z = -scale.z; + columns[2] = -columns[2]; + } + } + + const OvMaths::FMatrix3 rotationMatrix{ + columns[0].x, columns[1].x, columns[2].x, + columns[0].y, columns[1].y, columns[2].y, + columns[0].z, columns[1].z, columns[2].z + }; + + rotation = OvMaths::FQuaternion::Normalize(OvMaths::FQuaternion(rotationMatrix)); + return OvMaths::FTransform(position, rotation, scale); + } + + float WrapTime(float p_value, float p_duration) + { + if (p_duration <= 0.0f) + { + return 0.0f; + } + + const auto wrapped = std::fmod(p_value, p_duration); + return wrapped < 0.0f ? wrapped + p_duration : wrapped; + } + + template + T SampleKeys( + const std::vector>& p_keys, + float p_time, + float p_duration, + const T& p_defaultValue, + bool p_looping, + TLerp p_lerp, + size_t* p_cursor = nullptr, + bool p_useCursorHint = false + ) + { + if (p_keys.empty()) + { + return p_defaultValue; + } + + if (p_keys.size() == 1) + { + return p_keys.front().value; + } + + const auto sampleWithSegment = [&](const auto& p_prev, const auto& p_next, float p_segmentDuration, float p_alphaBias = 0.0f) + { + if (p_segmentDuration <= std::numeric_limits::epsilon()) + { + return p_prev.value; + } + + const float alpha = std::clamp(((p_time - p_prev.time) + p_alphaBias) / p_segmentDuration, 0.0f, 1.0f); + return p_lerp(p_prev.value, p_next.value, alpha); + }; + + if (p_useCursorHint && p_cursor) + { + size_t cursor = std::min(*p_cursor, p_keys.size() - 1); + while (cursor + 1 < p_keys.size() && p_time >= p_keys[cursor + 1].time) + { + ++cursor; + } + + *p_cursor = cursor; + + if (cursor + 1 < p_keys.size()) + { + return sampleWithSegment(p_keys[cursor], p_keys[cursor + 1], p_keys[cursor + 1].time - p_keys[cursor].time); + } + + if (!p_looping) + { + return p_keys.back().value; + } + + const auto& prev = p_keys.back(); + const auto& next = p_keys.front(); + const float segmentDuration = (p_duration - prev.time) + next.time; + return sampleWithSegment(prev, next, segmentDuration); + } + + if (!p_looping) + { + if (p_time <= p_keys.front().time) + { + if (p_cursor) + { + *p_cursor = 0; + } + + return p_keys.front().value; + } + + if (p_time >= p_keys.back().time) + { + if (p_cursor) + { + *p_cursor = p_keys.size() - 1; + } + + return p_keys.back().value; + } + } + + const auto nextIt = std::upper_bound( + p_keys.begin(), + p_keys.end(), + p_time, + [](float p_lhs, const auto& p_rhs) { return p_lhs < p_rhs.time; } + ); + + if (nextIt == p_keys.end()) + { + if (!p_looping) + { + if (p_cursor) + { + *p_cursor = p_keys.size() - 1; + } + + return p_keys.back().value; + } + + const auto& prev = p_keys.back(); + const auto& next = p_keys.front(); + + const float segmentDuration = (p_duration - prev.time) + next.time; + if (p_cursor) + { + *p_cursor = p_keys.size() - 1; + } + + return sampleWithSegment(prev, next, segmentDuration); + } + + if (nextIt == p_keys.begin()) + { + if (p_cursor) + { + *p_cursor = 0; + } + + return nextIt->value; + } + + const auto& prev = *std::prev(nextIt); + const auto& next = *nextIt; + if (p_cursor) + { + *p_cursor = static_cast(std::distance(p_keys.begin(), std::prev(nextIt))); + } + + return sampleWithSegment(prev, next, next.time - prev.time); + } +} + +OvCore::ECS::Components::CSkinnedMeshRenderer::CSkinnedMeshRenderer(ECS::Actor& p_owner) : + AComponent(p_owner) +{ + NotifyModelChanged(); +} + +std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetName() +{ + return "Skinned Mesh Renderer"; +} + +std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetTypeName() +{ + return std::string{ ComponentTraits::Name }; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::NotifyModelChanged() +{ + m_model = nullptr; + SyncWithModel(); +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasCompatibleModel() const +{ + return m_model && m_model->IsSkinned() && m_model->GetSkeleton().has_value(); +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasSkinningData() const +{ + return HasCompatibleModel() && !m_boneMatrices.empty(); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::SetEnabled(bool p_value) +{ + m_enabled = p_value; +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::IsEnabled() const +{ + return m_enabled; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::Play() +{ + m_playing = true; + m_poseEvaluationAccumulator = 0.0f; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::Pause() +{ + m_playing = false; + m_poseEvaluationAccumulator = 0.0f; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::Stop() +{ + m_playing = false; + m_currentTimeTicks = 0.0f; + m_poseEvaluationAccumulator = 0.0f; + EvaluatePose(); +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::IsPlaying() const +{ + return m_playing; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::SetLooping(bool p_value) +{ + m_looping = p_value; +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::IsLooping() const +{ + return m_looping; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::SetPlaybackSpeed(float p_value) +{ + m_playbackSpeed = p_value; +} + +float OvCore::ECS::Components::CSkinnedMeshRenderer::GetPlaybackSpeed() const +{ + return m_playbackSpeed; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::SetTime(float p_timeSeconds) +{ + if (!HasCompatibleModel() || !m_animationIndex.has_value()) + { + return; + } + + const auto& animation = m_model->GetAnimations().at(*m_animationIndex); + const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; + + m_currentTimeTicks = p_timeSeconds * ticksPerSecond; + if (m_looping) + { + m_currentTimeTicks = WrapTime(m_currentTimeTicks, animation.duration); + } + else + { + m_currentTimeTicks = std::clamp(m_currentTimeTicks, 0.0f, animation.duration); + } + + m_poseEvaluationAccumulator = 0.0f; + EvaluatePose(); +} + +float OvCore::ECS::Components::CSkinnedMeshRenderer::GetTime() const +{ + if (!HasCompatibleModel() || !m_animationIndex.has_value()) + { + return 0.0f; + } + + const auto& animation = m_model->GetAnimations().at(*m_animationIndex); + const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; + return ticksPerSecond > 0.0f ? m_currentTimeTicks / ticksPerSecond : 0.0f; +} + +uint32_t OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationCount() const +{ + return static_cast(m_animationNames.size()); +} + +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationName(uint32_t p_index) const +{ + if (p_index < m_animationNames.size()) + { + return m_animationNames[p_index]; + } + + return std::nullopt; +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(uint32_t p_index) +{ + if (!HasCompatibleModel() || p_index >= m_model->GetAnimations().size()) + { + return false; + } + + m_animationIndex = p_index; + m_currentTimeTicks = 0.0f; + m_poseEvaluationAccumulator = 0.0f; + EvaluatePose(); + return true; +} + +bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(const std::string& p_name) +{ + if (!HasCompatibleModel()) + { + return false; + } + + const auto& animations = m_model->GetAnimations(); + + const auto found = std::find_if(animations.begin(), animations.end(), [&p_name](const auto& p_animation) + { + return p_animation.name == p_name; + }); + + if (found == animations.end()) + { + return false; + } + + return SetAnimation(static_cast(std::distance(animations.begin(), found))); +} + +int32_t OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationIndex() const +{ + return m_animationIndex.has_value() ? static_cast(*m_animationIndex) : -1; +} + +std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimation() const +{ + if (!m_animationIndex.has_value()) + { + return {}; + } + + const auto animationName = GetAnimationName(*m_animationIndex); + return animationName.has_value() ? *animationName : std::string{}; +} + +const std::vector& OvCore::ECS::Components::CSkinnedMeshRenderer::GetBoneMatrices() const +{ + return m_boneMatrices; +} + +const std::vector& OvCore::ECS::Components::CSkinnedMeshRenderer::GetBoneMatricesTransposed() const +{ + return m_boneMatricesTransposed; +} + +uint64_t OvCore::ECS::Components::CSkinnedMeshRenderer::GetPoseVersion() const +{ + return m_poseVersion; +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::OnUpdate(float p_deltaTime) +{ + if (!owner.IsActive()) + { + return; + } + + SyncWithModel(); + + if (!m_enabled || !HasCompatibleModel()) + { + return; + } + + if (m_playing) + { + const float previousTimeTicks = m_currentTimeTicks; + const bool wasPlaying = m_playing; + + UpdatePlayback(p_deltaTime); + + const bool timeChanged = std::abs(m_currentTimeTicks - previousTimeTicks) > std::numeric_limits::epsilon(); + const bool playbackStateChanged = wasPlaying != m_playing; + + if (timeChanged || playbackStateChanged) + { + const float clampedPoseEvaluationRate = std::max(0.0f, m_poseEvaluationRate); + const bool hasRateLimit = clampedPoseEvaluationRate > std::numeric_limits::epsilon(); + + if (hasRateLimit) + { + m_poseEvaluationAccumulator += p_deltaTime; + const float updatePeriod = 1.0f / clampedPoseEvaluationRate; + if (m_poseEvaluationAccumulator < updatePeriod && !playbackStateChanged) + { + return; + } + + m_poseEvaluationAccumulator = std::fmod(m_poseEvaluationAccumulator, updatePeriod); + } + else + { + m_poseEvaluationAccumulator = 0.0f; + } + + EvaluatePose(); + } + } +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "enabled", m_enabled); + OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "playing", m_playing); + OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "looping", m_looping); + OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "playback_speed", m_playbackSpeed); + OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "pose_eval_rate", m_poseEvaluationRate); + OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "time_ticks", m_currentTimeTicks); + OvCore::Helpers::Serializer::SerializeString(p_doc, p_node, "animation", GetAnimation()); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "enabled", m_enabled); + OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "playing", m_playing); + OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "looping", m_looping); + OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "playback_speed", m_playbackSpeed); + OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "pose_eval_rate", m_poseEvaluationRate); + OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "time_ticks", m_currentTimeTicks); + OvCore::Helpers::Serializer::DeserializeString(p_doc, p_node, "animation", m_deserializedAnimationName); + m_poseEvaluationRate = std::max(0.0f, m_poseEvaluationRate); + m_poseEvaluationAccumulator = 0.0f; + + NotifyModelChanged(); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal::WidgetContainer& p_root) +{ + SyncWithModel(); + + using namespace OvCore::Helpers; + + GUIDrawer::DrawBoolean(p_root, "Enabled", m_enabled); + GUIDrawer::DrawBoolean(p_root, "Playing", m_playing); + GUIDrawer::DrawBoolean(p_root, "Looping", m_looping); + GUIDrawer::DrawScalar(p_root, "Playback Speed", m_playbackSpeed, 0.01f, -10.0f, 10.0f); + GUIDrawer::DrawScalar(p_root, "Pose Eval Rate", m_poseEvaluationRate, 1.0f, 0.0f, 240.0f); + m_poseEvaluationRate = std::max(0.0f, m_poseEvaluationRate); + GUIDrawer::DrawScalar( + p_root, + "Time (Seconds)", + [this]() { return GetTime(); }, + [this](float p_value) { SetTime(p_value); }, + 0.01f, + 0.0f, + std::max(GetAnimationDurationSeconds(), 3600.0f) + ); + + GUIDrawer::CreateTitle(p_root, "Active Animation"); + auto& animationChoice = p_root.CreateWidget(GetAnimationIndex()); + animationChoice.choices.emplace(-1, ""); + + for (size_t i = 0; i < m_animationNames.size(); ++i) + { + animationChoice.choices.emplace(static_cast(i), m_animationNames[i]); + } + + animationChoice.ValueChangedEvent += [this](int p_choice) + { + if (p_choice < 0) + { + m_animationIndex = std::nullopt; + m_currentTimeTicks = 0.0f; + m_poseEvaluationAccumulator = 0.0f; + EvaluatePose(); + } + else + { + SetAnimation(static_cast(p_choice)); + } + }; + + if (!HasCompatibleModel()) + { + p_root.CreateWidget("No skinned model assigned"); + } + else if (m_animationNames.empty()) + { + p_root.CreateWidget("Model has no animation clips"); + } +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::SyncWithModel() +{ + const auto modelRenderer = owner.GetComponent(); + const auto model = modelRenderer ? modelRenderer->GetModel() : nullptr; + + if (m_model == model) + { + return; + } + + m_model = model; + RebuildRuntimeData(); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::RebuildRuntimeData() +{ + const float preservedTimeTicks = m_currentTimeTicks; + const std::optional preservedAnimationIndex = m_animationIndex; + const std::string requestedAnimationName = m_deserializedAnimationName; + + m_animationNames.clear(); + m_bindPoseTransforms.clear(); + m_localPose.clear(); + m_globalPose.clear(); + m_boneMatrices.clear(); + m_boneMatricesTransposed.clear(); + m_trackSamplingCursors.clear(); + m_animationIndex = std::nullopt; + m_currentTimeTicks = preservedTimeTicks; + m_poseEvaluationAccumulator = 0.0f; + m_lastSampleTimeTicks = 0.0f; + m_lastSampledAnimationIndex = std::nullopt; + + if (!HasCompatibleModel()) + { + return; + } + + const auto& skeleton = m_model->GetSkeleton().value(); + const auto& animations = m_model->GetAnimations(); + + m_bindPoseTransforms.reserve(skeleton.nodes.size()); + for (const auto& node : skeleton.nodes) + { + m_bindPoseTransforms.push_back(DecomposeTransform(node.localBindTransform)); + } + + m_localPose.resize(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); + m_globalPose.resize(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); + m_boneMatrices.resize(skeleton.bones.size(), OvMaths::FMatrix4::Identity); + m_boneMatricesTransposed.resize(skeleton.bones.size(), OvMaths::FMatrix4::Identity); + + m_animationNames.reserve(animations.size()); + for (const auto& animation : animations) + { + m_animationNames.push_back(animation.name); + } + + if (!animations.empty()) + { + if (!requestedAnimationName.empty()) + { + const auto found = std::find(m_animationNames.begin(), m_animationNames.end(), requestedAnimationName); + m_animationIndex = found != m_animationNames.end() ? + std::optional{ static_cast(std::distance(m_animationNames.begin(), found)) } : + std::optional{ 0 }; + } + else if (preservedAnimationIndex.has_value() && *preservedAnimationIndex < animations.size()) + { + m_animationIndex = *preservedAnimationIndex; + } + } + + if (m_animationIndex.has_value() && *m_animationIndex < animations.size()) + { + const auto& animation = animations.at(*m_animationIndex); + if (m_looping) + { + m_currentTimeTicks = WrapTime(m_currentTimeTicks, animation.duration); + } + else + { + m_currentTimeTicks = std::clamp(m_currentTimeTicks, 0.0f, animation.duration); + } + } + else + { + m_currentTimeTicks = 0.0f; + } + + m_deserializedAnimationName.clear(); + EvaluatePose(); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::EvaluatePose() +{ + if (!HasCompatibleModel()) + { + return; + } + + const auto& skeleton = m_model->GetSkeleton().value(); + + if (m_bindPoseTransforms.size() != skeleton.nodes.size()) + { + m_bindPoseTransforms.clear(); + m_bindPoseTransforms.reserve(skeleton.nodes.size()); + + for (const auto& node : skeleton.nodes) + { + m_bindPoseTransforms.push_back(DecomposeTransform(node.localBindTransform)); + } + } + + if (m_localPose.size() != skeleton.nodes.size()) + { + m_localPose.assign(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); + } + + if (m_globalPose.size() != skeleton.nodes.size()) + { + m_globalPose.assign(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); + } + + if (m_boneMatrices.size() != skeleton.bones.size()) + { + m_boneMatrices.assign(skeleton.bones.size(), OvMaths::FMatrix4::Identity); + } + + if (m_boneMatricesTransposed.size() != skeleton.bones.size()) + { + m_boneMatricesTransposed.assign(skeleton.bones.size(), OvMaths::FMatrix4::Identity); + } + + for (size_t nodeIndex = 0; nodeIndex < skeleton.nodes.size(); ++nodeIndex) + { + m_localPose[nodeIndex] = skeleton.nodes[nodeIndex].localBindTransform; + } + + if (m_animationIndex.has_value() && *m_animationIndex < m_model->GetAnimations().size()) + { + const auto& animation = m_model->GetAnimations().at(*m_animationIndex); + const float duration = std::max(animation.duration, 0.0f); + const float sampleTime = + duration > 0.0f ? + (m_looping ? WrapTime(m_currentTimeTicks, duration) : std::clamp(m_currentTimeTicks, 0.0f, duration)) : + 0.0f; + + if (m_trackSamplingCursors.size() != animation.tracks.size()) + { + m_trackSamplingCursors.assign(animation.tracks.size(), TrackSamplingCursor{}); + } + + const bool useSamplingCursorHint = + m_lastSampledAnimationIndex == m_animationIndex && + sampleTime >= m_lastSampleTimeTicks; + + if (!useSamplingCursorHint) + { + for (auto& cursor : m_trackSamplingCursors) + { + cursor = {}; + } + } + + for (size_t trackIndex = 0; trackIndex < animation.tracks.size(); ++trackIndex) + { + const auto& track = animation.tracks[trackIndex]; + auto& trackCursor = m_trackSamplingCursors[trackIndex]; + + if (track.nodeIndex >= m_localPose.size() || track.nodeIndex >= m_bindPoseTransforms.size()) + { + continue; + } + + const auto& bindPoseTransform = m_bindPoseTransforms[track.nodeIndex]; + + const OvMaths::FVector3 sampledPosition = SampleKeys( + track.positionKeys, + sampleTime, + duration, + bindPoseTransform.GetLocalPosition(), + m_looping, + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); }, + &trackCursor.positionKeyIndex, + useSamplingCursorHint + ); + + const OvMaths::FQuaternion sampledRotation = SampleKeys( + track.rotationKeys, + sampleTime, + duration, + bindPoseTransform.GetLocalRotation(), + m_looping, + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FQuaternion::Slerp(p_a, p_b, p_alpha); }, + &trackCursor.rotationKeyIndex, + useSamplingCursorHint + ); + + const OvMaths::FVector3 sampledScale = SampleKeys( + track.scaleKeys, + sampleTime, + duration, + bindPoseTransform.GetLocalScale(), + m_looping, + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); }, + &trackCursor.scaleKeyIndex, + useSamplingCursorHint + ); + + const OvMaths::FTransform sampled(sampledPosition, sampledRotation, sampledScale); + + m_localPose[track.nodeIndex] = sampled.GetLocalMatrix(); + } + + m_lastSampledAnimationIndex = m_animationIndex; + m_lastSampleTimeTicks = sampleTime; + } + else + { + m_lastSampledAnimationIndex = std::nullopt; + m_lastSampleTimeTicks = 0.0f; + } + + for (size_t nodeIndex = 0; nodeIndex < skeleton.nodes.size(); ++nodeIndex) + { + const auto parentIndex = skeleton.nodes[nodeIndex].parentIndex; + m_globalPose[nodeIndex] = + parentIndex >= 0 ? + m_globalPose[static_cast(parentIndex)] * m_localPose[nodeIndex] : + m_localPose[nodeIndex]; + } + + for (size_t boneIndex = 0; boneIndex < skeleton.bones.size(); ++boneIndex) + { + const auto& bone = skeleton.bones[boneIndex]; + if (bone.nodeIndex < m_globalPose.size()) + { + m_boneMatrices[boneIndex] = + m_globalPose[bone.nodeIndex] * + bone.offsetMatrix; + } + else + { + m_boneMatrices[boneIndex] = OvMaths::FMatrix4::Identity; + } + + m_boneMatricesTransposed[boneIndex] = OvMaths::FMatrix4::Transpose(m_boneMatrices[boneIndex]); + } + + ++m_poseVersion; +} + +float OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationDurationSeconds() const +{ + if (!HasCompatibleModel() || !m_animationIndex.has_value()) + { + return 0.0f; + } + + const auto& animation = m_model->GetAnimations().at(*m_animationIndex); + return animation.GetDurationSeconds(); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::UpdatePlayback(float p_deltaTime) +{ + if (!HasCompatibleModel() || !m_animationIndex.has_value()) + { + return; + } + + const auto& animation = m_model->GetAnimations().at(*m_animationIndex); + if (animation.duration <= 0.0f) + { + return; + } + + if (std::abs(m_playbackSpeed) <= std::numeric_limits::epsilon()) + { + return; + } + + const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; + m_currentTimeTicks += p_deltaTime * ticksPerSecond * m_playbackSpeed; + + if (m_looping) + { + m_currentTimeTicks = WrapTime(m_currentTimeTicks, animation.duration); + } + else + { + const float clamped = std::clamp(m_currentTimeTicks, 0.0f, animation.duration); + const bool reachedStart = clamped <= 0.0f && m_playbackSpeed < 0.0f; + const bool reachedEnd = clamped >= animation.duration && m_playbackSpeed > 0.0f; + m_currentTimeTicks = clamped; + if (reachedStart || reachedEnd) + { + m_playing = false; + } + } +} diff --git a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp index f7266702f..349cf4759 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SceneRenderer.cpp @@ -5,10 +5,12 @@ */ #include +#include #include #include #include +#include #include #include #include @@ -18,6 +20,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -27,6 +32,7 @@ namespace { using namespace OvCore::Rendering; + const std::string kSkinningFeatureName{ SkinningUtils::kFeatureName }; class SceneRenderPass : public OvRendering::Core::ARenderPass { @@ -168,6 +174,7 @@ OvCore::Rendering::SceneRenderer::SceneRenderer(OvRendering::Context::Driver& p_ AddFeature(); AddFeature(); + AddFeature(); AddFeature() .Include() @@ -271,6 +278,8 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS if (!model) continue; const auto materialRenderer = modelRenderer->owner.GetComponent(); if (!materialRenderer) continue; + const auto* skinnedRenderer = owner.GetComponent(); + const bool hasSkinning = SkinningUtils::IsSkinningActive(skinnedRenderer); const auto& transform = owner.transform.GetFTransform(); const auto& materials = materialRenderer->GetMaterials(); @@ -313,6 +322,11 @@ SceneRenderer::SceneDrawablesDescriptor OvCore::Rendering::SceneRenderer::ParseS materialRenderer->GetUserMatrix() }); + if (hasSkinning && mesh->HasSkinningData()) + { + SkinningUtils::ApplyDescriptor(drawable, *skinnedRenderer); + } + result.drawables.push_back(drawable); } } @@ -345,6 +359,7 @@ SceneRenderer::SceneFilteredDrawablesDescriptor OvCore::Rendering::SceneRenderer for (const auto& drawable : p_drawables.drawables) { const auto& desc = drawable.GetDescriptor(); + const bool hasSkinningDescriptor = drawable.HasDescriptor(); // Skip drawables that do not satisfy the required visibility flags if (!SatisfiesVisibility(desc.visibilityFlags, p_filteringInput.requiredVisibilityFlags)) @@ -371,13 +386,10 @@ SceneRenderer::SceneFilteredDrawablesDescriptor OvCore::Rendering::SceneRenderer } // Perform frustum culling if enabled - if (frustum && desc.bounds.has_value()) + if (frustum && desc.bounds.has_value() && !hasSkinningDescriptor) { ZoneScopedN("Frustum Culling"); - // Get the engine drawable descriptor to access transform information - const auto& engineDesc = drawable.GetDescriptor(); - if (!frustum->BoundingSphereInFrustum(desc.bounds.value(), desc.actor.transform.GetFTransform())) { continue; // Skip this drawable as it's outside the frustum @@ -397,6 +409,19 @@ SceneRenderer::SceneFilteredDrawablesDescriptor OvCore::Rendering::SceneRenderer drawableCopy.material = targetMaterial; drawableCopy.stateMask = targetMaterial->GenerateStateMask(); + if ( + hasSkinningDescriptor && + targetMaterial->HasShader() && + targetMaterial->SupportsFeature(kSkinningFeatureName) + ) + { + drawableCopy.featureSetOverride = SkinningUtils::BuildFeatureSet(&targetMaterial->GetFeatures()); + } + else + { + drawableCopy.featureSetOverride = std::nullopt; + } + // Categorize drawable based on their type. // This is also where sorting happens, using // the multimap key. diff --git a/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp b/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp index a6ce073f3..cb3d3d3ef 100644 --- a/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp @@ -4,17 +4,24 @@ * @licence: MIT */ +#include +#include + #include +#include #include #include #include #include +#include #include #include #include constexpr uint8_t kMaxShadowMaps = 1; +const std::string kShadowPassName = "SHADOW_PASS"; +const std::string kSkinningFeatureName = std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }; OvCore::Rendering::ShadowRenderPass::ShadowRenderPass(OvRendering::Core::CompositeRenderer& p_renderer) : OvRendering::Core::ARenderPass(p_renderer) @@ -91,6 +98,8 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( OvCore::SceneSystem::Scene& p_scene ) { + using namespace OvCore::Rendering; + for (auto modelRenderer : p_scene.GetFastAccessComponents().modelRenderers) { auto& actor = modelRenderer->owner; @@ -106,6 +115,9 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( continue; } + const auto skinnedRenderer = actor.template GetComponent(); + const bool hasSkinning = SkinningUtils::IsSkinningActive(skinnedRenderer); + const auto& materials = materialRenderer->GetMaterials(); const auto& modelMatrix = actor.transform.GetWorldMatrix(); @@ -113,11 +125,15 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( { if (auto material = materials.at(mesh->GetMaterialIndex()); material && material->IsValid() && material->IsShadowCaster()) { - const std::string shadowPassName = "SHADOW_PASS"; + const bool materialHasShadowPass = material->HasPass(kShadowPassName); + const bool canUseMaterialShadowPass = + materialHasShadowPass && + (!hasSkinning || material->SupportsFeature(kSkinningFeatureName)); - // If the material has shadow pass, use it, otherwise use the shadow fallback material + // If the material has a compatible shadow pass, use it. + // Otherwise, use the shadow fallback material. auto& targetMaterial = - material->HasPass(shadowPassName) ? + canUseMaterialShadowPass ? *material : m_shadowMaterial; @@ -138,13 +154,18 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( drawable.stateMask.frontfaceCulling = false; drawable.stateMask.backfaceCulling = false; - drawable.pass = shadowPassName; + drawable.pass = kShadowPassName; drawable.AddDescriptor({ modelMatrix, materialRenderer->GetUserMatrix() }); + if (hasSkinning && targetMaterial.SupportsFeature(kSkinningFeatureName)) + { + SkinningUtils::ApplyToDrawable(drawable, *skinnedRenderer, &targetMaterial.GetFeatures()); + } + m_renderer.DrawEntity(p_pso, drawable); } } diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp new file mode 100644 index 000000000..cc2ea7033 --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp @@ -0,0 +1,153 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include + +#include +#include +#include + +namespace +{ + const std::string kSkinningFeatureName{ OvCore::Rendering::SkinningUtils::kFeatureName }; +} + +OvCore::Rendering::SkinningRenderFeature::SkinningRenderFeature( + OvRendering::Core::CompositeRenderer& p_renderer, + OvRendering::Features::EFeatureExecutionPolicy p_executionPolicy, + uint32_t p_bufferBindingPoint +) : + ARenderFeature(p_renderer, p_executionPolicy), + m_bufferBindingPoint(p_bufferBindingPoint) +{ + for (auto& skinningBuffer : m_skinningBuffers) + { + skinningBuffer = std::make_unique(); + } + + m_identityBuffer = std::make_unique(); +} + +uint32_t OvCore::Rendering::SkinningRenderFeature::GetBufferBindingPoint() const +{ + return m_bufferBindingPoint; +} + +void OvCore::Rendering::SkinningRenderFeature::OnBeginFrame(const OvRendering::Data::FrameDescriptor& p_frameDescriptor) +{ + (void)p_frameDescriptor; + m_skinningBufferIndex = (m_skinningBufferIndex + 1) % kSkinningBufferRingSize; + + m_lastPalettePtr = nullptr; + m_lastPaletteCount = 0; + m_lastPoseVersion = 0; + m_boundPalette = EBoundPalette::NONE; + m_boundPalettePtr = nullptr; + m_boundPaletteCount = 0; + m_boundPoseVersion = 0; + + if (m_identityBuffer->GetSize() == 0) + { + const auto identity = OvMaths::FMatrix4::Transpose(OvMaths::FMatrix4::Identity); + m_identityBuffer->Allocate(sizeof(OvMaths::FMatrix4), OvRendering::Settings::EAccessSpecifier::STATIC_DRAW); + m_identityBuffer->Upload(&identity); + } +} + +void OvCore::Rendering::SkinningRenderFeature::OnEndFrame() +{ + GetCurrentSkinningBuffer().Unbind(); + m_identityBuffer->Unbind(); + m_boundPalette = EBoundPalette::NONE; + m_boundPalettePtr = nullptr; + m_boundPaletteCount = 0; + m_boundPoseVersion = 0; +} + +void OvCore::Rendering::SkinningRenderFeature::OnBeforeDraw( + OvRendering::Data::PipelineState& p_pso, + const OvRendering::Entities::Drawable& p_drawable +) +{ + (void)p_pso; + + OvTools::Utils::OptRef skinningDescriptor; + const bool hasSkinningDescriptor = p_drawable.TryGetDescriptor(skinningDescriptor); + + const bool usesSkinningVariant = + (p_drawable.featureSetOverride.has_value() && p_drawable.featureSetOverride->contains(kSkinningFeatureName)) || + (p_drawable.material.has_value() && p_drawable.material->GetFeatures().contains(kSkinningFeatureName)); + + if (!usesSkinningVariant) + { + return; + } + + if (!hasSkinningDescriptor || + !skinningDescriptor->matrices || + skinningDescriptor->count == 0) + { + BindIdentityPalette(); + return; + } + + const bool mustUpload = + m_lastPalettePtr != skinningDescriptor->matrices || + m_lastPaletteCount != skinningDescriptor->count || + m_lastPoseVersion != skinningDescriptor->poseVersion; + + auto& skinningBuffer = GetCurrentSkinningBuffer(); + + if (mustUpload) + { + const auto uploadSize = static_cast(skinningDescriptor->count) * sizeof(OvMaths::FMatrix4); + if (skinningBuffer.GetSize() < uploadSize) + { + skinningBuffer.Allocate(uploadSize, OvRendering::Settings::EAccessSpecifier::STREAM_DRAW); + } + + skinningBuffer.Upload(skinningDescriptor->matrices, OvRendering::HAL::BufferMemoryRange{ + .offset = 0, + .size = uploadSize + }); + + m_lastPalettePtr = skinningDescriptor->matrices; + m_lastPaletteCount = skinningDescriptor->count; + m_lastPoseVersion = skinningDescriptor->poseVersion; + } + + const bool mustBindSkinningPalette = + m_boundPalette != EBoundPalette::SKINNING || + m_boundPalettePtr != skinningDescriptor->matrices || + m_boundPaletteCount != skinningDescriptor->count || + m_boundPoseVersion != skinningDescriptor->poseVersion; + + if (mustBindSkinningPalette) + { + skinningBuffer.Bind(m_bufferBindingPoint); + m_boundPalette = EBoundPalette::SKINNING; + m_boundPalettePtr = skinningDescriptor->matrices; + m_boundPaletteCount = skinningDescriptor->count; + m_boundPoseVersion = skinningDescriptor->poseVersion; + } +} + +OvRendering::HAL::ShaderStorageBuffer& OvCore::Rendering::SkinningRenderFeature::GetCurrentSkinningBuffer() const +{ + return *m_skinningBuffers[m_skinningBufferIndex]; +} + +void OvCore::Rendering::SkinningRenderFeature::BindIdentityPalette() const +{ + if (m_boundPalette != EBoundPalette::IDENTITY) + { + m_identityBuffer->Bind(m_bufferBindingPoint); + m_boundPalette = EBoundPalette::IDENTITY; + m_boundPalettePtr = nullptr; + m_boundPaletteCount = 0; + m_boundPoseVersion = 0; + } +} diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp new file mode 100644 index 000000000..082bf38a7 --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp @@ -0,0 +1,54 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include + +#include +#include +#include +#include + +bool OvCore::Rendering::SkinningUtils::IsSkinningActive(const OvCore::ECS::Components::CSkinnedMeshRenderer* p_renderer) +{ + return p_renderer && p_renderer->IsEnabled() && p_renderer->HasSkinningData(); +} + +OvRendering::Data::FeatureSet OvCore::Rendering::SkinningUtils::BuildFeatureSet(const OvRendering::Data::FeatureSet* p_baseFeatures) +{ + OvRendering::Data::FeatureSet features = p_baseFeatures ? *p_baseFeatures : OvRendering::Data::FeatureSet{}; + features.insert(std::string{ kFeatureName }); + return features; +} + +void OvCore::Rendering::SkinningUtils::ApplyDescriptor( + OvRendering::Entities::Drawable& p_drawable, + const OvCore::ECS::Components::CSkinnedMeshRenderer& p_renderer +) +{ + const auto& boneMatrices = p_renderer.GetBoneMatricesTransposed(); + + if (p_drawable.HasDescriptor()) + { + p_drawable.RemoveDescriptor(); + } + + p_drawable.AddDescriptor({ + .matrices = boneMatrices.data(), + .count = static_cast(boneMatrices.size()), + .poseVersion = p_renderer.GetPoseVersion() + }); +} + +void OvCore::Rendering::SkinningUtils::ApplyToDrawable( + OvRendering::Entities::Drawable& p_drawable, + const OvCore::ECS::Components::CSkinnedMeshRenderer& p_renderer, + const OvRendering::Data::FeatureSet* p_baseFeatures +) +{ + p_drawable.featureSetOverride = BuildFeatureSet(p_baseFeatures); + ApplyDescriptor(p_drawable, p_renderer); +} diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp index 8a9ed57b6..5b32f8b39 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,7 @@ void BindLuaActor(sol::state& p_luaState) "GetAmbientSphereLight", &Actor::GetComponent, "GetModelRenderer", &Actor::GetComponent, "GetMaterialRenderer", &Actor::GetComponent, + "GetSkinnedMeshRenderer", &Actor::GetComponent, "GetAudioSource", &Actor::GetComponent, "GetAudioListener", &Actor::GetComponent, "GetPostProcessStack", & Actor::GetComponent, @@ -91,6 +93,7 @@ void BindLuaActor(sol::state& p_luaState) "AddAmbientBoxLight", &Actor::AddComponent, "AddAmbientSphereLight", &Actor::AddComponent, "AddMaterialRenderer", &Actor::AddComponent, + "AddSkinnedMeshRenderer", &Actor::AddComponent, "AddAudioSource", &Actor::AddComponent, "AddAudioListener", &Actor::AddComponent, "AddPostProcessStack", & Actor::AddComponent, @@ -108,6 +111,7 @@ void BindLuaActor(sol::state& p_luaState) "RemoveAmbientBoxLight", &Actor::RemoveComponent, "RemoveAmbientSphereLight", &Actor::RemoveComponent, "RemoveMaterialRenderer", &Actor::RemoveComponent, + "RemoveSkinnedMeshRenderer", &Actor::RemoveComponent, "RemoveAudioSource", &Actor::RemoveComponent, "RemoveAudioListener", &Actor::RemoveComponent, "RemovePostProcessStack", & Actor::RemoveComponent, diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp index 6a84dbb3e..462dfbbd6 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -87,6 +88,33 @@ void BindLuaComponents(sol::state& p_luaState) "GetUserMatrixElement", &CMaterialRenderer::GetUserMatrixElement ); + p_luaState.new_usertype("SkinnedMeshRenderer", + sol::base_classes, sol::bases(), + "NotifyModelChanged", &CSkinnedMeshRenderer::NotifyModelChanged, + "HasCompatibleModel", &CSkinnedMeshRenderer::HasCompatibleModel, + "HasSkinningData", &CSkinnedMeshRenderer::HasSkinningData, + "SetEnabled", &CSkinnedMeshRenderer::SetEnabled, + "IsEnabled", &CSkinnedMeshRenderer::IsEnabled, + "Play", &CSkinnedMeshRenderer::Play, + "Pause", &CSkinnedMeshRenderer::Pause, + "Stop", &CSkinnedMeshRenderer::Stop, + "IsPlaying", &CSkinnedMeshRenderer::IsPlaying, + "SetLooping", &CSkinnedMeshRenderer::SetLooping, + "IsLooping", &CSkinnedMeshRenderer::IsLooping, + "SetPlaybackSpeed", &CSkinnedMeshRenderer::SetPlaybackSpeed, + "GetPlaybackSpeed", &CSkinnedMeshRenderer::GetPlaybackSpeed, + "SetTime", &CSkinnedMeshRenderer::SetTime, + "GetTime", &CSkinnedMeshRenderer::GetTime, + "GetAnimationCount", &CSkinnedMeshRenderer::GetAnimationCount, + "GetAnimationName", &CSkinnedMeshRenderer::GetAnimationName, + "SetAnimation", sol::overload( + sol::resolve(&CSkinnedMeshRenderer::SetAnimation), + sol::resolve(&CSkinnedMeshRenderer::SetAnimation) + ), + "GetAnimationIndex", &CSkinnedMeshRenderer::GetAnimationIndex, + "GetAnimation", &CSkinnedMeshRenderer::GetAnimation + ); + p_luaState.new_enum("CollisionDetectionMode", { {"DISCRETE", OvPhysics::Entities::PhysicalObject::ECollisionDetectionMode::DISCRETE}, {"CONTINUOUS", OvPhysics::Entities::PhysicalObject::ECollisionDetectionMode::CONTINUOUS} diff --git a/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h b/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h index 95e4bae64..1be264c25 100644 --- a/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h +++ b/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -51,11 +52,24 @@ namespace OvEditor::Rendering void DrawActorToStencil(OvRendering::Data::PipelineState p_pso, OvCore::ECS::Actor& p_actor); void DrawActorOutline(OvRendering::Data::PipelineState p_pso, OvCore::ECS::Actor& p_actor, const OvMaths::FVector4& p_color); - void DrawModelToStencil(OvRendering::Data::PipelineState p_pso, const OvMaths::FMatrix4& p_worldMatrix, OvRendering::Resources::Model& p_model, OvTools::Utils::OptRef p_materials = std::nullopt); - void DrawModelOutline(OvRendering::Data::PipelineState p_pso, const OvMaths::FMatrix4& p_worldMatrix, OvRendering::Resources::Model& p_model, const OvMaths::FVector4& p_color, OvTools::Utils::OptRef p_materials = std::nullopt); + void DrawModelToStencil( + OvRendering::Data::PipelineState p_pso, + const OvMaths::FMatrix4& p_worldMatrix, + OvRendering::Resources::Model& p_model, + OvTools::Utils::OptRef p_materials = std::nullopt, + const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer = nullptr + ); + void DrawModelOutline( + OvRendering::Data::PipelineState p_pso, + const OvMaths::FMatrix4& p_worldMatrix, + OvRendering::Resources::Model& p_model, + const OvMaths::FVector4& p_color, + OvTools::Utils::OptRef p_materials = std::nullopt, + const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer = nullptr + ); private: OvCore::Resources::Material m_stencilFillMaterial; OvCore::Resources::Material m_outlineMaterial; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp index eda6b74a4..2da38efff 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp @@ -49,10 +49,30 @@ namespace ); } - auto CreateShader(const std::filesystem::path& p_path) + auto CreateShader(const std::filesystem::path& p_path, const std::filesystem::path& p_editorAssetsPath) { + auto pathParser = [p_editorAssetsPath](const std::string& p_includePath) + { + const auto normalizedIncludePath = OvTools::Utils::PathParser::MakeNonWindowsStyle(p_includePath); + const auto includePath = std::filesystem::path{ normalizedIncludePath }; + + if (includePath.is_absolute()) + { + return includePath.lexically_normal().string(); + } + + if (!normalizedIncludePath.empty() && normalizedIncludePath.front() == ':') + { + const auto engineAssetsPath = p_editorAssetsPath.parent_path() / "Engine"; + return (engineAssetsPath / normalizedIncludePath.substr(1)).lexically_normal().string(); + } + + return (p_editorAssetsPath / includePath).lexically_normal().string(); + }; + return OvRendering::Resources::Loaders::ShaderLoader::Create( - p_path.string() + p_path.string(), + pathParser ); } @@ -128,11 +148,11 @@ OvEditor::Core::EditorResources::EditorResources(const std::string& p_editorAsse }; m_shaders = { - {"Grid", CreateShader(shadersFolder / "Grid.ovfx")}, - {"Gizmo", CreateShader(shadersFolder / "Gizmo.ovfx")}, - {"Billboard", CreateShader(shadersFolder / "Billboard.ovfx")}, - {"PickingFallback", CreateShader(shadersFolder / "PickingFallback.ovfx")}, - {"OutlineFallback", CreateShader(shadersFolder / "OutlineFallback.ovfx")} + {"Grid", CreateShader(shadersFolder / "Grid.ovfx", editorAssetsPath)}, + {"Gizmo", CreateShader(shadersFolder / "Gizmo.ovfx", editorAssetsPath)}, + {"Billboard", CreateShader(shadersFolder / "Billboard.ovfx", editorAssetsPath)}, + {"PickingFallback", CreateShader(shadersFolder / "PickingFallback.ovfx", editorAssetsPath)}, + {"OutlineFallback", CreateShader(shadersFolder / "OutlineFallback.ovfx", editorAssetsPath)} }; // Ensure that all resources have been loaded successfully diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index efc74dd3a..16c60db6c 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,7 @@ namespace CreateComponentInfo("Ambient Box Light"), CreateComponentInfo("Ambient Sphere Light"), CreateComponentInfo("Material Renderer"), + CreateComponentInfo("Skinned Mesh Renderer"), CreateComponentInfo("Audio Source"), CreateComponentInfo("Audio Listener"), CreateComponentInfo("Post Process Stack"), diff --git a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp index a1b6cac0e..87a6e8467 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp @@ -5,7 +5,9 @@ */ #include +#include #include +#include #include #include #include @@ -17,6 +19,61 @@ namespace constexpr uint32_t kStencilMask = 0xFF; constexpr int32_t kStencilReference = 1; constexpr std::string_view kOutlinePassName = "OUTLINE_PASS"; + + using MaterialList = OvCore::ECS::Components::CMaterialRenderer::MaterialList; + + OvCore::Resources::Material* FindMeshMaterial( + OvTools::Utils::OptRef p_materials, + uint32_t p_materialIndex + ) + { + if (!p_materials.has_value() || p_materialIndex >= kMaxMaterialCount) + { + return nullptr; + } + + return p_materials->at(static_cast(p_materialIndex)); + } + + OvCore::Resources::Material& ResolveOutlineMaterial( + uint32_t p_materialIndex, + std::string_view p_passName, + OvTools::Utils::OptRef p_materials, + bool p_hasSkinning, + OvCore::Resources::Material& p_fallbackMaterial + ) + { + if (p_hasSkinning) + { + return p_fallbackMaterial; + } + + auto* material = FindMeshMaterial(p_materials, p_materialIndex); + if (material && material->IsValid() && material->HasPass(std::string{ p_passName })) + { + return *material; + } + + return p_fallbackMaterial; + } + + void ApplySkinningIfNeeded( + OvRendering::Entities::Drawable& p_drawable, + const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer, + OvCore::Resources::Material& p_targetMaterial + ) + { + if (!OvCore::Rendering::SkinningUtils::IsSkinningActive(p_skinnedRenderer)) + { + return; + } + + OvCore::Rendering::SkinningUtils::ApplyToDrawable( + p_drawable, + *p_skinnedRenderer, + &p_targetMaterial.GetFeatures() + ); + } } OvEditor::Rendering::OutlineRenderFeature::OutlineRenderFeature( @@ -84,11 +141,13 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawActorToStencil(OvRendering:: { if (auto materialRenderer = p_actor.GetComponent()) { + const auto skinnedRenderer = p_actor.GetComponent(); DrawModelToStencil( p_pso, p_actor.transform.GetWorldMatrix(), *modelRenderer->GetModel(), - materialRenderer->GetMaterials() + materialRenderer->GetMaterials(), + skinnedRenderer ); } } @@ -135,12 +194,14 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawActorOutline( { if (auto materialRenderer = p_actor.GetComponent()) { + const auto skinnedRenderer = p_actor.GetComponent(); DrawModelOutline( p_pso, p_actor.transform.GetWorldMatrix(), *modelRenderer->GetModel(), p_color, - materialRenderer->GetMaterials() + materialRenderer->GetMaterials(), + skinnedRenderer ); } } @@ -178,23 +239,22 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelToStencil( OvRendering::Data::PipelineState p_pso, const OvMaths::FMatrix4& p_worldMatrix, OvRendering::Resources::Model& p_model, - OvTools::Utils::OptRef p_materials + OvTools::Utils::OptRef p_materials, + const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer ) { const std::string outlinePassName{ kOutlinePassName }; + const bool hasSkinning = OvCore::Rendering::SkinningUtils::IsSkinningActive(p_skinnedRenderer); for (auto mesh : p_model.GetMeshes()) { - auto getStencilMaterial = [&]() -> OvCore::Resources::Material& { - auto material = p_materials.has_value() ? p_materials->at(mesh->GetMaterialIndex()) : nullptr; - if (material && material->IsValid() && material->HasPass(outlinePassName)) - { - return *material; - } - return m_stencilFillMaterial; - }; - - auto& targetMaterial = getStencilMaterial(); + auto& targetMaterial = ResolveOutlineMaterial( + mesh->GetMaterialIndex(), + outlinePassName, + p_materials, + hasSkinning, + m_stencilFillMaterial + ); auto stateMask = targetMaterial.GenerateStateMask(); @@ -212,6 +272,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelToStencil( element.pass = outlinePassName; element.AddDescriptor(engineDrawableDescriptor); + ApplySkinningIfNeeded(element, p_skinnedRenderer, targetMaterial); m_renderer.DrawEntity(p_pso, element); } @@ -222,23 +283,22 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelOutline( const OvMaths::FMatrix4& p_worldMatrix, OvRendering::Resources::Model& p_model, const OvMaths::FVector4& p_color, - OvTools::Utils::OptRef p_materials + OvTools::Utils::OptRef p_materials, + const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer ) { const std::string outlinePassName{ kOutlinePassName }; + const bool hasSkinning = OvCore::Rendering::SkinningUtils::IsSkinningActive(p_skinnedRenderer); for (auto mesh : p_model.GetMeshes()) { - auto getStencilMaterial = [&]() -> OvCore::Resources::Material& { - auto material = p_materials.has_value() ? p_materials->at(mesh->GetMaterialIndex()) : nullptr; - if (material && material->IsValid() && material->HasPass(outlinePassName)) - { - return *material; - } - return m_stencilFillMaterial; - }; - - auto& targetMaterial = getStencilMaterial(); + auto& targetMaterial = ResolveOutlineMaterial( + mesh->GetMaterialIndex(), + outlinePassName, + p_materials, + hasSkinning, + m_outlineMaterial + ); // Set the outline color property if it exists if (targetMaterial.GetProperty("_OutlineColor")) @@ -261,6 +321,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelOutline( drawable.pass = outlinePassName; drawable.AddDescriptor(engineDrawableDescriptor); + ApplySkinningIfNeeded(drawable, p_skinnedRenderer, targetMaterial); m_renderer.DrawEntity(p_pso, drawable); } diff --git a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp index ad22db4b7..722ed0df4 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp @@ -5,10 +5,13 @@ */ #include +#include #include +#include #include #include +#include #include #include @@ -20,6 +23,8 @@ namespace { + const std::string kPickingPassName = "PICKING_PASS"; + void PreparePickingMaterial( const OvCore::ECS::Actor& p_actor, OvRendering::Data::Material& p_material, @@ -161,30 +166,42 @@ void OvEditor::Rendering::PickingRenderPass::DrawPickableModels( auto drawPickableModels = [&](auto drawables) { for (auto& drawable : drawables) { - const std::string pickingPassName = "PICKING_PASS"; + const auto& actor = drawable.template GetDescriptor().actor; + const auto skinnedRenderer = actor.template GetComponent(); + const bool hasSkinning = OvCore::Rendering::SkinningUtils::IsSkinningActive(skinnedRenderer); + + if (hasSkinning) + { + auto& targetMaterial = m_actorPickingFallbackMaterial; + + PreparePickingMaterial(actor, targetMaterial); + + OvRendering::Entities::Drawable finalDrawable = drawable; + finalDrawable.material = &targetMaterial; + finalDrawable.stateMask = targetMaterial.GenerateStateMask(); + finalDrawable.stateMask.frontfaceCulling = false; + finalDrawable.stateMask.backfaceCulling = false; + finalDrawable.pass = kPickingPassName; + + OvCore::Rendering::SkinningUtils::ApplyToDrawable(finalDrawable, *skinnedRenderer, &targetMaterial.GetFeatures()); + m_renderer.DrawEntity(p_pso, finalDrawable); + continue; + } - // If the material has picking pass, use it, otherwise use the picking fallback material auto& targetMaterial = - (drawable.material && drawable.material->IsValid() && drawable.material->HasPass(pickingPassName)) ? + drawable.material && + drawable.material->IsValid() && + drawable.material->HasPass(kPickingPassName) ? drawable.material.value() : m_actorPickingFallbackMaterial; - const auto& actor = drawable.template GetDescriptor().actor; - PreparePickingMaterial(actor, targetMaterial); - // Prioritize using the actual material state mask. - auto stateMask = - drawable.material && drawable.material->IsValid() ? - drawable.material->GenerateStateMask() : - targetMaterial.GenerateStateMask(); - OvRendering::Entities::Drawable finalDrawable = drawable; - finalDrawable.material = targetMaterial; - finalDrawable.stateMask = stateMask; - finalDrawable.stateMask.frontfaceCulling = false; - finalDrawable.stateMask.backfaceCulling = false; - finalDrawable.pass = pickingPassName; + finalDrawable.material = &targetMaterial; + finalDrawable.stateMask = targetMaterial.GenerateStateMask(); + finalDrawable.pass = kPickingPassName; + finalDrawable.featureSetOverride = std::nullopt; m_renderer.DrawEntity(p_pso, finalDrawable); } diff --git a/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h b/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h new file mode 100644 index 000000000..dd0e648a6 --- /dev/null +++ b/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h @@ -0,0 +1,117 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace OvRendering::Animation +{ + constexpr uint8_t kMaxBonesPerVertex = 4; + + struct SkeletonNode + { + std::string name; + int32_t parentIndex = -1; + int32_t boneIndex = -1; + OvMaths::FMatrix4 localBindTransform = OvMaths::FMatrix4::Identity; + OvMaths::FMatrix4 globalBindTransform = OvMaths::FMatrix4::Identity; + }; + + struct Bone + { + std::string name; + uint32_t nodeIndex = 0; + OvMaths::FMatrix4 offsetMatrix = OvMaths::FMatrix4::Identity; + }; + + struct Skeleton + { + std::vector nodes; + std::vector bones; + std::unordered_map nodeByName; + std::unordered_map boneByName; + OvMaths::FMatrix4 globalInverseTransform = OvMaths::FMatrix4::Identity; + + bool IsValid() const + { + return !nodes.empty() && !bones.empty(); + } + + std::optional FindNodeIndex(std::string_view p_name) const + { + if (auto found = nodeByName.find(std::string{ p_name }); found != nodeByName.end()) + { + return found->second; + } + + return std::nullopt; + } + + std::optional FindBoneIndex(std::string_view p_name) const + { + if (auto found = boneByName.find(std::string{ p_name }); found != boneByName.end()) + { + return found->second; + } + + return std::nullopt; + } + }; + + template + struct Keyframe + { + float time = 0.0f; // Ticks + T value{}; + }; + + struct NodeAnimationTrack + { + uint32_t nodeIndex = 0; + std::vector> positionKeys; + std::vector> rotationKeys; + std::vector> scaleKeys; + }; + + struct SkeletalAnimation + { + std::string name; + float duration = 0.0f; // Ticks + float ticksPerSecond = 25.0f; + std::vector tracks; + std::unordered_map trackByNodeIndex; + + bool IsValid() const + { + return duration > 0.0f && !tracks.empty(); + } + + float GetDurationSeconds() const + { + return ticksPerSecond > 0.0f ? duration / ticksPerSecond : 0.0f; + } + + const NodeAnimationTrack* FindTrack(uint32_t p_nodeIndex) const + { + if (auto found = trackByNodeIndex.find(p_nodeIndex); found != trackByNodeIndex.end()) + { + return &tracks.at(found->second); + } + + return nullptr; + } + }; +} diff --git a/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h index b992fd7a2..85bfe9860 100644 --- a/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h +++ b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h @@ -6,6 +6,8 @@ #pragma once +#include + namespace OvRendering::Geometry { /** @@ -18,5 +20,7 @@ namespace OvRendering::Geometry float normals[3]; float tangent[3]; float bitangent[3]; + float boneIDs[Animation::kMaxBonesPerVertex]; + float boneWeights[Animation::kMaxBonesPerVertex]; }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Mesh.h b/Sources/OvRendering/include/OvRendering/Resources/Mesh.h index 2b18cad30..77df296fc 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Mesh.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Mesh.h @@ -32,7 +32,8 @@ namespace OvRendering::Resources Mesh( std::span p_vertices, std::span p_indices, - uint32_t p_materialIndex = 0 + uint32_t p_materialIndex = 0, + bool p_hasSkinningData = false ); /** @@ -65,6 +66,11 @@ namespace OvRendering::Resources */ uint32_t GetMaterialIndex() const; + /** + * Returns true if this mesh stores skinning vertex attributes + */ + bool HasSkinningData() const; + private: void Upload(std::span p_vertices, std::span p_indices); void ComputeBoundingSphere(std::span p_vertices); @@ -73,6 +79,7 @@ namespace OvRendering::Resources const uint32_t m_vertexCount; const uint32_t m_indicesCount; const uint32_t m_materialIndex; + const bool m_hasSkinningData; HAL::VertexArray m_vertexArray; HAL::VertexBuffer m_vertexBuffer; @@ -80,4 +87,4 @@ namespace OvRendering::Resources Geometry::BoundingSphere m_boundingSphere; }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Model.h b/Sources/OvRendering/include/OvRendering/Resources/Model.h index 01d7e0491..057d45ac5 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Model.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Model.h @@ -9,7 +9,9 @@ #include #include #include +#include +#include "OvRendering/Animation/SkeletalData.h" #include "OvRendering/Resources/Mesh.h" namespace OvRendering::Resources @@ -39,6 +41,27 @@ namespace OvRendering::Resources */ const OvRendering::Geometry::BoundingSphere& GetBoundingSphere() const; + /** + * Returns true if this model has skinning data + */ + bool IsSkinned() const; + + /** + * Returns the skeleton data (if any) + */ + const std::optional& GetSkeleton() const; + + /** + * Returns all animations available on this model + */ + const std::vector& GetAnimations() const; + + /** + * Finds an animation by name + * @param p_name + */ + const Animation::SkeletalAnimation* FindAnimation(std::string_view p_name) const; + private: Model(const std::string& p_path); ~Model(); @@ -51,7 +74,9 @@ namespace OvRendering::Resources private: std::vector m_meshes; std::vector m_materialNames; + std::optional m_skeleton; + std::vector m_animations; Geometry::BoundingSphere m_boundingSphere; }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h index 67413b83c..e173c0291 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h @@ -4,15 +4,14 @@ * @licence: MIT */ -#include +#pragma once +#include #include "OvRendering/Geometry/Vertex.h" #include "OvRendering/Resources/Mesh.h" #include "OvRendering/Resources/Parsers/IModelParser.h" -#pragma once - namespace OvRendering::Resources::Parsers { /** @@ -33,12 +32,24 @@ namespace OvRendering::Resources::Parsers const std::string& p_fileName, std::vector& p_meshes, std::vector& p_materials, + std::optional& p_skeleton, + std::vector& p_animations, EModelParserFlags p_parserFlags ) override; private: - void ProcessMaterials(const struct aiScene* p_scene, std::vector& p_materials);; - void ProcessNode(void* p_transform, struct aiNode* p_node, const struct aiScene* p_scene, std::vector& p_meshes); - void ProcessMesh(void* p_transform, struct aiMesh* p_mesh, const struct aiScene* p_scene, std::vector& p_outVertices, std::vector& p_outIndices); + void BuildSkeleton(const struct aiScene* p_scene, Animation::Skeleton& p_skeleton); + void ProcessAnimations(const struct aiScene* p_scene, const Animation::Skeleton& p_skeleton, std::vector& p_animations); + void ProcessMaterials(const struct aiScene* p_scene, std::vector& p_materials); + void ProcessNode(void* p_transform, struct aiNode* p_node, const struct aiScene* p_scene, std::vector& p_meshes, Animation::Skeleton* p_skeleton); + void ProcessMesh( + void* p_transform, + struct aiMesh* p_mesh, + const struct aiScene* p_scene, + std::vector& p_outVertices, + std::vector& p_outIndices, + Animation::Skeleton* p_skeleton, + bool& p_outHasSkinningData + ); }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h index a7795d328..76c443b00 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h @@ -6,8 +6,10 @@ #pragma once +#include #include +#include "OvRendering/Animation/SkeletalData.h" #include "OvRendering/Resources/Mesh.h" #include "OvRendering/Resources/Parsers/EModelParserFlags.h" @@ -31,7 +33,9 @@ namespace OvRendering::Resources::Parsers const std::string& p_fileName, std::vector& p_meshes, std::vector& p_materials, + std::optional& p_skeleton, + std::vector& p_animations, EModelParserFlags p_parserFlags ) = 0; }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/src/OvRendering/Resources/Loaders/ModelLoader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ModelLoader.cpp index d6ffaab76..0ee0c84cd 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Loaders/ModelLoader.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ModelLoader.cpp @@ -4,6 +4,8 @@ * @licence: MIT */ +#include + #include "OvRendering/Resources/Loaders/ModelLoader.h" OvRendering::Resources::Parsers::AssimpParser OvRendering::Resources::Loaders::ModelLoader::__ASSIMP; @@ -12,7 +14,14 @@ OvRendering::Resources::Model* OvRendering::Resources::Loaders::ModelLoader::Cre { Model* result = new Model(p_filepath); - if (__ASSIMP.LoadModel(p_filepath, result->m_meshes, result->m_materialNames, p_parserFlags)) + if (__ASSIMP.LoadModel( + p_filepath, + result->m_meshes, + result->m_materialNames, + result->m_skeleton, + result->m_animations, + p_parserFlags + )) { result->ComputeBoundingSphere(); return result; @@ -31,7 +40,9 @@ void OvRendering::Resources::Loaders::ModelLoader::Reload(Model& p_model, const { p_model.m_meshes = newModel->m_meshes; p_model.m_materialNames = newModel->m_materialNames; - p_model.m_boundingSphere = newModel->m_boundingSphere; + p_model.m_skeleton = std::move(newModel->m_skeleton); + p_model.m_animations = std::move(newModel->m_animations); + p_model.m_boundingSphere = newModel->m_boundingSphere; newModel->m_meshes.clear(); delete newModel; } diff --git a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp index 6e45ddc81..2f795549b 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp @@ -7,18 +7,60 @@ #include #include #include +#include +#include #include #include +namespace +{ + struct StaticVertex + { + float position[3]; + float texCoords[2]; + float normals[3]; + float tangent[3]; + float bitangent[3]; + }; + + StaticVertex ToStaticVertex(const OvRendering::Geometry::Vertex& p_vertex) + { + StaticVertex result{}; + + result.position[0] = p_vertex.position[0]; + result.position[1] = p_vertex.position[1]; + result.position[2] = p_vertex.position[2]; + + result.texCoords[0] = p_vertex.texCoords[0]; + result.texCoords[1] = p_vertex.texCoords[1]; + + result.normals[0] = p_vertex.normals[0]; + result.normals[1] = p_vertex.normals[1]; + result.normals[2] = p_vertex.normals[2]; + + result.tangent[0] = p_vertex.tangent[0]; + result.tangent[1] = p_vertex.tangent[1]; + result.tangent[2] = p_vertex.tangent[2]; + + result.bitangent[0] = p_vertex.bitangent[0]; + result.bitangent[1] = p_vertex.bitangent[1]; + result.bitangent[2] = p_vertex.bitangent[2]; + + return result; + } +} + OvRendering::Resources::Mesh::Mesh( std::span p_vertices, std::span p_indices, - uint32_t p_materialIndex + uint32_t p_materialIndex, + bool p_hasSkinningData ) : m_vertexCount(static_cast(p_vertices.size())), m_indicesCount(static_cast(p_indices.size())), - m_materialIndex(p_materialIndex) + m_materialIndex(p_materialIndex), + m_hasSkinningData(p_hasSkinningData) { Upload(p_vertices, p_indices); ComputeBoundingSphere(p_vertices); @@ -54,23 +96,64 @@ uint32_t OvRendering::Resources::Mesh::GetMaterialIndex() const return m_materialIndex; } +bool OvRendering::Resources::Mesh::HasSkinningData() const +{ + return m_hasSkinningData; +} + void OvRendering::Resources::Mesh::Upload(std::span p_vertices, std::span p_indices) { - if (m_vertexBuffer.Allocate(p_vertices.size_bytes())) + const auto staticLayout = std::to_array({ + { Settings::EDataType::FLOAT, 3 }, // position + { Settings::EDataType::FLOAT, 2 }, // texCoords + { Settings::EDataType::FLOAT, 3 }, // normal + { Settings::EDataType::FLOAT, 3 }, // tangent + { Settings::EDataType::FLOAT, 3 } // bitangent + }); + + const auto skinnedLayout = std::to_array({ + { Settings::EDataType::FLOAT, 3 }, // position + { Settings::EDataType::FLOAT, 2 }, // texCoords + { Settings::EDataType::FLOAT, 3 }, // normal + { Settings::EDataType::FLOAT, 3 }, // tangent + { Settings::EDataType::FLOAT, 3 }, // bitangent + { Settings::EDataType::FLOAT, 4 }, // bone IDs + { Settings::EDataType::FLOAT, 4 } // bone weights + }); + + std::vector staticVertices; + const void* vertexData = nullptr; + uint64_t vertexBufferSize = 0; + Settings::VertexAttributeLayout layout = staticLayout; + + if (m_hasSkinningData) + { + vertexData = p_vertices.data(); + vertexBufferSize = p_vertices.size_bytes(); + layout = skinnedLayout; + } + else + { + staticVertices.reserve(p_vertices.size()); + + for (const auto& vertex : p_vertices) + { + staticVertices.push_back(ToStaticVertex(vertex)); + } + + vertexData = staticVertices.data(); + vertexBufferSize = static_cast(staticVertices.size()) * sizeof(StaticVertex); + } + + if (m_vertexBuffer.Allocate(vertexBufferSize)) { - m_vertexBuffer.Upload(p_vertices.data()); + m_vertexBuffer.Upload(vertexData); if (m_indexBuffer.Allocate(p_indices.size_bytes())) { m_indexBuffer.Upload(p_indices.data()); - m_vertexArray.SetLayout(std::to_array({ - { Settings::EDataType::FLOAT, 3 }, // position - { Settings::EDataType::FLOAT, 2 }, // texCoords - { Settings::EDataType::FLOAT, 3 }, // normal - { Settings::EDataType::FLOAT, 3 }, // tangent - { Settings::EDataType::FLOAT, 3 } // bitangent - }), m_vertexBuffer, m_indexBuffer); + m_vertexArray.SetLayout(layout, m_vertexBuffer, m_indexBuffer); } else { @@ -94,9 +177,9 @@ void OvRendering::Resources::Mesh::ComputeBoundingSphere(std::span::max(); float minZ = std::numeric_limits::max(); - float maxX = std::numeric_limits::min(); - float maxY = std::numeric_limits::min(); - float maxZ = std::numeric_limits::min(); + float maxX = std::numeric_limits::lowest(); + float maxY = std::numeric_limits::lowest(); + float maxZ = std::numeric_limits::lowest(); for (const auto& vertex : p_vertices) { diff --git a/Sources/OvRendering/src/OvRendering/Resources/Model.cpp b/Sources/OvRendering/src/OvRendering/Resources/Model.cpp index cc827fd0a..cf72d72ce 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Model.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Model.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "OvRendering/Resources/Model.h" @@ -14,6 +15,31 @@ const OvRendering::Geometry::BoundingSphere& OvRendering::Resources::Model::GetB return m_boundingSphere; } +bool OvRendering::Resources::Model::IsSkinned() const +{ + return m_skeleton.has_value() && m_skeleton->IsValid(); +} + +const std::optional& OvRendering::Resources::Model::GetSkeleton() const +{ + return m_skeleton; +} + +const std::vector& OvRendering::Resources::Model::GetAnimations() const +{ + return m_animations; +} + +const OvRendering::Animation::SkeletalAnimation* OvRendering::Resources::Model::FindAnimation(std::string_view p_name) const +{ + const auto found = std::find_if(m_animations.begin(), m_animations.end(), [p_name](const auto& p_animation) + { + return p_animation.name == p_name; + }); + + return found != m_animations.end() ? &(*found) : nullptr; +} + OvRendering::Resources::Model::Model(const std::string & p_path) : path(p_path) { } @@ -41,9 +67,9 @@ void OvRendering::Resources::Model::ComputeBoundingSphere() float minY = std::numeric_limits::max(); float minZ = std::numeric_limits::max(); - float maxX = std::numeric_limits::min(); - float maxY = std::numeric_limits::min(); - float maxZ = std::numeric_limits::min(); + float maxX = std::numeric_limits::lowest(); + float maxY = std::numeric_limits::lowest(); + float maxZ = std::numeric_limits::lowest(); for (const auto& mesh : m_meshes) { diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index 071e05af1..1dd3ce706 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -4,10 +4,15 @@ * @licence: MIT */ +#include +#include +#include +#include + #include -#include #include #include +#include #include #include @@ -30,33 +35,258 @@ namespace OVLOG_WARNING("AssimpParser: OPTIMIZE_GRAPH and PRE_TRANSFORM_VERTICES are mutually exclusive. OPTIMIZE_GRAPH will be ignored."); } + p_flags |= TRIANGULATE; + p_flags |= LIMIT_BONE_WEIGHTS; + p_flags &= ~PRE_TRANSFORM_VERTICES; + p_flags &= ~DEBONE; + return p_flags; } + + OvMaths::FMatrix4 ToMatrix4(const aiMatrix4x4& p_matrix) + { + return { + p_matrix.a1, p_matrix.a2, p_matrix.a3, p_matrix.a4, + p_matrix.b1, p_matrix.b2, p_matrix.b3, p_matrix.b4, + p_matrix.c1, p_matrix.c2, p_matrix.c3, p_matrix.c4, + p_matrix.d1, p_matrix.d2, p_matrix.d3, p_matrix.d4 + }; + } + + bool AddBoneData(OvRendering::Geometry::Vertex& p_vertex, uint32_t p_boneIndex, float p_weight) + { + if (!std::isfinite(p_weight) || p_weight <= 0.0f) + { + return false; + } + + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + if (p_vertex.boneWeights[i] <= 0.0f) + { + p_vertex.boneIDs[i] = static_cast(p_boneIndex); + p_vertex.boneWeights[i] = p_weight; + return true; + } + } + + auto minSlot = 0u; + for (uint8_t i = 1; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + if (p_vertex.boneWeights[i] < p_vertex.boneWeights[minSlot]) + { + minSlot = i; + } + } + + if (p_weight > p_vertex.boneWeights[minSlot]) + { + p_vertex.boneIDs[minSlot] = static_cast(p_boneIndex); + p_vertex.boneWeights[minSlot] = p_weight; + return true; + } + + return false; + } + + float GetBoneWeightSum(const OvRendering::Geometry::Vertex& p_vertex) + { + float sum = 0.0f; + + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + sum += p_vertex.boneWeights[i]; + } + + return sum; + } + + void NormalizeBoneData(OvRendering::Geometry::Vertex& p_vertex) + { + const float sum = GetBoneWeightSum(p_vertex); + + if (sum > 0.0f) + { + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + p_vertex.boneWeights[i] /= sum; + } + } + } + } -bool OvRendering::Resources::Parsers::AssimpParser::LoadModel(const std::string & p_fileName, std::vector& p_meshes, std::vector& p_materials, EModelParserFlags p_parserFlags) +bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( + const std::string& p_fileName, + std::vector& p_meshes, + std::vector& p_materials, + std::optional& p_skeleton, + std::vector& p_animations, + EModelParserFlags p_parserFlags +) { Assimp::Importer import; // Fix the flags to avoid conflicts/invalid scenarios. // This is a workaround, ideally the editor UI should not allow this to happen. - p_parserFlags = FixFlags(p_parserFlags); + p_parserFlags = FixFlags(p_parserFlags); const aiScene* scene = import.ReadFile(p_fileName, static_cast(p_parserFlags)); if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) + { return false; + } ProcessMaterials(scene, p_materials); + const bool hasBones = [&scene]() + { + for (uint32_t i = 0; i < scene->mNumMeshes; ++i) + { + if (const auto mesh = scene->mMeshes[i]; mesh && mesh->HasBones()) + { + return true; + } + } + + return false; + }(); + + p_skeleton.reset(); + p_animations.clear(); + + if (hasBones) + { + p_skeleton.emplace(); + BuildSkeleton(scene, p_skeleton.value()); + ProcessAnimations(scene, p_skeleton.value(), p_animations); + } + aiMatrix4x4 identity; + ProcessNode(&identity, scene->mRootNode, scene, p_meshes, p_skeleton ? &p_skeleton.value() : nullptr); - ProcessNode(&identity, scene->mRootNode, scene, p_meshes); + if (p_skeleton && p_skeleton->bones.empty()) + { + p_skeleton.reset(); + p_animations.clear(); + } return true; } -void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiScene * p_scene, std::vector& p_materials) +void OvRendering::Resources::Parsers::AssimpParser::BuildSkeleton(const aiScene* p_scene, Animation::Skeleton& p_skeleton) +{ + const auto addNodeRecursive = [&p_skeleton]( + const auto& p_self, + aiNode* p_node, + int32_t p_parentIndex, + const OvMaths::FMatrix4& p_parentGlobal + ) -> void + { + const auto localBind = ToMatrix4(p_node->mTransformation); + const auto globalBind = p_parentGlobal * localBind; + const auto currentIndex = static_cast(p_skeleton.nodes.size()); + + p_skeleton.nodes.push_back({ + .name = p_node->mName.C_Str(), + .parentIndex = p_parentIndex, + .boneIndex = -1, + .localBindTransform = localBind, + .globalBindTransform = globalBind + }); + + p_skeleton.nodeByName.emplace(p_node->mName.C_Str(), currentIndex); + + for (uint32_t i = 0; i < p_node->mNumChildren; ++i) + { + p_self( + p_self, + p_node->mChildren[i], + static_cast(currentIndex), + globalBind + ); + } + }; + + addNodeRecursive(addNodeRecursive, p_scene->mRootNode, -1, OvMaths::FMatrix4::Identity); + + auto globalInverse = p_scene->mRootNode->mTransformation; + globalInverse.Inverse(); + p_skeleton.globalInverseTransform = ToMatrix4(globalInverse); +} + +void OvRendering::Resources::Parsers::AssimpParser::ProcessAnimations( + const aiScene* p_scene, + const Animation::Skeleton& p_skeleton, + std::vector& p_animations +) +{ + p_animations.reserve(p_scene->mNumAnimations); + + for (uint32_t animIndex = 0; animIndex < p_scene->mNumAnimations; ++animIndex) + { + const auto* animation = p_scene->mAnimations[animIndex]; + + Animation::SkeletalAnimation outAnimation{ + .name = animation->mName.length > 0 ? animation->mName.C_Str() : ("Animation_" + std::to_string(animIndex)), + .duration = static_cast(animation->mDuration), + .ticksPerSecond = animation->mTicksPerSecond > 0.0 ? static_cast(animation->mTicksPerSecond) : 25.0f + }; + + outAnimation.tracks.reserve(animation->mNumChannels); + + for (uint32_t channelIndex = 0; channelIndex < animation->mNumChannels; ++channelIndex) + { + const auto* channel = animation->mChannels[channelIndex]; + const std::string nodeName = channel->mNodeName.C_Str(); + + if (auto foundNode = p_skeleton.FindNodeIndex(nodeName)) + { + Animation::NodeAnimationTrack track; + track.nodeIndex = foundNode.value(); + + track.positionKeys.reserve(channel->mNumPositionKeys); + for (uint32_t i = 0; i < channel->mNumPositionKeys; ++i) + { + const auto& key = channel->mPositionKeys[i]; + track.positionKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z } + }); + } + + track.rotationKeys.reserve(channel->mNumRotationKeys); + for (uint32_t i = 0; i < channel->mNumRotationKeys; ++i) + { + const auto& key = channel->mRotationKeys[i]; + track.rotationKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z, key.mValue.w } + }); + } + + track.scaleKeys.reserve(channel->mNumScalingKeys); + for (uint32_t i = 0; i < channel->mNumScalingKeys; ++i) + { + const auto& key = channel->mScalingKeys[i]; + track.scaleKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z } + }); + } + + const auto trackIndex = static_cast(outAnimation.tracks.size()); + outAnimation.trackByNodeIndex.emplace(track.nodeIndex, trackIndex); + outAnimation.tracks.push_back(std::move(track)); + } + } + + p_animations.push_back(std::move(outAnimation)); + } +} + +void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiScene* p_scene, std::vector& p_materials) { for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i) { @@ -70,7 +300,13 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiSce } } -void OvRendering::Resources::Parsers::AssimpParser::ProcessNode(void* p_transform, aiNode * p_node, const aiScene * p_scene, std::vector& p_meshes) +void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( + void* p_transform, + aiNode* p_node, + const aiScene* p_scene, + std::vector& p_meshes, + Animation::Skeleton* p_skeleton +) { aiMatrix4x4 nodeTransformation = *reinterpret_cast(p_transform) * p_node->mTransformation; @@ -79,21 +315,41 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessNode(void* p_transfor { std::vector vertices; std::vector indices; + bool hasSkinningData = false; aiMesh* mesh = p_scene->mMeshes[p_node->mMeshes[i]]; - ProcessMesh(&nodeTransformation, mesh, p_scene, vertices, indices); - p_meshes.push_back(new Mesh(vertices, indices, mesh->mMaterialIndex)); // The model will handle mesh destruction + + ProcessMesh(&nodeTransformation, mesh, p_scene, vertices, indices, p_skeleton, hasSkinningData); + + if (vertices.empty() || indices.empty()) + { + continue; + } + + p_meshes.push_back(new Mesh(vertices, indices, mesh->mMaterialIndex, hasSkinningData)); // The model will handle mesh destruction } // Then do the same for each of its children for (uint32_t i = 0; i < p_node->mNumChildren; ++i) { - ProcessNode(&nodeTransformation, p_node->mChildren[i], p_scene, p_meshes); + ProcessNode(&nodeTransformation, p_node->mChildren[i], p_scene, p_meshes, p_skeleton); } } -void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh(void* p_transform, aiMesh* p_mesh, const aiScene* p_scene, std::vector& p_outVertices, std::vector& p_outIndices) +void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( + void* p_transform, + aiMesh* p_mesh, + const aiScene* p_scene, + std::vector& p_outVertices, + std::vector& p_outIndices, + Animation::Skeleton* p_skeleton, + bool& p_outHasSkinningData +) { + (void)p_scene; + p_outHasSkinningData = false; + aiMatrix4x4 meshTransformation = *reinterpret_cast(p_transform); + p_outVertices.reserve(p_mesh->mNumVertices); for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) { @@ -103,33 +359,103 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh(void* p_transfor const aiVector3D tangent = meshTransformation * (p_mesh->mTangents ? p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); const aiVector3D bitangent = meshTransformation * (p_mesh->mBitangents ? p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - p_outVertices.push_back({ - position.x, - position.y, - position.z, - texCoords.x, - texCoords.y, - normal.x, - normal.y, - normal.z, - tangent.x, - tangent.y, - tangent.z, - // Assimp calculates the tangent space vectors in a right-handed system. - // But our shader code expects a left-handed system. - // Multiplying the bitangent by -1 will convert it to a left-handed system. - // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. - -bitangent.x, - -bitangent.y, - -bitangent.z - }); + Geometry::Vertex vertex{}; + vertex.position[0] = position.x; + vertex.position[1] = position.y; + vertex.position[2] = position.z; + vertex.texCoords[0] = texCoords.x; + vertex.texCoords[1] = texCoords.y; + vertex.normals[0] = normal.x; + vertex.normals[1] = normal.y; + vertex.normals[2] = normal.z; + vertex.tangent[0] = tangent.x; + vertex.tangent[1] = tangent.y; + vertex.tangent[2] = tangent.z; + // Assimp calculates the tangent space vectors in a right-handed system. + // But our shader code expects a left-handed system. + // Multiplying the bitangent by -1 will convert it to a left-handed system. + // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. + vertex.bitangent[0] = -bitangent.x; + vertex.bitangent[1] = -bitangent.y; + vertex.bitangent[2] = -bitangent.z; + + p_outVertices.push_back(vertex); } for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID) { auto& face = p_mesh->mFaces[faceID]; - for (size_t indexID = 0; indexID < 3; ++indexID) - p_outIndices.push_back(face.mIndices[indexID]); + if (face.mNumIndices < 3) + { + continue; + } + + const auto a = face.mIndices[0]; + const auto b = face.mIndices[1]; + const auto c = face.mIndices[2]; + if (a >= p_mesh->mNumVertices || b >= p_mesh->mNumVertices || c >= p_mesh->mNumVertices) + { + continue; + } + + p_outIndices.push_back(a); + p_outIndices.push_back(b); + p_outIndices.push_back(c); + } + + if (!p_skeleton || !p_mesh->HasBones()) + { + return; + } + + for (uint32_t boneID = 0; boneID < p_mesh->mNumBones; ++boneID) + { + const aiBone* bone = p_mesh->mBones[boneID]; + const std::string boneName = bone->mName.C_Str(); + const auto offsetMatrix = ToMatrix4(bone->mOffsetMatrix); + + auto nodeIndex = p_skeleton->FindNodeIndex(boneName); + if (!nodeIndex) + { + OVLOG_WARNING("AssimpParser: Bone '" + boneName + "' has no matching node in hierarchy and will be ignored."); + continue; + } + + uint32_t boneIndex = 0; + + if (auto existing = p_skeleton->FindBoneIndex(boneName)) + { + boneIndex = existing.value(); + } + else + { + boneIndex = static_cast(p_skeleton->bones.size()); + p_skeleton->boneByName.emplace(boneName, boneIndex); + p_skeleton->bones.push_back({ + .name = boneName, + .nodeIndex = nodeIndex.value(), + .offsetMatrix = offsetMatrix + }); + } + + p_skeleton->nodes[nodeIndex.value()].boneIndex = static_cast(boneIndex); + + for (uint32_t weightID = 0; weightID < bone->mNumWeights; ++weightID) + { + const auto& weight = bone->mWeights[weightID]; + if (weight.mVertexId < p_outVertices.size()) + { + if (AddBoneData(p_outVertices[weight.mVertexId], boneIndex, weight.mWeight)) + { + p_outHasSkinningData = true; + } + } + } + } + + for (auto& vertex : p_outVertices) + { + NormalizeBoneData(vertex); } } From 871d1ed3511e938f75a8891829ef6e8b015831b8 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 17:43:45 -0400 Subject: [PATCH 02/18] Various improvements - More extensive use of `FTransform` - Simplified skinning calculations - Fixed PickingRenderPass culling - Now properly supports custom outline shaders - Deprecated PRE_TRANSFORM_VERTICES - Removed auto-add SkinnedMeshRenderer when model changes --- .../ECS/Components/CSkinnedMeshRenderer.h | 13 - .../OvCore/ECS/Components/CModelRenderer.cpp | 13 +- .../ECS/Components/CSkinnedMeshRenderer.cpp | 279 +++--------------- .../ResourceManagement/ModelManager.cpp | 2 +- .../src/OvEditor/Panels/AssetProperties.cpp | 2 +- .../Rendering/OutlineRenderFeature.cpp | 13 +- .../OvEditor/Rendering/PickingRenderPass.cpp | 2 + .../OvRendering/Animation/SkeletalData.h | 12 +- .../Resources/Parsers/AssimpParser.cpp | 83 ++---- 9 files changed, 75 insertions(+), 344 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h index b82d53404..7faa4c536 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -6,7 +6,6 @@ #pragma once -#include #include #include #include @@ -14,7 +13,6 @@ #include #include -#include namespace OvCore::ECS { class Actor; } namespace OvRendering::Resources { class Model; } @@ -197,13 +195,6 @@ namespace OvCore::ECS::Components virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; private: - struct TrackSamplingCursor - { - size_t positionKeyIndex = 0; - size_t rotationKeyIndex = 0; - size_t scaleKeyIndex = 0; - }; - void SyncWithModel(); void RebuildRuntimeData(); void EvaluatePose(); @@ -227,14 +218,10 @@ namespace OvCore::ECS::Components uint64_t m_poseVersion = 0; std::vector m_animationNames; - std::vector m_bindPoseTransforms; std::vector m_localPose; std::vector m_globalPose; std::vector m_boneMatrices; std::vector m_boneMatricesTransposed; - std::vector m_trackSamplingCursors; - float m_lastSampleTimeTicks = 0.0f; - std::optional m_lastSampledAnimationIndex = std::nullopt; }; template<> diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp index de0a52b87..0d3878cd7 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp @@ -44,15 +44,6 @@ void OvCore::ECS::Components::CModelRenderer::SetModel(OvRendering::Resources::M { m_model = p_model; m_modelChangedEvent.Invoke(); - - if (m_model && m_model->IsSkinned()) - { - owner.AddComponent().NotifyModelChanged(); - } - else if (auto skinnedRenderer = owner.GetComponent()) - { - skinnedRenderer->NotifyModelChanged(); - } } OvRendering::Resources::Model * OvCore::ECS::Components::CModelRenderer::GetModel() const @@ -90,9 +81,7 @@ void OvCore::ECS::Components::CModelRenderer::OnSerialize(tinyxml2::XMLDocument void OvCore::ECS::Components::CModelRenderer::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode* p_node) { - OvRendering::Resources::Model* deserializedModel = nullptr; - OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", deserializedModel); - SetModel(deserializedModel); + OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", m_model); OvCore::Helpers::Serializer::DeserializeInt(p_doc, p_node, "frustum_behaviour", reinterpret_cast(m_frustumBehaviour)); OvCore::Helpers::Serializer::DeserializeVec3(p_doc, p_node, "custom_bounding_sphere_position", m_customBoundingSphere.position); OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "custom_bounding_sphere_radius", m_customBoundingSphere.radius); diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp index 44a6a7175..0756b6856 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -15,74 +15,12 @@ #include #include #include +#include #include #include namespace { - constexpr float kDefaultTicksPerSecond = 25.0f; - - OvMaths::FTransform DecomposeTransform(const OvMaths::FMatrix4& p_matrix) - { - constexpr float kEpsilon = 1e-8f; - OvMaths::FVector3 position = OvMaths::FVector3::Zero; - OvMaths::FQuaternion rotation = OvMaths::FQuaternion::Identity; - OvMaths::FVector3 scale = OvMaths::FVector3::One; - - position = { - p_matrix.data[3], - p_matrix.data[7], - p_matrix.data[11] - }; - - OvMaths::FVector3 columns[3] = { - { p_matrix.data[0], p_matrix.data[4], p_matrix.data[8] }, - { p_matrix.data[1], p_matrix.data[5], p_matrix.data[9] }, - { p_matrix.data[2], p_matrix.data[6], p_matrix.data[10] } - }; - - scale.x = OvMaths::FVector3::Length(columns[0]); - scale.y = OvMaths::FVector3::Length(columns[1]); - scale.z = OvMaths::FVector3::Length(columns[2]); - - if (scale.x > kEpsilon) columns[0] /= scale.x; - if (scale.y > kEpsilon) columns[1] /= scale.y; - if (scale.z > kEpsilon) columns[2] /= scale.z; - - const float basisDeterminant = OvMaths::FVector3::Dot( - OvMaths::FVector3::Cross(columns[0], columns[1]), - columns[2] - ); - - if (basisDeterminant < 0.0f) - { - if (scale.x >= scale.y && scale.x >= scale.z) - { - scale.x = -scale.x; - columns[0] = -columns[0]; - } - else if (scale.y >= scale.x && scale.y >= scale.z) - { - scale.y = -scale.y; - columns[1] = -columns[1]; - } - else - { - scale.z = -scale.z; - columns[2] = -columns[2]; - } - } - - const OvMaths::FMatrix3 rotationMatrix{ - columns[0].x, columns[1].x, columns[2].x, - columns[0].y, columns[1].y, columns[2].y, - columns[0].z, columns[1].z, columns[2].z - }; - - rotation = OvMaths::FQuaternion::Normalize(OvMaths::FQuaternion(rotationMatrix)); - return OvMaths::FTransform(position, rotation, scale); - } - float WrapTime(float p_value, float p_duration) { if (p_duration <= 0.0f) @@ -101,9 +39,7 @@ namespace float p_duration, const T& p_defaultValue, bool p_looping, - TLerp p_lerp, - size_t* p_cursor = nullptr, - bool p_useCursorHint = false + TLerp p_lerp ) { if (p_keys.empty()) @@ -116,64 +52,10 @@ namespace return p_keys.front().value; } - const auto sampleWithSegment = [&](const auto& p_prev, const auto& p_next, float p_segmentDuration, float p_alphaBias = 0.0f) - { - if (p_segmentDuration <= std::numeric_limits::epsilon()) - { - return p_prev.value; - } - - const float alpha = std::clamp(((p_time - p_prev.time) + p_alphaBias) / p_segmentDuration, 0.0f, 1.0f); - return p_lerp(p_prev.value, p_next.value, alpha); - }; - - if (p_useCursorHint && p_cursor) - { - size_t cursor = std::min(*p_cursor, p_keys.size() - 1); - while (cursor + 1 < p_keys.size() && p_time >= p_keys[cursor + 1].time) - { - ++cursor; - } - - *p_cursor = cursor; - - if (cursor + 1 < p_keys.size()) - { - return sampleWithSegment(p_keys[cursor], p_keys[cursor + 1], p_keys[cursor + 1].time - p_keys[cursor].time); - } - - if (!p_looping) - { - return p_keys.back().value; - } - - const auto& prev = p_keys.back(); - const auto& next = p_keys.front(); - const float segmentDuration = (p_duration - prev.time) + next.time; - return sampleWithSegment(prev, next, segmentDuration); - } - if (!p_looping) { - if (p_time <= p_keys.front().time) - { - if (p_cursor) - { - *p_cursor = 0; - } - - return p_keys.front().value; - } - - if (p_time >= p_keys.back().time) - { - if (p_cursor) - { - *p_cursor = p_keys.size() - 1; - } - - return p_keys.back().value; - } + if (p_time <= p_keys.front().time) return p_keys.front().value; + if (p_time >= p_keys.back().time) return p_keys.back().value; } const auto nextIt = std::upper_bound( @@ -183,48 +65,37 @@ namespace [](float p_lhs, const auto& p_rhs) { return p_lhs < p_rhs.time; } ); + const auto interpolate = [&](const auto& p_prev, const auto& p_next, float p_segmentDuration) + { + if (p_segmentDuration <= std::numeric_limits::epsilon()) + { + return p_prev.value; + } + + const float alpha = std::clamp((p_time - p_prev.time) / p_segmentDuration, 0.0f, 1.0f); + return p_lerp(p_prev.value, p_next.value, alpha); + }; + if (nextIt == p_keys.end()) { if (!p_looping) { - if (p_cursor) - { - *p_cursor = p_keys.size() - 1; - } - return p_keys.back().value; } const auto& prev = p_keys.back(); const auto& next = p_keys.front(); - - const float segmentDuration = (p_duration - prev.time) + next.time; - if (p_cursor) - { - *p_cursor = p_keys.size() - 1; - } - - return sampleWithSegment(prev, next, segmentDuration); + return interpolate(prev, next, (p_duration - prev.time) + next.time); } if (nextIt == p_keys.begin()) { - if (p_cursor) - { - *p_cursor = 0; - } - return nextIt->value; } const auto& prev = *std::prev(nextIt); const auto& next = *nextIt; - if (p_cursor) - { - *p_cursor = static_cast(std::distance(p_keys.begin(), std::prev(nextIt))); - } - - return sampleWithSegment(prev, next, next.time - prev.time); + return interpolate(prev, next, next.time - prev.time); } } @@ -323,7 +194,7 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::SetTime(float p_timeSeconds) } const auto& animation = m_model->GetAnimations().at(*m_animationIndex); - const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; + const float ticksPerSecond = animation.GetEffectiveTicksPerSecond(); m_currentTimeTicks = p_timeSeconds * ticksPerSecond; if (m_looping) @@ -347,8 +218,8 @@ float OvCore::ECS::Components::CSkinnedMeshRenderer::GetTime() const } const auto& animation = m_model->GetAnimations().at(*m_animationIndex); - const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; - return ticksPerSecond > 0.0f ? m_currentTimeTicks / ticksPerSecond : 0.0f; + const float ticksPerSecond = animation.GetEffectiveTicksPerSecond(); + return m_currentTimeTicks / ticksPerSecond; } uint32_t OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationCount() const @@ -586,17 +457,13 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::RebuildRuntimeData() const std::string requestedAnimationName = m_deserializedAnimationName; m_animationNames.clear(); - m_bindPoseTransforms.clear(); m_localPose.clear(); m_globalPose.clear(); m_boneMatrices.clear(); m_boneMatricesTransposed.clear(); - m_trackSamplingCursors.clear(); m_animationIndex = std::nullopt; m_currentTimeTicks = preservedTimeTicks; m_poseEvaluationAccumulator = 0.0f; - m_lastSampleTimeTicks = 0.0f; - m_lastSampledAnimationIndex = std::nullopt; if (!HasCompatibleModel()) { @@ -606,12 +473,6 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::RebuildRuntimeData() const auto& skeleton = m_model->GetSkeleton().value(); const auto& animations = m_model->GetAnimations(); - m_bindPoseTransforms.reserve(skeleton.nodes.size()); - for (const auto& node : skeleton.nodes) - { - m_bindPoseTransforms.push_back(DecomposeTransform(node.localBindTransform)); - } - m_localPose.resize(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); m_globalPose.resize(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); m_boneMatrices.resize(skeleton.bones.size(), OvMaths::FMatrix4::Identity); @@ -668,37 +529,6 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::EvaluatePose() const auto& skeleton = m_model->GetSkeleton().value(); - if (m_bindPoseTransforms.size() != skeleton.nodes.size()) - { - m_bindPoseTransforms.clear(); - m_bindPoseTransforms.reserve(skeleton.nodes.size()); - - for (const auto& node : skeleton.nodes) - { - m_bindPoseTransforms.push_back(DecomposeTransform(node.localBindTransform)); - } - } - - if (m_localPose.size() != skeleton.nodes.size()) - { - m_localPose.assign(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); - } - - if (m_globalPose.size() != skeleton.nodes.size()) - { - m_globalPose.assign(skeleton.nodes.size(), OvMaths::FMatrix4::Identity); - } - - if (m_boneMatrices.size() != skeleton.bones.size()) - { - m_boneMatrices.assign(skeleton.bones.size(), OvMaths::FMatrix4::Identity); - } - - if (m_boneMatricesTransposed.size() != skeleton.bones.size()) - { - m_boneMatricesTransposed.assign(skeleton.bones.size(), OvMaths::FMatrix4::Identity); - } - for (size_t nodeIndex = 0; nodeIndex < skeleton.nodes.size(); ++nodeIndex) { m_localPose[nodeIndex] = skeleton.nodes[nodeIndex].localBindTransform; @@ -713,80 +543,45 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::EvaluatePose() (m_looping ? WrapTime(m_currentTimeTicks, duration) : std::clamp(m_currentTimeTicks, 0.0f, duration)) : 0.0f; - if (m_trackSamplingCursors.size() != animation.tracks.size()) - { - m_trackSamplingCursors.assign(animation.tracks.size(), TrackSamplingCursor{}); - } - - const bool useSamplingCursorHint = - m_lastSampledAnimationIndex == m_animationIndex && - sampleTime >= m_lastSampleTimeTicks; - - if (!useSamplingCursorHint) + for (const auto& track : animation.tracks) { - for (auto& cursor : m_trackSamplingCursors) - { - cursor = {}; - } - } - - for (size_t trackIndex = 0; trackIndex < animation.tracks.size(); ++trackIndex) - { - const auto& track = animation.tracks[trackIndex]; - auto& trackCursor = m_trackSamplingCursors[trackIndex]; - - if (track.nodeIndex >= m_localPose.size() || track.nodeIndex >= m_bindPoseTransforms.size()) + if (track.nodeIndex >= m_localPose.size()) { continue; } - const auto& bindPoseTransform = m_bindPoseTransforms[track.nodeIndex]; + const auto& node = skeleton.nodes[track.nodeIndex]; const OvMaths::FVector3 sampledPosition = SampleKeys( track.positionKeys, sampleTime, duration, - bindPoseTransform.GetLocalPosition(), + node.bindPosition, m_looping, - [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); }, - &trackCursor.positionKeyIndex, - useSamplingCursorHint + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); } ); const OvMaths::FQuaternion sampledRotation = SampleKeys( track.rotationKeys, sampleTime, duration, - bindPoseTransform.GetLocalRotation(), + node.bindRotation, m_looping, - [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FQuaternion::Slerp(p_a, p_b, p_alpha); }, - &trackCursor.rotationKeyIndex, - useSamplingCursorHint + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FQuaternion::Slerp(p_a, p_b, p_alpha); } ); const OvMaths::FVector3 sampledScale = SampleKeys( track.scaleKeys, sampleTime, duration, - bindPoseTransform.GetLocalScale(), + node.bindScale, m_looping, - [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); }, - &trackCursor.scaleKeyIndex, - useSamplingCursorHint + [](const auto& p_a, const auto& p_b, float p_alpha) { return OvMaths::FVector3::Lerp(p_a, p_b, p_alpha); } ); const OvMaths::FTransform sampled(sampledPosition, sampledRotation, sampledScale); - m_localPose[track.nodeIndex] = sampled.GetLocalMatrix(); } - - m_lastSampledAnimationIndex = m_animationIndex; - m_lastSampleTimeTicks = sampleTime; - } - else - { - m_lastSampledAnimationIndex = std::nullopt; - m_lastSampleTimeTicks = 0.0f; } for (size_t nodeIndex = 0; nodeIndex < skeleton.nodes.size(); ++nodeIndex) @@ -801,16 +596,10 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::EvaluatePose() for (size_t boneIndex = 0; boneIndex < skeleton.bones.size(); ++boneIndex) { const auto& bone = skeleton.bones[boneIndex]; - if (bone.nodeIndex < m_globalPose.size()) - { - m_boneMatrices[boneIndex] = - m_globalPose[bone.nodeIndex] * - bone.offsetMatrix; - } - else - { - m_boneMatrices[boneIndex] = OvMaths::FMatrix4::Identity; - } + m_boneMatrices[boneIndex] = + bone.nodeIndex < m_globalPose.size() ? + m_globalPose[bone.nodeIndex] * bone.offsetMatrix : + OvMaths::FMatrix4::Identity; m_boneMatricesTransposed[boneIndex] = OvMaths::FMatrix4::Transpose(m_boneMatrices[boneIndex]); } @@ -847,7 +636,7 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::UpdatePlayback(float p_delta return; } - const float ticksPerSecond = animation.ticksPerSecond > 0.0f ? animation.ticksPerSecond : kDefaultTicksPerSecond; + const float ticksPerSecond = animation.GetEffectiveTicksPerSecond(); m_currentTimeTicks += p_deltaTime * ticksPerSecond * m_playbackSpeed; if (m_looping) diff --git a/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp index 00d9b7107..6be240273 100644 --- a/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp +++ b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp @@ -22,7 +22,7 @@ OvRendering::Resources::Parsers::EModelParserFlags GetAssetMetadata(const std::s if (metaFile.GetOrDefault("GEN_NORMALS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::GEN_NORMALS; if (metaFile.GetOrDefault("GEN_SMOOTH_NORMALS", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::GEN_SMOOTH_NORMALS; if (metaFile.GetOrDefault("SPLIT_LARGE_MESHES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::SPLIT_LARGE_MESHES; - if (metaFile.GetOrDefault("PRE_TRANSFORM_VERTICES", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::PRE_TRANSFORM_VERTICES; + if (metaFile.GetOrDefault("PRE_TRANSFORM_VERTICES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::PRE_TRANSFORM_VERTICES; if (metaFile.GetOrDefault("LIMIT_BONE_WEIGHTS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::LIMIT_BONE_WEIGHTS; if (metaFile.GetOrDefault("VALIDATE_DATA_STRUCTURE", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::VALIDATE_DATA_STRUCTURE; if (metaFile.GetOrDefault("IMPROVE_CACHE_LOCALITY", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::IMPROVE_CACHE_LOCALITY; diff --git a/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp index cebd2444f..fec6124f5 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp @@ -219,7 +219,7 @@ void OvEditor::Panels::AssetProperties::CreateModelSettings() m_metadata->Add("GEN_NORMALS", false); m_metadata->Add("GEN_SMOOTH_NORMALS", true); m_metadata->Add("SPLIT_LARGE_MESHES", false); - m_metadata->Add("PRE_TRANSFORM_VERTICES", true); + m_metadata->Add("PRE_TRANSFORM_VERTICES", false); m_metadata->Add("LIMIT_BONE_WEIGHTS", false); m_metadata->Add("VALIDATE_DATA_STRUCTURE", false); m_metadata->Add("IMPROVE_CACHE_LOCALITY", true); diff --git a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp index 87a6e8467..b33266c6c 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp @@ -43,15 +43,16 @@ namespace OvCore::Resources::Material& p_fallbackMaterial ) { - if (p_hasSkinning) - { - return p_fallbackMaterial; - } - auto* material = FindMeshMaterial(p_materials, p_materialIndex); if (material && material->IsValid() && material->HasPass(std::string{ p_passName })) { - return *material; + const bool skinningCompatible = !p_hasSkinning || + material->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); + + if (skinningCompatible) + { + return *material; + } } return p_fallbackMaterial; diff --git a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp index 722ed0df4..c77672b82 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp @@ -200,6 +200,8 @@ void OvEditor::Rendering::PickingRenderPass::DrawPickableModels( OvRendering::Entities::Drawable finalDrawable = drawable; finalDrawable.material = &targetMaterial; finalDrawable.stateMask = targetMaterial.GenerateStateMask(); + finalDrawable.stateMask.frontfaceCulling = false; + finalDrawable.stateMask.backfaceCulling = false; finalDrawable.pass = kPickingPassName; finalDrawable.featureSetOverride = std::nullopt; diff --git a/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h b/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h index dd0e648a6..bb852e4a1 100644 --- a/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h +++ b/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h @@ -27,7 +27,11 @@ namespace OvRendering::Animation int32_t parentIndex = -1; int32_t boneIndex = -1; OvMaths::FMatrix4 localBindTransform = OvMaths::FMatrix4::Identity; - OvMaths::FMatrix4 globalBindTransform = OvMaths::FMatrix4::Identity; + + // Decomposed from localBindTransform — used as default values when a track has no keys + OvMaths::FVector3 bindPosition; + OvMaths::FQuaternion bindRotation; + OvMaths::FVector3 bindScale = OvMaths::FVector3(1.0f, 1.0f, 1.0f); }; struct Bone @@ -43,7 +47,6 @@ namespace OvRendering::Animation std::vector bones; std::unordered_map nodeByName; std::unordered_map boneByName; - OvMaths::FMatrix4 globalInverseTransform = OvMaths::FMatrix4::Identity; bool IsValid() const { @@ -104,6 +107,11 @@ namespace OvRendering::Animation return ticksPerSecond > 0.0f ? duration / ticksPerSecond : 0.0f; } + float GetEffectiveTicksPerSecond() const + { + return ticksPerSecond > 0.0f ? ticksPerSecond : 25.0f; + } + const NodeAnimationTrack* FindTrack(uint32_t p_nodeIndex) const { if (auto found = trackByNodeIndex.find(p_nodeIndex); found != trackByNodeIndex.end()) diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index 1dd3ce706..dd724fd52 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -37,7 +37,13 @@ namespace p_flags |= TRIANGULATE; p_flags |= LIMIT_BONE_WEIGHTS; - p_flags &= ~PRE_TRANSFORM_VERTICES; + + if (static_cast(p_flags & PRE_TRANSFORM_VERTICES)) + { + p_flags &= ~PRE_TRANSFORM_VERTICES; + OVLOG_WARNING("AssimpParser: PRE_TRANSFORM_VERTICES is incompatible with skeletal animation and has been removed."); + } + p_flags &= ~DEBONE; return p_flags; @@ -60,6 +66,7 @@ namespace return false; } + // aiProcess_LimitBoneWeights guarantees at most kMaxBonesPerVertex influences per vertex. for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) { if (p_vertex.boneWeights[i] <= 0.0f) @@ -70,50 +77,9 @@ namespace } } - auto minSlot = 0u; - for (uint8_t i = 1; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - if (p_vertex.boneWeights[i] < p_vertex.boneWeights[minSlot]) - { - minSlot = i; - } - } - - if (p_weight > p_vertex.boneWeights[minSlot]) - { - p_vertex.boneIDs[minSlot] = static_cast(p_boneIndex); - p_vertex.boneWeights[minSlot] = p_weight; - return true; - } - return false; } - float GetBoneWeightSum(const OvRendering::Geometry::Vertex& p_vertex) - { - float sum = 0.0f; - - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - sum += p_vertex.boneWeights[i]; - } - - return sum; - } - - void NormalizeBoneData(OvRendering::Geometry::Vertex& p_vertex) - { - const float sum = GetBoneWeightSum(p_vertex); - - if (sum > 0.0f) - { - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - p_vertex.boneWeights[i] /= sum; - } - } - } - } bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( @@ -180,40 +146,34 @@ void OvRendering::Resources::Parsers::AssimpParser::BuildSkeleton(const aiScene* const auto addNodeRecursive = [&p_skeleton]( const auto& p_self, aiNode* p_node, - int32_t p_parentIndex, - const OvMaths::FMatrix4& p_parentGlobal + int32_t p_parentIndex ) -> void { - const auto localBind = ToMatrix4(p_node->mTransformation); - const auto globalBind = p_parentGlobal * localBind; const auto currentIndex = static_cast(p_skeleton.nodes.size()); + aiVector3D aiPosition, aiScale; + aiQuaternion aiRotation; + p_node->mTransformation.Decompose(aiScale, aiRotation, aiPosition); + p_skeleton.nodes.push_back({ .name = p_node->mName.C_Str(), .parentIndex = p_parentIndex, .boneIndex = -1, - .localBindTransform = localBind, - .globalBindTransform = globalBind + .localBindTransform = ToMatrix4(p_node->mTransformation), + .bindPosition = { aiPosition.x, aiPosition.y, aiPosition.z }, + .bindRotation = { aiRotation.x, aiRotation.y, aiRotation.z, aiRotation.w }, + .bindScale = { aiScale.x, aiScale.y, aiScale.z } }); p_skeleton.nodeByName.emplace(p_node->mName.C_Str(), currentIndex); for (uint32_t i = 0; i < p_node->mNumChildren; ++i) { - p_self( - p_self, - p_node->mChildren[i], - static_cast(currentIndex), - globalBind - ); + p_self(p_self, p_node->mChildren[i], static_cast(currentIndex)); } }; - addNodeRecursive(addNodeRecursive, p_scene->mRootNode, -1, OvMaths::FMatrix4::Identity); - - auto globalInverse = p_scene->mRootNode->mTransformation; - globalInverse.Inverse(); - p_skeleton.globalInverseTransform = ToMatrix4(globalInverse); + addNodeRecursive(addNodeRecursive, p_scene->mRootNode, -1); } void OvRendering::Resources::Parsers::AssimpParser::ProcessAnimations( @@ -453,9 +413,4 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( } } } - - for (auto& vertex : p_outVertices) - { - NormalizeBoneData(vertex); - } } From 647acbfa18d22186a8009fef5d6bc51ef1e2163d Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 17:57:57 -0400 Subject: [PATCH 03/18] Optional index and proper T-Pose when no anim set --- .../ECS/Components/CSkinnedMeshRenderer.h | 8 ++-- .../ECS/Components/CSkinnedMeshRenderer.cpp | 44 +++++++++++-------- .../Lua/Bindings/LuaComponentsBindings.cpp | 2 +- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h index 7faa4c536..78cc98b5a 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -132,10 +132,10 @@ namespace OvCore::ECS::Components std::optional GetAnimationName(uint32_t p_index) const; /** - * Sets the active animation by index + * Sets the active animation by index. Pass std::nullopt to clear and return to T-pose. * @param p_index */ - bool SetAnimation(uint32_t p_index); + bool SetAnimation(std::optional p_index); /** * Sets the active animation by name @@ -144,9 +144,9 @@ namespace OvCore::ECS::Components bool SetAnimation(const std::string& p_name); /** - * Returns the active animation index (-1 if none) + * Returns the active animation index, or std::nullopt if none is set */ - int32_t GetAnimationIndex() const; + std::optional GetAnimationIndex() const; /** * Returns the active animation name (empty if none) diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp index 0756b6856..3208d056c 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -128,7 +129,7 @@ bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasCompatibleModel() const bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasSkinningData() const { - return HasCompatibleModel() && !m_boneMatrices.empty(); + return HasCompatibleModel() && m_animationIndex.has_value() && !m_boneMatrices.empty(); } void OvCore::ECS::Components::CSkinnedMeshRenderer::SetEnabled(bool p_value) @@ -237,9 +238,18 @@ std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetAni return std::nullopt; } -bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(uint32_t p_index) +bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(std::optional p_index) { - if (!HasCompatibleModel() || p_index >= m_model->GetAnimations().size()) + if (!p_index.has_value()) + { + m_animationIndex = std::nullopt; + m_currentTimeTicks = 0.0f; + m_poseEvaluationAccumulator = 0.0f; + EvaluatePose(); + return true; + } + + if (!HasCompatibleModel() || *p_index >= m_model->GetAnimations().size()) { return false; } @@ -273,9 +283,9 @@ bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(const std::stri return SetAnimation(static_cast(std::distance(animations.begin(), found))); } -int32_t OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationIndex() const +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationIndex() const { - return m_animationIndex.has_value() ? static_cast(*m_animationIndex) : -1; + return m_animationIndex; } std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimation() const @@ -403,7 +413,8 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: ); GUIDrawer::CreateTitle(p_root, "Active Animation"); - auto& animationChoice = p_root.CreateWidget(GetAnimationIndex()); + const int currentAnimIndex = GetAnimationIndex().has_value() ? static_cast(*GetAnimationIndex()) : -1; + auto& animationChoice = p_root.CreateWidget(currentAnimIndex); animationChoice.choices.emplace(-1, ""); for (size_t i = 0; i < m_animationNames.size(); ++i) @@ -411,20 +422,15 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: animationChoice.choices.emplace(static_cast(i), m_animationNames[i]); } - animationChoice.ValueChangedEvent += [this](int p_choice) + auto& animDispatcher = animationChoice.AddPlugin>(); + animDispatcher.RegisterGatherer([this] { - if (p_choice < 0) - { - m_animationIndex = std::nullopt; - m_currentTimeTicks = 0.0f; - m_poseEvaluationAccumulator = 0.0f; - EvaluatePose(); - } - else - { - SetAnimation(static_cast(p_choice)); - } - }; + return GetAnimationIndex().has_value() ? static_cast(*GetAnimationIndex()) : -1; + }); + animDispatcher.RegisterProvider([this](int p_choice) + { + SetAnimation(p_choice >= 0 ? std::make_optional(static_cast(p_choice)) : std::nullopt); + }); if (!HasCompatibleModel()) { diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp index 462dfbbd6..90e4e26f2 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -108,7 +108,7 @@ void BindLuaComponents(sol::state& p_luaState) "GetAnimationCount", &CSkinnedMeshRenderer::GetAnimationCount, "GetAnimationName", &CSkinnedMeshRenderer::GetAnimationName, "SetAnimation", sol::overload( - sol::resolve(&CSkinnedMeshRenderer::SetAnimation), + sol::resolve)>(&CSkinnedMeshRenderer::SetAnimation), sol::resolve(&CSkinnedMeshRenderer::SetAnimation) ), "GetAnimationIndex", &CSkinnedMeshRenderer::GetAnimationIndex, From 6a8f3f204549dfdf3ce87479ac51ff639ec2c3c7 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 18:39:02 -0400 Subject: [PATCH 04/18] Vertex and SkinnedVertex now fully separated --- Resources/Editor/Shaders/OutlineFallback.ovfx | 4 +- Resources/Editor/Shaders/PickingFallback.ovfx | 4 +- .../Engine/Shaders/Common/Skinning.ovfxh | 9 +- Resources/Engine/Shaders/ShadowFallback.ovfx | 4 +- Resources/Engine/Shaders/Standard.ovfx | 4 +- Resources/Engine/Shaders/Unlit.ovfx | 4 +- .../include/OvRendering/Geometry/Vertex.h | 13 +- .../include/OvRendering/Resources/Mesh.h | 19 +- .../Resources/Parsers/AssimpParser.h | 7 +- .../OvRendering/Settings/VertexAttribute.h | 1 + .../OvRendering/HAL/OpenGL/GLVertexArray.cpp | 29 ++- .../src/OvRendering/Resources/Mesh.cpp | 175 ++++++++---------- .../Resources/Parsers/AssimpParser.cpp | 175 +++++++++--------- 13 files changed, 237 insertions(+), 211 deletions(-) diff --git a/Resources/Editor/Shaders/OutlineFallback.ovfx b/Resources/Editor/Shaders/OutlineFallback.ovfx index 0e8793bc9..c06790245 100644 --- a/Resources/Editor/Shaders/OutlineFallback.ovfx +++ b/Resources/Editor/Shaders/OutlineFallback.ovfx @@ -5,8 +5,10 @@ #version 450 core layout(location = 0) in vec3 geo_Pos; -layout(location = 5) in vec4 geo_BoneIDs; +#if defined(SKINNING) +layout(location = 5) in uvec4 geo_BoneIDs; layout(location = 6) in vec4 geo_BoneWeights; +#endif #include ":Shaders/Common/Skinning.ovfxh" diff --git a/Resources/Editor/Shaders/PickingFallback.ovfx b/Resources/Editor/Shaders/PickingFallback.ovfx index 373ba1d88..e5c55fe71 100644 --- a/Resources/Editor/Shaders/PickingFallback.ovfx +++ b/Resources/Editor/Shaders/PickingFallback.ovfx @@ -5,8 +5,10 @@ #version 450 core layout (location = 0) in vec3 geo_Pos; -layout (location = 5) in vec4 geo_BoneIDs; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; layout (location = 6) in vec4 geo_BoneWeights; +#endif layout (std140) uniform EngineUBO { diff --git a/Resources/Engine/Shaders/Common/Skinning.ovfxh b/Resources/Engine/Shaders/Common/Skinning.ovfxh index ad0d90b64..7283bf6f4 100644 --- a/Resources/Engine/Shaders/Common/Skinning.ovfxh +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -1,7 +1,7 @@ #include ":Shaders/Common/Buffers/SkinningSSBO.ovfxh" void ApplyBone( - float p_boneID, + uint p_boneID, float p_boneWeight, int p_boneCount, inout mat4 p_skinning, @@ -10,16 +10,15 @@ void ApplyBone( { if (p_boneWeight > 0.0) { - const int index = int(floor(p_boneID + 0.5)); - if (index >= 0 && index < p_boneCount) + if (int(p_boneID) < p_boneCount) { - p_skinning += ssbo_Bones[index] * p_boneWeight; + p_skinning += ssbo_Bones[p_boneID] * p_boneWeight; p_appliedWeight += p_boneWeight; } } } -mat4 ComputeSkinningMatrix(vec4 p_boneIDs, vec4 p_boneWeights) +mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) { const int boneCount = ssbo_Bones.length(); diff --git a/Resources/Engine/Shaders/ShadowFallback.ovfx b/Resources/Engine/Shaders/ShadowFallback.ovfx index ebbd6804d..63488d097 100644 --- a/Resources/Engine/Shaders/ShadowFallback.ovfx +++ b/Resources/Engine/Shaders/ShadowFallback.ovfx @@ -5,8 +5,10 @@ #version 450 core layout (location = 0) in vec3 geo_Pos; -layout (location = 5) in vec4 geo_BoneIDs; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; layout (location = 6) in vec4 geo_BoneWeights; +#endif #include ":Shaders/Common/Buffers/EngineUBO.ovfxh" #include ":Shaders/Common/Skinning.ovfxh" diff --git a/Resources/Engine/Shaders/Standard.ovfx b/Resources/Engine/Shaders/Standard.ovfx index 7de497d90..d12f86b14 100644 --- a/Resources/Engine/Shaders/Standard.ovfx +++ b/Resources/Engine/Shaders/Standard.ovfx @@ -19,8 +19,10 @@ layout (location = 1) in vec2 geo_TexCoords; layout (location = 2) in vec3 geo_Normal; layout (location = 3) in vec3 geo_Tangent; layout (location = 4) in vec3 geo_Bitangent; -layout (location = 5) in vec4 geo_BoneIDs; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; layout (location = 6) in vec4 geo_BoneWeights; +#endif out VS_OUT { diff --git a/Resources/Engine/Shaders/Unlit.ovfx b/Resources/Engine/Shaders/Unlit.ovfx index 8feb4d651..f41504289 100644 --- a/Resources/Engine/Shaders/Unlit.ovfx +++ b/Resources/Engine/Shaders/Unlit.ovfx @@ -12,8 +12,10 @@ layout (location = 0) in vec3 geo_Pos; layout (location = 1) in vec2 geo_TexCoords; -layout (location = 5) in vec4 geo_BoneIDs; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; layout (location = 6) in vec4 geo_BoneWeights; +#endif out VS_OUT { diff --git a/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h index 85bfe9860..b1453e5f1 100644 --- a/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h +++ b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h @@ -6,6 +6,8 @@ #pragma once +#include + #include namespace OvRendering::Geometry @@ -20,7 +22,14 @@ namespace OvRendering::Geometry float normals[3]; float tangent[3]; float bitangent[3]; - float boneIDs[Animation::kMaxBonesPerVertex]; - float boneWeights[Animation::kMaxBonesPerVertex]; + }; + + /** + * Extends Vertex with skeletal animation data + */ + struct SkinnedVertex : Vertex + { + uint32_t boneIDs[Animation::kMaxBonesPerVertex] = {}; + float boneWeights[Animation::kMaxBonesPerVertex] = {}; }; } diff --git a/Sources/OvRendering/include/OvRendering/Resources/Mesh.h b/Sources/OvRendering/include/OvRendering/Resources/Mesh.h index 77df296fc..370745ea4 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Mesh.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Mesh.h @@ -24,7 +24,7 @@ namespace OvRendering::Resources { public: /** - * Create a mesh with the given vertices, indices and material index + * Create a non-skinned mesh with the given vertices, indices and material index * @param p_vertices * @param p_indices * @param p_materialIndex @@ -32,8 +32,19 @@ namespace OvRendering::Resources Mesh( std::span p_vertices, std::span p_indices, - uint32_t p_materialIndex = 0, - bool p_hasSkinningData = false + uint32_t p_materialIndex = 0 + ); + + /** + * Create a skinned mesh with the given vertices, indices and material index + * @param p_vertices + * @param p_indices + * @param p_materialIndex + */ + Mesh( + std::span p_vertices, + std::span p_indices, + uint32_t p_materialIndex = 0 ); /** @@ -73,7 +84,7 @@ namespace OvRendering::Resources private: void Upload(std::span p_vertices, std::span p_indices); - void ComputeBoundingSphere(std::span p_vertices); + void Upload(std::span p_vertices, std::span p_indices); private: const uint32_t m_vertexCount; diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h index e173c0291..bea78580e 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h @@ -42,14 +42,11 @@ namespace OvRendering::Resources::Parsers void ProcessAnimations(const struct aiScene* p_scene, const Animation::Skeleton& p_skeleton, std::vector& p_animations); void ProcessMaterials(const struct aiScene* p_scene, std::vector& p_materials); void ProcessNode(void* p_transform, struct aiNode* p_node, const struct aiScene* p_scene, std::vector& p_meshes, Animation::Skeleton* p_skeleton); - void ProcessMesh( + Mesh* ProcessMesh( void* p_transform, struct aiMesh* p_mesh, const struct aiScene* p_scene, - std::vector& p_outVertices, - std::vector& p_outIndices, - Animation::Skeleton* p_skeleton, - bool& p_outHasSkinningData + Animation::Skeleton* p_skeleton ); }; } diff --git a/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h b/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h index 21b596cbd..ccf31d232 100644 --- a/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h +++ b/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h @@ -20,6 +20,7 @@ namespace OvRendering::Settings EDataType type = EDataType::FLOAT; uint8_t count = 4; bool normalized = false; + bool integer = false; }; using VertexAttributeLayout = std::span; diff --git a/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp index a2c70a428..6f4df05f8 100644 --- a/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp +++ b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp @@ -95,14 +95,27 @@ void OvRendering::HAL::GLVertexArray::SetLayout( glEnableVertexAttribArray(attributeIndex); - glVertexAttribPointer( - static_cast(attributeIndex), - static_cast(attribute.count), - EnumToValue(attribute.type), - static_cast(attribute.normalized), - static_cast(totalSize), - reinterpret_cast(currentOffset) - ); + if (attribute.integer) + { + glVertexAttribIPointer( + static_cast(attributeIndex), + static_cast(attribute.count), + EnumToValue(attribute.type), + static_cast(totalSize), + reinterpret_cast(currentOffset) + ); + } + else + { + glVertexAttribPointer( + static_cast(attributeIndex), + static_cast(attribute.count), + EnumToValue(attribute.type), + static_cast(attribute.normalized), + static_cast(totalSize), + reinterpret_cast(currentOffset) + ); + } const uint64_t typeSize = GetDataTypeSizeInBytes(attribute.type); const uint64_t attributeSize = typeSize * attribute.count; diff --git a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp index 2f795549b..265ea85ef 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp @@ -15,55 +15,73 @@ namespace { - struct StaticVertex + template + OvRendering::Geometry::BoundingSphere ComputeBoundingSphere(std::span p_vertices) { - float position[3]; - float texCoords[2]; - float normals[3]; - float tangent[3]; - float bitangent[3]; - }; - - StaticVertex ToStaticVertex(const OvRendering::Geometry::Vertex& p_vertex) - { - StaticVertex result{}; - - result.position[0] = p_vertex.position[0]; - result.position[1] = p_vertex.position[1]; - result.position[2] = p_vertex.position[2]; - - result.texCoords[0] = p_vertex.texCoords[0]; - result.texCoords[1] = p_vertex.texCoords[1]; - - result.normals[0] = p_vertex.normals[0]; - result.normals[1] = p_vertex.normals[1]; - result.normals[2] = p_vertex.normals[2]; + OvRendering::Geometry::BoundingSphere sphere{}; + sphere.position = OvMaths::FVector3::Zero; + sphere.radius = 0.0f; - result.tangent[0] = p_vertex.tangent[0]; - result.tangent[1] = p_vertex.tangent[1]; - result.tangent[2] = p_vertex.tangent[2]; - - result.bitangent[0] = p_vertex.bitangent[0]; - result.bitangent[1] = p_vertex.bitangent[1]; - result.bitangent[2] = p_vertex.bitangent[2]; + if (!p_vertices.empty()) + { + float minX = std::numeric_limits::max(); + float minY = std::numeric_limits::max(); + float minZ = std::numeric_limits::max(); + + float maxX = std::numeric_limits::lowest(); + float maxY = std::numeric_limits::lowest(); + float maxZ = std::numeric_limits::lowest(); + + for (const auto& vertex : p_vertices) + { + minX = std::min(minX, vertex.position[0]); + minY = std::min(minY, vertex.position[1]); + minZ = std::min(minZ, vertex.position[2]); + + maxX = std::max(maxX, vertex.position[0]); + maxY = std::max(maxY, vertex.position[1]); + maxZ = std::max(maxZ, vertex.position[2]); + } + + sphere.position = OvMaths::FVector3{ minX + maxX, minY + maxY, minZ + maxZ } / 2.0f; + + for (const auto& vertex : p_vertices) + { + const auto& position = reinterpret_cast(vertex.position); + sphere.radius = std::max(sphere.radius, OvMaths::FVector3::Distance(sphere.position, position)); + } + } - return result; + return sphere; } } OvRendering::Resources::Mesh::Mesh( std::span p_vertices, std::span p_indices, - uint32_t p_materialIndex, - bool p_hasSkinningData + uint32_t p_materialIndex +) : + m_vertexCount(static_cast(p_vertices.size())), + m_indicesCount(static_cast(p_indices.size())), + m_materialIndex(p_materialIndex), + m_hasSkinningData(false) +{ + Upload(p_vertices, p_indices); + m_boundingSphere = ComputeBoundingSphere(p_vertices); +} + +OvRendering::Resources::Mesh::Mesh( + std::span p_vertices, + std::span p_indices, + uint32_t p_materialIndex ) : m_vertexCount(static_cast(p_vertices.size())), m_indicesCount(static_cast(p_indices.size())), m_materialIndex(p_materialIndex), - m_hasSkinningData(p_hasSkinningData) + m_hasSkinningData(true) { Upload(p_vertices, p_indices); - ComputeBoundingSphere(p_vertices); + m_boundingSphere = ComputeBoundingSphere(p_vertices); } void OvRendering::Resources::Mesh::Bind() const @@ -103,7 +121,7 @@ bool OvRendering::Resources::Mesh::HasSkinningData() const void OvRendering::Resources::Mesh::Upload(std::span p_vertices, std::span p_indices) { - const auto staticLayout = std::to_array({ + const auto layout = std::to_array({ { Settings::EDataType::FLOAT, 3 }, // position { Settings::EDataType::FLOAT, 2 }, // texCoords { Settings::EDataType::FLOAT, 3 }, // normal @@ -111,48 +129,13 @@ void OvRendering::Resources::Mesh::Upload(std::span p_ve { Settings::EDataType::FLOAT, 3 } // bitangent }); - const auto skinnedLayout = std::to_array({ - { Settings::EDataType::FLOAT, 3 }, // position - { Settings::EDataType::FLOAT, 2 }, // texCoords - { Settings::EDataType::FLOAT, 3 }, // normal - { Settings::EDataType::FLOAT, 3 }, // tangent - { Settings::EDataType::FLOAT, 3 }, // bitangent - { Settings::EDataType::FLOAT, 4 }, // bone IDs - { Settings::EDataType::FLOAT, 4 } // bone weights - }); - - std::vector staticVertices; - const void* vertexData = nullptr; - uint64_t vertexBufferSize = 0; - Settings::VertexAttributeLayout layout = staticLayout; - - if (m_hasSkinningData) + if (m_vertexBuffer.Allocate(p_vertices.size_bytes())) { - vertexData = p_vertices.data(); - vertexBufferSize = p_vertices.size_bytes(); - layout = skinnedLayout; - } - else - { - staticVertices.reserve(p_vertices.size()); - - for (const auto& vertex : p_vertices) - { - staticVertices.push_back(ToStaticVertex(vertex)); - } - - vertexData = staticVertices.data(); - vertexBufferSize = static_cast(staticVertices.size()) * sizeof(StaticVertex); - } - - if (m_vertexBuffer.Allocate(vertexBufferSize)) - { - m_vertexBuffer.Upload(vertexData); + m_vertexBuffer.Upload(p_vertices.data()); if (m_indexBuffer.Allocate(p_indices.size_bytes())) { m_indexBuffer.Upload(p_indices.data()); - m_vertexArray.SetLayout(layout, m_vertexBuffer, m_indexBuffer); } else @@ -166,38 +149,36 @@ void OvRendering::Resources::Mesh::Upload(std::span p_ve } } -void OvRendering::Resources::Mesh::ComputeBoundingSphere(std::span p_vertices) +void OvRendering::Resources::Mesh::Upload(std::span p_vertices, std::span p_indices) { - m_boundingSphere.position = OvMaths::FVector3::Zero; - m_boundingSphere.radius = 0.0f; + const auto layout = std::to_array({ + { Settings::EDataType::FLOAT, 3, false, false }, // position + { Settings::EDataType::FLOAT, 2, false, false }, // texCoords + { Settings::EDataType::FLOAT, 3, false, false }, // normal + { Settings::EDataType::FLOAT, 3, false, false }, // tangent + { Settings::EDataType::FLOAT, 3, false, false }, // bitangent + { Settings::EDataType::UNSIGNED_INT, 4, false, true }, // boneIDs (integer) + { Settings::EDataType::FLOAT, 4, false, false } // boneWeights + }); - if (!p_vertices.empty()) + if (m_vertexBuffer.Allocate(p_vertices.size_bytes())) { - float minX = std::numeric_limits::max(); - float minY = std::numeric_limits::max(); - float minZ = std::numeric_limits::max(); + m_vertexBuffer.Upload(p_vertices.data()); - float maxX = std::numeric_limits::lowest(); - float maxY = std::numeric_limits::lowest(); - float maxZ = std::numeric_limits::lowest(); - - for (const auto& vertex : p_vertices) + if (m_indexBuffer.Allocate(p_indices.size_bytes())) { - minX = std::min(minX, vertex.position[0]); - minY = std::min(minY, vertex.position[1]); - minZ = std::min(minZ, vertex.position[2]); - - maxX = std::max(maxX, vertex.position[0]); - maxY = std::max(maxY, vertex.position[1]); - maxZ = std::max(maxZ, vertex.position[2]); + m_indexBuffer.Upload(p_indices.data()); + m_vertexArray.SetLayout(layout, m_vertexBuffer, m_indexBuffer); } - - m_boundingSphere.position = OvMaths::FVector3{ minX + maxX, minY + maxY, minZ + maxZ } / 2.0f; - - for (const auto& vertex : p_vertices) + else { - const auto& position = reinterpret_cast(vertex.position); - m_boundingSphere.radius = std::max(m_boundingSphere.radius, OvMaths::FVector3::Distance(m_boundingSphere.position, position)); + OVLOG_WARNING("Empty index buffer!"); } } + else + { + OVLOG_WARNING("Empty vertex buffer!"); + } } + + diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index dd724fd52..9b589c9bc 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -59,7 +59,7 @@ namespace }; } - bool AddBoneData(OvRendering::Geometry::Vertex& p_vertex, uint32_t p_boneIndex, float p_weight) + bool AddBoneData(OvRendering::Geometry::SkinnedVertex& p_vertex, uint32_t p_boneIndex, float p_weight) { if (!std::isfinite(p_weight) || p_weight <= 0.0f) { @@ -71,7 +71,7 @@ namespace { if (p_vertex.boneWeights[i] <= 0.0f) { - p_vertex.boneIDs[i] = static_cast(p_boneIndex); + p_vertex.boneIDs[i] = p_boneIndex; p_vertex.boneWeights[i] = p_weight; return true; } @@ -273,19 +273,11 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( // Process all the node's meshes (if any) for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) { - std::vector vertices; - std::vector indices; - bool hasSkinningData = false; aiMesh* mesh = p_scene->mMeshes[p_node->mMeshes[i]]; - - ProcessMesh(&nodeTransformation, mesh, p_scene, vertices, indices, p_skeleton, hasSkinningData); - - if (vertices.empty() || indices.empty()) + if (Mesh* result = ProcessMesh(&nodeTransformation, mesh, p_scene, p_skeleton)) { - continue; + p_meshes.push_back(result); // The model will handle mesh destruction } - - p_meshes.push_back(new Mesh(vertices, indices, mesh->mMaterialIndex, hasSkinningData)); // The model will handle mesh destruction } // Then do the same for each of its children @@ -295,56 +287,23 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( } } -void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( +OvRendering::Resources::Mesh* OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( void* p_transform, aiMesh* p_mesh, const aiScene* p_scene, - std::vector& p_outVertices, - std::vector& p_outIndices, - Animation::Skeleton* p_skeleton, - bool& p_outHasSkinningData + Animation::Skeleton* p_skeleton ) { (void)p_scene; - p_outHasSkinningData = false; aiMatrix4x4 meshTransformation = *reinterpret_cast(p_transform); - p_outVertices.reserve(p_mesh->mNumVertices); - - for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) - { - const aiVector3D position = meshTransformation * p_mesh->mVertices[i]; - const aiVector3D texCoords = p_mesh->mTextureCoords[0] ? p_mesh->mTextureCoords[0][i] : aiVector3D(0.0f, 0.0f, 0.0f); - const aiVector3D normal = meshTransformation * (p_mesh->mNormals ? p_mesh->mNormals[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - const aiVector3D tangent = meshTransformation * (p_mesh->mTangents ? p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - const aiVector3D bitangent = meshTransformation * (p_mesh->mBitangents ? p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - - Geometry::Vertex vertex{}; - vertex.position[0] = position.x; - vertex.position[1] = position.y; - vertex.position[2] = position.z; - vertex.texCoords[0] = texCoords.x; - vertex.texCoords[1] = texCoords.y; - vertex.normals[0] = normal.x; - vertex.normals[1] = normal.y; - vertex.normals[2] = normal.z; - vertex.tangent[0] = tangent.x; - vertex.tangent[1] = tangent.y; - vertex.tangent[2] = tangent.z; - // Assimp calculates the tangent space vectors in a right-handed system. - // But our shader code expects a left-handed system. - // Multiplying the bitangent by -1 will convert it to a left-handed system. - // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. - vertex.bitangent[0] = -bitangent.x; - vertex.bitangent[1] = -bitangent.y; - vertex.bitangent[2] = -bitangent.z; - p_outVertices.push_back(vertex); - } + std::vector indices; + indices.reserve(p_mesh->mNumFaces * 3); for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID) { - auto& face = p_mesh->mFaces[faceID]; + const auto& face = p_mesh->mFaces[faceID]; if (face.mNumIndices < 3) { @@ -359,58 +318,104 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( continue; } - p_outIndices.push_back(a); - p_outIndices.push_back(b); - p_outIndices.push_back(c); + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); } - if (!p_skeleton || !p_mesh->HasBones()) + if (p_mesh->mNumVertices == 0 || indices.empty()) { - return; + return nullptr; } - for (uint32_t boneID = 0; boneID < p_mesh->mNumBones; ++boneID) + auto fillGeometry = [&](Geometry::Vertex& v, uint32_t i) { - const aiBone* bone = p_mesh->mBones[boneID]; - const std::string boneName = bone->mName.C_Str(); - const auto offsetMatrix = ToMatrix4(bone->mOffsetMatrix); - - auto nodeIndex = p_skeleton->FindNodeIndex(boneName); - if (!nodeIndex) - { - OVLOG_WARNING("AssimpParser: Bone '" + boneName + "' has no matching node in hierarchy and will be ignored."); - continue; - } + const aiVector3D position = meshTransformation * p_mesh->mVertices[i]; + const aiVector3D texCoords = p_mesh->mTextureCoords[0] ? p_mesh->mTextureCoords[0][i] : aiVector3D(0.0f, 0.0f, 0.0f); + const aiVector3D normal = meshTransformation * (p_mesh->mNormals ? p_mesh->mNormals[i] : aiVector3D(0.0f, 0.0f, 0.0f)); + const aiVector3D tangent = meshTransformation * (p_mesh->mTangents ? p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); + const aiVector3D bitangent = meshTransformation * (p_mesh->mBitangents ? p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - uint32_t boneIndex = 0; + v.position[0] = position.x; + v.position[1] = position.y; + v.position[2] = position.z; + v.texCoords[0] = texCoords.x; + v.texCoords[1] = texCoords.y; + v.normals[0] = normal.x; + v.normals[1] = normal.y; + v.normals[2] = normal.z; + v.tangent[0] = tangent.x; + v.tangent[1] = tangent.y; + v.tangent[2] = tangent.z; + // Assimp calculates the tangent space vectors in a right-handed system. + // But our shader code expects a left-handed system. + // Multiplying the bitangent by -1 will convert it to a left-handed system. + // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. + v.bitangent[0] = -bitangent.x; + v.bitangent[1] = -bitangent.y; + v.bitangent[2] = -bitangent.z; + }; - if (auto existing = p_skeleton->FindBoneIndex(boneName)) + if (p_skeleton && p_mesh->HasBones()) + { + std::vector vertices(p_mesh->mNumVertices); + for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) { - boneIndex = existing.value(); + fillGeometry(vertices[i], i); } - else + + for (uint32_t boneID = 0; boneID < p_mesh->mNumBones; ++boneID) { - boneIndex = static_cast(p_skeleton->bones.size()); - p_skeleton->boneByName.emplace(boneName, boneIndex); - p_skeleton->bones.push_back({ - .name = boneName, - .nodeIndex = nodeIndex.value(), - .offsetMatrix = offsetMatrix - }); - } + const aiBone* bone = p_mesh->mBones[boneID]; + const std::string boneName = bone->mName.C_Str(); + const auto offsetMatrix = ToMatrix4(bone->mOffsetMatrix); + + auto nodeIndex = p_skeleton->FindNodeIndex(boneName); + if (!nodeIndex) + { + OVLOG_WARNING("AssimpParser: Bone '" + boneName + "' has no matching node in hierarchy and will be ignored."); + continue; + } - p_skeleton->nodes[nodeIndex.value()].boneIndex = static_cast(boneIndex); + uint32_t boneIndex = 0; - for (uint32_t weightID = 0; weightID < bone->mNumWeights; ++weightID) - { - const auto& weight = bone->mWeights[weightID]; - if (weight.mVertexId < p_outVertices.size()) + if (auto existing = p_skeleton->FindBoneIndex(boneName)) { - if (AddBoneData(p_outVertices[weight.mVertexId], boneIndex, weight.mWeight)) + boneIndex = existing.value(); + } + else + { + boneIndex = static_cast(p_skeleton->bones.size()); + p_skeleton->boneByName.emplace(boneName, boneIndex); + p_skeleton->bones.push_back({ + .name = boneName, + .nodeIndex = nodeIndex.value(), + .offsetMatrix = offsetMatrix + }); + } + + p_skeleton->nodes[nodeIndex.value()].boneIndex = static_cast(boneIndex); + + for (uint32_t weightID = 0; weightID < bone->mNumWeights; ++weightID) + { + const auto& weight = bone->mWeights[weightID]; + if (weight.mVertexId < vertices.size()) { - p_outHasSkinningData = true; + AddBoneData(vertices[weight.mVertexId], boneIndex, weight.mWeight); } } } + + return new Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); + } + else + { + std::vector vertices(p_mesh->mNumVertices); + for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) + { + fillGeometry(vertices[i], i); + } + + return new Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); } } From 8a4efeeaac8da551ce75ee2f8ca8250db4128bee Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 18:52:37 -0400 Subject: [PATCH 05/18] Added NotifyModelChanged back --- .../OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp index 0d3878cd7..22c38c6f8 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CModelRenderer.cpp @@ -27,6 +27,9 @@ OvCore::ECS::Components::CModelRenderer::CModelRenderer(ECS::Actor& p_owner) : A { if (auto materialRenderer = owner.GetComponent()) materialRenderer->UpdateMaterialList(); + + if (auto skinnedMeshRenderer = owner.GetComponent()) + skinnedMeshRenderer->NotifyModelChanged(); }; } @@ -81,7 +84,9 @@ void OvCore::ECS::Components::CModelRenderer::OnSerialize(tinyxml2::XMLDocument void OvCore::ECS::Components::CModelRenderer::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XMLNode* p_node) { - OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", m_model); + OvRendering::Resources::Model* model = nullptr; + OvCore::Helpers::Serializer::DeserializeModel(p_doc, p_node, "model", model); + SetModel(model); OvCore::Helpers::Serializer::DeserializeInt(p_doc, p_node, "frustum_behaviour", reinterpret_cast(m_frustumBehaviour)); OvCore::Helpers::Serializer::DeserializeVec3(p_doc, p_node, "custom_bounding_sphere_position", m_customBoundingSphere.position); OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "custom_bounding_sphere_radius", m_customBoundingSphere.radius); From 908052276ec61e89a6f4a681b5529e29b6c3bf9d Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 19:13:20 -0400 Subject: [PATCH 06/18] Added engine_feature keyword for hidden/private features --- Resources/Editor/Shaders/OutlineFallback.ovfx | 2 +- Resources/Editor/Shaders/PickingFallback.ovfx | 2 +- Resources/Engine/Shaders/ShadowFallback.ovfx | 2 +- Resources/Engine/Shaders/Standard.ovfx | 2 +- Resources/Engine/Shaders/Unlit.ovfx | 2 +- .../include/OvRendering/Resources/Shader.h | 14 ++++-- .../src/OvRendering/Data/Material.cpp | 4 +- .../Resources/Loaders/ShaderLoader.cpp | 47 ++++++++++++++----- .../src/OvRendering/Resources/Shader.cpp | 23 +++++++-- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/Resources/Editor/Shaders/OutlineFallback.ovfx b/Resources/Editor/Shaders/OutlineFallback.ovfx index c06790245..d07444187 100644 --- a/Resources/Editor/Shaders/OutlineFallback.ovfx +++ b/Resources/Editor/Shaders/OutlineFallback.ovfx @@ -1,5 +1,5 @@ #pass OUTLINE_PASS -#feature SKINNING +#engine_feature SKINNING #shader vertex #version 450 core diff --git a/Resources/Editor/Shaders/PickingFallback.ovfx b/Resources/Editor/Shaders/PickingFallback.ovfx index e5c55fe71..755845278 100644 --- a/Resources/Editor/Shaders/PickingFallback.ovfx +++ b/Resources/Editor/Shaders/PickingFallback.ovfx @@ -1,5 +1,5 @@ #pass PICKING_PASS -#feature SKINNING +#engine_feature SKINNING #shader vertex #version 450 core diff --git a/Resources/Engine/Shaders/ShadowFallback.ovfx b/Resources/Engine/Shaders/ShadowFallback.ovfx index 63488d097..9608a4a7f 100644 --- a/Resources/Engine/Shaders/ShadowFallback.ovfx +++ b/Resources/Engine/Shaders/ShadowFallback.ovfx @@ -1,5 +1,5 @@ #pass SHADOW_PASS -#feature SKINNING +#engine_feature SKINNING #shader vertex #version 450 core diff --git a/Resources/Engine/Shaders/Standard.ovfx b/Resources/Engine/Shaders/Standard.ovfx index d12f86b14..ee3f724eb 100644 --- a/Resources/Engine/Shaders/Standard.ovfx +++ b/Resources/Engine/Shaders/Standard.ovfx @@ -5,7 +5,7 @@ #feature NORMAL_MAPPING #feature DISTANCE_FADE #feature SPECULAR_WORKFLOW -#feature SKINNING +#engine_feature SKINNING #shader vertex #version 450 core diff --git a/Resources/Engine/Shaders/Unlit.ovfx b/Resources/Engine/Shaders/Unlit.ovfx index f41504289..adeef4cb5 100644 --- a/Resources/Engine/Shaders/Unlit.ovfx +++ b/Resources/Engine/Shaders/Unlit.ovfx @@ -1,7 +1,7 @@ #pass SHADOW_PASS #feature ALPHA_CLIPPING -#feature SKINNING +#engine_feature SKINNING #shader vertex #version 450 core diff --git a/Sources/OvRendering/include/OvRendering/Resources/Shader.h b/Sources/OvRendering/include/OvRendering/Resources/Shader.h index 4e753d65b..755ebdebd 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Shader.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Shader.h @@ -52,10 +52,16 @@ namespace OvRendering::Resources ); /** - * Returns supported features + * Returns user-configurable features (declared with #feature). + * Engine-controlled features (declared with #engine_feature) are excluded. */ const Data::FeatureSet& GetFeatures() const; + /** + * Returns engine-controlled features (declared with #engine_feature). + */ + const Data::FeatureSet& GetEngineFeatures() const; + /** * Returns supported passes */ @@ -69,11 +75,12 @@ namespace OvRendering::Resources private: Shader( const std::string p_path, - Variants&& p_variants + Variants&& p_variants, + Data::FeatureSet p_engineFeatures = {} ); ~Shader() = default; - void SetVariants(Variants&& p_variants); + void SetVariants(Variants&& p_variants, Data::FeatureSet p_engineFeatures = {}); public: const std::string path; @@ -81,6 +88,7 @@ namespace OvRendering::Resources private: std::unordered_set m_passes; Data::FeatureSet m_features; + Data::FeatureSet m_engineFeatures; Variants m_variants; }; } diff --git a/Sources/OvRendering/src/OvRendering/Data/Material.cpp b/Sources/OvRendering/src/OvRendering/Data/Material.cpp index a0737cfb7..e9179c793 100644 --- a/Sources/OvRendering/src/OvRendering/Data/Material.cpp +++ b/Sources/OvRendering/src/OvRendering/Data/Material.cpp @@ -474,7 +474,9 @@ bool OvRendering::Data::Material::HasFeature(const std::string& p_feature) const bool OvRendering::Data::Material::SupportsFeature(const std::string& p_feature) const { - return m_shader->GetFeatures().contains(p_feature); + return m_shader && + (m_shader->GetFeatures().contains(p_feature) || + m_shader->GetEngineFeatures().contains(p_feature)); } bool OvRendering::Data::Material::HasPass(const std::string& p_pass) const diff --git a/Sources/OvRendering/src/OvRendering/Resources/Loaders/ShaderLoader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ShaderLoader.cpp index 199b795cb..0c678f06e 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Loaders/ShaderLoader.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ShaderLoader.cpp @@ -31,6 +31,7 @@ namespace { constexpr std::string_view kPassToken = "#pass"; constexpr std::string_view kFeatureToken = "#feature"; + constexpr std::string_view kEngineFeatureToken = "#engine_feature"; constexpr std::string_view kVertexShaderToken = "#shader vertex"; constexpr std::string_view kFragmentShaderToken = "#shader fragment"; constexpr std::string_view kIncludeToken = "#include"; @@ -64,7 +65,8 @@ namespace const std::string vertexShader; const std::string fragmentShader; const std::unordered_set passes; - const OvRendering::Data::FeatureSet features; + const OvRendering::Data::FeatureSet userFeatures; + const OvRendering::Data::FeatureSet engineFeatures; }; struct ShaderAssembleResult @@ -72,6 +74,7 @@ namespace const ShaderInputInfo inputInfo; const uint32_t failures; // How many variants failed to compile OvRendering::Resources::Shader::Variants variants; + OvRendering::Data::FeatureSet engineFeatures; }; struct ShaderStageDesc @@ -383,7 +386,8 @@ void main() std::istringstream stream(p_shaderLoadResult.source); // Add this line to create a stringstream from shaderCode std::string line; std::unordered_map shaderSources; - OvRendering::Data::FeatureSet features; + OvRendering::Data::FeatureSet userFeatures; + OvRendering::Data::FeatureSet engineFeatures; std::unordered_set passes; passes.emplace(); // Default pass if none is specified (empty string) @@ -408,6 +412,21 @@ void main() passes.insert(passName); } + else if (trimmedLine.starts_with(Grammar::kEngineFeatureToken)) + { + std::string featureName; + featureName.reserve(16); + + for (auto& c : trimmedLine | + std::views::drop(Grammar::kEngineFeatureToken.size()) | + std::views::drop_while(isspace) | + std::views::take_while([](char c) { return !isspace(c); })) + { + featureName += c; + } + + engineFeatures.insert(featureName); + } else if (trimmedLine.starts_with(Grammar::kFeatureToken)) { std::string featureName; @@ -421,7 +440,7 @@ void main() featureName += c; } - features.insert(featureName); + userFeatures.insert(featureName); } else if (trimmedLine.starts_with(Grammar::kVertexShaderToken)) { @@ -442,7 +461,8 @@ void main() .vertexShader = shaderSources[EShaderType::VERTEX].str(), .fragmentShader = shaderSources[EShaderType::FRAGMENT].str(), .passes = passes, - .features = features + .userFeatures = userFeatures, + .engineFeatures = engineFeatures }; } @@ -453,7 +473,10 @@ void main() { const auto startTime = std::chrono::high_resolution_clock::now(); - const auto featureVariantCount = (size_t{ 1UL } << p_parseResult.features.size()); + OvRendering::Data::FeatureSet allFeatures = p_parseResult.userFeatures; + allFeatures.insert(p_parseResult.engineFeatures.begin(), p_parseResult.engineFeatures.end()); + + const auto featureVariantCount = (size_t{ 1UL } << allFeatures.size()); uint32_t failures = 0; @@ -470,11 +493,11 @@ void main() for (size_t i = 0; i < featureVariantCount; ++i) { OvRendering::Data::FeatureSet featureSet; - for (size_t j = 0; j < p_parseResult.features.size(); ++j) + for (size_t j = 0; j < allFeatures.size(); ++j) { if (i & (size_t{ 1UL } << j)) { - featureSet.insert(*std::next(p_parseResult.features.begin(), j)); + featureSet.insert(*std::next(allFeatures.begin(), j)); } } @@ -545,7 +568,8 @@ void main() return ShaderAssembleResult{ p_parseResult.inputInfo, failures, - std::move(variants) + std::move(variants), + p_parseResult.engineFeatures }; } @@ -577,7 +601,8 @@ void main() .vertexShader = p_vertexShader, .fragmentShader = p_fragmentShader, .passes = {{}}, // Default pass (empty string) - .features = {} // No support for features in embedded shaders + .userFeatures = {}, // No support for features in embedded shaders + .engineFeatures = {} }; return AssembleShader(shaderParseResult); @@ -599,7 +624,7 @@ namespace OvRendering::Resources::Loaders Shader* ShaderLoader::Create(const std::string& p_filePath, FilePathParserCallback p_pathParser) { auto result = CompileShaderFromFile(p_filePath, p_pathParser); - return new Shader(p_filePath, std::move(result.variants)); + return new Shader(p_filePath, std::move(result.variants), std::move(result.engineFeatures)); } Shader* ShaderLoader::CreateFromSource(const std::string& p_vertexShader, const std::string& p_fragmentShader) @@ -614,7 +639,7 @@ namespace OvRendering::Resources::Loaders if (result.failures == 0) { - p_shader.SetVariants(std::move(result.variants)); + p_shader.SetVariants(std::move(result.variants), std::move(result.engineFeatures)); } else { diff --git a/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp index a90aaa38f..932e46fcc 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp @@ -43,6 +43,11 @@ const OvRendering::Data::FeatureSet& OvRendering::Resources::Shader::GetFeatures return m_features; } +const OvRendering::Data::FeatureSet& OvRendering::Resources::Shader::GetEngineFeatures() const +{ + return m_engineFeatures; +} + const std::unordered_set& OvRendering::Resources::Shader::GetPasses() const { return m_passes; @@ -50,28 +55,36 @@ const std::unordered_set& OvRendering::Resources::Shader::GetPasses OvRendering::Resources::Shader::Shader( const std::string p_path, - Variants&& p_variants + Variants&& p_variants, + Data::FeatureSet p_engineFeatures ) : path(p_path) { - SetVariants(std::move(p_variants)); + SetVariants(std::move(p_variants), std::move(p_engineFeatures)); } -void OvRendering::Resources::Shader::SetVariants(Variants&& p_variants) +void OvRendering::Resources::Shader::SetVariants(Variants&& p_variants, Data::FeatureSet p_engineFeatures) { ValidateVariants(p_variants); m_variants = std::move(p_variants); + m_engineFeatures = std::move(p_engineFeatures); m_passes.clear(); m_features.clear(); - // Find all passes & features based on the compiled variants + // Find all passes & user features based on the compiled variants (engine features are excluded) for (const auto& [pass, featureVariants] : m_variants) { m_passes.insert(pass); for (const auto& featureSet : featureVariants | std::views::keys) { - m_features.insert(featureSet.begin(), featureSet.end()); + for (const auto& feature : featureSet) + { + if (!m_engineFeatures.contains(feature)) + { + m_features.insert(feature); + } + } } } } From 563b0deea70d8f462783d8c1b47e50cd8e95df3b Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 19:31:08 -0400 Subject: [PATCH 07/18] Slightly cleaner asset path resolving for editor shaders --- .../src/OvEditor/Core/EditorResources.cpp | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp index 2da38efff..28dddcecc 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp @@ -51,28 +51,27 @@ namespace auto CreateShader(const std::filesystem::path& p_path, const std::filesystem::path& p_editorAssetsPath) { - auto pathParser = [p_editorAssetsPath](const std::string& p_includePath) - { - const auto normalizedIncludePath = OvTools::Utils::PathParser::MakeNonWindowsStyle(p_includePath); - const auto includePath = std::filesystem::path{ normalizedIncludePath }; + const auto engineAssetsPath = p_editorAssetsPath.parent_path() / "Engine"; - if (includePath.is_absolute()) + return OvRendering::Resources::Loaders::ShaderLoader::Create( + p_path.string(), + [p_editorAssetsPath, engineAssetsPath](const std::string& p_includePath) { - return includePath.lexically_normal().string(); - } + const auto normalizedPath = OvTools::Utils::PathParser::MakeNonWindowsStyle(p_includePath); + const auto includePath = std::filesystem::path{ normalizedPath }; - if (!normalizedIncludePath.empty() && normalizedIncludePath.front() == ':') - { - const auto engineAssetsPath = p_editorAssetsPath.parent_path() / "Engine"; - return (engineAssetsPath / normalizedIncludePath.substr(1)).lexically_normal().string(); - } + if (includePath.is_absolute()) + { + return includePath.lexically_normal().string(); + } - return (p_editorAssetsPath / includePath).lexically_normal().string(); - }; + if (!normalizedPath.empty() && normalizedPath.front() == ':') + { + return (engineAssetsPath / normalizedPath.substr(1)).lexically_normal().string(); + } - return OvRendering::Resources::Loaders::ShaderLoader::Create( - p_path.string(), - pathParser + return (p_editorAssetsPath / includePath).lexically_normal().string(); + } ); } From 8c4d7c15a86b85fc43371963b23f3f08531c6eeb Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 19:41:27 -0400 Subject: [PATCH 08/18] Fixed skinning applied to fallback materials even when the initial material didn't support it --- .../src/OvCore/Rendering/ShadowRenderPass.cpp | 13 +++---- .../Rendering/OutlineRenderFeature.cpp | 38 +++++++++---------- .../OvEditor/Rendering/PickingRenderPass.cpp | 4 +- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp b/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp index cb3d3d3ef..86c4d467b 100644 --- a/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/ShadowRenderPass.cpp @@ -125,15 +125,12 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( { if (auto material = materials.at(mesh->GetMaterialIndex()); material && material->IsValid() && material->IsShadowCaster()) { - const bool materialHasShadowPass = material->HasPass(kShadowPassName); - const bool canUseMaterialShadowPass = - materialHasShadowPass && - (!hasSkinning || material->SupportsFeature(kSkinningFeatureName)); + // Skinning is only applied if the original material explicitly supports it. + const bool skinningEnabled = hasSkinning && material->SupportsFeature(kSkinningFeatureName); - // If the material has a compatible shadow pass, use it. - // Otherwise, use the shadow fallback material. + // If the material has a shadow pass, use it. Otherwise, use the shadow fallback. auto& targetMaterial = - canUseMaterialShadowPass ? + material->HasPass(kShadowPassName) ? *material : m_shadowMaterial; @@ -161,7 +158,7 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( materialRenderer->GetUserMatrix() }); - if (hasSkinning && targetMaterial.SupportsFeature(kSkinningFeatureName)) + if (skinningEnabled && targetMaterial.SupportsFeature(kSkinningFeatureName)) { SkinningUtils::ApplyToDrawable(drawable, *skinnedRenderer, &targetMaterial.GetFeatures()); } diff --git a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp index b33266c6c..9b0985e16 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/OutlineRenderFeature.cpp @@ -39,20 +39,13 @@ namespace uint32_t p_materialIndex, std::string_view p_passName, OvTools::Utils::OptRef p_materials, - bool p_hasSkinning, OvCore::Resources::Material& p_fallbackMaterial ) { auto* material = FindMeshMaterial(p_materials, p_materialIndex); if (material && material->IsValid() && material->HasPass(std::string{ p_passName })) { - const bool skinningCompatible = !p_hasSkinning || - material->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); - - if (skinningCompatible) - { - return *material; - } + return *material; } return p_fallbackMaterial; @@ -61,19 +54,18 @@ namespace void ApplySkinningIfNeeded( OvRendering::Entities::Drawable& p_drawable, const OvCore::ECS::Components::CSkinnedMeshRenderer* p_skinnedRenderer, + bool p_skinningEnabled, OvCore::Resources::Material& p_targetMaterial ) { - if (!OvCore::Rendering::SkinningUtils::IsSkinningActive(p_skinnedRenderer)) + if (p_skinningEnabled) { - return; + OvCore::Rendering::SkinningUtils::ApplyToDrawable( + p_drawable, + *p_skinnedRenderer, + &p_targetMaterial.GetFeatures() + ); } - - OvCore::Rendering::SkinningUtils::ApplyToDrawable( - p_drawable, - *p_skinnedRenderer, - &p_targetMaterial.GetFeatures() - ); } } @@ -249,11 +241,14 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelToStencil( for (auto mesh : p_model.GetMeshes()) { + const auto* originalMaterial = FindMeshMaterial(p_materials, mesh->GetMaterialIndex()); + const bool skinningEnabled = hasSkinning && originalMaterial && + originalMaterial->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); + auto& targetMaterial = ResolveOutlineMaterial( mesh->GetMaterialIndex(), outlinePassName, p_materials, - hasSkinning, m_stencilFillMaterial ); @@ -273,7 +268,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelToStencil( element.pass = outlinePassName; element.AddDescriptor(engineDrawableDescriptor); - ApplySkinningIfNeeded(element, p_skinnedRenderer, targetMaterial); + ApplySkinningIfNeeded(element, p_skinnedRenderer, skinningEnabled, targetMaterial); m_renderer.DrawEntity(p_pso, element); } @@ -293,11 +288,14 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelOutline( for (auto mesh : p_model.GetMeshes()) { + const auto* originalMaterial = FindMeshMaterial(p_materials, mesh->GetMaterialIndex()); + const bool skinningEnabled = hasSkinning && originalMaterial && + originalMaterial->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); + auto& targetMaterial = ResolveOutlineMaterial( mesh->GetMaterialIndex(), outlinePassName, p_materials, - hasSkinning, m_outlineMaterial ); @@ -322,7 +320,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelOutline( drawable.pass = outlinePassName; drawable.AddDescriptor(engineDrawableDescriptor); - ApplySkinningIfNeeded(drawable, p_skinnedRenderer, targetMaterial); + ApplySkinningIfNeeded(drawable, p_skinnedRenderer, skinningEnabled, targetMaterial); m_renderer.DrawEntity(p_pso, drawable); } diff --git a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp index c77672b82..730f9431a 100644 --- a/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp +++ b/Sources/OvEditor/src/OvEditor/Rendering/PickingRenderPass.cpp @@ -169,8 +169,10 @@ void OvEditor::Rendering::PickingRenderPass::DrawPickableModels( const auto& actor = drawable.template GetDescriptor().actor; const auto skinnedRenderer = actor.template GetComponent(); const bool hasSkinning = OvCore::Rendering::SkinningUtils::IsSkinningActive(skinnedRenderer); + const bool skinningEnabled = hasSkinning && drawable.material && + drawable.material->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); - if (hasSkinning) + if (skinningEnabled) { auto& targetMaterial = m_actorPickingFallbackMaterial; From 997862c52f6d30ddb3f59316556ac9081108881f Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 20:06:52 -0400 Subject: [PATCH 09/18] Cleanup: simplify skinning render feature state, remove dead code - Add Describable::SetDescriptor() to atomically add-or-replace a descriptor - Use SetDescriptor in SkinningUtils::ApplyDescriptor, removing the redundant HasDescriptor/RemoveDescriptor/AddDescriptor pattern - Group SkinningRenderFeature's 7 loose state fields into two inner structs (UploadedPaletteState, BoundPaletteState) for clarity - Remove unused p_scene parameter from AssimpParser::ProcessMesh (was immediately suppressed with (void)p_scene) - Replace self-referential lambda in BuildSkeleton with a plain recursive free function in the anonymous namespace - Remove unused CSkinnedMeshRenderer::GetBoneMatrices() (non-transposed version was never called outside the class itself) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ECS/Components/CSkinnedMeshRenderer.h | 5 -- .../OvCore/Rendering/SkinningRenderFeature.h | 24 ++++--- .../ECS/Components/CSkinnedMeshRenderer.cpp | 5 -- .../Rendering/SkinningRenderFeature.cpp | 44 +++++-------- .../src/OvCore/Rendering/SkinningUtils.cpp | 7 +-- .../include/OvRendering/Data/Describable.h | 6 ++ .../include/OvRendering/Data/Describable.inl | 6 ++ .../Resources/Parsers/AssimpParser.h | 1 - .../Resources/Parsers/AssimpParser.cpp | 63 +++++++++---------- 9 files changed, 72 insertions(+), 89 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h index 78cc98b5a..05e8985ff 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -153,11 +153,6 @@ namespace OvCore::ECS::Components */ std::string GetAnimation() const; - /** - * Returns the current skinning matrix palette - */ - const std::vector& GetBoneMatrices() const; - /** * Returns the transposed skinning matrix palette ready for GPU upload */ diff --git a/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h b/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h index 534c40bdb..f7464af18 100644 --- a/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h +++ b/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h @@ -60,17 +60,27 @@ namespace OvCore::Rendering SKINNING }; + struct UploadedPaletteState + { + const OvMaths::FMatrix4* ptr = nullptr; + uint32_t count = 0; + uint64_t poseVersion = 0; + }; + + struct BoundPaletteState + { + EBoundPalette type = EBoundPalette::NONE; + const OvMaths::FMatrix4* ptr = nullptr; + uint32_t count = 0; + uint64_t poseVersion = 0; + }; + uint32_t m_bufferBindingPoint; mutable uint32_t m_skinningBufferIndex = kSkinningBufferRingSize - 1; std::array, kSkinningBufferRingSize> m_skinningBuffers; std::unique_ptr m_identityBuffer; - const OvMaths::FMatrix4* m_lastPalettePtr = nullptr; - uint32_t m_lastPaletteCount = 0; - uint64_t m_lastPoseVersion = 0; - mutable EBoundPalette m_boundPalette = EBoundPalette::NONE; - mutable const OvMaths::FMatrix4* m_boundPalettePtr = nullptr; - mutable uint32_t m_boundPaletteCount = 0; - mutable uint64_t m_boundPoseVersion = 0; + UploadedPaletteState m_lastUploaded; + mutable BoundPaletteState m_bound; }; } diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp index 3208d056c..fb9ee723e 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -299,11 +299,6 @@ std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimation() const return animationName.has_value() ? *animationName : std::string{}; } -const std::vector& OvCore::ECS::Components::CSkinnedMeshRenderer::GetBoneMatrices() const -{ - return m_boneMatrices; -} - const std::vector& OvCore::ECS::Components::CSkinnedMeshRenderer::GetBoneMatricesTransposed() const { return m_boneMatricesTransposed; diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp index cc2ea7033..42f1035df 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp @@ -41,13 +41,8 @@ void OvCore::Rendering::SkinningRenderFeature::OnBeginFrame(const OvRendering::D (void)p_frameDescriptor; m_skinningBufferIndex = (m_skinningBufferIndex + 1) % kSkinningBufferRingSize; - m_lastPalettePtr = nullptr; - m_lastPaletteCount = 0; - m_lastPoseVersion = 0; - m_boundPalette = EBoundPalette::NONE; - m_boundPalettePtr = nullptr; - m_boundPaletteCount = 0; - m_boundPoseVersion = 0; + m_lastUploaded = {}; + m_bound = {}; if (m_identityBuffer->GetSize() == 0) { @@ -61,10 +56,7 @@ void OvCore::Rendering::SkinningRenderFeature::OnEndFrame() { GetCurrentSkinningBuffer().Unbind(); m_identityBuffer->Unbind(); - m_boundPalette = EBoundPalette::NONE; - m_boundPalettePtr = nullptr; - m_boundPaletteCount = 0; - m_boundPoseVersion = 0; + m_bound = {}; } void OvCore::Rendering::SkinningRenderFeature::OnBeforeDraw( @@ -95,9 +87,9 @@ void OvCore::Rendering::SkinningRenderFeature::OnBeforeDraw( } const bool mustUpload = - m_lastPalettePtr != skinningDescriptor->matrices || - m_lastPaletteCount != skinningDescriptor->count || - m_lastPoseVersion != skinningDescriptor->poseVersion; + m_lastUploaded.ptr != skinningDescriptor->matrices || + m_lastUploaded.count != skinningDescriptor->count || + m_lastUploaded.poseVersion != skinningDescriptor->poseVersion; auto& skinningBuffer = GetCurrentSkinningBuffer(); @@ -114,24 +106,19 @@ void OvCore::Rendering::SkinningRenderFeature::OnBeforeDraw( .size = uploadSize }); - m_lastPalettePtr = skinningDescriptor->matrices; - m_lastPaletteCount = skinningDescriptor->count; - m_lastPoseVersion = skinningDescriptor->poseVersion; + m_lastUploaded = { skinningDescriptor->matrices, skinningDescriptor->count, skinningDescriptor->poseVersion }; } const bool mustBindSkinningPalette = - m_boundPalette != EBoundPalette::SKINNING || - m_boundPalettePtr != skinningDescriptor->matrices || - m_boundPaletteCount != skinningDescriptor->count || - m_boundPoseVersion != skinningDescriptor->poseVersion; + m_bound.type != EBoundPalette::SKINNING || + m_bound.ptr != skinningDescriptor->matrices || + m_bound.count != skinningDescriptor->count || + m_bound.poseVersion != skinningDescriptor->poseVersion; if (mustBindSkinningPalette) { skinningBuffer.Bind(m_bufferBindingPoint); - m_boundPalette = EBoundPalette::SKINNING; - m_boundPalettePtr = skinningDescriptor->matrices; - m_boundPaletteCount = skinningDescriptor->count; - m_boundPoseVersion = skinningDescriptor->poseVersion; + m_bound = { EBoundPalette::SKINNING, skinningDescriptor->matrices, skinningDescriptor->count, skinningDescriptor->poseVersion }; } } @@ -142,12 +129,9 @@ OvRendering::HAL::ShaderStorageBuffer& OvCore::Rendering::SkinningRenderFeature: void OvCore::Rendering::SkinningRenderFeature::BindIdentityPalette() const { - if (m_boundPalette != EBoundPalette::IDENTITY) + if (m_bound.type != EBoundPalette::IDENTITY) { m_identityBuffer->Bind(m_bufferBindingPoint); - m_boundPalette = EBoundPalette::IDENTITY; - m_boundPalettePtr = nullptr; - m_boundPaletteCount = 0; - m_boundPoseVersion = 0; + m_bound = { EBoundPalette::IDENTITY }; } } diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp index 082bf38a7..209b8ea3e 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp @@ -31,12 +31,7 @@ void OvCore::Rendering::SkinningUtils::ApplyDescriptor( { const auto& boneMatrices = p_renderer.GetBoneMatricesTransposed(); - if (p_drawable.HasDescriptor()) - { - p_drawable.RemoveDescriptor(); - } - - p_drawable.AddDescriptor({ + p_drawable.SetDescriptor({ .matrices = boneMatrices.data(), .count = static_cast(boneMatrices.size()), .poseVersion = p_renderer.GetPoseVersion() diff --git a/Sources/OvRendering/include/OvRendering/Data/Describable.h b/Sources/OvRendering/include/OvRendering/Data/Describable.h index a6af5f34a..605c57e14 100644 --- a/Sources/OvRendering/include/OvRendering/Data/Describable.h +++ b/Sources/OvRendering/include/OvRendering/Data/Describable.h @@ -27,6 +27,12 @@ namespace OvRendering::Data template void AddDescriptor(T&& p_descriptor); + /** + * Add or replace a descriptor (no-op if same type already exists with the same value) + */ + template + void SetDescriptor(T&& p_descriptor); + /** * Remove a descriptor */ diff --git a/Sources/OvRendering/include/OvRendering/Data/Describable.inl b/Sources/OvRendering/include/OvRendering/Data/Describable.inl index 059b6318f..e576ba1cc 100644 --- a/Sources/OvRendering/include/OvRendering/Data/Describable.inl +++ b/Sources/OvRendering/include/OvRendering/Data/Describable.inl @@ -18,6 +18,12 @@ namespace OvRendering::Data m_descriptors.emplace(typeid(T), std::move(p_descriptor)); } + template + inline void Describable::SetDescriptor(T&& p_descriptor) + { + m_descriptors.insert_or_assign(typeid(T), std::move(p_descriptor)); + } + template inline void Describable::RemoveDescriptor() { diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h index bea78580e..38d93bf0d 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h @@ -45,7 +45,6 @@ namespace OvRendering::Resources::Parsers Mesh* ProcessMesh( void* p_transform, struct aiMesh* p_mesh, - const struct aiScene* p_scene, Animation::Skeleton* p_skeleton ); }; diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index 9b589c9bc..e0367076b 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -80,6 +80,32 @@ namespace return false; } + void AddSkeletonNodeRecursive(OvRendering::Animation::Skeleton& p_skeleton, aiNode* p_node, int32_t p_parentIndex) + { + const auto currentIndex = static_cast(p_skeleton.nodes.size()); + + aiVector3D aiPosition, aiScale; + aiQuaternion aiRotation; + p_node->mTransformation.Decompose(aiScale, aiRotation, aiPosition); + + p_skeleton.nodes.push_back({ + .name = p_node->mName.C_Str(), + .parentIndex = p_parentIndex, + .boneIndex = -1, + .localBindTransform = ToMatrix4(p_node->mTransformation), + .bindPosition = { aiPosition.x, aiPosition.y, aiPosition.z }, + .bindRotation = { aiRotation.x, aiRotation.y, aiRotation.z, aiRotation.w }, + .bindScale = { aiScale.x, aiScale.y, aiScale.z } + }); + + p_skeleton.nodeByName.emplace(p_node->mName.C_Str(), currentIndex); + + for (uint32_t i = 0; i < p_node->mNumChildren; ++i) + { + AddSkeletonNodeRecursive(p_skeleton, p_node->mChildren[i], static_cast(currentIndex)); + } + } + } bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( @@ -143,37 +169,7 @@ bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( void OvRendering::Resources::Parsers::AssimpParser::BuildSkeleton(const aiScene* p_scene, Animation::Skeleton& p_skeleton) { - const auto addNodeRecursive = [&p_skeleton]( - const auto& p_self, - aiNode* p_node, - int32_t p_parentIndex - ) -> void - { - const auto currentIndex = static_cast(p_skeleton.nodes.size()); - - aiVector3D aiPosition, aiScale; - aiQuaternion aiRotation; - p_node->mTransformation.Decompose(aiScale, aiRotation, aiPosition); - - p_skeleton.nodes.push_back({ - .name = p_node->mName.C_Str(), - .parentIndex = p_parentIndex, - .boneIndex = -1, - .localBindTransform = ToMatrix4(p_node->mTransformation), - .bindPosition = { aiPosition.x, aiPosition.y, aiPosition.z }, - .bindRotation = { aiRotation.x, aiRotation.y, aiRotation.z, aiRotation.w }, - .bindScale = { aiScale.x, aiScale.y, aiScale.z } - }); - - p_skeleton.nodeByName.emplace(p_node->mName.C_Str(), currentIndex); - - for (uint32_t i = 0; i < p_node->mNumChildren; ++i) - { - p_self(p_self, p_node->mChildren[i], static_cast(currentIndex)); - } - }; - - addNodeRecursive(addNodeRecursive, p_scene->mRootNode, -1); + AddSkeletonNodeRecursive(p_skeleton, p_scene->mRootNode, -1); } void OvRendering::Resources::Parsers::AssimpParser::ProcessAnimations( @@ -274,7 +270,7 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) { aiMesh* mesh = p_scene->mMeshes[p_node->mMeshes[i]]; - if (Mesh* result = ProcessMesh(&nodeTransformation, mesh, p_scene, p_skeleton)) + if (Mesh* result = ProcessMesh(&nodeTransformation, mesh, p_skeleton)) { p_meshes.push_back(result); // The model will handle mesh destruction } @@ -290,12 +286,9 @@ void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( OvRendering::Resources::Mesh* OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( void* p_transform, aiMesh* p_mesh, - const aiScene* p_scene, Animation::Skeleton* p_skeleton ) { - (void)p_scene; - aiMatrix4x4 meshTransformation = *reinterpret_cast(p_transform); std::vector indices; From 102624cb8cefc32a0c40dfe3e9d9dd402b00bce1 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 20:14:54 -0400 Subject: [PATCH 10/18] Fix: warn when DEBONE flag is stripped in AssimpParser DEBONE removes bone data from meshes, making it incompatible with skeletal animation. Mirror the PRE_TRANSFORM_VERTICES pattern: only strip the flag when it is actually set, and emit a warning so the user knows why their flag was ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/OvRendering/Resources/Parsers/AssimpParser.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index e0367076b..ccd7ca470 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -44,7 +44,11 @@ namespace OVLOG_WARNING("AssimpParser: PRE_TRANSFORM_VERTICES is incompatible with skeletal animation and has been removed."); } - p_flags &= ~DEBONE; + if (static_cast(p_flags & DEBONE)) + { + p_flags &= ~DEBONE; + OVLOG_WARNING("AssimpParser: DEBONE removes bone data and is incompatible with skeletal animation. DEBONE will be ignored."); + } return p_flags; } From b7bd97b3e1b8b2ef539100e1d8a132f8fdf36ff7 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sat, 4 Apr 2026 23:07:18 -0400 Subject: [PATCH 11/18] Removed redundant "bool integer" member in VertexAttribute --- .../include/OvRendering/Settings/VertexAttribute.h | 1 - .../src/OvRendering/HAL/OpenGL/GLVertexArray.cpp | 6 +++++- .../OvRendering/src/OvRendering/Resources/Mesh.cpp | 14 +++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h b/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h index ccf31d232..21b596cbd 100644 --- a/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h +++ b/Sources/OvRendering/include/OvRendering/Settings/VertexAttribute.h @@ -20,7 +20,6 @@ namespace OvRendering::Settings EDataType type = EDataType::FLOAT; uint8_t count = 4; bool normalized = false; - bool integer = false; }; using VertexAttributeLayout = std::span; diff --git a/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp index 6f4df05f8..a3c0bee4e 100644 --- a/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp +++ b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp @@ -95,7 +95,11 @@ void OvRendering::HAL::GLVertexArray::SetLayout( glEnableVertexAttribArray(attributeIndex); - if (attribute.integer) + const bool isIntegerType = + attribute.type != Settings::EDataType::FLOAT && + attribute.type != Settings::EDataType::DOUBLE; + + if (isIntegerType && !attribute.normalized) { glVertexAttribIPointer( static_cast(attributeIndex), diff --git a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp index 265ea85ef..7b07de86b 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp @@ -152,13 +152,13 @@ void OvRendering::Resources::Mesh::Upload(std::span p_ve void OvRendering::Resources::Mesh::Upload(std::span p_vertices, std::span p_indices) { const auto layout = std::to_array({ - { Settings::EDataType::FLOAT, 3, false, false }, // position - { Settings::EDataType::FLOAT, 2, false, false }, // texCoords - { Settings::EDataType::FLOAT, 3, false, false }, // normal - { Settings::EDataType::FLOAT, 3, false, false }, // tangent - { Settings::EDataType::FLOAT, 3, false, false }, // bitangent - { Settings::EDataType::UNSIGNED_INT, 4, false, true }, // boneIDs (integer) - { Settings::EDataType::FLOAT, 4, false, false } // boneWeights + { Settings::EDataType::FLOAT, 3 }, // position + { Settings::EDataType::FLOAT, 2 }, // texCoords + { Settings::EDataType::FLOAT, 3 }, // normal + { Settings::EDataType::FLOAT, 3 }, // tangent + { Settings::EDataType::FLOAT, 3 }, // bitangent + { Settings::EDataType::UNSIGNED_INT, 4 }, // boneIDs + { Settings::EDataType::FLOAT, 4 } // boneWeights }); if (m_vertexBuffer.Allocate(p_vertices.size_bytes())) From d2c737aa2d2a2db58436774a4736d393862f75a7 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 12:58:25 -0400 Subject: [PATCH 12/18] Cleanup model flags and removed unnecessary rules --- .../ResourceManagement/ModelManager.cpp | 4 +- .../src/OvEditor/Panels/AssetProperties.cpp | 4 +- .../Resources/Parsers/EModelParserFlags.h | 72 +++++++++---------- .../Resources/Parsers/IModelParser.h | 11 +-- .../Resources/Parsers/AssimpParser.cpp | 15 ---- 5 files changed, 47 insertions(+), 59 deletions(-) diff --git a/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp index 6be240273..d1017c1e9 100644 --- a/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp +++ b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp @@ -23,7 +23,7 @@ OvRendering::Resources::Parsers::EModelParserFlags GetAssetMetadata(const std::s if (metaFile.GetOrDefault("GEN_SMOOTH_NORMALS", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::GEN_SMOOTH_NORMALS; if (metaFile.GetOrDefault("SPLIT_LARGE_MESHES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::SPLIT_LARGE_MESHES; if (metaFile.GetOrDefault("PRE_TRANSFORM_VERTICES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::PRE_TRANSFORM_VERTICES; - if (metaFile.GetOrDefault("LIMIT_BONE_WEIGHTS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::LIMIT_BONE_WEIGHTS; + if (metaFile.GetOrDefault("LIMIT_BONE_WEIGHTS", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::LIMIT_BONE_WEIGHTS; if (metaFile.GetOrDefault("VALIDATE_DATA_STRUCTURE", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::VALIDATE_DATA_STRUCTURE; if (metaFile.GetOrDefault("IMPROVE_CACHE_LOCALITY", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::IMPROVE_CACHE_LOCALITY; if (metaFile.GetOrDefault("REMOVE_REDUNDANT_MATERIALS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::REMOVE_REDUNDANT_MATERIALS; @@ -39,7 +39,7 @@ OvRendering::Resources::Parsers::EModelParserFlags GetAssetMetadata(const std::s if (metaFile.GetOrDefault("FLIP_UVS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::FLIP_UVS; if (metaFile.GetOrDefault("FLIP_WINDING_ORDER", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::FLIP_WINDING_ORDER; if (metaFile.GetOrDefault("SPLIT_BY_BONE_COUNT", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::SPLIT_BY_BONE_COUNT; - if (metaFile.GetOrDefault("DEBONE", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::DEBONE; + if (metaFile.GetOrDefault("DEBONE", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::DEBONE; if (metaFile.GetOrDefault("GLOBAL_SCALE", true)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::GLOBAL_SCALE; if (metaFile.GetOrDefault("EMBED_TEXTURES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::EMBED_TEXTURES; if (metaFile.GetOrDefault("FORCE_GEN_NORMALS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::FORCE_GEN_NORMALS; diff --git a/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp index fec6124f5..8bc1fe79a 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp @@ -220,7 +220,7 @@ void OvEditor::Panels::AssetProperties::CreateModelSettings() m_metadata->Add("GEN_SMOOTH_NORMALS", true); m_metadata->Add("SPLIT_LARGE_MESHES", false); m_metadata->Add("PRE_TRANSFORM_VERTICES", false); - m_metadata->Add("LIMIT_BONE_WEIGHTS", false); + m_metadata->Add("LIMIT_BONE_WEIGHTS", true); m_metadata->Add("VALIDATE_DATA_STRUCTURE", false); m_metadata->Add("IMPROVE_CACHE_LOCALITY", true); m_metadata->Add("REMOVE_REDUNDANT_MATERIALS", false); @@ -236,7 +236,7 @@ void OvEditor::Panels::AssetProperties::CreateModelSettings() m_metadata->Add("FLIP_UVS", false); m_metadata->Add("FLIP_WINDING_ORDER", false); m_metadata->Add("SPLIT_BY_BONE_COUNT", false); - m_metadata->Add("DEBONE", true); + m_metadata->Add("DEBONE", false); m_metadata->Add("GLOBAL_SCALE", true); m_metadata->Add("EMBED_TEXTURES", false); m_metadata->Add("FORCE_GEN_NORMALS", false); diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h index ee079c865..f8272b746 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h @@ -9,49 +9,49 @@ namespace OvRendering::Resources::Parsers { /** - * Some flags that can be used for model parsing + * Flags that can be used for model parsing */ enum class EModelParserFlags : uint32_t { - NONE = 0x0, - CALC_TANGENT_SPACE = 0x1, - JOIN_IDENTICAL_VERTICES = 0x2, - MAKE_LEFT_HANDED = 0x4, - TRIANGULATE = 0x8, - REMOVE_COMPONENT = 0x10, - GEN_NORMALS = 0x20, - GEN_SMOOTH_NORMALS = 0x40, - SPLIT_LARGE_MESHES = 0x80, - PRE_TRANSFORM_VERTICES = 0x100, - LIMIT_BONE_WEIGHTS = 0x200, - VALIDATE_DATA_STRUCTURE = 0x400, - IMPROVE_CACHE_LOCALITY = 0x800, - REMOVE_REDUNDANT_MATERIALS = 0x1000, - FIX_INFACING_NORMALS = 0x2000, - SORT_BY_PTYPE = 0x8000, - FIND_DEGENERATES = 0x10000, - FIND_INVALID_DATA = 0x20000, - GEN_UV_COORDS = 0x40000, - TRANSFORM_UV_COORDS = 0x80000, - FIND_INSTANCES = 0x100000, - OPTIMIZE_MESHES = 0x200000, - OPTIMIZE_GRAPH = 0x400000, - FLIP_UVS = 0x800000, - FLIP_WINDING_ORDER = 0x1000000, - SPLIT_BY_BONE_COUNT = 0x2000000, - DEBONE = 0x4000000, - GLOBAL_SCALE = 0x8000000, - EMBED_TEXTURES = 0x10000000, - FORCE_GEN_NORMALS = 0x20000000, - DROP_NORMALS = 0x40000000, - GEN_BOUNDING_BOXES = 0x80000000 + NONE = 0x0, + CALC_TANGENT_SPACE = 0x1, + JOIN_IDENTICAL_VERTICES = 0x2, + MAKE_LEFT_HANDED = 0x4, + TRIANGULATE = 0x8, + REMOVE_COMPONENT = 0x10, + GEN_NORMALS = 0x20, + GEN_SMOOTH_NORMALS = 0x40, + SPLIT_LARGE_MESHES = 0x80, + PRE_TRANSFORM_VERTICES = 0x100, + LIMIT_BONE_WEIGHTS = 0x200, + VALIDATE_DATA_STRUCTURE = 0x400, + IMPROVE_CACHE_LOCALITY = 0x800, + REMOVE_REDUNDANT_MATERIALS = 0x1000, + FIX_INFACING_NORMALS = 0x2000, + SORT_BY_PTYPE = 0x8000, + FIND_DEGENERATES = 0x10000, + FIND_INVALID_DATA = 0x20000, + GEN_UV_COORDS = 0x40000, + TRANSFORM_UV_COORDS = 0x80000, + FIND_INSTANCES = 0x100000, + OPTIMIZE_MESHES = 0x200000, + OPTIMIZE_GRAPH = 0x400000, + FLIP_UVS = 0x800000, + FLIP_WINDING_ORDER = 0x1000000, + SPLIT_BY_BONE_COUNT = 0x2000000, + DEBONE = 0x4000000, + GLOBAL_SCALE = 0x8000000, + EMBED_TEXTURES = 0x10000000, + FORCE_GEN_NORMALS = 0x20000000, + DROP_NORMALS = 0x40000000, + GEN_BOUNDING_BOXES = 0x80000000 }; - - inline EModelParserFlags operator~ (EModelParserFlags a) { return (EModelParserFlags)~(int)a; } + + inline EModelParserFlags operator~(EModelParserFlags a) { return (EModelParserFlags) ~(int)a; } inline EModelParserFlags operator| (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a | (int)b); } inline EModelParserFlags operator& (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a & (int)b); } inline EModelParserFlags operator^ (EModelParserFlags a, EModelParserFlags b) { return (EModelParserFlags)((int)a ^ (int)b); } inline EModelParserFlags& operator|= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a |= (int)b); } inline EModelParserFlags& operator&= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a &= (int)b); } inline EModelParserFlags& operator^= (EModelParserFlags& a, EModelParserFlags b) { return (EModelParserFlags&)((int&)a ^= (int)b); } -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h index 76c443b00..09094e1a4 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h @@ -9,9 +9,9 @@ #include #include -#include "OvRendering/Animation/SkeletalData.h" -#include "OvRendering/Resources/Mesh.h" -#include "OvRendering/Resources/Parsers/EModelParserFlags.h" +#include +#include +#include namespace OvRendering::Resources::Parsers { @@ -22,10 +22,13 @@ namespace OvRendering::Resources::Parsers { public: /** - * Load meshes from a file + * Load meshes data from a file * Return true on success * @param p_filename * @param p_meshes + * @param p_materials + * @param p_skeleton + * @param p_animation * @param p_parserFlags */ virtual bool LoadModel diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index ccd7ca470..2725044f8 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -35,21 +35,6 @@ namespace OVLOG_WARNING("AssimpParser: OPTIMIZE_GRAPH and PRE_TRANSFORM_VERTICES are mutually exclusive. OPTIMIZE_GRAPH will be ignored."); } - p_flags |= TRIANGULATE; - p_flags |= LIMIT_BONE_WEIGHTS; - - if (static_cast(p_flags & PRE_TRANSFORM_VERTICES)) - { - p_flags &= ~PRE_TRANSFORM_VERTICES; - OVLOG_WARNING("AssimpParser: PRE_TRANSFORM_VERTICES is incompatible with skeletal animation and has been removed."); - } - - if (static_cast(p_flags & DEBONE)) - { - p_flags &= ~DEBONE; - OVLOG_WARNING("AssimpParser: DEBONE removes bone data and is incompatible with skeletal animation. DEBONE will be ignored."); - } - return p_flags; } From b444541d69063db5dad8b8ee2462a9e843220bdf Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 13:19:36 -0400 Subject: [PATCH 13/18] Simplified GPU skinning and forced normalized weights --- .../Engine/Shaders/Common/Skinning.ovfxh | 46 +++---------------- .../Resources/Parsers/EModelParserFlags.h | 2 + .../Resources/Parsers/AssimpParser.cpp | 18 +++++++- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/Resources/Engine/Shaders/Common/Skinning.ovfxh b/Resources/Engine/Shaders/Common/Skinning.ovfxh index 7283bf6f4..388e23e1b 100644 --- a/Resources/Engine/Shaders/Common/Skinning.ovfxh +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -1,46 +1,14 @@ #include ":Shaders/Common/Buffers/SkinningSSBO.ovfxh" -void ApplyBone( - uint p_boneID, - float p_boneWeight, - int p_boneCount, - inout mat4 p_skinning, - inout float p_appliedWeight -) -{ - if (p_boneWeight > 0.0) - { - if (int(p_boneID) < p_boneCount) - { - p_skinning += ssbo_Bones[p_boneID] * p_boneWeight; - p_appliedWeight += p_boneWeight; - } - } -} - mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) { - const int boneCount = ssbo_Bones.length(); - - mat4 skinning = mat4(0.0); - float appliedWeight = 0.0; + mat4 skinMat = mat4(0.0); - for (int i = 0; i < 4; ++i) - { - ApplyBone(p_boneIDs[i], p_boneWeights[i], boneCount, skinning, appliedWeight); - } + skinMat += p_boneWeights.x * ssbo_Bones[p_boneIDs.x]; + skinMat += p_boneWeights.y * ssbo_Bones[p_boneIDs.y]; + skinMat += p_boneWeights.z * ssbo_Bones[p_boneIDs.z]; + skinMat += p_boneWeights.w * ssbo_Bones[p_boneIDs.w]; - if (appliedWeight <= 0.0) - { - return mat4(1.0); - } - - if (appliedWeight == 1.0) - { - return skinning; - } - - return appliedWeight < 1.0 ? - skinning + mat4(1.0 - appliedWeight) : - skinning / appliedWeight; + return skinMat; } + diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h index f8272b746..62a617c63 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h @@ -6,6 +6,8 @@ #pragma once +#include + namespace OvRendering::Resources::Parsers { /** diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index 2725044f8..bb7b452a8 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -388,9 +388,25 @@ OvRendering::Resources::Mesh* OvRendering::Resources::Parsers::AssimpParser::Pro } } + for (auto& vertex : vertices) + { + float weightSum = 0.0f; + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + weightSum += vertex.boneWeights[i]; + } + + if (weightSum > 0.0f) + { + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + vertex.boneWeights[i] /= weightSum; + } + } + } + return new Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); } - else { std::vector vertices(p_mesh->mNumVertices); for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) From b99e5216f14b3b10f2d53a71203abe3a9e918bd5 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 13:28:16 -0400 Subject: [PATCH 14/18] Cleanup AssimpParser by using anynonymous functions --- .../Resources/Parsers/AssimpParser.h | 16 +- .../Resources/Parsers/AssimpParser.cpp | 503 +++++++++--------- 2 files changed, 245 insertions(+), 274 deletions(-) diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h index 38d93bf0d..7e5714cd3 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h @@ -8,9 +8,8 @@ #include -#include "OvRendering/Geometry/Vertex.h" -#include "OvRendering/Resources/Mesh.h" -#include "OvRendering/Resources/Parsers/IModelParser.h" +#include +#include namespace OvRendering::Resources::Parsers { @@ -36,16 +35,5 @@ namespace OvRendering::Resources::Parsers std::vector& p_animations, EModelParserFlags p_parserFlags ) override; - - private: - void BuildSkeleton(const struct aiScene* p_scene, Animation::Skeleton& p_skeleton); - void ProcessAnimations(const struct aiScene* p_scene, const Animation::Skeleton& p_skeleton, std::vector& p_animations); - void ProcessMaterials(const struct aiScene* p_scene, std::vector& p_materials); - void ProcessNode(void* p_transform, struct aiNode* p_node, const struct aiScene* p_scene, std::vector& p_meshes, Animation::Skeleton* p_skeleton); - Mesh* ProcessMesh( - void* p_transform, - struct aiMesh* p_mesh, - Animation::Skeleton* p_skeleton - ); }; } diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index bb7b452a8..325cbcb0f 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -95,325 +95,308 @@ namespace } } -} - -bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( - const std::string& p_fileName, - std::vector& p_meshes, - std::vector& p_materials, - std::optional& p_skeleton, - std::vector& p_animations, - EModelParserFlags p_parserFlags -) -{ - Assimp::Importer import; - - // Fix the flags to avoid conflicts/invalid scenarios. - // This is a workaround, ideally the editor UI should not allow this to happen. - p_parserFlags = FixFlags(p_parserFlags); - - const aiScene* scene = import.ReadFile(p_fileName, static_cast(p_parserFlags)); - - if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) - { - return false; - } - - ProcessMaterials(scene, p_materials); - - const bool hasBones = [&scene]() + void ProcessMaterials(const aiScene* p_scene, std::vector& p_materials) { - for (uint32_t i = 0; i < scene->mNumMeshes; ++i) + for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i) { - if (const auto mesh = scene->mMeshes[i]; mesh && mesh->HasBones()) + aiMaterial* material = p_scene->mMaterials[i]; + if (material) { - return true; + aiString name; + aiGetMaterialString(material, AI_MATKEY_NAME, &name); + p_materials.push_back(name.C_Str()); } } + } - return false; - }(); + void ProcessAnimations( + const aiScene* p_scene, + const OvRendering::Animation::Skeleton& p_skeleton, + std::vector& p_animations + ) + { + p_animations.reserve(p_scene->mNumAnimations); - p_skeleton.reset(); - p_animations.clear(); + for (uint32_t animIndex = 0; animIndex < p_scene->mNumAnimations; ++animIndex) + { + const auto* animation = p_scene->mAnimations[animIndex]; - if (hasBones) - { - p_skeleton.emplace(); - BuildSkeleton(scene, p_skeleton.value()); - ProcessAnimations(scene, p_skeleton.value(), p_animations); - } + OvRendering::Animation::SkeletalAnimation outAnimation{ + .name = animation->mName.length > 0 ? animation->mName.C_Str() : ("Animation_" + std::to_string(animIndex)), + .duration = static_cast(animation->mDuration), + .ticksPerSecond = animation->mTicksPerSecond > 0.0 ? static_cast(animation->mTicksPerSecond) : 25.0f + }; - aiMatrix4x4 identity; - ProcessNode(&identity, scene->mRootNode, scene, p_meshes, p_skeleton ? &p_skeleton.value() : nullptr); + outAnimation.tracks.reserve(animation->mNumChannels); - if (p_skeleton && p_skeleton->bones.empty()) - { - p_skeleton.reset(); - p_animations.clear(); + for (uint32_t channelIndex = 0; channelIndex < animation->mNumChannels; ++channelIndex) + { + const auto* channel = animation->mChannels[channelIndex]; + const std::string nodeName = channel->mNodeName.C_Str(); + + if (auto foundNode = p_skeleton.FindNodeIndex(nodeName)) + { + OvRendering::Animation::NodeAnimationTrack track; + track.nodeIndex = foundNode.value(); + + track.positionKeys.reserve(channel->mNumPositionKeys); + for (uint32_t i = 0; i < channel->mNumPositionKeys; ++i) + { + const auto& key = channel->mPositionKeys[i]; + track.positionKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z } + }); + } + + track.rotationKeys.reserve(channel->mNumRotationKeys); + for (uint32_t i = 0; i < channel->mNumRotationKeys; ++i) + { + const auto& key = channel->mRotationKeys[i]; + track.rotationKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z, key.mValue.w } + }); + } + + track.scaleKeys.reserve(channel->mNumScalingKeys); + for (uint32_t i = 0; i < channel->mNumScalingKeys; ++i) + { + const auto& key = channel->mScalingKeys[i]; + track.scaleKeys.push_back({ + .time = static_cast(key.mTime), + .value = { key.mValue.x, key.mValue.y, key.mValue.z } + }); + } + + const auto trackIndex = static_cast(outAnimation.tracks.size()); + outAnimation.trackByNodeIndex.emplace(track.nodeIndex, trackIndex); + outAnimation.tracks.push_back(std::move(track)); + } + } + + p_animations.push_back(std::move(outAnimation)); + } } - return true; -} + OvRendering::Resources::Mesh* ProcessMesh( + const aiMatrix4x4& p_transform, + aiMesh* p_mesh, + OvRendering::Animation::Skeleton* p_skeleton + ) + { + std::vector indices; + indices.reserve(p_mesh->mNumFaces * 3); -void OvRendering::Resources::Parsers::AssimpParser::BuildSkeleton(const aiScene* p_scene, Animation::Skeleton& p_skeleton) -{ - AddSkeletonNodeRecursive(p_skeleton, p_scene->mRootNode, -1); -} + for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID) + { + const auto& face = p_mesh->mFaces[faceID]; -void OvRendering::Resources::Parsers::AssimpParser::ProcessAnimations( - const aiScene* p_scene, - const Animation::Skeleton& p_skeleton, - std::vector& p_animations -) -{ - p_animations.reserve(p_scene->mNumAnimations); + if (face.mNumIndices < 3) + { + continue; + } - for (uint32_t animIndex = 0; animIndex < p_scene->mNumAnimations; ++animIndex) - { - const auto* animation = p_scene->mAnimations[animIndex]; + const auto a = face.mIndices[0]; + const auto b = face.mIndices[1]; + const auto c = face.mIndices[2]; + if (a >= p_mesh->mNumVertices || b >= p_mesh->mNumVertices || c >= p_mesh->mNumVertices) + { + continue; + } - Animation::SkeletalAnimation outAnimation{ - .name = animation->mName.length > 0 ? animation->mName.C_Str() : ("Animation_" + std::to_string(animIndex)), - .duration = static_cast(animation->mDuration), - .ticksPerSecond = animation->mTicksPerSecond > 0.0 ? static_cast(animation->mTicksPerSecond) : 25.0f - }; + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); + } - outAnimation.tracks.reserve(animation->mNumChannels); + if (p_mesh->mNumVertices == 0 || indices.empty()) + { + return nullptr; + } - for (uint32_t channelIndex = 0; channelIndex < animation->mNumChannels; ++channelIndex) + auto fillGeometry = [&](OvRendering::Geometry::Vertex& v, uint32_t i) { - const auto* channel = animation->mChannels[channelIndex]; - const std::string nodeName = channel->mNodeName.C_Str(); + const aiVector3D position = p_transform * p_mesh->mVertices[i]; + const aiVector3D texCoords = p_mesh->mTextureCoords[0] ? p_mesh->mTextureCoords[0][i] : aiVector3D(0.0f, 0.0f, 0.0f); + const aiVector3D normal = p_transform * (p_mesh->mNormals ? p_mesh->mNormals[i] : aiVector3D(0.0f, 0.0f, 0.0f)); + const aiVector3D tangent = p_transform * (p_mesh->mTangents ? p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); + const aiVector3D bitangent = p_transform * (p_mesh->mBitangents ? p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); + + v.position[0] = position.x; + v.position[1] = position.y; + v.position[2] = position.z; + v.texCoords[0] = texCoords.x; + v.texCoords[1] = texCoords.y; + v.normals[0] = normal.x; + v.normals[1] = normal.y; + v.normals[2] = normal.z; + v.tangent[0] = tangent.x; + v.tangent[1] = tangent.y; + v.tangent[2] = tangent.z; + // Assimp calculates the tangent space vectors in a right-handed system. + // But our shader code expects a left-handed system. + // Multiplying the bitangent by -1 will convert it to a left-handed system. + // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. + v.bitangent[0] = -bitangent.x; + v.bitangent[1] = -bitangent.y; + v.bitangent[2] = -bitangent.z; + }; - if (auto foundNode = p_skeleton.FindNodeIndex(nodeName)) + if (p_skeleton && p_mesh->HasBones()) + { + std::vector vertices(p_mesh->mNumVertices); + for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) + { + fillGeometry(vertices[i], i); + } + + for (uint32_t boneID = 0; boneID < p_mesh->mNumBones; ++boneID) { - Animation::NodeAnimationTrack track; - track.nodeIndex = foundNode.value(); + const aiBone* bone = p_mesh->mBones[boneID]; + const std::string boneName = bone->mName.C_Str(); + const auto offsetMatrix = ToMatrix4(bone->mOffsetMatrix); - track.positionKeys.reserve(channel->mNumPositionKeys); - for (uint32_t i = 0; i < channel->mNumPositionKeys; ++i) + auto nodeIndex = p_skeleton->FindNodeIndex(boneName); + if (!nodeIndex) { - const auto& key = channel->mPositionKeys[i]; - track.positionKeys.push_back({ - .time = static_cast(key.mTime), - .value = { key.mValue.x, key.mValue.y, key.mValue.z } - }); + OVLOG_WARNING("AssimpParser: Bone '" + boneName + "' has no matching node in hierarchy and will be ignored."); + continue; } - track.rotationKeys.reserve(channel->mNumRotationKeys); - for (uint32_t i = 0; i < channel->mNumRotationKeys; ++i) + uint32_t boneIndex = 0; + + if (auto existing = p_skeleton->FindBoneIndex(boneName)) { - const auto& key = channel->mRotationKeys[i]; - track.rotationKeys.push_back({ - .time = static_cast(key.mTime), - .value = { key.mValue.x, key.mValue.y, key.mValue.z, key.mValue.w } + boneIndex = existing.value(); + } + else + { + boneIndex = static_cast(p_skeleton->bones.size()); + p_skeleton->boneByName.emplace(boneName, boneIndex); + p_skeleton->bones.push_back({ + .name = boneName, + .nodeIndex = nodeIndex.value(), + .offsetMatrix = offsetMatrix }); } - track.scaleKeys.reserve(channel->mNumScalingKeys); - for (uint32_t i = 0; i < channel->mNumScalingKeys; ++i) + p_skeleton->nodes[nodeIndex.value()].boneIndex = static_cast(boneIndex); + + for (uint32_t weightID = 0; weightID < bone->mNumWeights; ++weightID) { - const auto& key = channel->mScalingKeys[i]; - track.scaleKeys.push_back({ - .time = static_cast(key.mTime), - .value = { key.mValue.x, key.mValue.y, key.mValue.z } - }); + const auto& weight = bone->mWeights[weightID]; + if (weight.mVertexId < vertices.size()) + { + AddBoneData(vertices[weight.mVertexId], boneIndex, weight.mWeight); + } + } + } + + for (auto& vertex : vertices) + { + float weightSum = 0.0f; + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + weightSum += vertex.boneWeights[i]; } - const auto trackIndex = static_cast(outAnimation.tracks.size()); - outAnimation.trackByNodeIndex.emplace(track.nodeIndex, trackIndex); - outAnimation.tracks.push_back(std::move(track)); + if (weightSum > 0.0f) + { + for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) + { + vertex.boneWeights[i] /= weightSum; + } + } } + + return new OvRendering::Resources::Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); } + else + { + std::vector vertices(p_mesh->mNumVertices); + for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) + { + fillGeometry(vertices[i], i); + } - p_animations.push_back(std::move(outAnimation)); + return new OvRendering::Resources::Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); + } } -} -void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiScene* p_scene, std::vector& p_materials) -{ - for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i) + void ProcessNode( + const aiMatrix4x4& p_transform, + aiNode* p_node, + const aiScene* p_scene, + std::vector& p_meshes, + OvRendering::Animation::Skeleton* p_skeleton + ) { - aiMaterial* material = p_scene->mMaterials[i]; - if (material) + const aiMatrix4x4 nodeTransform = p_transform * p_node->mTransformation; + + for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) { - aiString name; - aiGetMaterialString(material, AI_MATKEY_NAME, &name); - p_materials.push_back(name.C_Str()); + if (OvRendering::Resources::Mesh* result = ProcessMesh(nodeTransform, p_scene->mMeshes[p_node->mMeshes[i]], p_skeleton)) + { + p_meshes.push_back(result); // The model will handle mesh destruction + } } - } -} - -void OvRendering::Resources::Parsers::AssimpParser::ProcessNode( - void* p_transform, - aiNode* p_node, - const aiScene* p_scene, - std::vector& p_meshes, - Animation::Skeleton* p_skeleton -) -{ - aiMatrix4x4 nodeTransformation = *reinterpret_cast(p_transform) * p_node->mTransformation; - // Process all the node's meshes (if any) - for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) - { - aiMesh* mesh = p_scene->mMeshes[p_node->mMeshes[i]]; - if (Mesh* result = ProcessMesh(&nodeTransformation, mesh, p_skeleton)) + for (uint32_t i = 0; i < p_node->mNumChildren; ++i) { - p_meshes.push_back(result); // The model will handle mesh destruction + ProcessNode(nodeTransform, p_node->mChildren[i], p_scene, p_meshes, p_skeleton); } } - - // Then do the same for each of its children - for (uint32_t i = 0; i < p_node->mNumChildren; ++i) - { - ProcessNode(&nodeTransformation, p_node->mChildren[i], p_scene, p_meshes, p_skeleton); - } } -OvRendering::Resources::Mesh* OvRendering::Resources::Parsers::AssimpParser::ProcessMesh( - void* p_transform, - aiMesh* p_mesh, - Animation::Skeleton* p_skeleton +bool OvRendering::Resources::Parsers::AssimpParser::LoadModel( + const std::string& p_fileName, + std::vector& p_meshes, + std::vector& p_materials, + std::optional& p_skeleton, + std::vector& p_animations, + EModelParserFlags p_parserFlags ) { - aiMatrix4x4 meshTransformation = *reinterpret_cast(p_transform); - - std::vector indices; - indices.reserve(p_mesh->mNumFaces * 3); - - for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID) - { - const auto& face = p_mesh->mFaces[faceID]; - - if (face.mNumIndices < 3) - { - continue; - } + Assimp::Importer import; - const auto a = face.mIndices[0]; - const auto b = face.mIndices[1]; - const auto c = face.mIndices[2]; - if (a >= p_mesh->mNumVertices || b >= p_mesh->mNumVertices || c >= p_mesh->mNumVertices) - { - continue; - } + // Fix the flags to avoid conflicts/invalid scenarios. + // This is a workaround, ideally the editor UI should not allow this to happen. + p_parserFlags = FixFlags(p_parserFlags); - indices.push_back(a); - indices.push_back(b); - indices.push_back(c); - } + const aiScene* scene = import.ReadFile(p_fileName, static_cast(p_parserFlags)); - if (p_mesh->mNumVertices == 0 || indices.empty()) + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { - return nullptr; + return false; } - auto fillGeometry = [&](Geometry::Vertex& v, uint32_t i) - { - const aiVector3D position = meshTransformation * p_mesh->mVertices[i]; - const aiVector3D texCoords = p_mesh->mTextureCoords[0] ? p_mesh->mTextureCoords[0][i] : aiVector3D(0.0f, 0.0f, 0.0f); - const aiVector3D normal = meshTransformation * (p_mesh->mNormals ? p_mesh->mNormals[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - const aiVector3D tangent = meshTransformation * (p_mesh->mTangents ? p_mesh->mTangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - const aiVector3D bitangent = meshTransformation * (p_mesh->mBitangents ? p_mesh->mBitangents[i] : aiVector3D(0.0f, 0.0f, 0.0f)); - - v.position[0] = position.x; - v.position[1] = position.y; - v.position[2] = position.z; - v.texCoords[0] = texCoords.x; - v.texCoords[1] = texCoords.y; - v.normals[0] = normal.x; - v.normals[1] = normal.y; - v.normals[2] = normal.z; - v.tangent[0] = tangent.x; - v.tangent[1] = tangent.y; - v.tangent[2] = tangent.z; - // Assimp calculates the tangent space vectors in a right-handed system. - // But our shader code expects a left-handed system. - // Multiplying the bitangent by -1 will convert it to a left-handed system. - // Learn OpenGL also uses a left-handed tangent space for normal mapping and parallax mapping. - v.bitangent[0] = -bitangent.x; - v.bitangent[1] = -bitangent.y; - v.bitangent[2] = -bitangent.z; - }; - - if (p_skeleton && p_mesh->HasBones()) - { - std::vector vertices(p_mesh->mNumVertices); - for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) - { - fillGeometry(vertices[i], i); - } - - for (uint32_t boneID = 0; boneID < p_mesh->mNumBones; ++boneID) - { - const aiBone* bone = p_mesh->mBones[boneID]; - const std::string boneName = bone->mName.C_Str(); - const auto offsetMatrix = ToMatrix4(bone->mOffsetMatrix); - - auto nodeIndex = p_skeleton->FindNodeIndex(boneName); - if (!nodeIndex) - { - OVLOG_WARNING("AssimpParser: Bone '" + boneName + "' has no matching node in hierarchy and will be ignored."); - continue; - } - - uint32_t boneIndex = 0; - - if (auto existing = p_skeleton->FindBoneIndex(boneName)) - { - boneIndex = existing.value(); - } - else - { - boneIndex = static_cast(p_skeleton->bones.size()); - p_skeleton->boneByName.emplace(boneName, boneIndex); - p_skeleton->bones.push_back({ - .name = boneName, - .nodeIndex = nodeIndex.value(), - .offsetMatrix = offsetMatrix - }); - } + ProcessMaterials(scene, p_materials); - p_skeleton->nodes[nodeIndex.value()].boneIndex = static_cast(boneIndex); + bool hasBones = false; + for (uint32_t i = 0; i < scene->mNumMeshes && !hasBones; ++i) + { + hasBones = scene->mMeshes[i] && scene->mMeshes[i]->HasBones(); + } - for (uint32_t weightID = 0; weightID < bone->mNumWeights; ++weightID) - { - const auto& weight = bone->mWeights[weightID]; - if (weight.mVertexId < vertices.size()) - { - AddBoneData(vertices[weight.mVertexId], boneIndex, weight.mWeight); - } - } - } + p_skeleton.reset(); + p_animations.clear(); - for (auto& vertex : vertices) - { - float weightSum = 0.0f; - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - weightSum += vertex.boneWeights[i]; - } + if (hasBones) + { + p_skeleton.emplace(); + AddSkeletonNodeRecursive(p_skeleton.value(), scene->mRootNode, -1); + ProcessAnimations(scene, p_skeleton.value(), p_animations); + } - if (weightSum > 0.0f) - { - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - vertex.boneWeights[i] /= weightSum; - } - } - } + ProcessNode(aiMatrix4x4{}, scene->mRootNode, scene, p_meshes, p_skeleton ? &p_skeleton.value() : nullptr); - return new Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); - } + if (p_skeleton && p_skeleton->bones.empty()) { - std::vector vertices(p_mesh->mNumVertices); - for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) - { - fillGeometry(vertices[i], i); - } - - return new Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); + p_skeleton.reset(); + p_animations.clear(); } + + return true; } + From 0a947581e3e1b8e15a26db02d097e30cea6727d5 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 14:10:53 -0400 Subject: [PATCH 15/18] Cleaning up SkinnedMeshRenderer methods --- .../ECS/Components/CSkinnedMeshRenderer.h | 18 +---- .../ECS/Components/CSkinnedMeshRenderer.cpp | 72 ++++++++----------- .../src/OvCore/Rendering/SkinningUtils.cpp | 2 +- .../Lua/Bindings/LuaComponentsBindings.cpp | 7 +- 4 files changed, 31 insertions(+), 68 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h index 05e8985ff..21ec634ce 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -46,27 +46,11 @@ namespace OvCore::ECS::Components */ void NotifyModelChanged(); - /** - * Returns true if a valid skinned model is currently attached - */ - bool HasCompatibleModel() const; - /** * Returns true if the component has a valid skinning palette ready for rendering */ bool HasSkinningData() const; - /** - * Enable / disable this component - * @param p_value - */ - void SetEnabled(bool p_value); - - /** - * Returns true if the component is enabled - */ - bool IsEnabled() const; - /** * Start animation playback */ @@ -190,6 +174,7 @@ namespace OvCore::ECS::Components virtual void OnInspector(OvUI::Internal::WidgetContainer& p_root) override; private: + bool HasCompatibleModel() const; void SyncWithModel(); void RebuildRuntimeData(); void EvaluatePose(); @@ -199,7 +184,6 @@ namespace OvCore::ECS::Components private: const OvRendering::Resources::Model* m_model = nullptr; - bool m_enabled = true; bool m_playing = true; bool m_looping = true; float m_playbackSpeed = 1.0f; diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp index fb9ee723e..23262390a 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -122,26 +122,11 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::NotifyModelChanged() SyncWithModel(); } -bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasCompatibleModel() const -{ - return m_model && m_model->IsSkinned() && m_model->GetSkeleton().has_value(); -} - bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasSkinningData() const { return HasCompatibleModel() && m_animationIndex.has_value() && !m_boneMatrices.empty(); } -void OvCore::ECS::Components::CSkinnedMeshRenderer::SetEnabled(bool p_value) -{ - m_enabled = p_value; -} - -bool OvCore::ECS::Components::CSkinnedMeshRenderer::IsEnabled() const -{ - return m_enabled; -} - void OvCore::ECS::Components::CSkinnedMeshRenderer::Play() { m_playing = true; @@ -318,50 +303,46 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnUpdate(float p_deltaTime) SyncWithModel(); - if (!m_enabled || !HasCompatibleModel()) + if (!HasCompatibleModel() || !m_playing) { return; } - if (m_playing) - { - const float previousTimeTicks = m_currentTimeTicks; - const bool wasPlaying = m_playing; + const float previousTimeTicks = m_currentTimeTicks; + const bool wasPlaying = m_playing; - UpdatePlayback(p_deltaTime); + UpdatePlayback(p_deltaTime); - const bool timeChanged = std::abs(m_currentTimeTicks - previousTimeTicks) > std::numeric_limits::epsilon(); - const bool playbackStateChanged = wasPlaying != m_playing; + const bool timeChanged = std::abs(m_currentTimeTicks - previousTimeTicks) > std::numeric_limits::epsilon(); + const bool playbackStateChanged = wasPlaying != m_playing; - if (timeChanged || playbackStateChanged) - { - const float clampedPoseEvaluationRate = std::max(0.0f, m_poseEvaluationRate); - const bool hasRateLimit = clampedPoseEvaluationRate > std::numeric_limits::epsilon(); + if (timeChanged || playbackStateChanged) + { + const float clampedPoseEvaluationRate = std::max(0.0f, m_poseEvaluationRate); + const bool hasRateLimit = clampedPoseEvaluationRate > std::numeric_limits::epsilon(); - if (hasRateLimit) - { - m_poseEvaluationAccumulator += p_deltaTime; - const float updatePeriod = 1.0f / clampedPoseEvaluationRate; - if (m_poseEvaluationAccumulator < updatePeriod && !playbackStateChanged) - { - return; - } - - m_poseEvaluationAccumulator = std::fmod(m_poseEvaluationAccumulator, updatePeriod); - } - else + if (hasRateLimit) + { + m_poseEvaluationAccumulator += p_deltaTime; + const float updatePeriod = 1.0f / clampedPoseEvaluationRate; + if (m_poseEvaluationAccumulator < updatePeriod && !playbackStateChanged) { - m_poseEvaluationAccumulator = 0.0f; + return; } - EvaluatePose(); + m_poseEvaluationAccumulator = std::fmod(m_poseEvaluationAccumulator, updatePeriod); } + else + { + m_poseEvaluationAccumulator = 0.0f; + } + + EvaluatePose(); } } void OvCore::ECS::Components::CSkinnedMeshRenderer::OnSerialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) { - OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "enabled", m_enabled); OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "playing", m_playing); OvCore::Helpers::Serializer::SerializeBoolean(p_doc, p_node, "looping", m_looping); OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "playback_speed", m_playbackSpeed); @@ -372,7 +353,6 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnSerialize(tinyxml2::XMLDoc void OvCore::ECS::Components::CSkinnedMeshRenderer::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) { - OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "enabled", m_enabled); OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "playing", m_playing); OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_node, "looping", m_looping); OvCore::Helpers::Serializer::DeserializeFloat(p_doc, p_node, "playback_speed", m_playbackSpeed); @@ -391,7 +371,6 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: using namespace OvCore::Helpers; - GUIDrawer::DrawBoolean(p_root, "Enabled", m_enabled); GUIDrawer::DrawBoolean(p_root, "Playing", m_playing); GUIDrawer::DrawBoolean(p_root, "Looping", m_looping); GUIDrawer::DrawScalar(p_root, "Playback Speed", m_playbackSpeed, 0.01f, -10.0f, 10.0f); @@ -437,6 +416,11 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: } } +bool OvCore::ECS::Components::CSkinnedMeshRenderer::HasCompatibleModel() const +{ + return m_model && m_model->IsSkinned() && m_model->GetSkeleton().has_value(); +} + void OvCore::ECS::Components::CSkinnedMeshRenderer::SyncWithModel() { const auto modelRenderer = owner.GetComponent(); diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp index 209b8ea3e..05d7802b6 100644 --- a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp @@ -14,7 +14,7 @@ bool OvCore::Rendering::SkinningUtils::IsSkinningActive(const OvCore::ECS::Components::CSkinnedMeshRenderer* p_renderer) { - return p_renderer && p_renderer->IsEnabled() && p_renderer->HasSkinningData(); + return p_renderer && p_renderer->HasSkinningData(); } OvRendering::Data::FeatureSet OvCore::Rendering::SkinningUtils::BuildFeatureSet(const OvRendering::Data::FeatureSet* p_baseFeatures) diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp index 90e4e26f2..6a5535497 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -90,11 +90,6 @@ void BindLuaComponents(sol::state& p_luaState) p_luaState.new_usertype("SkinnedMeshRenderer", sol::base_classes, sol::bases(), - "NotifyModelChanged", &CSkinnedMeshRenderer::NotifyModelChanged, - "HasCompatibleModel", &CSkinnedMeshRenderer::HasCompatibleModel, - "HasSkinningData", &CSkinnedMeshRenderer::HasSkinningData, - "SetEnabled", &CSkinnedMeshRenderer::SetEnabled, - "IsEnabled", &CSkinnedMeshRenderer::IsEnabled, "Play", &CSkinnedMeshRenderer::Play, "Pause", &CSkinnedMeshRenderer::Pause, "Stop", &CSkinnedMeshRenderer::Stop, @@ -118,7 +113,7 @@ void BindLuaComponents(sol::state& p_luaState) p_luaState.new_enum("CollisionDetectionMode", { {"DISCRETE", OvPhysics::Entities::PhysicalObject::ECollisionDetectionMode::DISCRETE}, {"CONTINUOUS", OvPhysics::Entities::PhysicalObject::ECollisionDetectionMode::CONTINUOUS} - }); + }); p_luaState.new_usertype("PhysicalObject", sol::base_classes, sol::bases(), From 9ee07bae2045a7a8abc0d796297e3273325e8e00 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 14:57:36 -0400 Subject: [PATCH 16/18] Renamed some functions for consistency --- .../ECS/Components/CSkinnedMeshRenderer.h | 4 ++-- .../ECS/Components/CSkinnedMeshRenderer.cpp | 17 +++++++++-------- .../Lua/Bindings/LuaComponentsBindings.cpp | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h index 21ec634ce..b009b8153 100644 --- a/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -130,12 +130,12 @@ namespace OvCore::ECS::Components /** * Returns the active animation index, or std::nullopt if none is set */ - std::optional GetAnimationIndex() const; + std::optional GetActiveAnimationIndex() const; /** * Returns the active animation name (empty if none) */ - std::string GetAnimation() const; + std::optional GetActiveAnimationName() const; /** * Returns the transposed skinning matrix palette ready for GPU upload diff --git a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp index 23262390a..055a19d55 100644 --- a/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -268,20 +270,19 @@ bool OvCore::ECS::Components::CSkinnedMeshRenderer::SetAnimation(const std::stri return SetAnimation(static_cast(std::distance(animations.begin(), found))); } -std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimationIndex() const +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetActiveAnimationIndex() const { return m_animationIndex; } -std::string OvCore::ECS::Components::CSkinnedMeshRenderer::GetAnimation() const +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetActiveAnimationName() const { if (!m_animationIndex.has_value()) { - return {}; + return std::nullopt; } - const auto animationName = GetAnimationName(*m_animationIndex); - return animationName.has_value() ? *animationName : std::string{}; + return GetAnimationName(m_animationIndex.value()); } const std::vector& OvCore::ECS::Components::CSkinnedMeshRenderer::GetBoneMatricesTransposed() const @@ -348,7 +349,7 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnSerialize(tinyxml2::XMLDoc OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "playback_speed", m_playbackSpeed); OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "pose_eval_rate", m_poseEvaluationRate); OvCore::Helpers::Serializer::SerializeFloat(p_doc, p_node, "time_ticks", m_currentTimeTicks); - OvCore::Helpers::Serializer::SerializeString(p_doc, p_node, "animation", GetAnimation()); + OvCore::Helpers::Serializer::SerializeString(p_doc, p_node, "animation", GetActiveAnimationName().value_or(std::string{})); } void OvCore::ECS::Components::CSkinnedMeshRenderer::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) @@ -387,7 +388,7 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: ); GUIDrawer::CreateTitle(p_root, "Active Animation"); - const int currentAnimIndex = GetAnimationIndex().has_value() ? static_cast(*GetAnimationIndex()) : -1; + const int currentAnimIndex = GetActiveAnimationIndex().has_value() ? static_cast(*GetActiveAnimationIndex()) : -1; auto& animationChoice = p_root.CreateWidget(currentAnimIndex); animationChoice.choices.emplace(-1, ""); @@ -399,7 +400,7 @@ void OvCore::ECS::Components::CSkinnedMeshRenderer::OnInspector(OvUI::Internal:: auto& animDispatcher = animationChoice.AddPlugin>(); animDispatcher.RegisterGatherer([this] { - return GetAnimationIndex().has_value() ? static_cast(*GetAnimationIndex()) : -1; + return GetActiveAnimationIndex().has_value() ? static_cast(*GetActiveAnimationIndex()) : -1; }); animDispatcher.RegisterProvider([this](int p_choice) { diff --git a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp index 6a5535497..27f0bdd9a 100644 --- a/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp +++ b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaComponentsBindings.cpp @@ -106,8 +106,8 @@ void BindLuaComponents(sol::state& p_luaState) sol::resolve)>(&CSkinnedMeshRenderer::SetAnimation), sol::resolve(&CSkinnedMeshRenderer::SetAnimation) ), - "GetAnimationIndex", &CSkinnedMeshRenderer::GetAnimationIndex, - "GetAnimation", &CSkinnedMeshRenderer::GetAnimation + "GetActiveAnimationIndex", &CSkinnedMeshRenderer::GetActiveAnimationIndex, + "GetActiveAnimationName", &CSkinnedMeshRenderer::GetActiveAnimationName ); p_luaState.new_enum("CollisionDetectionMode", { From 495625a01600edad5560447a8380129b90111755 Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 16:50:25 -0400 Subject: [PATCH 17/18] No more pre-normalization of weights, KISS --- .../Engine/Shaders/Common/Skinning.ovfxh | 25 ++++++++++++++----- .../Resources/Parsers/AssimpParser.cpp | 17 ------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Resources/Engine/Shaders/Common/Skinning.ovfxh b/Resources/Engine/Shaders/Common/Skinning.ovfxh index 388e23e1b..abfe605d2 100644 --- a/Resources/Engine/Shaders/Common/Skinning.ovfxh +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -2,13 +2,26 @@ mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) { - mat4 skinMat = mat4(0.0); + const int boneCount = ssbo_Bones.length(); - skinMat += p_boneWeights.x * ssbo_Bones[p_boneIDs.x]; - skinMat += p_boneWeights.y * ssbo_Bones[p_boneIDs.y]; - skinMat += p_boneWeights.z * ssbo_Bones[p_boneIDs.z]; - skinMat += p_boneWeights.w * ssbo_Bones[p_boneIDs.w]; + mat4 skinning = mat4(0.0); + float totalWeight = 0.0; - return skinMat; + for (int i = 0; i < 4; ++i) + { + float w = p_boneWeights[i]; + if (w > 0.0 && int(p_boneIDs[i]) < boneCount) + { + skinning += ssbo_Bones[p_boneIDs[i]] * w; + totalWeight += w; + } + } + + if (totalWeight <= 0.0) return mat4(1.0); + if (totalWeight == 1.0) return skinning; + + return totalWeight < 1.0 + ? skinning + mat4(1.0 - totalWeight) + : skinning / totalWeight; } diff --git a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp index 325cbcb0f..41170c5ee 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Parsers/AssimpParser.cpp @@ -293,23 +293,6 @@ namespace } } - for (auto& vertex : vertices) - { - float weightSum = 0.0f; - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - weightSum += vertex.boneWeights[i]; - } - - if (weightSum > 0.0f) - { - for (uint8_t i = 0; i < OvRendering::Animation::kMaxBonesPerVertex; ++i) - { - vertex.boneWeights[i] /= weightSum; - } - } - } - return new OvRendering::Resources::Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); } else From f3790a2d463c9d087e3177a6fcac0d933c838bcc Mon Sep 17 00:00:00 2001 From: Adrien GIVRY Date: Sun, 5 Apr 2026 18:43:24 -0400 Subject: [PATCH 18/18] Fixed skinning shader --- Resources/Engine/Shaders/Common/Skinning.ovfxh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Resources/Engine/Shaders/Common/Skinning.ovfxh b/Resources/Engine/Shaders/Common/Skinning.ovfxh index abfe605d2..364e04e9f 100644 --- a/Resources/Engine/Shaders/Common/Skinning.ovfxh +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -3,7 +3,6 @@ mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) { const int boneCount = ssbo_Bones.length(); - mat4 skinning = mat4(0.0); float totalWeight = 0.0; @@ -18,10 +17,7 @@ mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) } if (totalWeight <= 0.0) return mat4(1.0); - if (totalWeight == 1.0) return skinning; - return totalWeight < 1.0 - ? skinning + mat4(1.0 - totalWeight) - : skinning / totalWeight; + return skinning / totalWeight; }