diff --git a/Resources/Editor/Shaders/OutlineFallback.ovfx b/Resources/Editor/Shaders/OutlineFallback.ovfx index eb19c839..d0744418 100644 --- a/Resources/Editor/Shaders/OutlineFallback.ovfx +++ b/Resources/Editor/Shaders/OutlineFallback.ovfx @@ -1,7 +1,16 @@ +#pass OUTLINE_PASS +#engine_feature SKINNING + #shader vertex #version 450 core layout(location = 0) in vec3 geo_Pos; +#if defined(SKINNING) +layout(location = 5) in uvec4 geo_BoneIDs; +layout(location = 6) in vec4 geo_BoneWeights; +#endif + +#include ":Shaders/Common/Skinning.ovfxh" layout(std140) uniform EngineUBO { @@ -14,7 +23,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 4674e553..75584527 100644 --- a/Resources/Editor/Shaders/PickingFallback.ovfx +++ b/Resources/Editor/Shaders/PickingFallback.ovfx @@ -1,7 +1,14 @@ +#pass PICKING_PASS +#engine_feature SKINNING + #shader vertex #version 450 core layout (location = 0) in vec3 geo_Pos; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; +#endif layout (std140) uniform EngineUBO { @@ -12,9 +19,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 00000000..69fcc7ff --- /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 00000000..364e04e9 --- /dev/null +++ b/Resources/Engine/Shaders/Common/Skinning.ovfxh @@ -0,0 +1,23 @@ +#include ":Shaders/Common/Buffers/SkinningSSBO.ovfxh" + +mat4 ComputeSkinningMatrix(uvec4 p_boneIDs, vec4 p_boneWeights) +{ + const int boneCount = ssbo_Bones.length(); + mat4 skinning = mat4(0.0); + float totalWeight = 0.0; + + 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); + + return skinning / totalWeight; +} + diff --git a/Resources/Engine/Shaders/ShadowFallback.ovfx b/Resources/Engine/Shaders/ShadowFallback.ovfx index ff1f3dc0..9608a4a7 100644 --- a/Resources/Engine/Shaders/ShadowFallback.ovfx +++ b/Resources/Engine/Shaders/ShadowFallback.ovfx @@ -1,13 +1,26 @@ +#pass SHADOW_PASS +#engine_feature SKINNING + #shader vertex #version 450 core layout (location = 0) in vec3 geo_Pos; +#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" 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 5092a077..ee3f724e 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 +#engine_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,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; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; +#endif out VS_OUT { @@ -32,10 +38,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 40b8d7f8..adeef4cb 100644 --- a/Resources/Engine/Shaders/Unlit.ovfx +++ b/Resources/Engine/Shaders/Unlit.ovfx @@ -1,15 +1,21 @@ #pass SHADOW_PASS #feature ALPHA_CLIPPING +#engine_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; +#if defined(SKINNING) +layout (location = 5) in uvec4 geo_BoneIDs; +layout (location = 6) in vec4 geo_BoneWeights; +#endif out VS_OUT { @@ -19,7 +25,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 00000000..b009b815 --- /dev/null +++ b/Sources/OvCore/include/OvCore/ECS/Components/CSkinnedMeshRenderer.h @@ -0,0 +1,211 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#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 the component has a valid skinning palette ready for rendering + */ + bool HasSkinningData() 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. Pass std::nullopt to clear and return to T-pose. + * @param p_index + */ + bool SetAnimation(std::optional p_index); + + /** + * Sets the active animation by name + * @param p_name + */ + bool SetAnimation(const std::string& p_name); + + /** + * Returns the active animation index, or std::nullopt if none is set + */ + std::optional GetActiveAnimationIndex() const; + + /** + * Returns the active animation name (empty if none) + */ + std::optional GetActiveAnimationName() 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: + bool HasCompatibleModel() const; + void SyncWithModel(); + void RebuildRuntimeData(); + void EvaluatePose(); + float GetAnimationDurationSeconds() const; + void UpdatePlayback(float p_deltaTime); + + private: + const OvRendering::Resources::Model* m_model = nullptr; + + 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_localPose; + std::vector m_globalPose; + std::vector m_boneMatrices; + std::vector m_boneMatricesTransposed; + }; + + 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 00000000..d34c139a --- /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 00000000..f7464af1 --- /dev/null +++ b/Sources/OvCore/include/OvCore/Rendering/SkinningRenderFeature.h @@ -0,0 +1,86 @@ +/** +* @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 + }; + + 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; + + UploadedPaletteState m_lastUploaded; + mutable BoundPaletteState m_bound; + }; +} diff --git a/Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h b/Sources/OvCore/include/OvCore/Rendering/SkinningUtils.h new file mode 100644 index 00000000..b5976a4a --- /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 2ab0ae9b..7c009a49 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 0c33fe22..22c38c6f 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 @@ -26,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(); }; } @@ -80,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); 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 00000000..055a19d5 --- /dev/null +++ b/Sources/OvCore/src/OvCore/ECS/Components/CSkinnedMeshRenderer.cpp @@ -0,0 +1,643 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + 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 + ) + { + if (p_keys.empty()) + { + return p_defaultValue; + } + + if (p_keys.size() == 1) + { + return p_keys.front().value; + } + + if (!p_looping) + { + 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( + p_keys.begin(), + p_keys.end(), + p_time, + [](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) + { + return p_keys.back().value; + } + + const auto& prev = p_keys.back(); + const auto& next = p_keys.front(); + return interpolate(prev, next, (p_duration - prev.time) + next.time); + } + + if (nextIt == p_keys.begin()) + { + return nextIt->value; + } + + const auto& prev = *std::prev(nextIt); + const auto& next = *nextIt; + return interpolate(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::HasSkinningData() const +{ + return HasCompatibleModel() && m_animationIndex.has_value() && !m_boneMatrices.empty(); +} + +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.GetEffectiveTicksPerSecond(); + + 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.GetEffectiveTicksPerSecond(); + return m_currentTimeTicks / ticksPerSecond; +} + +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(std::optional p_index) +{ + 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; + } + + 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))); +} + +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetActiveAnimationIndex() const +{ + return m_animationIndex; +} + +std::optional OvCore::ECS::Components::CSkinnedMeshRenderer::GetActiveAnimationName() const +{ + if (!m_animationIndex.has_value()) + { + return std::nullopt; + } + + return GetAnimationName(m_animationIndex.value()); +} + +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 (!HasCompatibleModel() || !m_playing) + { + return; + } + + 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, "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", GetActiveAnimationName().value_or(std::string{})); +} + +void OvCore::ECS::Components::CSkinnedMeshRenderer::OnDeserialize(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode* p_node) +{ + 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, "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"); + const int currentAnimIndex = GetActiveAnimationIndex().has_value() ? static_cast(*GetActiveAnimationIndex()) : -1; + auto& animationChoice = p_root.CreateWidget(currentAnimIndex); + animationChoice.choices.emplace(-1, ""); + + for (size_t i = 0; i < m_animationNames.size(); ++i) + { + animationChoice.choices.emplace(static_cast(i), m_animationNames[i]); + } + + auto& animDispatcher = animationChoice.AddPlugin>(); + animDispatcher.RegisterGatherer([this] + { + return GetActiveAnimationIndex().has_value() ? static_cast(*GetActiveAnimationIndex()) : -1; + }); + animDispatcher.RegisterProvider([this](int p_choice) + { + SetAnimation(p_choice >= 0 ? std::make_optional(static_cast(p_choice)) : std::nullopt); + }); + + if (!HasCompatibleModel()) + { + p_root.CreateWidget("No skinned model assigned"); + } + else if (m_animationNames.empty()) + { + p_root.CreateWidget("Model has no animation clips"); + } +} + +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(); + 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_localPose.clear(); + m_globalPose.clear(); + m_boneMatrices.clear(); + m_boneMatricesTransposed.clear(); + m_animationIndex = std::nullopt; + m_currentTimeTicks = preservedTimeTicks; + m_poseEvaluationAccumulator = 0.0f; + + if (!HasCompatibleModel()) + { + return; + } + + const auto& skeleton = m_model->GetSkeleton().value(); + const auto& animations = m_model->GetAnimations(); + + 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(); + + 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; + + for (const auto& track : animation.tracks) + { + if (track.nodeIndex >= m_localPose.size()) + { + continue; + } + + const auto& node = skeleton.nodes[track.nodeIndex]; + + const OvMaths::FVector3 sampledPosition = SampleKeys( + track.positionKeys, + sampleTime, + duration, + 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); } + ); + + const OvMaths::FQuaternion sampledRotation = SampleKeys( + track.rotationKeys, + sampleTime, + duration, + 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); } + ); + + const OvMaths::FVector3 sampledScale = SampleKeys( + track.scaleKeys, + sampleTime, + duration, + 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); } + ); + + const OvMaths::FTransform sampled(sampledPosition, sampledRotation, sampledScale); + m_localPose[track.nodeIndex] = sampled.GetLocalMatrix(); + } + } + + 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]; + 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]); + } + + ++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.GetEffectiveTicksPerSecond(); + 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 f7266702..349cf475 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 a6ce073f..86c4d467 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,12 @@ void OvCore::Rendering::ShadowRenderPass::_DrawShadows( { if (auto material = materials.at(mesh->GetMaterialIndex()); material && material->IsValid() && material->IsShadowCaster()) { - const std::string shadowPassName = "SHADOW_PASS"; + // Skinning is only applied if the original material explicitly supports it. + const bool skinningEnabled = hasSkinning && material->SupportsFeature(kSkinningFeatureName); - // If the material has 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 = - material->HasPass(shadowPassName) ? + material->HasPass(kShadowPassName) ? *material : m_shadowMaterial; @@ -138,13 +151,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 (skinningEnabled && 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 00000000..42f1035d --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningRenderFeature.cpp @@ -0,0 +1,137 @@ +/** +* @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_lastUploaded = {}; + m_bound = {}; + + 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_bound = {}; +} + +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_lastUploaded.ptr != skinningDescriptor->matrices || + m_lastUploaded.count != skinningDescriptor->count || + m_lastUploaded.poseVersion != 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_lastUploaded = { skinningDescriptor->matrices, skinningDescriptor->count, skinningDescriptor->poseVersion }; + } + + const bool mustBindSkinningPalette = + 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_bound = { EBoundPalette::SKINNING, skinningDescriptor->matrices, skinningDescriptor->count, skinningDescriptor->poseVersion }; + } +} + +OvRendering::HAL::ShaderStorageBuffer& OvCore::Rendering::SkinningRenderFeature::GetCurrentSkinningBuffer() const +{ + return *m_skinningBuffers[m_skinningBufferIndex]; +} + +void OvCore::Rendering::SkinningRenderFeature::BindIdentityPalette() const +{ + if (m_bound.type != EBoundPalette::IDENTITY) + { + m_identityBuffer->Bind(m_bufferBindingPoint); + m_bound = { EBoundPalette::IDENTITY }; + } +} diff --git a/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp new file mode 100644 index 00000000..05d7802b --- /dev/null +++ b/Sources/OvCore/src/OvCore/Rendering/SkinningUtils.cpp @@ -0,0 +1,49 @@ +/** +* @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->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(); + + p_drawable.SetDescriptor({ + .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/ResourceManagement/ModelManager.cpp b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp index 00d9b710..d1017c1e 100644 --- a/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp +++ b/Sources/OvCore/src/OvCore/ResourceManagement/ModelManager.cpp @@ -22,8 +22,8 @@ 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("LIMIT_BONE_WEIGHTS", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::LIMIT_BONE_WEIGHTS; + if (metaFile.GetOrDefault("PRE_TRANSFORM_VERTICES", false)) flags |= OvRendering::Resources::Parsers::EModelParserFlags::PRE_TRANSFORM_VERTICES; + 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/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp b/Sources/OvCore/src/OvCore/Scripting/Lua/Bindings/LuaActorBindings.cpp index 8a9ed57b..5b32f8b3 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 6a84dbb3..27f0bdd9 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,10 +88,32 @@ void BindLuaComponents(sol::state& p_luaState) "GetUserMatrixElement", &CMaterialRenderer::GetUserMatrixElement ); + p_luaState.new_usertype("SkinnedMeshRenderer", + sol::base_classes, sol::bases(), + "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) + ), + "GetActiveAnimationIndex", &CSkinnedMeshRenderer::GetActiveAnimationIndex, + "GetActiveAnimationName", &CSkinnedMeshRenderer::GetActiveAnimationName + ); + 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(), diff --git a/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h b/Sources/OvEditor/include/OvEditor/Rendering/OutlineRenderFeature.h index 95e4bae6..1be264c2 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 eda6b74a..28dddcec 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp @@ -49,10 +49,29 @@ namespace ); } - auto CreateShader(const std::filesystem::path& p_path) + auto CreateShader(const std::filesystem::path& p_path, const std::filesystem::path& p_editorAssetsPath) { + const auto engineAssetsPath = p_editorAssetsPath.parent_path() / "Engine"; + return OvRendering::Resources::Loaders::ShaderLoader::Create( - p_path.string() + p_path.string(), + [p_editorAssetsPath, engineAssetsPath](const std::string& p_includePath) + { + const auto normalizedPath = OvTools::Utils::PathParser::MakeNonWindowsStyle(p_includePath); + const auto includePath = std::filesystem::path{ normalizedPath }; + + if (includePath.is_absolute()) + { + return includePath.lexically_normal().string(); + } + + if (!normalizedPath.empty() && normalizedPath.front() == ':') + { + return (engineAssetsPath / normalizedPath.substr(1)).lexically_normal().string(); + } + + return (p_editorAssetsPath / includePath).lexically_normal().string(); + } ); } @@ -128,11 +147,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/AssetProperties.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp index cebd2444..8bc1fe79 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetProperties.cpp @@ -219,8 +219,8 @@ 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("LIMIT_BONE_WEIGHTS", false); + m_metadata->Add("PRE_TRANSFORM_VERTICES", 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/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index efc74dd3..16c60db6 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 a1b6cac0..9b0985e1 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,54 @@ 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, + OvCore::Resources::Material& 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, + bool p_skinningEnabled, + OvCore::Resources::Material& p_targetMaterial + ) + { + if (p_skinningEnabled) + { + OvCore::Rendering::SkinningUtils::ApplyToDrawable( + p_drawable, + *p_skinnedRenderer, + &p_targetMaterial.GetFeatures() + ); + } + } } OvEditor::Rendering::OutlineRenderFeature::OutlineRenderFeature( @@ -84,11 +134,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 +187,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 +232,25 @@ 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; - }; + const auto* originalMaterial = FindMeshMaterial(p_materials, mesh->GetMaterialIndex()); + const bool skinningEnabled = hasSkinning && originalMaterial && + originalMaterial->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); - auto& targetMaterial = getStencilMaterial(); + auto& targetMaterial = ResolveOutlineMaterial( + mesh->GetMaterialIndex(), + outlinePassName, + p_materials, + m_stencilFillMaterial + ); auto stateMask = targetMaterial.GenerateStateMask(); @@ -212,6 +268,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelToStencil( element.pass = outlinePassName; element.AddDescriptor(engineDrawableDescriptor); + ApplySkinningIfNeeded(element, p_skinnedRenderer, skinningEnabled, targetMaterial); m_renderer.DrawEntity(p_pso, element); } @@ -222,23 +279,25 @@ 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; - }; + const auto* originalMaterial = FindMeshMaterial(p_materials, mesh->GetMaterialIndex()); + const bool skinningEnabled = hasSkinning && originalMaterial && + originalMaterial->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); - auto& targetMaterial = getStencilMaterial(); + auto& targetMaterial = ResolveOutlineMaterial( + mesh->GetMaterialIndex(), + outlinePassName, + p_materials, + m_outlineMaterial + ); // Set the outline color property if it exists if (targetMaterial.GetProperty("_OutlineColor")) @@ -261,6 +320,7 @@ void OvEditor::Rendering::OutlineRenderFeature::DrawModelOutline( drawable.pass = outlinePassName; drawable.AddDescriptor(engineDrawableDescriptor); + 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 ad22db4b..730f9431 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,46 @@ 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); + const bool skinningEnabled = hasSkinning && drawable.material && + drawable.material->SupportsFeature(std::string{ OvCore::Rendering::SkinningUtils::kFeatureName }); + + if (skinningEnabled) + { + 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.material = &targetMaterial; + finalDrawable.stateMask = targetMaterial.GenerateStateMask(); finalDrawable.stateMask.frontfaceCulling = false; finalDrawable.stateMask.backfaceCulling = false; - finalDrawable.pass = pickingPassName; + 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 00000000..bb852e4a --- /dev/null +++ b/Sources/OvRendering/include/OvRendering/Animation/SkeletalData.h @@ -0,0 +1,125 @@ +/** +* @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; + + // 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 + { + 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; + + 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; + } + + 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()) + { + return &tracks.at(found->second); + } + + return nullptr; + } + }; +} diff --git a/Sources/OvRendering/include/OvRendering/Data/Describable.h b/Sources/OvRendering/include/OvRendering/Data/Describable.h index a6af5f34..605c57e1 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 059b6318..e576ba1c 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/Geometry/Vertex.h b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h index b992fd7a..b1453e5f 100644 --- a/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h +++ b/Sources/OvRendering/include/OvRendering/Geometry/Vertex.h @@ -6,6 +6,10 @@ #pragma once +#include + +#include + namespace OvRendering::Geometry { /** @@ -19,4 +23,13 @@ namespace OvRendering::Geometry float tangent[3]; float bitangent[3]; }; -} \ No newline at end of file + + /** + * 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 2b18cad3..370745ea 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 @@ -35,6 +35,18 @@ namespace OvRendering::Resources 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 + ); + /** * Bind the mesh (Actually bind its VAO) */ @@ -65,14 +77,20 @@ 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); + void Upload(std::span p_vertices, std::span p_indices); private: 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 +98,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 01d7e049..057d45ac 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 67413b83..7e5714cd 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/AssimpParser.h @@ -4,14 +4,12 @@ * @licence: MIT */ -#include - +#pragma once -#include "OvRendering/Geometry/Vertex.h" -#include "OvRendering/Resources/Mesh.h" -#include "OvRendering/Resources/Parsers/IModelParser.h" +#include -#pragma once +#include +#include namespace OvRendering::Resources::Parsers { @@ -33,12 +31,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 ) 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); }; -} \ No newline at end of file +} diff --git a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h index ee079c86..62a617c6 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/EModelParserFlags.h @@ -6,52 +6,54 @@ #pragma once +#include + 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 a7795d32..09094e1a 100644 --- a/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h +++ b/Sources/OvRendering/include/OvRendering/Resources/Parsers/IModelParser.h @@ -6,10 +6,12 @@ #pragma once +#include #include -#include "OvRendering/Resources/Mesh.h" -#include "OvRendering/Resources/Parsers/EModelParserFlags.h" +#include +#include +#include namespace OvRendering::Resources::Parsers { @@ -20,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 @@ -31,7 +36,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/include/OvRendering/Resources/Shader.h b/Sources/OvRendering/include/OvRendering/Resources/Shader.h index 4e753d65..755ebdeb 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 a0737cfb..e9179c79 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/HAL/OpenGL/GLVertexArray.cpp b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp index a2c70a42..a3c0bee4 100644 --- a/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp +++ b/Sources/OvRendering/src/OvRendering/HAL/OpenGL/GLVertexArray.cpp @@ -95,14 +95,31 @@ 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) - ); + const bool isIntegerType = + attribute.type != Settings::EDataType::FLOAT && + attribute.type != Settings::EDataType::DOUBLE; + + if (isIntegerType && !attribute.normalized) + { + 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/Loaders/ModelLoader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ModelLoader.cpp index d6ffaab7..0ee0c84c 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/Loaders/ShaderLoader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Loaders/ShaderLoader.cpp index 199b795c..0c678f06 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/Mesh.cpp b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp index 6e45ddc8..7b07de86 100644 --- a/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp +++ b/Sources/OvRendering/src/OvRendering/Resources/Mesh.cpp @@ -7,10 +7,55 @@ #include #include #include +#include +#include #include #include +namespace +{ + template + OvRendering::Geometry::BoundingSphere ComputeBoundingSphere(std::span p_vertices) + { + OvRendering::Geometry::BoundingSphere sphere{}; + sphere.position = OvMaths::FVector3::Zero; + sphere.radius = 0.0f; + + 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 sphere; + } +} + OvRendering::Resources::Mesh::Mesh( std::span p_vertices, std::span p_indices, @@ -18,10 +63,25 @@ OvRendering::Resources::Mesh::Mesh( ) : m_vertexCount(static_cast(p_vertices.size())), m_indicesCount(static_cast(p_indices.size())), - m_materialIndex(p_materialIndex) + 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(true) { Upload(p_vertices, p_indices); - ComputeBoundingSphere(p_vertices); + m_boundingSphere = ComputeBoundingSphere(p_vertices); } void OvRendering::Resources::Mesh::Bind() const @@ -54,8 +114,21 @@ 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) { + const auto layout = 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 + }); + if (m_vertexBuffer.Allocate(p_vertices.size_bytes())) { m_vertexBuffer.Upload(p_vertices.data()); @@ -63,14 +136,7 @@ void OvRendering::Resources::Mesh::Upload(std::span p_ve 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 { @@ -83,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 }, // 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 (!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(); - - float maxX = std::numeric_limits::min(); - float maxY = std::numeric_limits::min(); - float maxZ = std::numeric_limits::min(); + m_vertexBuffer.Upload(p_vertices.data()); - 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/Model.cpp b/Sources/OvRendering/src/OvRendering/Resources/Model.cpp index cc827fd0..cf72d72c 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 071e05af..41170c5e 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 @@ -32,104 +37,349 @@ namespace return p_flags; } -} -bool OvRendering::Resources::Parsers::AssimpParser::LoadModel(const std::string & p_fileName, std::vector& p_meshes, std::vector& p_materials, EModelParserFlags p_parserFlags) -{ - Assimp::Importer import; + 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 + }; + } - // 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); + bool AddBoneData(OvRendering::Geometry::SkinnedVertex& p_vertex, uint32_t p_boneIndex, float p_weight) + { + if (!std::isfinite(p_weight) || p_weight <= 0.0f) + { + return false; + } - const aiScene* scene = import.ReadFile(p_fileName, static_cast(p_parserFlags)); + // 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) + { + p_vertex.boneIDs[i] = p_boneIndex; + p_vertex.boneWeights[i] = p_weight; + return true; + } + } - if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) return false; + } - ProcessMaterials(scene, p_materials); + void AddSkeletonNodeRecursive(OvRendering::Animation::Skeleton& p_skeleton, aiNode* p_node, int32_t p_parentIndex) + { + const auto currentIndex = static_cast(p_skeleton.nodes.size()); - aiMatrix4x4 identity; + aiVector3D aiPosition, aiScale; + aiQuaternion aiRotation; + p_node->mTransformation.Decompose(aiScale, aiRotation, aiPosition); - ProcessNode(&identity, scene->mRootNode, scene, p_meshes); + 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 } + }); - return true; -} + p_skeleton.nodeByName.emplace(p_node->mName.C_Str(), currentIndex); -void OvRendering::Resources::Parsers::AssimpParser::ProcessMaterials(const aiScene * p_scene, std::vector& p_materials) -{ - for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i) + for (uint32_t i = 0; i < p_node->mNumChildren; ++i) + { + AddSkeletonNodeRecursive(p_skeleton, p_node->mChildren[i], static_cast(currentIndex)); + } + } + + void ProcessMaterials(const aiScene* p_scene, std::vector& p_materials) { - aiMaterial* material = p_scene->mMaterials[i]; - if (material) + for (uint32_t i = 0; i < p_scene->mNumMaterials; ++i) { - aiString name; - aiGetMaterialString(material, AI_MATKEY_NAME, &name); - p_materials.push_back(name.C_Str()); + aiMaterial* material = p_scene->mMaterials[i]; + if (material) + { + aiString name; + aiGetMaterialString(material, AI_MATKEY_NAME, &name); + p_materials.push_back(name.C_Str()); + } } } -} -void OvRendering::Resources::Parsers::AssimpParser::ProcessNode(void* p_transform, aiNode * p_node, const aiScene * p_scene, std::vector& p_meshes) -{ - aiMatrix4x4 nodeTransformation = *reinterpret_cast(p_transform) * p_node->mTransformation; + void ProcessAnimations( + const aiScene* p_scene, + const OvRendering::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]; + + 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 + }; + + 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)) + { + 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 } + }); + } - // Process all the node's meshes (if any) - for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) + 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)); + } + } + + OvRendering::Resources::Mesh* ProcessMesh( + const aiMatrix4x4& p_transform, + aiMesh* p_mesh, + OvRendering::Animation::Skeleton* p_skeleton + ) { - std::vector vertices; std::vector indices; - 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 + 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; + } + + 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; + } + + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); + } + + if (p_mesh->mNumVertices == 0 || indices.empty()) + { + return nullptr; + } + + auto fillGeometry = [&](OvRendering::Geometry::Vertex& v, uint32_t i) + { + 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 (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 + }); + } + + 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()) + { + AddBoneData(vertices[weight.mVertexId], boneIndex, weight.mWeight); + } + } + } + + 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); + } + + return new OvRendering::Resources::Mesh(std::span(vertices), std::span(indices), p_mesh->mMaterialIndex); + } } - // Then do the same for each of its children - for (uint32_t i = 0; i < p_node->mNumChildren; ++i) + void ProcessNode( + const aiMatrix4x4& p_transform, + aiNode* p_node, + const aiScene* p_scene, + std::vector& p_meshes, + OvRendering::Animation::Skeleton* p_skeleton + ) { - ProcessNode(&nodeTransformation, p_node->mChildren[i], p_scene, p_meshes); + const aiMatrix4x4 nodeTransform = p_transform * p_node->mTransformation; + + for (uint32_t i = 0; i < p_node->mNumMeshes; ++i) + { + 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 + } + } + + for (uint32_t i = 0; i < p_node->mNumChildren; ++i) + { + ProcessNode(nodeTransform, 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) +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); + 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)); - for (uint32_t i = 0; i < p_mesh->mNumVertices; ++i) + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { - 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)); - - 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 - }); + return false; } - for (uint32_t faceID = 0; faceID < p_mesh->mNumFaces; ++faceID) + ProcessMaterials(scene, p_materials); + + bool hasBones = false; + for (uint32_t i = 0; i < scene->mNumMeshes && !hasBones; ++i) { - auto& face = p_mesh->mFaces[faceID]; + hasBones = scene->mMeshes[i] && scene->mMeshes[i]->HasBones(); + } - for (size_t indexID = 0; indexID < 3; ++indexID) - p_outIndices.push_back(face.mIndices[indexID]); + p_skeleton.reset(); + p_animations.clear(); + + if (hasBones) + { + p_skeleton.emplace(); + AddSkeletonNodeRecursive(p_skeleton.value(), scene->mRootNode, -1); + ProcessAnimations(scene, p_skeleton.value(), p_animations); } + + ProcessNode(aiMatrix4x4{}, scene->mRootNode, scene, p_meshes, p_skeleton ? &p_skeleton.value() : nullptr); + + if (p_skeleton && p_skeleton->bones.empty()) + { + p_skeleton.reset(); + p_animations.clear(); + } + + return true; } + diff --git a/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp b/Sources/OvRendering/src/OvRendering/Resources/Shader.cpp index a90aaa38..932e46fc 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); + } + } } } }