diff --git a/pxr/usd/usdPhysics/CMakeLists.txt b/pxr/usd/usdPhysics/CMakeLists.txt index f794ddc17d..403a3dfe2d 100644 --- a/pxr/usd/usdPhysics/CMakeLists.txt +++ b/pxr/usd/usdPhysics/CMakeLists.txt @@ -91,6 +91,7 @@ pxr_library(usdPhysics pxr_test_scripts( testenv/testUsdPhysicsMetrics.py testenv/testUsdPhysicsRigidBodyAPI.py + testenv/testUsdPhysicsCollisionGroupAPI.py ) pxr_register_test(testUsdPhysicsMetrics @@ -105,3 +106,8 @@ pxr_register_test(testUsdPhysicsRigidBodyAPI EXPECTED_RETURN_CODE 0 ) +pxr_register_test(testUsdPhysicsCollisionGroupAPI + PYTHON + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdPhysicsCollisionGroupAPI" + EXPECTED_RETURN_CODE 0 +) \ No newline at end of file diff --git a/pxr/usd/usdPhysics/collisionGroup.cpp b/pxr/usd/usdPhysics/collisionGroup.cpp index b466571f43..ecf2b9040a 100644 --- a/pxr/usd/usdPhysics/collisionGroup.cpp +++ b/pxr/usd/usdPhysics/collisionGroup.cpp @@ -103,6 +103,40 @@ UsdPhysicsCollisionGroup::_GetTfType() const return _GetStaticTfType(); } +UsdAttribute +UsdPhysicsCollisionGroup::GetMergeGroupNameAttr() const +{ + return GetPrim().GetAttribute(UsdPhysicsTokens->physicsMergeGroup); +} + +UsdAttribute +UsdPhysicsCollisionGroup::CreateMergeGroupNameAttr(VtValue const &defaultValue, bool writeSparsely) const +{ + return UsdSchemaBase::_CreateAttr(UsdPhysicsTokens->physicsMergeGroup, + SdfValueTypeNames->String, + /* custom = */ false, + SdfVariabilityVarying, + defaultValue, + writeSparsely); +} + +UsdAttribute +UsdPhysicsCollisionGroup::GetInvertFilteredGroupsAttr() const +{ + return GetPrim().GetAttribute(UsdPhysicsTokens->physicsInvertFilteredGroups); +} + +UsdAttribute +UsdPhysicsCollisionGroup::CreateInvertFilteredGroupsAttr(VtValue const &defaultValue, bool writeSparsely) const +{ + return UsdSchemaBase::_CreateAttr(UsdPhysicsTokens->physicsInvertFilteredGroups, + SdfValueTypeNames->Bool, + /* custom = */ false, + SdfVariabilityVarying, + defaultValue, + writeSparsely); +} + UsdRelationship UsdPhysicsCollisionGroup::GetFilteredGroupsRel() const { @@ -116,13 +150,30 @@ UsdPhysicsCollisionGroup::CreateFilteredGroupsRel() const /* custom = */ false); } +namespace { +static inline TfTokenVector +_ConcatenateAttributeNames(const TfTokenVector& left,const TfTokenVector& right) +{ + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} + /*static*/ const TfTokenVector& UsdPhysicsCollisionGroup::GetSchemaAttributeNames(bool includeInherited) { - static TfTokenVector localNames; + static TfTokenVector localNames = { + UsdPhysicsTokens->physicsMergeGroup, + UsdPhysicsTokens->physicsInvertFilteredGroups, + }; static TfTokenVector allNames = - UsdTyped::GetSchemaAttributeNames(true); + _ConcatenateAttributeNames( + UsdTyped::GetSchemaAttributeNames(true), + localNames); if (includeInherited) return allNames; @@ -141,8 +192,21 @@ PXR_NAMESPACE_CLOSE_SCOPE // ===================================================================== // // --(BEGIN CUSTOM CODE)-- +#include "pxr/usd/usd/primRange.h" +#include + PXR_NAMESPACE_OPEN_SCOPE +// Helper private function to get index into the collision table +// Given indices of the 2 collision groups idxA < idxB and current group size +static +unsigned int +_GetCollisionTableIndex(const unsigned int idxA, const unsigned int idxB, + const unsigned int groupSize) +{ + const unsigned int numSkippedEntries = (idxA * idxA + idxA) / 2; + return (idxA * groupSize - numSkippedEntries + idxB); +} UsdCollectionAPI UsdPhysicsCollisionGroup::GetCollidersCollectionAPI() const @@ -150,4 +214,197 @@ UsdPhysicsCollisionGroup::GetCollidersCollectionAPI() const return UsdCollectionAPI(GetPrim(), UsdPhysicsTokens->colliders); } +const SdfPathVector& +UsdPhysicsCollisionGroup::CollisionGroupTable::GetCollisionGroups() const +{ + return _groups; +} + +bool +UsdPhysicsCollisionGroup::CollisionGroupTable::IsCollisionEnabled( + const unsigned int idxA, const unsigned int idxB) const +{ + // check if indices are legal + // - both idxA and idxB are by definition >=0 + // - are < the group size. + if (idxA < _groups.size() && idxB < _groups.size()) + { + const unsigned int minGroup = std::min(idxA, idxB); + const unsigned int maxGroup = std::max(idxA, idxB); + const unsigned int tableIndex = + _GetCollisionTableIndex(minGroup, maxGroup, _groups.size()); + return _enabled[tableIndex]; + } + + // If the groups aren't in the table or we've been passed invalid groups, + // return true, as groups will collide by default. + return true; +} + +bool +UsdPhysicsCollisionGroup::CollisionGroupTable::IsCollisionEnabled( + const SdfPath& primA, const SdfPath& primB) const +{ + auto a = std::find(_groups.begin(), _groups.end(), primA); + auto b = std::find(_groups.begin(), _groups.end(), primB); + return IsCollisionEnabled(std::distance(_groups.begin(), a), + std::distance(_groups.begin(), b)); +} + + +UsdPhysicsCollisionGroup::CollisionGroupTable +UsdPhysicsCollisionGroup::ComputeCollisionGroupTable(const UsdStage& stage) +{ + // First, collect all the collision groups, as we want to iterate over them + // several times + std::vector allSceneGroups; + const UsdPrimRange range(stage.GetPseudoRoot()); + for (const UsdPrim& prim : range) + { + if (prim.IsA()) + { + allSceneGroups.emplace_back(prim); + } + } + + // Assign a number to every collision group; in order to handle merge + // groups, some prims will have non-unique indices + // Using SdfPath, rather than prim, as the filtered groups rel gives us a + // path. + std::map primPathToIndex; + std::map mergeGroupNameToIndex; + unsigned int nextPrimId = 0; + + for (const UsdPhysicsCollisionGroup& collisionGroup : allSceneGroups) + { + UsdAttribute mergeGroupAttr = collisionGroup.GetMergeGroupNameAttr(); + + // If the group doesn't have a merge group, we can just add it to the + // table: + if (!mergeGroupAttr.IsAuthored()) + { + primPathToIndex[collisionGroup.GetPath()] = nextPrimId; + nextPrimId++; + } + else + { + std::string mergeGroupName; + mergeGroupAttr.Get(&mergeGroupName); + auto foundGroup = mergeGroupNameToIndex.find(mergeGroupName); + if (foundGroup != mergeGroupNameToIndex.end()) + { + primPathToIndex[collisionGroup.GetPath()] = foundGroup->second; + } + else + { + mergeGroupNameToIndex[mergeGroupName] = nextPrimId; + primPathToIndex[collisionGroup.GetPath()] = nextPrimId; + nextPrimId++; + } + } + } + + // Now, we've seen "nextPrimId" different unique groups after accounting + // for the merge groups. Calculate the collision table for those groups. + + // First, resize the table and set to default-collide. We're only going to + // use the upper diagonal, as the table is symmetric: + std::vector mergedTable; + mergedTable.resize( ((nextPrimId + 1) * nextPrimId) / 2, true); + + for (const UsdPhysicsCollisionGroup& groupA : allSceneGroups) + { + unsigned int groupAIdx = primPathToIndex[groupA.GetPath()]; + + // Extract the indices for each filtered group in "prim": + std::vector filteredGroupIndices; + { + UsdRelationship filteredGroups = groupA.GetFilteredGroupsRel(); + SdfPathVector filteredTargets; + filteredGroups.GetTargets(&filteredTargets); + filteredGroupIndices.reserve(filteredTargets.size()); + for(const SdfPath& path : filteredTargets) + { + filteredGroupIndices.push_back(primPathToIndex[path]); + } + } + + bool invertedFilter; + UsdAttribute invertedAttr = groupA.GetInvertFilteredGroupsAttr(); + invertedAttr.Get(&invertedFilter); + + // Now, we are ready to apply the filter rules for "prim": + if (!invertedAttr.IsAuthored() || !invertedFilter) + { + // This is the usual case; collisions against all the + // filteredTargets should be disabled + for (unsigned int groupBIdx : filteredGroupIndices) + { + // Disable aIdx -v- bIdx + const unsigned int minGroup = std::min(groupAIdx, groupBIdx); + const unsigned int maxGroup = std::max(groupAIdx, groupBIdx); + const unsigned int tableIndex = + _GetCollisionTableIndex(minGroup, maxGroup, nextPrimId); + mergedTable[tableIndex] = false; + } + } + else + { + // This is the less common case; disable collisions against all + // groups except the filtered targets + std::set requestedGroups( + filteredGroupIndices.begin(), filteredGroupIndices.end()); + for(unsigned int groupBIdx = 0; groupBIdx < nextPrimId; groupBIdx++) + { + if (requestedGroups.find(groupBIdx) == requestedGroups.end()) + { + // Disable aIdx -v- bIdx + const unsigned int minGroup = std::min(groupAIdx, groupBIdx); + const unsigned int maxGroup = std::max(groupAIdx, groupBIdx); + const unsigned int tableIndex = + _GetCollisionTableIndex(minGroup, maxGroup, nextPrimId); + mergedTable[tableIndex] = false; + } + } + } + } + + // Finally, we can calculate the output table, based on the merged table + CollisionGroupTable res; + res._groups.reserve(allSceneGroups.size()); + for (const UsdPhysicsCollisionGroup& collisionGroup : allSceneGroups) + { + res._groups.push_back(collisionGroup.GetPrim().GetPrimPath()); + } + res._enabled.resize( + ((res._groups.size() + 1) * res._groups.size()) / 2, true); + + // Iterate over every group A and B, and use the merged table to determine + // if they collide. + for (unsigned int iA = 0; iA < allSceneGroups.size(); iA++) + { + for (unsigned int iB = iA; iB < allSceneGroups.size(); iB++) + { + // Determine if the groups at iA and iB collide: + const unsigned int groupAId = + primPathToIndex[allSceneGroups[iA].GetPath()]; + const unsigned int groupBId = + primPathToIndex[allSceneGroups[iB].GetPath()]; + const unsigned int mergedTableIndex = + _GetCollisionTableIndex(groupAId, groupBId, nextPrimId); + bool enabledInMergeTable = mergedTable[mergedTableIndex]; + + // And use that to populate the output table: + const unsigned int minGroup = std::min(iA, iB); + const unsigned int maxGroup = std::max(iA, iB); + const unsigned int tableIndex = + _GetCollisionTableIndex(minGroup, maxGroup, + allSceneGroups.size()); + res._enabled[tableIndex] = enabledInMergeTable; + } + } + + return res; +} + PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/usd/usdPhysics/collisionGroup.h b/pxr/usd/usdPhysics/collisionGroup.h index 569f525ec9..abab12127d 100644 --- a/pxr/usd/usdPhysics/collisionGroup.h +++ b/pxr/usd/usdPhysics/collisionGroup.h @@ -157,6 +157,56 @@ class UsdPhysicsCollisionGroup : public UsdTyped USDPHYSICS_API const TfType &_GetTfType() const override; +public: + // --------------------------------------------------------------------- // + // MERGEGROUPNAME + // --------------------------------------------------------------------- // + /// If non-empty, any collision groups in a stage with a matching + /// mergeGroup should be considered to refer to the same collection. Matching + /// collision groups should behave as if there were a single group containing + /// referenced colliders and filter groups from both collections. + /// + /// | || + /// | -- | -- | + /// | Declaration | `string physics:mergeGroup` | + /// | C++ Type | std::string | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->String | + USDPHYSICS_API + UsdAttribute GetMergeGroupNameAttr() const; + + /// See GetMergeGroupNameAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + USDPHYSICS_API + UsdAttribute CreateMergeGroupNameAttr(VtValue const &defaultValue = VtValue(), bool writeSparsely=false) const; + +public: + // --------------------------------------------------------------------- // + // INVERTFILTEREDGROUPS + // --------------------------------------------------------------------- // + /// Normally, the filter will disable collisions against the selected + /// filter groups. However, if this option is set, the filter will disable + /// collisions against all colliders except for those in the selected filter + /// groups. + /// + /// | || + /// | -- | -- | + /// | Declaration | `bool physics:invertFilteredGroups` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + USDPHYSICS_API + UsdAttribute GetInvertFilteredGroupsAttr() const; + + /// See GetInvertFilteredGroupsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + USDPHYSICS_API + UsdAttribute CreateInvertFilteredGroupsAttr(VtValue const &defaultValue = VtValue(), bool writeSparsely=false) const; + public: // --------------------------------------------------------------------- // // FILTEREDGROUPS @@ -188,6 +238,42 @@ class UsdPhysicsCollisionGroup : public UsdTyped /// what colliders belong to the CollisionGroup. USDPHYSICS_API UsdCollectionAPI GetCollidersCollectionAPI() const; + + /// Utility structure generated by ComputeCollisionGroupTable(); contains a + /// table describing which pairs of collision groups have collisions + /// enabled/disabled by the filtering rules. + struct CollisionGroupTable + { + /// Return the set of all UsdPhysicsCollisionGroup which this table + /// contains + USDPHYSICS_API + const SdfPathVector& GetCollisionGroups() const; + + /// Return true if the groups at indices \a idxA and \a idxB collide + USDPHYSICS_API + bool IsCollisionEnabled(const unsigned int idxA, + const unsigned int idxB) const; + + /// Return true if the groups \a primA and \a primB collide + USDPHYSICS_API + bool IsCollisionEnabled(const SdfPath& primA, + const SdfPath& primB) const; + + protected: + friend class UsdPhysicsCollisionGroup; + // < All collision groups known to this table + SdfPathVector _groups; + // < 2D table, with one element per collision-group-pair. Entry is false + // if collision is disabled by a filtering rule. + std::vector _enabled; + }; + + /// Compute a table encoding all the collision groups filter rules for a + /// stage. This can be used as a reference to validate an implementation of + /// the collision groups filters. The returned table is diagonally + /// symmetric. + static USDPHYSICS_API + CollisionGroupTable ComputeCollisionGroupTable(const UsdStage& stage); }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/usd/usdPhysics/generatedSchema.usda b/pxr/usd/usdPhysics/generatedSchema.usda index 08ad650fbf..5d867cc8a4 100644 --- a/pxr/usd/usdPhysics/generatedSchema.usda +++ b/pxr/usd/usdPhysics/generatedSchema.usda @@ -203,6 +203,20 @@ class PhysicsCollisionGroup "PhysicsCollisionGroup" ( doc = """References a list of PhysicsCollisionGroups with which collisions should be ignored.""" ) + bool physics:invertFilteredGroups ( + displayName = "Invert Filtered Groups" + doc = """Normally, the filter will disable collisions against the selected + filter groups. However, if this option is set, the filter will disable + collisions against all colliders except for those in the selected filter + groups.""" + ) + string physics:mergeGroup ( + displayName = "Merge With Groups" + doc = """If non-empty, any collision groups in a stage with a matching + mergeGroup should be considered to refer to the same collection. Matching + collision groups should behave as if there were a single group containing + referenced colliders and filter groups from both collections.""" + ) } class "PhysicsFilteredPairsAPI" ( diff --git a/pxr/usd/usdPhysics/overview.dox b/pxr/usd/usdPhysics/overview.dox index 83db657907..e8dd6fbc5d 100644 --- a/pxr/usd/usdPhysics/overview.dox +++ b/pxr/usd/usdPhysics/overview.dox @@ -432,12 +432,41 @@ occurs often. One might need the sword of a game character to pass through an enemy rather than to bounce off, while wanting it to bounce off walls, for example. -We define a CollisionGroup as an IsA schema with a UsdCollectionAPI applied, -that defines the membership of colliders (objects with a -UsdPhysicsCollisionAPI) in the group. Each group also has a multi-target -relationship to other groups (potentially including itself) with which it needs -to not collide. Colliders not in any CollisionGroup collide with all other -colliders in the scene. +We define a CollisionGroup as an IsA schema with a UsdCollectionAPI applied, +that defines the membership of colliders (objects with a UsdPhysicsCollisionAPI) +in the group. Each group also has a multi-target relationship to other groups +(potentially including itself) with which it needs to not collide. Colliders not +in any CollisionGroup collide with all other colliders in the scene, except for +those colliders which have disabled collision by default. + +For behavioral or performance reasons, it is sometimes useful to configure a +collider whose collision is disabled by default. To support this, the +CollisionGroup has an invertFilteredGroups option; when this value is set, an +implementation should disable collisions against all other colliders except for +those in the referenced filteredGroups. Note that the collisions which were not +disabled may still be further restricted by additional collision groups or pair +filters. + +When composing a stage from multiple USD scenes, it may be desirable to merge +CollisionGroups, particularly if the stage composition is not known ahead of +time. For example, a stage composed of multiple characters, each with a ragdoll +as well as a character "controller" - the character controllers should not +collide with the ragdoll instances. A CollisionGroup contains an optional +mergeGroupName, and all groups with matching name should be considered to be +part of a single group, whose members and filter groups should be the union of +the merged groups. This allows the character file to define a group which +disables the controller-versus-ragdoll collisions; by assigning a +mergeGroupName to this group, the controller can be filtered against ragdoll +bodies in all other instances of the character. + +Care should be taken when merging groups with differing invertFilteredGroups +options; merging of groups should only ever cause collision pairs to become +disabled - i.e. a filter cannot re-enable a pair that has been disabled by any +other group. Consider an inverted group which references only GroupX (i.e. +disables collisions with everything except those in GroupX). When merging this +group with a non-inverting group referencing the same GroupX (i.e. disables +collisions against GroupX) - the merged group will collide with nothing, since +the combined rules of the merged groups will filter out any other body. \subsubsection usdPhysics_pairwise_filtering Pairwise Filtering diff --git a/pxr/usd/usdPhysics/schema.usda b/pxr/usd/usdPhysics/schema.usda index 743ca4419b..6bae3496e6 100644 --- a/pxr/usd/usdPhysics/schema.usda +++ b/pxr/usd/usdPhysics/schema.usda @@ -414,6 +414,30 @@ class PhysicsCollisionGroup "PhysicsCollisionGroup" doc = """References a list of PhysicsCollisionGroups with which collisions should be ignored.""" ) + + string physics:mergeGroup ( + customData = { + string apiName = "mergeGroupName" + } + + displayName = "Merge With Groups" + doc = """If non-empty, any collision groups in a stage with a matching + mergeGroup should be considered to refer to the same collection. Matching + collision groups should behave as if there were a single group containing + referenced colliders and filter groups from both collections.""" + ) + + bool physics:invertFilteredGroups ( + customData = { + string apiName = "invertFilteredGroups" + } + + displayName = "Invert Filtered Groups" + doc = """Normally, the filter will disable collisions against the selected + filter groups. However, if this option is set, the filter will disable + collisions against all colliders except for those in the selected filter + groups.""" + ) } class "PhysicsFilteredPairsAPI" diff --git a/pxr/usd/usdPhysics/testenv/testUsdPhysicsCollisionGroupAPI.py b/pxr/usd/usdPhysics/testenv/testUsdPhysicsCollisionGroupAPI.py new file mode 100644 index 0000000000..6beb4497f2 --- /dev/null +++ b/pxr/usd/usdPhysics/testenv/testUsdPhysicsCollisionGroupAPI.py @@ -0,0 +1,193 @@ +#!/pxrpythonsubst +# +# Copyright 2021 Pixar +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. + +import sys, os, unittest +from pxr import Tf, Usd, Sdf, UsdGeom, UsdShade, Gf, UsdPhysics + +class TestUsdPhysicsCollisionGroupAPI(unittest.TestCase): + def validate_table_symmetry(self, table): + for iA, a in enumerate(table.GetGroups()): + for iB, b in enumerate(table.GetGroups()): + self.assertEqual(table.IsCollisionEnabled(iA, iB), + table.IsCollisionEnabled(iB, iA)) + self.assertEqual(table.IsCollisionEnabled(a, b), + table.IsCollisionEnabled(b, a)) + self.assertEqual(table.IsCollisionEnabled(a, b), + table.IsCollisionEnabled(iA, iB)) + + def test_collision_group_table(self): + stage = Usd.Stage.CreateInMemory() + self.assertTrue(stage) + + a = UsdPhysics.CollisionGroup.Define(stage, "/a") + b = UsdPhysics.CollisionGroup.Define(stage, "/b") + c = UsdPhysics.CollisionGroup.Define(stage, "/c") + + b.CreateFilteredGroupsRel().AddTarget(c.GetPath()) + c.CreateFilteredGroupsRel().AddTarget(c.GetPath()) + + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + + # Check the results contain all the groups: + self.assertTrue(len(table.GetGroups()) == 3) + self.assertTrue(a.GetPrim().GetPath() in table.GetGroups()) + self.assertTrue(b.GetPrim().GetPath() in table.GetGroups()) + self.assertTrue(c.GetPrim().GetPath() in table.GetGroups()) + + # A should collide with everything + # B should only collide with A and B + # C should only collide with A + self.assertTrue(table.IsCollisionEnabled(a, a)); + self.assertTrue(table.IsCollisionEnabled(a, b)); + self.assertTrue(table.IsCollisionEnabled(a, c)); + self.assertTrue(table.IsCollisionEnabled(b, b)); + self.assertFalse(table.IsCollisionEnabled(b, c)); + self.assertFalse(table.IsCollisionEnabled(c, c)); + self.validate_table_symmetry(table) + + + def test_collision_group_inversion(self): + stage = Usd.Stage.CreateInMemory() + self.assertTrue(stage) + + a = UsdPhysics.CollisionGroup.Define(stage, "/a") + b = UsdPhysics.CollisionGroup.Define(stage, "/b") + c = UsdPhysics.CollisionGroup.Define(stage, "/c") + + a.CreateFilteredGroupsRel().AddTarget(c.GetPath()) + a.CreateInvertFilteredGroupsAttr().Set(True) + + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + + # A should collide with only C + # B should collide with only B and C + # C should collide with only B and C + self.assertFalse(table.IsCollisionEnabled(a, a)); + self.assertFalse(table.IsCollisionEnabled(a, b)); + self.assertTrue(table.IsCollisionEnabled(a, c)); + self.assertTrue(table.IsCollisionEnabled(b, b)); + self.assertTrue(table.IsCollisionEnabled(b, c)); + self.assertTrue(table.IsCollisionEnabled(c, c)); + self.validate_table_symmetry(table) + + # Explicitly test the inversion scenario which may "re-enable" a + # collision filter pair that has been disabled (refer docs on why care + # should be taken to avoid such scenarios) + allOthers = UsdPhysics.CollisionGroup.Define(stage, '/allOthers') + + # - grpX is set to ONLY collide with grpXCollider by setting an inversion + grpXCollider = UsdPhysics.CollisionGroup.Define(stage, '/grpXCollider') + grpX = UsdPhysics.CollisionGroup.Define(stage, '/grpX') + grpX.CreateFilteredGroupsRel().AddTarget(grpXCollider.GetPath()) + grpX.CreateInvertFilteredGroupsAttr().Set(True) + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + self.assertTrue(table.IsCollisionEnabled(grpX, grpXCollider)) + self.assertFalse(table.IsCollisionEnabled(grpX, allOthers)) + + # - grpX is added to a new merge group "mergetest" + grpX.CreateMergeGroupNameAttr().Set("mergeTest") + + # - grpA now creates a filter to disable its collision with grpXCollider + grpA = UsdPhysics.CollisionGroup.Define(stage, '/grpA') + grpA.CreateFilteredGroupsRel().AddTarget(grpXCollider.GetPath()) + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + self.assertFalse(table.IsCollisionEnabled(grpA, grpXCollider)) + # - above doesn't affect any of grpX's collision pairs + self.assertTrue(table.IsCollisionEnabled(grpX, grpXCollider)) + self.assertFalse(table.IsCollisionEnabled(grpX, allOthers)) + + # - grpA is now added to same "mergetest" merge group (care was not + # taken in doing so and this disables all collision pairs!!) + grpA.CreateMergeGroupNameAttr().Set("mergeTest") + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + self.assertFalse(table.IsCollisionEnabled(grpX, grpXCollider)) + self.assertFalse(table.IsCollisionEnabled(grpX, allOthers)) + self.assertFalse(table.IsCollisionEnabled(grpA, grpXCollider)) + self.assertFalse(table.IsCollisionEnabled(grpA, allOthers)) + + def test_collision_group_simple_merging(self): + stage = Usd.Stage.CreateInMemory() + self.assertTrue(stage) + + a = UsdPhysics.CollisionGroup.Define(stage, "/a") + b = UsdPhysics.CollisionGroup.Define(stage, "/b") + c = UsdPhysics.CollisionGroup.Define(stage, "/c") + + a.CreateFilteredGroupsRel().AddTarget(c.GetPath()) + # Assign A and B to the same merge group: + a.CreateMergeGroupNameAttr().Set("mergeTest") + b.CreateMergeGroupNameAttr().Set("mergeTest") + + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + + # A should collide with only A and B + # B should collide with only A and B + # C should collide with only C + self.assertTrue(table.IsCollisionEnabled(a, a)); + self.assertTrue(table.IsCollisionEnabled(a, b)); + self.assertFalse(table.IsCollisionEnabled(a, c)); + self.assertTrue(table.IsCollisionEnabled(b, b)); + self.assertFalse(table.IsCollisionEnabled(b, c)); + self.assertTrue(table.IsCollisionEnabled(c, c)); + self.validate_table_symmetry(table) + + def test_collision_group_complex_merging(self): + stage = Usd.Stage.CreateInMemory() + self.assertTrue(stage) + + a = UsdPhysics.CollisionGroup.Define(stage, "/a") + b = UsdPhysics.CollisionGroup.Define(stage, "/b") + c = UsdPhysics.CollisionGroup.Define(stage, "/c") + d = UsdPhysics.CollisionGroup.Define(stage, "/d") + + a.CreateFilteredGroupsRel().AddTarget(c.GetPath()) + # Assign A and B to the same merge group: + a.CreateMergeGroupNameAttr().Set("mergeAB") + b.CreateMergeGroupNameAttr().Set("mergeAB") + # Assign C and D to the same merge group: + c.CreateMergeGroupNameAttr().Set("mergeCD") + d.CreateMergeGroupNameAttr().Set("mergeCD") + + table = UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage) + + # A should collide with only A and B + # B should collide with only A and B + # C should collide with only C and D + # D should collide with only C and D + self.assertTrue(table.IsCollisionEnabled(a, a)); + self.assertTrue(table.IsCollisionEnabled(a, b)); + self.assertFalse(table.IsCollisionEnabled(a, c)); + self.assertFalse(table.IsCollisionEnabled(a, d)); + + self.assertTrue(table.IsCollisionEnabled(b, b)); + self.assertFalse(table.IsCollisionEnabled(b, c)); + self.assertFalse(table.IsCollisionEnabled(b, d)); + + self.assertTrue(table.IsCollisionEnabled(c, c)); + self.assertTrue(table.IsCollisionEnabled(c, d)); + self.assertTrue(table.IsCollisionEnabled(d, d)); + self.validate_table_symmetry(table) + +if __name__ == "__main__": + unittest.main() diff --git a/pxr/usd/usdPhysics/tokens.cpp b/pxr/usd/usdPhysics/tokens.cpp index 983b5e3a75..76c1126f11 100644 --- a/pxr/usd/usdPhysics/tokens.cpp +++ b/pxr/usd/usdPhysics/tokens.cpp @@ -68,6 +68,7 @@ UsdPhysicsTokensType::UsdPhysicsTokensType() : physicsFilteredPairs("physics:filteredPairs", TfToken::Immortal), physicsGravityDirection("physics:gravityDirection", TfToken::Immortal), physicsGravityMagnitude("physics:gravityMagnitude", TfToken::Immortal), + physicsInvertFilteredGroups("physics:invertFilteredGroups", TfToken::Immortal), physicsJointEnabled("physics:jointEnabled", TfToken::Immortal), physicsKinematicEnabled("physics:kinematicEnabled", TfToken::Immortal), physicsLocalPos0("physics:localPos0", TfToken::Immortal), @@ -77,6 +78,7 @@ UsdPhysicsTokensType::UsdPhysicsTokensType() : physicsLowerLimit("physics:lowerLimit", TfToken::Immortal), physicsMass("physics:mass", TfToken::Immortal), physicsMaxDistance("physics:maxDistance", TfToken::Immortal), + physicsMergeGroup("physics:mergeGroup", TfToken::Immortal), physicsMinDistance("physics:minDistance", TfToken::Immortal), physicsPrincipalAxes("physics:principalAxes", TfToken::Immortal), physicsRestitution("physics:restitution", TfToken::Immortal), @@ -138,6 +140,7 @@ UsdPhysicsTokensType::UsdPhysicsTokensType() : physicsFilteredPairs, physicsGravityDirection, physicsGravityMagnitude, + physicsInvertFilteredGroups, physicsJointEnabled, physicsKinematicEnabled, physicsLocalPos0, @@ -147,6 +150,7 @@ UsdPhysicsTokensType::UsdPhysicsTokensType() : physicsLowerLimit, physicsMass, physicsMaxDistance, + physicsMergeGroup, physicsMinDistance, physicsPrincipalAxes, physicsRestitution, diff --git a/pxr/usd/usdPhysics/tokens.h b/pxr/usd/usdPhysics/tokens.h index d1ca99da3e..06a79cb455 100644 --- a/pxr/usd/usdPhysics/tokens.h +++ b/pxr/usd/usdPhysics/tokens.h @@ -230,6 +230,10 @@ struct UsdPhysicsTokensType { /// /// UsdPhysicsScene const TfToken physicsGravityMagnitude; + /// \brief "physics:invertFilteredGroups" + /// + /// UsdPhysicsCollisionGroup + const TfToken physicsInvertFilteredGroups; /// \brief "physics:jointEnabled" /// /// UsdPhysicsJoint @@ -266,6 +270,10 @@ struct UsdPhysicsTokensType { /// /// UsdPhysicsDistanceJoint const TfToken physicsMaxDistance; + /// \brief "physics:mergeGroup" + /// + /// UsdPhysicsCollisionGroup + const TfToken physicsMergeGroup; /// \brief "physics:minDistance" /// /// UsdPhysicsDistanceJoint diff --git a/pxr/usd/usdPhysics/wrapCollisionGroup.cpp b/pxr/usd/usdPhysics/wrapCollisionGroup.cpp index 0616f53edc..0e8f273a67 100644 --- a/pxr/usd/usdPhysics/wrapCollisionGroup.cpp +++ b/pxr/usd/usdPhysics/wrapCollisionGroup.cpp @@ -48,6 +48,20 @@ namespace { // fwd decl. WRAP_CUSTOM; + +static UsdAttribute +_CreateMergeGroupNameAttr(UsdPhysicsCollisionGroup &self, + object defaultVal, bool writeSparsely) { + return self.CreateMergeGroupNameAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->String), writeSparsely); +} + +static UsdAttribute +_CreateInvertFilteredGroupsAttr(UsdPhysicsCollisionGroup &self, + object defaultVal, bool writeSparsely) { + return self.CreateInvertFilteredGroupsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} static std::string _Repr(const UsdPhysicsCollisionGroup &self) @@ -90,6 +104,20 @@ void wrapUsdPhysicsCollisionGroup() .def(!self) + + .def("GetMergeGroupNameAttr", + &This::GetMergeGroupNameAttr) + .def("CreateMergeGroupNameAttr", + &_CreateMergeGroupNameAttr, + (arg("defaultValue")=object(), + arg("writeSparsely")=false)) + + .def("GetInvertFilteredGroupsAttr", + &This::GetInvertFilteredGroupsAttr) + .def("CreateInvertFilteredGroupsAttr", + &_CreateInvertFilteredGroupsAttr, + (arg("defaultValue")=object(), + arg("writeSparsely")=false)) .def("GetFilteredGroupsRel", @@ -121,12 +149,68 @@ void wrapUsdPhysicsCollisionGroup() // ===================================================================== // // --(BEGIN CUSTOM CODE)-- +#include + namespace { +static SdfPathVector +_WrapGetGroups(const UsdPhysicsCollisionGroup::CollisionGroupTable &table) +{ + return table.GetCollisionGroups(); +} + +static bool +_WrapIsCollisionEnabled(const UsdPhysicsCollisionGroup::CollisionGroupTable &table, +boost::python::object a, boost::python::object b) +{ + boost::python::extract extractIntA(a); + boost::python::extract extractIntB(b); + if (extractIntA.check() && extractIntB.check()) + { + return table.IsCollisionEnabled(extractIntA(), extractIntB()); + } + + boost::python::extract extractPathA(a); + boost::python::extract extractPathB(b); + if (extractPathA.check() && extractPathB.check()) + { + return table.IsCollisionEnabled(extractPathA(), extractPathB()); + } + + boost::python::extract extractColGroupA(a); + boost::python::extract extractColGroupB(b); + if (extractColGroupA.check() && extractColGroupB.check()) + { + return table.IsCollisionEnabled(extractColGroupA().GetPrim().GetPrimPath(), extractColGroupB().GetPrim().GetPrimPath()); + } + + boost::python::extract extractPrimA(a); + boost::python::extract extractPrimB(b); + if (extractPrimA.check() && extractPrimB.check()) + { + return table.IsCollisionEnabled(extractPrimA().GetPrimPath(), extractPrimB().GetPrimPath()); + } + + return true; +} + WRAP_CUSTOM { + typedef UsdPhysicsCollisionGroup This; + _class .def("GetCollidersCollectionAPI", - &UsdPhysicsCollisionGroup::GetCollidersCollectionAPI) + &This::GetCollidersCollectionAPI) + .def("ComputeCollisionGroupTable", + &This::ComputeCollisionGroupTable, + arg("stage"), + return_value_policy()) + .staticmethod("ComputeCollisionGroupTable") + ; + + class_("CollisionGroupTable") + .def("GetGroups", &_WrapGetGroups, + return_value_policy()) + .def("IsCollisionEnabled", &_WrapIsCollisionEnabled) ; } diff --git a/pxr/usd/usdPhysics/wrapTokens.cpp b/pxr/usd/usdPhysics/wrapTokens.cpp index 419c3b429f..4f15e94689 100644 --- a/pxr/usd/usdPhysics/wrapTokens.cpp +++ b/pxr/usd/usdPhysics/wrapTokens.cpp @@ -106,6 +106,7 @@ void wrapUsdPhysicsTokens() _AddToken(cls, "physicsFilteredPairs", UsdPhysicsTokens->physicsFilteredPairs); _AddToken(cls, "physicsGravityDirection", UsdPhysicsTokens->physicsGravityDirection); _AddToken(cls, "physicsGravityMagnitude", UsdPhysicsTokens->physicsGravityMagnitude); + _AddToken(cls, "physicsInvertFilteredGroups", UsdPhysicsTokens->physicsInvertFilteredGroups); _AddToken(cls, "physicsJointEnabled", UsdPhysicsTokens->physicsJointEnabled); _AddToken(cls, "physicsKinematicEnabled", UsdPhysicsTokens->physicsKinematicEnabled); _AddToken(cls, "physicsLocalPos0", UsdPhysicsTokens->physicsLocalPos0); @@ -115,6 +116,7 @@ void wrapUsdPhysicsTokens() _AddToken(cls, "physicsLowerLimit", UsdPhysicsTokens->physicsLowerLimit); _AddToken(cls, "physicsMass", UsdPhysicsTokens->physicsMass); _AddToken(cls, "physicsMaxDistance", UsdPhysicsTokens->physicsMaxDistance); + _AddToken(cls, "physicsMergeGroup", UsdPhysicsTokens->physicsMergeGroup); _AddToken(cls, "physicsMinDistance", UsdPhysicsTokens->physicsMinDistance); _AddToken(cls, "physicsPrincipalAxes", UsdPhysicsTokens->physicsPrincipalAxes); _AddToken(cls, "physicsRestitution", UsdPhysicsTokens->physicsRestitution);