diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.cpp index 24134b2d17..e06b19eb92 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -138,6 +139,35 @@ namespace AZ::Render } } } + + // Hit detection colliders + if (CheckBitsAny(renderFlags, EMotionFX::ActorRenderFlags::HitDetectionColliders)) + { + RenderColliders(debugDisplay, EMotionFX::PhysicsSetup::HitDetection, instance, + renderActorSettings.m_hitDetectionColliderColor, renderActorSettings.m_selectedHitDetectionColliderColor); + } + + // Cloth colliders + if (CheckBitsAny(renderFlags, EMotionFX::ActorRenderFlags::ClothColliders)) + { + RenderColliders(debugDisplay, EMotionFX::PhysicsSetup::Cloth, instance, + renderActorSettings.m_clothColliderColor, renderActorSettings.m_selectedClothColliderColor); + } + + // Simulated object colliders + if (CheckBitsAny(renderFlags, EMotionFX::ActorRenderFlags::SimulatedObjectColliders)) + { + RenderColliders(debugDisplay, EMotionFX::PhysicsSetup::SimulatedObjectCollider, instance, + renderActorSettings.m_simulatedObjectColliderColor, renderActorSettings.m_selectedSimulatedObjectColliderColor); + } + + // Ragdoll + const bool renderRagdollColliders = AZ::RHI::CheckBitsAny(renderFlags, EMotionFX::ActorRenderFlags::RagdollColliders); + const bool renderRagdollJointLimits = AZ::RHI::CheckBitsAny(renderFlags, EMotionFX::ActorRenderFlags::RagdollJointLimits); + if (renderRagdollColliders || renderRagdollJointLimits) + { + RenderRagdoll(debugDisplay, instance, renderRagdollColliders, renderRagdollJointLimits); + } } float AtomActorDebugDraw::CalculateScaleMultiplier(EMotionFX::ActorInstance* instance) const @@ -813,4 +843,270 @@ namespace AZ::Render } } } + + void AtomActorDebugDraw::RenderColliders(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::ShapeColliderPairList& colliders, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const AZ::Color& colliderColor) const + { + if (!debugDisplay) + { + return; + } + + const size_t nodeIndex = node->GetNodeIndex(); + + for (const auto& collider : colliders) + { +#ifndef EMFX_SCALE_DISABLED + const AZ::Vector3& worldScale = actorInstance->GetTransformData()->GetCurrentPose()->GetModelSpaceTransform(nodeIndex).m_scale; +#else + const AZ::Vector3 worldScale = AZ::Vector3::CreateOne(); +#endif + + const EMotionFX::Transform colliderOffsetTransform(collider.first->m_position, collider.first->m_rotation); + const EMotionFX::Transform& actorInstanceGlobalTransform = actorInstance->GetWorldSpaceTransform(); + const EMotionFX::Transform& emfxNodeGlobalTransform = + actorInstance->GetTransformData()->GetCurrentPose()->GetModelSpaceTransform(nodeIndex); + const EMotionFX::Transform emfxColliderGlobalTransformNoScale = + colliderOffsetTransform * emfxNodeGlobalTransform * actorInstanceGlobalTransform; + + const AZ::TypeId colliderType = collider.second->RTTI_GetType(); + if (colliderType == azrtti_typeid()) + { + Physics::SphereShapeConfiguration* sphere = static_cast(collider.second.get()); + + // O3DE Physics scaling rules: The maximum component from the node scale will be multiplied by the radius of the sphere. + const float radius = sphere->m_radius * + MCore::Max3(static_cast(worldScale.GetX()), static_cast(worldScale.GetY()), + static_cast(worldScale.GetZ())); + + debugDisplay->DepthTestOff(); + debugDisplay->SetColor(colliderColor); + debugDisplay->DrawWireSphere(emfxColliderGlobalTransformNoScale.m_position, radius); + } + else if (colliderType == azrtti_typeid()) + { + Physics::CapsuleShapeConfiguration* capsule = static_cast(collider.second.get()); + + // O3DE Physics scaling rules: The maximum of the X/Y scale components of the node scale will be multiplied by the radius of + // the capsule. The Z component of the entity scale will be multiplied by the height of the capsule. + const float radius = + capsule->m_radius * MCore::Max(static_cast(worldScale.GetX()), static_cast(worldScale.GetY())); + const float height = capsule->m_height * static_cast(worldScale.GetZ()); + + debugDisplay->DepthTestOff(); + debugDisplay->SetColor(colliderColor); + debugDisplay->DrawWireCapsule( + emfxColliderGlobalTransformNoScale.m_position, emfxColliderGlobalTransformNoScale.ToAZTransform().GetBasisZ(), radius, height); + } + else if (colliderType == azrtti_typeid()) + { + Physics::BoxShapeConfiguration* box = static_cast(collider.second.get()); + + // O3DE Physics scaling rules: Each component of the box dimensions will be scaled by the node's world scale. + AZ::Vector3 dimensions = box->m_dimensions; + dimensions *= worldScale; + + debugDisplay->DepthTestOff(); + debugDisplay->SetColor(colliderColor); + debugDisplay->DrawWireBox( + emfxColliderGlobalTransformNoScale.m_position, emfxColliderGlobalTransformNoScale.m_position + dimensions); + } + } + } + + void AtomActorDebugDraw::RenderColliders(AzFramework::DebugDisplayRequests* debugDisplay, + EMotionFX::PhysicsSetup::ColliderConfigType colliderConfigType, + EMotionFX::ActorInstance* actorInstance, + const AZ::Color& defaultColor, + const AZ::Color& selectedColor) const + { + if (colliderConfigType == EMotionFX::PhysicsSetup::Unknown) + { + return; + } + + const EMotionFX::Actor* actor = actorInstance->GetActor(); + const AZStd::shared_ptr& physicsSetup = actor->GetPhysicsSetup(); + const Physics::CharacterColliderConfiguration* colliderConfig = physicsSetup->GetColliderConfigByType(colliderConfigType); + + if (colliderConfig) + { + const AZStd::unordered_set* cachedSelectedJointIndices; + EMotionFX::JointSelectionRequestBus::BroadcastResult( + cachedSelectedJointIndices, &EMotionFX::JointSelectionRequests::FindSelectedJointIndices, actorInstance); + + for (const Physics::CharacterColliderNodeConfiguration& nodeConfig : colliderConfig->m_nodes) + { + const EMotionFX::Node* joint = actor->GetSkeleton()->FindNodeByName(nodeConfig.m_name.c_str()); + if (joint) + { + const bool jointSelected = cachedSelectedJointIndices && + (cachedSelectedJointIndices->empty() || cachedSelectedJointIndices->find(joint->GetNodeIndex()) != cachedSelectedJointIndices->end()); + + const AzPhysics::ShapeColliderPairList& colliders = nodeConfig.m_shapes; + RenderColliders(debugDisplay, colliders, actorInstance, joint, jointSelected ? selectedColor : defaultColor); + } + } + } + } + + void AtomActorDebugDraw::RenderJointFrame(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::JointConfiguration& configuration, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const AZ::Color& color) const + { + const EMotionFX::Transform& actorInstanceWorldSpaceTransform = actorInstance->GetWorldSpaceTransform(); + const EMotionFX::Pose* currentPose = actorInstance->GetTransformData()->GetCurrentPose(); + const EMotionFX::Transform childJointLocalSpaceTransform(AZ::Vector3::CreateZero(), configuration.m_childLocalRotation); + const EMotionFX::Transform childModelSpaceTransform = + childJointLocalSpaceTransform * currentPose->GetModelSpaceTransform(node->GetNodeIndex()); + const EMotionFX::Transform jointChildWorldSpaceTransformNoScale = (childModelSpaceTransform * actorInstanceWorldSpaceTransform); + AZ::Vector3 dir = jointChildWorldSpaceTransformNoScale.ToAZTransform().GetBasisX(); + + debugDisplay->SetColor(color); + debugDisplay->DrawArrow(jointChildWorldSpaceTransformNoScale.m_position, jointChildWorldSpaceTransformNoScale.m_position + dir, 0.1f); + } + + void AtomActorDebugDraw::JointLimitRenderData::Clear() + { + m_vertexBuffer.clear(); + m_indexBuffer.clear(); + m_lineBuffer.clear(); + m_lineValidityBuffer.clear(); + } + + void AtomActorDebugDraw::RenderJointLimit(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::JointConfiguration& configuration, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const EMotionFX::Node* parentNode, + const AZ::Color& regularColor, + const AZ::Color& violatedColor) + { + const size_t nodeIndex = node->GetNodeIndex(); + const size_t parentNodeIndex = parentNode->GetNodeIndex(); + const EMotionFX::Transform& actorInstanceWorldTransform = actorInstance->GetWorldSpaceTransform(); + const EMotionFX::Pose* currentPose = actorInstance->GetTransformData()->GetCurrentPose(); + const AZ::Quaternion& parentOrientation = currentPose->GetModelSpaceTransform(parentNodeIndex).m_rotation; + const AZ::Quaternion& childOrientation = currentPose->GetModelSpaceTransform(nodeIndex).m_rotation; + + m_jointLimitRenderData.Clear(); + if (auto* jointHelpers = AZ::Interface::Get()) + { + jointHelpers->GenerateJointLimitVisualizationData( + configuration, parentOrientation, childOrientation, s_scale, s_angularSubdivisions, s_radialSubdivisions, + m_jointLimitRenderData.m_vertexBuffer, m_jointLimitRenderData.m_indexBuffer, + m_jointLimitRenderData.m_lineBuffer, m_jointLimitRenderData.m_lineValidityBuffer); + } + + EMotionFX::Transform jointModelSpaceTransform = currentPose->GetModelSpaceTransform(parentNodeIndex); + jointModelSpaceTransform.m_position = currentPose->GetModelSpaceTransform(nodeIndex).m_position; + const EMotionFX::Transform jointGlobalTransformNoScale = jointModelSpaceTransform * actorInstanceWorldTransform; + + const size_t numLineBufferEntries = m_jointLimitRenderData.m_lineBuffer.size(); + if (m_jointLimitRenderData.m_lineValidityBuffer.size() * 2 != numLineBufferEntries) + { + AZ_ErrorOnce("EMotionFX", false, "Unexpected buffer size in joint limit visualization for node %s", node->GetName()); + return; + } + + for (size_t i = 0; i < numLineBufferEntries; i += 2) + { + const AZ::Color& lineColor = m_jointLimitRenderData.m_lineValidityBuffer[i / 2] ? regularColor : violatedColor; + debugDisplay->DepthTestOff(); + debugDisplay->DrawLine( + jointGlobalTransformNoScale.TransformPoint(m_jointLimitRenderData.m_lineBuffer[i]), + jointGlobalTransformNoScale.TransformPoint(m_jointLimitRenderData.m_lineBuffer[i + 1]), lineColor.GetAsVector4(), lineColor.GetAsVector4() + ); + } + } + + void AtomActorDebugDraw::RenderRagdoll(AzFramework::DebugDisplayRequests* debugDisplay, + EMotionFX::ActorInstance* actorInstance, + bool renderColliders, + bool renderJointLimits) + { + const EMotionFX::Actor* actor = actorInstance->GetActor(); + const EMotionFX::Skeleton* skeleton = actor->GetSkeleton(); + const size_t numNodes = skeleton->GetNumNodes(); + const AZStd::shared_ptr& physicsSetup = actor->GetPhysicsSetup(); + const Physics::RagdollConfiguration& ragdollConfig = physicsSetup->GetRagdollConfig(); + const AZStd::vector& ragdollNodes = ragdollConfig.m_nodes; + const Physics::CharacterColliderConfiguration& colliderConfig = ragdollConfig.m_colliders; + const EMotionFX::RagdollInstance* ragdollInstance = actorInstance->GetRagdollInstance(); + + const AZ::Render::RenderActorSettings& settings = EMotionFX::GetRenderActorSettings(); + const AZ::Color& violatedColor = settings.m_violatedJointLimitColor; + const AZ::Color& defaultColor = settings.m_ragdollColliderColor; + const AZ::Color& selectedColor = settings.m_selectedRagdollColliderColor; + + const AZStd::unordered_set* cachedSelectedJointIndices; + EMotionFX::JointSelectionRequestBus::BroadcastResult( + cachedSelectedJointIndices, &EMotionFX::JointSelectionRequests::FindSelectedJointIndices, actorInstance); + + for (size_t nodeIndex = 0; nodeIndex < numNodes; ++nodeIndex) + { + const EMotionFX::Node* joint = skeleton->GetNode(nodeIndex); + const size_t jointIndex = joint->GetNodeIndex(); + + AZ::Outcome ragdollNodeIndex = AZ::Failure(); + if (ragdollInstance) + { + ragdollNodeIndex = ragdollInstance->GetRagdollNodeIndex(jointIndex); + } + else + { + ragdollNodeIndex = ragdollConfig.FindNodeConfigIndexByName(joint->GetNameString()); + } + + if (!ragdollNodeIndex.IsSuccess()) + { + continue; + } + + const bool jointSelected = cachedSelectedJointIndices && + (cachedSelectedJointIndices->empty() || cachedSelectedJointIndices->find(joint->GetNodeIndex()) != cachedSelectedJointIndices->end()); + + AZ::Color finalColor; + if (jointSelected) + { + finalColor = selectedColor; + } + else + { + finalColor = defaultColor; + } + + const Physics::RagdollNodeConfiguration& ragdollNode = ragdollNodes[ragdollNodeIndex.GetValue()]; + + if (renderColliders) + { + const Physics::CharacterColliderNodeConfiguration* colliderNodeConfig = + colliderConfig.FindNodeConfigByName(joint->GetNameString()); + if (colliderNodeConfig) + { + const AzPhysics::ShapeColliderPairList& colliders = colliderNodeConfig->m_shapes; + RenderColliders(debugDisplay, colliders, actorInstance, joint, finalColor); + } + } + + if (renderJointLimits && jointSelected) + { + const AZStd::shared_ptr& jointLimitConfig = ragdollNode.m_jointConfig; + if (jointLimitConfig) + { + const EMotionFX::Node* ragdollParentNode = physicsSetup->FindRagdollParentNode(joint); + if (ragdollParentNode) + { + RenderJointLimit(debugDisplay, *jointLimitConfig, actorInstance, joint, ragdollParentNode, finalColor, violatedColor); + RenderJointFrame(debugDisplay, *jointLimitConfig, actorInstance, joint, finalColor); + } + } + } + } + } } // namespace AZ::Render diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.h index 53086801c7..6b777db8e8 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorDebugDraw.h @@ -43,7 +43,6 @@ namespace AZ::Render void DebugDraw(const EMotionFX::ActorRenderFlags& renderFlags, EMotionFX::ActorInstance* instance); private: - float CalculateBoneScale(EMotionFX::ActorInstance* actorInstance, EMotionFX::Node* node); float CalculateScaleMultiplier(EMotionFX::ActorInstance* instance) const; void PrepareForMesh(EMotionFX::Mesh* mesh, const AZ::Transform& worldTM); @@ -83,6 +82,37 @@ namespace AZ::Render bool selected, //!< Set to true if you want to render the axis using the selection color. */ bool renderAxisName = false); + void RenderColliders(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::ShapeColliderPairList& colliders, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const AZ::Color& colliderColor) const; + + void RenderColliders(AzFramework::DebugDisplayRequests* debugDisplay, + EMotionFX::PhysicsSetup::ColliderConfigType colliderConfigType, + EMotionFX::ActorInstance* actorInstance, + const AZ::Color& defaultColor, + const AZ::Color& selectedColor) const; + + void RenderJointFrame(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::JointConfiguration& configuration, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const AZ::Color& color) const; + + // Ragdoll + void RenderJointLimit(AzFramework::DebugDisplayRequests* debugDisplay, + const AzPhysics::JointConfiguration& configuration, + const EMotionFX::ActorInstance* actorInstance, + const EMotionFX::Node* node, + const EMotionFX::Node* parentNode, + const AZ::Color& regularColor, + const AZ::Color& violatedColor); + void RenderRagdoll(AzFramework::DebugDisplayRequests* debugDisplay, + EMotionFX::ActorInstance* actorInstance, + bool renderColliders, + bool renderJointLimits); + EMotionFX::Mesh* m_currentMesh = nullptr; //!< A pointer to the mesh whose world space positions are in the pre-calculated positions buffer. //!< NULL in case we haven't pre-calculated any positions yet. AZStd::vector m_worldSpacePositions; //!< The buffer used to store world space positions for rendering normals @@ -97,6 +127,22 @@ namespace AZ::Render AZStd::vector m_auxColors; EntityId m_entityId; + // Joint limits + static constexpr float s_scale = 0.1f; + static constexpr AZ::u32 s_angularSubdivisions = 32; + static constexpr AZ::u32 s_radialSubdivisions = 2; + + struct JointLimitRenderData + { + AZStd::vector m_vertexBuffer; + AZStd::vector m_indexBuffer; + AZStd::vector m_lineBuffer; + AZStd::vector m_lineValidityBuffer; + + void Clear(); + }; + JointLimitRenderData m_jointLimitRenderData; + AzFramework::TextDrawParameters m_drawParams; AzFramework::FontDrawInterface* m_fontDrawInterface = nullptr; };