Merge pull request #5645 from aws-lumberyard-dev/avoid-bad-mass-ratios

Avoid bad mass ratios
monroegm-disable-blank-issue-2
greerdv 4 years ago committed by GitHub
commit 823b5bde0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -344,6 +344,55 @@ namespace PhysX
bool isAcceleration = true; bool isAcceleration = true;
return physx::PxD6JointDrive(stiffness, damping, forceLimit, isAcceleration); return physx::PxD6JointDrive(stiffness, damping, forceLimit, isAcceleration);
} }
AZStd::vector<DepthData> ComputeHierarchyDepths(const AZStd::vector<size_t>& parentIndices)
{
const size_t numNodes = parentIndices.size();
AZStd::vector<DepthData> nodeDepths(numNodes);
for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++)
{
nodeDepths[nodeIndex] = { -1, nodeIndex };
}
for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++)
{
if (nodeDepths[nodeIndex].m_depth != -1)
{
continue;
}
int depth = -1; // initial depth value for this node
int ancestorDepth = 0; // the depth of the first ancestor we find when iteratively visiting parents
bool ancestorFound = false; // whether we have found either an ancestor which already has a depth value, or the root
size_t currentIndex = nodeIndex;
while (!ancestorFound)
{
depth++;
if (depth > numNodes)
{
AZ_Error("PhysX Ragdoll", false, "Loop detected in hierarchy depth computation.");
return nodeDepths;
}
const size_t parentIndex = parentIndices[currentIndex];
if (parentIndex >= numNodes || nodeDepths[currentIndex].m_depth != -1)
{
ancestorFound = true;
ancestorDepth = (nodeDepths[currentIndex].m_depth != -1) ? nodeDepths[currentIndex].m_depth : 0;
}
currentIndex = parentIndex;
}
currentIndex = nodeIndex;
for (int i = depth; i >= 0; i--)
{
nodeDepths[currentIndex] = { ancestorDepth + i, currentIndex };
currentIndex = parentIndices[currentIndex];
}
}
return nodeDepths;
}
} // namespace Characters } // namespace Characters
} // namespace Utils } // namespace Utils
} // namespace PhysX } // namespace PhysX

@ -49,6 +49,18 @@ namespace PhysX
//! @param forceLimit The upper limit on the force the joint can apply to reach its target. //! @param forceLimit The upper limit on the force the joint can apply to reach its target.
//! @return The created joint drive. //! @return The created joint drive.
physx::PxD6JointDrive CreateD6JointDrive(float strength, float dampingRatio, float forceLimit); physx::PxD6JointDrive CreateD6JointDrive(float strength, float dampingRatio, float forceLimit);
//! Contains information about a node in a hierarchy and how deep it is in the hierarchy relative to the root.
struct DepthData
{
int m_depth = -1; //!< Depth of the joint in the hierarchy. The root has depth 0, its children depth 1, and so on.
size_t m_index = 0; //<! Index of the joint in the hierarchy.
};
//! Given information about the parent nodes for each node in a hierarchy, computes how deep each node is in the
//! hierarchy relative to the root level. Assumes that the input parent index data corresponds to a tree structure, i.e.
//! does not contain any cycles.
AZStd::vector<DepthData> ComputeHierarchyDepths(const AZStd::vector<size_t>& parentIndices);
} // namespace Characters } // namespace Characters
} // namespace Utils } // namespace Utils
} // namespace PhysX } // namespace PhysX

@ -7,20 +7,20 @@
*/ */
#include <AzCore/Component/Entity.h> #include <AzCore/Component/Entity.h>
#include <PhysXCharacters/API/CharacterUtils.h>
#include <AzCore/Component/TransformBus.h> #include <AzCore/Component/TransformBus.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h> #include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Physics/CharacterPhysicsDataBus.h> #include <AzFramework/Physics/CharacterPhysicsDataBus.h>
#include <AzFramework/Physics/Common/PhysicsSimulatedBody.h>
#include <AzFramework/Physics/PhysicsScene.h> #include <AzFramework/Physics/PhysicsScene.h>
#include <AzFramework/Physics/SystemBus.h> #include <AzFramework/Physics/SystemBus.h>
#include <AzFramework/Physics/Common/PhysicsSimulatedBody.h> #include <PhysXCharacters/API/CharacterUtils.h>
#include <PhysXCharacters/Components/RagdollComponent.h> #include <PhysXCharacters/Components/RagdollComponent.h>
namespace PhysX namespace PhysX
{ {
bool RagdollComponent::VersionConverter(AZ::SerializeContext& context, bool RagdollComponent::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
AZ::SerializeContext::DataElementNode& classElement)
{ {
// The element "PhysXRagdoll" was changed from a shared pointer to a unique pointer, but a version converter was // The element "PhysXRagdoll" was changed from a shared pointer to a unique pointer, but a version converter was
// not added at the time. This means there may be serialized data with either the shared or unique pointer, but // not added at the time. This means there may be serialized data with either the shared or unique pointer, but
@ -76,13 +76,13 @@ namespace PhysX
->Field("EnableJointProjection", &RagdollComponent::m_enableJointProjection) ->Field("EnableJointProjection", &RagdollComponent::m_enableJointProjection)
->Field("ProjectionLinearTol", &RagdollComponent::m_jointProjectionLinearTolerance) ->Field("ProjectionLinearTol", &RagdollComponent::m_jointProjectionLinearTolerance)
->Field("ProjectionAngularTol", &RagdollComponent::m_jointProjectionAngularToleranceDegrees) ->Field("ProjectionAngularTol", &RagdollComponent::m_jointProjectionAngularToleranceDegrees)
; ->Field("EnableMassRatioClamping", &RagdollComponent::m_enableMassRatioClamping)
->Field("MaxMassRatio", &RagdollComponent::m_maxMassRatio);
AZ::EditContext* editContext = serializeContext->GetEditContext(); AZ::EditContext* editContext = serializeContext->GetEditContext();
if (editContext) if (editContext)
{ {
editContext->Class<RagdollComponent>( editContext->Class<RagdollComponent>("PhysX Ragdoll", "Creates a PhysX ragdoll simulation for an animation actor.")
"PhysX Ragdoll", "Creates a PhysX ragdoll simulation for an animation actor.")
->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "PhysX") ->Attribute(AZ::Edit::Attributes::Category, "PhysX")
->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXRagdoll.svg") ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXRagdoll.svg")
@ -90,34 +90,49 @@ namespace PhysX
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c))
->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/ragdoll/") ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/ragdoll/")
->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->DataElement(AZ::Edit::UIHandlers::Default, &RagdollComponent::m_positionIterations, "Position Iteration Count", ->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_positionIterations, "Position Iteration Count",
"The frequency at which ragdoll collider positions are resolved. Higher values can increase fidelity but decrease " "The frequency at which ragdoll collider positions are resolved. Higher values can increase fidelity but decrease "
"performance. Very high values might introduce instability.") "performance. Very high values might introduce instability.")
->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 255) ->Attribute(AZ::Edit::Attributes::Max, 255)
->DataElement(AZ::Edit::UIHandlers::Default, &RagdollComponent::m_velocityIterations, "Velocity Iteration Count", ->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_velocityIterations, "Velocity Iteration Count",
"The frequency at which ragdoll collider velocities are resolved. Higher values can increase fidelity but decrease " "The frequency at which ragdoll collider velocities are resolved. Higher values can increase fidelity but decrease "
"performance. Very high values might introduce instability.") "performance. Very high values might introduce instability.")
->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 255) ->Attribute(AZ::Edit::Attributes::Max, 255)
->DataElement(AZ::Edit::UIHandlers::Default, &RagdollComponent::m_enableJointProjection, ->DataElement(
"Enable Joint Projection", "When active, preserves joint constraints in volatile simulations. " AZ::Edit::UIHandlers::Default, &RagdollComponent::m_enableJointProjection, "Enable Joint Projection",
"When active, preserves joint constraints in volatile simulations. "
"Might not be physically correct in all simulations.") "Might not be physically correct in all simulations.")
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree)
->DataElement(AZ::Edit::UIHandlers::Default, &RagdollComponent::m_jointProjectionLinearTolerance, ->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_jointProjectionLinearTolerance,
"Joint Projection Linear Tolerance", "Joint Projection Linear Tolerance",
"Maximum linear joint error. Projection is applied to linear joint errors above this value.") "Maximum linear joint error. Projection is applied to linear joint errors above this value.")
->Attribute(AZ::Edit::Attributes::Min, 0.0f) ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Step, 1e-3f) ->Attribute(AZ::Edit::Attributes::Step, 1e-3f)
->Attribute(AZ::Edit::Attributes::Visibility, &RagdollComponent::IsJointProjectionVisible) ->Attribute(AZ::Edit::Attributes::Visibility, &RagdollComponent::IsJointProjectionVisible)
->DataElement(AZ::Edit::UIHandlers::Default, &RagdollComponent::m_jointProjectionAngularToleranceDegrees, ->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_jointProjectionAngularToleranceDegrees,
"Joint Projection Angular Tolerance", "Joint Projection Angular Tolerance",
"Maximum angular joint error. Projection is applied to angular joint errors above this value.") "Maximum angular joint error. Projection is applied to angular joint errors above this value.")
->Attribute(AZ::Edit::Attributes::Min, 0.0f) ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Step, 0.1f) ->Attribute(AZ::Edit::Attributes::Step, 0.1f)
->Attribute(AZ::Edit::Attributes::Suffix, " degrees") ->Attribute(AZ::Edit::Attributes::Suffix, " degrees")
->Attribute(AZ::Edit::Attributes::Visibility, &RagdollComponent::IsJointProjectionVisible) ->Attribute(AZ::Edit::Attributes::Visibility, &RagdollComponent::IsJointProjectionVisible)
; ->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_enableMassRatioClamping, "Enable Mass Ratio Clamping",
"When active, ragdoll node mass values may be overridden to avoid unstable mass ratios.")
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree)
->DataElement(
AZ::Edit::UIHandlers::Default, &RagdollComponent::m_maxMassRatio, "Maximum Mass Ratio",
"The mass of the child body of a joint may be clamped to avoid its ratio with the parent "
"body mass exceeding this threshold.")
->Attribute(AZ::Edit::Attributes::Min, 1.0f)
->Attribute(AZ::Edit::Attributes::Step, 0.1f)
->Attribute(AZ::Edit::Attributes::Visibility, &RagdollComponent::IsMaxMassRatioVisible);
} }
} }
@ -126,11 +141,16 @@ namespace PhysX
} }
} }
bool RagdollComponent::IsJointProjectionVisible() bool RagdollComponent::IsJointProjectionVisible() const
{ {
return m_enableJointProjection; return m_enableJointProjection;
} }
bool RagdollComponent::IsMaxMassRatioVisible() const
{
return m_enableMassRatioClamping;
}
// AZ::Component // AZ::Component
void RagdollComponent::Init() void RagdollComponent::Init()
{ {
@ -272,7 +292,6 @@ namespace PhysX
return ragdoll->IsSimulated(); return ragdoll->IsSimulated();
} }
return false; return false;
} }
AZ::Aabb RagdollComponent::GetAabb() const AZ::Aabb RagdollComponent::GetAabb() const
@ -318,20 +337,19 @@ namespace PhysX
if (numNodes == 0) if (numNodes == 0)
{ {
AZ_Error("PhysX Ragdoll Component", false, AZ_Error(
"Ragdoll configuration has 0 nodes, ragdoll will not be created for entity \"%s\".", "PhysX Ragdoll Component", false, "Ragdoll configuration has 0 nodes, ragdoll will not be created for entity \"%s\".",
GetEntity()->GetName().c_str()); GetEntity()->GetName().c_str());
return; return;
} }
ragdollConfiguration.m_parentIndices.resize(numNodes); ragdollConfiguration.m_parentIndices.resize(numNodes);
for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++)
{ {
AZStd::string parentName; AZStd::string parentName;
AZStd::string nodeName = ragdollConfiguration.m_nodes[nodeIndex].m_debugName; AZStd::string nodeName = ragdollConfiguration.m_nodes[nodeIndex].m_debugName;
AzFramework::CharacterPhysicsDataRequestBus::EventResult(parentName, GetEntityId(), AzFramework::CharacterPhysicsDataRequestBus::EventResult(
&AzFramework::CharacterPhysicsDataRequests::GetParentNodeName, nodeName); parentName, GetEntityId(), &AzFramework::CharacterPhysicsDataRequests::GetParentNodeName, nodeName);
AZ::Outcome<size_t> parentIndex = Utils::Characters::GetNodeIndex(ragdollConfiguration, parentName); AZ::Outcome<size_t> parentIndex = Utils::Characters::GetNodeIndex(ragdollConfiguration, parentName);
ragdollConfiguration.m_parentIndices[nodeIndex] = parentIndex ? parentIndex.GetValue() : SIZE_MAX; ragdollConfiguration.m_parentIndices[nodeIndex] = parentIndex ? parentIndex.GetValue() : SIZE_MAX;
@ -339,8 +357,8 @@ namespace PhysX
} }
Physics::RagdollState bindPose; Physics::RagdollState bindPose;
AzFramework::CharacterPhysicsDataRequestBus::EventResult(bindPose, GetEntityId(), AzFramework::CharacterPhysicsDataRequestBus::EventResult(
&AzFramework::CharacterPhysicsDataRequests::GetBindPose, ragdollConfiguration); bindPose, GetEntityId(), &AzFramework::CharacterPhysicsDataRequests::GetBindPose, ragdollConfiguration);
AZ::Transform entityTransform = AZ::Transform::CreateIdentity(); AZ::Transform entityTransform = AZ::Transform::CreateIdentity();
AZ::TransformBus::EventResult(entityTransform, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM); AZ::TransformBus::EventResult(entityTransform, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM);
@ -354,13 +372,12 @@ namespace PhysX
m_ragdollHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &ragdollConfiguration); m_ragdollHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &ragdollConfiguration);
} }
auto* ragdoll = GetPhysXRagdoll(); auto* ragdoll = GetPhysXRagdoll();
if (ragdoll == nullptr || if (ragdoll == nullptr || m_ragdollHandle == AzPhysics::InvalidSimulatedBodyHandle)
m_ragdollHandle == AzPhysics::InvalidSimulatedBodyHandle)
{ {
AZ_Error("PhysX Ragdoll Component", false, "Failed to create ragdoll."); AZ_Error("PhysX Ragdoll Component", false, "Failed to create ragdoll.");
return; return;
} }
for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++)
{ {
if (physx::PxRigidDynamic* pxRigidBody = ragdoll->GetPxRigidDynamic(nodeIndex)) if (physx::PxRigidDynamic* pxRigidBody = ragdoll->GetPxRigidDynamic(nodeIndex))
@ -389,17 +406,63 @@ namespace PhysX
} }
} }
// If mass ratio clamping is enabled, iterate out from the root and clamp mass values
if (m_enableMassRatioClamping)
{
const float maxMassRatio = AZStd::GetMax(1.0f + AZ::Constants::FloatEpsilon, m_maxMassRatio);
// figure out the depth of each node in the tree, so that nodes can be visited from the root outwards
AZStd::vector<Utils::Characters::DepthData> nodeDepths =
Utils::Characters::ComputeHierarchyDepths(ragdollConfiguration.m_parentIndices);
AZStd::sort(
nodeDepths.begin(), nodeDepths.end(),
[](const Utils::Characters::DepthData& d1, const Utils::Characters::DepthData& d2)
{
return d1.m_depth < d2.m_depth;
});
bool massesClamped = false;
for (const auto& nodeDepth : nodeDepths)
{
const size_t nodeIndex = nodeDepth.m_index;
const size_t parentIndex = ragdollConfiguration.m_parentIndices[nodeIndex];
if (parentIndex < numNodes)
{
AzPhysics::RigidBody& nodeRigidBody = ragdoll->GetNode(nodeIndex)->GetRigidBody();
const float originalMass = nodeRigidBody.GetMass();
const float parentMass = ragdoll->GetNode(parentIndex)->GetRigidBody().GetMass();
const float minMass = parentMass / maxMassRatio;
const float maxMass = parentMass;
if (originalMass < minMass || originalMass > maxMass)
{
const float clampedMass = AZStd::clamp(originalMass, minMass, maxMass);
nodeRigidBody.SetMass(clampedMass);
massesClamped = true;
if (!AZ::IsClose(originalMass, 0.0f))
{
// scale the inertia proportionally to how the mass was modified
auto pxRigidBody = static_cast<physx::PxRigidDynamic*>(nodeRigidBody.GetNativePointer());
pxRigidBody->setMassSpaceInertiaTensor(clampedMass / originalMass * pxRigidBody->getMassSpaceInertiaTensor());
}
}
}
}
AZ_WarningOnce("PhysX Ragdoll", !massesClamped,
"Mass values for ragdoll on entity \"%s\" were modified based on max mass ratio setting to avoid instability.",
GetEntity()->GetName().c_str());
}
AzFramework::RagdollPhysicsRequestBus::Handler::BusConnect(GetEntityId()); AzFramework::RagdollPhysicsRequestBus::Handler::BusConnect(GetEntityId());
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(GetEntityId()); AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(GetEntityId());
AzFramework::RagdollPhysicsNotificationBus::Event(GetEntityId(), AzFramework::RagdollPhysicsNotificationBus::Event(GetEntityId(), &AzFramework::RagdollPhysicsNotifications::OnRagdollActivated);
&AzFramework::RagdollPhysicsNotifications::OnRagdollActivated);
} }
void RagdollComponent::DestroyRagdoll() void RagdollComponent::DestroyRagdoll()
{ {
if (m_ragdollHandle != AzPhysics::InvalidSimulatedBodyHandle && if (m_ragdollHandle != AzPhysics::InvalidSimulatedBodyHandle && m_attachedSceneHandle != AzPhysics::InvalidSceneHandle)
m_attachedSceneHandle != AzPhysics::InvalidSceneHandle)
{ {
AzFramework::RagdollPhysicsRequestBus::Handler::BusDisconnect(); AzFramework::RagdollPhysicsRequestBus::Handler::BusDisconnect();
AzFramework::RagdollPhysicsNotificationBus::Event( AzFramework::RagdollPhysicsNotificationBus::Event(
@ -421,8 +484,7 @@ namespace PhysX
const Ragdoll* RagdollComponent::GetPhysXRagdollConst() const const Ragdoll* RagdollComponent::GetPhysXRagdollConst() const
{ {
if (m_ragdollHandle == AzPhysics::InvalidSimulatedBodyHandle || if (m_ragdollHandle == AzPhysics::InvalidSimulatedBodyHandle || m_attachedSceneHandle == AzPhysics::InvalidSceneHandle)
m_attachedSceneHandle == AzPhysics::InvalidSceneHandle)
{ {
return nullptr; return nullptr;
} }

@ -103,7 +103,8 @@ namespace PhysX
Ragdoll* GetPhysXRagdoll(); Ragdoll* GetPhysXRagdoll();
const Ragdoll* GetPhysXRagdollConst() const; const Ragdoll* GetPhysXRagdollConst() const;
bool IsJointProjectionVisible(); bool IsJointProjectionVisible() const;
bool IsMaxMassRatioVisible() const;
AzPhysics::SimulatedBodyHandle m_ragdollHandle = AzPhysics::InvalidSimulatedBodyHandle; AzPhysics::SimulatedBodyHandle m_ragdollHandle = AzPhysics::InvalidSimulatedBodyHandle;
AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle;
@ -119,5 +120,9 @@ namespace PhysX
float m_jointProjectionLinearTolerance = 1e-3f; float m_jointProjectionLinearTolerance = 1e-3f;
/// Angular joint error (in degrees) above which projection will be applied. /// Angular joint error (in degrees) above which projection will be applied.
float m_jointProjectionAngularToleranceDegrees = 1.0f; float m_jointProjectionAngularToleranceDegrees = 1.0f;
/// Allows ragdoll node mass values to be overridden to avoid unstable mass ratios.
bool m_enableMassRatioClamping = false;
/// If mass ratio clamping is enabled, masses will be clamped to within this ratio.
float m_maxMassRatio = 2.0f;
}; };
} // namespace PhysX } // namespace PhysX

@ -367,4 +367,19 @@ namespace PhysX
float minZ = ragdoll->GetAabb().GetMin().GetZ(); float minZ = ragdoll->GetAabb().GetMin().GetZ();
EXPECT_NEAR(minZ, 0.0f, 0.05f); EXPECT_NEAR(minZ, 0.0f, 0.05f);
} }
TEST(ComputeHierarchyDepthsTest, DepthValuesCorrect)
{
AZStd::vector<size_t> parentIndices =
{ 3, 5, AZStd::numeric_limits<size_t>::max(), 1, 2, 9, 7, 4, 0, 6, 11, 12, 5, 14, 15, 16, 5, 18, 19, 4, 21, 22, 4 };
const AZStd::vector<Utils::Characters::DepthData> nodeDepths = Utils::Characters::ComputeHierarchyDepths(parentIndices);
std::vector<int> expectedDepths = { 8, 6, 0, 7, 1, 5, 3, 2, 9, 4, 8, 7, 6, 9, 8, 7, 6, 4, 3, 2, 4, 3, 2 };
for (size_t i = 0; i < parentIndices.size(); i++)
{
EXPECT_EQ(nodeDepths[i].m_depth, expectedDepths[i]);
}
}
} // namespace PhysX } // namespace PhysX

Loading…
Cancel
Save