From 4e4dcba41d8fa32f6a29804fbbaffe002ad9a927 Mon Sep 17 00:00:00 2001 From: ppt-adsk Date: Tue, 15 Oct 2024 12:51:04 -0400 Subject: [PATCH] Native instancing isolate select support, with test. (#186) --- .../sceneIndex/fvpIsolateSelectSceneIndex.cpp | 194 +++++++- .../sceneIndex/fvpIsolateSelectSceneIndex.h | 26 +- .../sceneIndex/fvpPathInterface.h | 33 +- lib/flowViewport/selection/CMakeLists.txt | 2 + lib/flowViewport/selection/fvpSelection.cpp | 27 +- lib/flowViewport/selection/fvpSelection.h | 9 +- .../selection/fvpSelectionTypes.cpp | 18 + .../selection/fvpSelectionTypes.h | 60 +++ .../sceneIndex/registration.cpp | 1 - .../mayaUsd/render/mayaToHydra/CMakeLists.txt | 3 + .../mayaToHydra/cpp/testIsolateSelect.cpp | 114 +++++ .../mayaToHydra/cpp/testIsolateSelect.py | 90 ++++ .../testUsdNativeInstancingIsolateSelect.py | 464 ++++++++++++++++++ 13 files changed, 997 insertions(+), 44 deletions(-) create mode 100644 lib/flowViewport/selection/fvpSelectionTypes.cpp create mode 100644 lib/flowViewport/selection/fvpSelectionTypes.h create mode 100644 test/lib/mayaUsd/render/mayaToHydra/cpp/testUsdNativeInstancingIsolateSelect.py diff --git a/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.cpp b/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.cpp index 1513a8bc3..a2a7a4c8d 100644 --- a/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.cpp +++ b/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.cpp @@ -23,20 +23,51 @@ #include #include +#include +#include PXR_NAMESPACE_USING_DIRECTIVE namespace { +using Dependencies = TfSmallVector; + const HdContainerDataSourceHandle visOff = HdVisibilitySchema::BuildRetained( HdRetainedTypedSampledDataSource::New(false)); +const HdDataSourceLocator instancerMaskLocator( + HdInstancerTopologySchemaTokens->instancerTopology, + HdInstancerTopologySchemaTokens->mask +); + bool disabled(const Fvp::SelectionConstPtr& a, const Fvp::SelectionConstPtr& b) { return !a && !b; } +void append(Fvp::Selection& a, const Fvp::Selection& b) +{ + for (const auto& entry : b) { + const auto& primSelections = entry.second; + for (const auto& primSelection : primSelections) { + a.Add(primSelection); + } + } +} + +Dependencies instancedPrim( + const Fvp::IsolateSelectSceneIndex& si, + const SdfPath& primPath +) +{ + auto prim = si.GetInputSceneIndex()->GetPrim(primPath); + auto instanceSchema = HdInstanceSchema::GetFromParent(prim.dataSource); + return (instanceSchema.IsDefined() ? Dependencies{{ + instanceSchema.GetInstancer()->GetTypedValue(0)}} : + Dependencies()); +} + } namespace FVP_NS_DEF { @@ -86,6 +117,17 @@ HdSceneIndexPrim IsolateSelectSceneIndex::GetPrim(const SdfPath& primPath) const return inputPrim; } + auto instancerMask = _instancerMasks.find(primPath); + if (instancerMask != _instancerMasks.end()) { + auto maskDs = HdRetainedTypedSampledDataSource>::New(instancerMask->second); + + inputPrim.dataSource = HdContainerDataSourceEditor(inputPrim.dataSource) + .Set(instancerMaskLocator, maskDs) + .Finish(); + + return inputPrim; + } + // If isolate selection is empty, then nothing is included (everything // is excluded), as desired. const bool included = @@ -208,12 +250,12 @@ void IsolateSelectSceneIndex::ReplaceIsolateSelection(const SelectionConstPtr& n return; } - _ReplaceIsolateSelection(newIsolateSelection); + _DirtyIsolateSelection(newIsolateSelection); _isolateSelection->Replace(*newIsolateSelection); } -void IsolateSelectSceneIndex::_ReplaceIsolateSelection(const SelectionConstPtr& newIsolateSelection) +void IsolateSelectSceneIndex::_DirtyIsolateSelection(const SelectionConstPtr& newIsolateSelection) { // Trivial case of going from disabled to disabled is an early out. if (disabled(_isolateSelection, newIsolateSelection)) { @@ -290,9 +332,23 @@ void IsolateSelectSceneIndex::SetViewport( return; } - _ReplaceIsolateSelection(newIsolateSelection); + // Add dependencies of the new isolate selection to protect them from being + // marked as invisible. + _AddDependencies(newIsolateSelection); + + _DirtyIsolateSelection(newIsolateSelection); + + // Collect all the instancers from the new isolate selection. + auto instancers = _CollectInstancers(newIsolateSelection); + + // Create the instance mask for each instancer. + auto newInstancerMasks = _CreateInstancerMasks(instancers, newIsolateSelection); + + // Dirty the instancer masks. + _DirtyInstancerMasks(newInstancerMasks); _isolateSelection = newIsolateSelection; + _instancerMasks = newInstancerMasks; _viewportId = viewportId; } @@ -350,4 +406,136 @@ void IsolateSelectSceneIndex::_DirtyVisibilityRecursive( } } +void IsolateSelectSceneIndex::_AddDependencies( + const SelectionPtr& isolateSelection +) +{ + // Iterate over the input isolate selection, and find the dependencies. + // As of 27-Sep-2024 only instancer dependencies are supported. + if (!isolateSelection) { + return; + } + + // Collect dependencies in this selection. + Selection dependencies; + for (const auto& primSelectionsEntry : *isolateSelection) { + for (const auto& primSelection : primSelectionsEntry.second) { + auto primDependencies = instancedPrim(*this, primSelection.primPath); + for (const auto& dependencyPath : primDependencies) { + dependencies.Add(PrimSelection{dependencyPath}); + } + } + } + + // Add the collected dependencies to the input isolate selection. + append(*isolateSelection, dependencies); +} + +IsolateSelectSceneIndex::Instancers +IsolateSelectSceneIndex::_CollectInstancers( + const SelectionConstPtr& isolateSelection +) const +{ + if (!isolateSelection) { + return {}; + } + + Instancers instancers; + for (const auto& primSelectionsEntry : *isolateSelection) { + for (const auto& primSelection : primSelectionsEntry.second) { + auto prim = GetInputSceneIndex()->GetPrim(primSelection.primPath); + auto instanceSchema = HdInstanceSchema::GetFromParent(prim.dataSource); + if (instanceSchema.IsDefined()) { + instancers.emplace_back(instanceSchema.GetInstancer()->GetTypedValue(0)); + } + } + } + + return instancers; +} + +IsolateSelectSceneIndex::InstancerMasks +IsolateSelectSceneIndex::_CreateInstancerMasks( + const Instancers& instancers, + const SelectionConstPtr& isolateSelection +) const +{ + // If isolate select is disabled, no instancer masks to compute. + if (!isolateSelection) { + return {}; + } + + // For each instancer, build its mask of visible instances by running all + // instances, given by instancerTopology.instanceLocations, through + // the isolate selection. This determines whether the instance is visible + // or not. Store the instancer mask for the instancer path. + InstancerMasks instancerMasks; + for (const auto& instancerPath : instancers) { + HdSceneIndexPrim instancerPrim = GetInputSceneIndex()->GetPrim(instancerPath); + HdInstancerTopologySchema instancerTopologySchema = HdInstancerTopologySchema::GetFromParent(instancerPrim.dataSource); + + // Documentation + // https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/instancerTopologySchema.h#L86 + // says that instanceLocations is only meaningful for native + // instancing, empty for point instancing. + auto instanceLocationsDs = instancerTopologySchema.GetInstanceLocations(); + if (!instanceLocationsDs) { + continue; + } + + auto instanceLocations = instanceLocationsDs->GetTypedValue(0.0f); + + VtArray instanceMask(instanceLocations.size()); + std::size_t i=0; + for (const auto& instanceLocation : instanceLocations) { + const bool included = isolateSelection->HasAncestorOrDescendantInclusive(instanceLocation); + instanceMask[i] = included; + ++i; + } + + instancerMasks[instancerPath] = instanceMask; + } + + return instancerMasks; +} + +void IsolateSelectSceneIndex::_DirtyInstancerMasks( + const InstancerMasks& newInstancerMasks +) +{ + // Keep paths in a set to minimize dirtying. + std::set dirtyPaths; + + // First clear old paths. + for (const auto& entry : _instancerMasks) { + dirtyPaths.insert(entry.first); + } + + // Then add new paths. + for (const auto& entry : newInstancerMasks) { + dirtyPaths.insert(entry.first); + } + + HdSceneIndexObserver::DirtiedPrimEntries dirtiedEntries; + + // Dirty all cleared and added prim paths. + for (const auto& primPath : dirtyPaths) { + _AddDirtyInstancerMaskEntry(primPath, &dirtiedEntries); + } + + _SendPrimsDirtied(dirtiedEntries); + +} + +void IsolateSelectSceneIndex::_AddDirtyInstancerMaskEntry( + const SdfPath& primPath, + HdSceneIndexObserver::DirtiedPrimEntries* dirtiedEntries +) const +{ + TF_DEBUG(FVP_ISOLATE_SELECT_SCENE_INDEX) + .Msg(" %s: marking %s mask locator dirty.\n", _viewportId.c_str(), primPath.GetText()); + + dirtiedEntries->emplace_back(primPath, instancerMaskLocator); +} + } //end of namespace FVP_NS_DEF diff --git a/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.h b/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.h index 775ea20e4..94be3f75f 100644 --- a/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.h +++ b/lib/flowViewport/sceneIndex/fvpIsolateSelectSceneIndex.h @@ -25,6 +25,7 @@ //Hydra headers #include +#include namespace FVP_NS_DEF { @@ -176,16 +177,39 @@ class IsolateSelectSceneIndex : PXR_NS::HdSceneIndexObserver::DirtiedPrimEntries* dirtiedEntries ) const; - void _ReplaceIsolateSelection(const SelectionConstPtr& selection); + void _DirtyIsolateSelection(const SelectionConstPtr& selection); void _InsertSelectedPaths( const SelectionConstPtr& selection, std::set& dirtyPaths ); + void _AddDependencies(const SelectionPtr& isolateSelection); + + using Instancers = PXR_NS::TfSmallVector; + using InstancerMask = PXR_NS::VtArray; + using InstancerMasks = std::map; + + // Collect all the instancers from the argument isolate selection. + Instancers _CollectInstancers( + const SelectionConstPtr& isolateSelection) const; + + // Create the instance mask for each instancer. + InstancerMasks _CreateInstancerMasks(const Instancers& instancers, + const SelectionConstPtr& isolateSelection) const; + + // Dirty the instancer masks. + void _DirtyInstancerMasks(const InstancerMasks& instancerMasks); + void _AddDirtyInstancerMaskEntry( + const PXR_NS::SdfPath& primPath, + PXR_NS::HdSceneIndexObserver::DirtiedPrimEntries* dirtiedEntries + ) const; + std::string _viewportId; SelectionPtr _isolateSelection{}; + + InstancerMasks _instancerMasks{}; }; }//end of namespace FVP_NS_DEF diff --git a/lib/flowViewport/sceneIndex/fvpPathInterface.h b/lib/flowViewport/sceneIndex/fvpPathInterface.h index 029dbb656..ad42b517b 100644 --- a/lib/flowViewport/sceneIndex/fvpPathInterface.h +++ b/lib/flowViewport/sceneIndex/fvpPathInterface.h @@ -17,6 +17,8 @@ #include "flowViewport/api.h" +#include "flowViewport/selection/fvpSelectionTypes.h" + #include #include @@ -32,37 +34,6 @@ class Path; namespace FVP_NS_DEF { -// Based on USD's HdInstanceIndicesSchema : -// https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/instanceIndicesSchema.h -struct InstancesSelection { - PXR_NS::SdfPath instancerPath; - int prototypeIndex; - std::vector instanceIndices; - - inline bool operator==(const InstancesSelection &rhs) const { - return instancerPath == rhs.instancerPath - && prototypeIndex == rhs.prototypeIndex - && instanceIndices == rhs.instanceIndices; - } -}; - -// Based on USD's HdSelectionSchema : -// https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/selectionSchema.h -struct PrimSelection -{ - PXR_NS::SdfPath primPath; - std::vector nestedInstanceIndices; - - inline bool operator==(const PrimSelection &rhs) const { - return primPath == rhs.primPath - && nestedInstanceIndices == rhs.nestedInstanceIndices; - } -}; - -// Using TfSmallVector to optimize for selections that map to a few prims, -// which is likely going to be the bulk of use cases. -using PrimSelections = PXR_NS::TfSmallVector; - /// \class PathInterface /// /// A pure interface class to allow for conversion between an application's diff --git a/lib/flowViewport/selection/CMakeLists.txt b/lib/flowViewport/selection/CMakeLists.txt index 06daeaa9d..87238d8c0 100644 --- a/lib/flowViewport/selection/CMakeLists.txt +++ b/lib/flowViewport/selection/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${TARGET_NAME} fvpSelectionFwd.cpp fvpSelectionTask.cpp fvpSelectionTracker.cpp + fvpSelectionTypes.cpp fvpPathMapper.cpp fvpPathMapperFwd.cpp fvpPathMapperRegistry.cpp @@ -19,6 +20,7 @@ set(HEADERS fvpSelectionFwd.h fvpSelectionTask.h fvpSelectionTracker.h + fvpSelectionTypes.h fvpPathMapper.h fvpPathMapperFwd.h fvpPathMapperRegistry.h diff --git a/lib/flowViewport/selection/fvpSelection.cpp b/lib/flowViewport/selection/fvpSelection.cpp index ac04120e4..276d8ca38 100644 --- a/lib/flowViewport/selection/fvpSelection.cpp +++ b/lib/flowViewport/selection/fvpSelection.cpp @@ -72,17 +72,22 @@ bool Selection::Remove(const PrimSelection& primSelection) return false; } + auto found = _pathToSelections.find(primSelection.primPath); + if (found == _pathToSelections.end()) { + return false; + } + auto& primSelections = found->second; + // Remove the specific selection auto itSelection = std::find( - _pathToSelections[primSelection.primPath].begin(), - _pathToSelections[primSelection.primPath].end(), - primSelection); - if (itSelection != _pathToSelections[primSelection.primPath].end()) { - _pathToSelections[primSelection.primPath].erase(itSelection); + primSelections.begin(), primSelections.end(), primSelection); + + if (itSelection != primSelections.end()) { + primSelections.erase(itSelection); } // If no selections remain, remove the entry entirely - if (_pathToSelections[primSelection.primPath].empty()) { + if (primSelections.empty()) { _pathToSelections.erase(primSelection.primPath); } @@ -254,4 +259,14 @@ HdDataSourceBaseHandle Selection::GetVectorDataSource( ); } +Selection::PrimSelectionsMap::const_iterator Selection::begin() const +{ + return _pathToSelections.begin(); +} + +Selection::PrimSelectionsMap::const_iterator Selection::end() const +{ + return _pathToSelections.end(); +} + } diff --git a/lib/flowViewport/selection/fvpSelection.h b/lib/flowViewport/selection/fvpSelection.h index c949762a2..7def4448b 100644 --- a/lib/flowViewport/selection/fvpSelection.h +++ b/lib/flowViewport/selection/fvpSelection.h @@ -18,7 +18,7 @@ #include "flowViewport/api.h" #include "flowViewport/selection/fvpSelectionFwd.h" -#include "flowViewport/sceneIndex/fvpPathInterface.h" +#include "flowViewport/selection/fvpSelectionTypes.h" #include #include @@ -43,6 +43,8 @@ class Selection { public: + using PrimSelectionsMap = std::map; + // Create a reference-counted Selection. FVP_API static SelectionPtr New(); @@ -109,11 +111,14 @@ class Selection PXR_NS::HdDataSourceBaseHandle GetVectorDataSource(const PXR_NS::SdfPath& primPath) const; + PrimSelectionsMap::const_iterator begin() const; + PrimSelectionsMap::const_iterator end() const; + private: // Maps prim path to selections to be returned by the vector data // source at locator selections. - std::map> _pathToSelections; + PrimSelectionsMap _pathToSelections; }; } diff --git a/lib/flowViewport/selection/fvpSelectionTypes.cpp b/lib/flowViewport/selection/fvpSelectionTypes.cpp new file mode 100644 index 000000000..f76456bd4 --- /dev/null +++ b/lib/flowViewport/selection/fvpSelectionTypes.cpp @@ -0,0 +1,18 @@ +// +// Copyright 2024 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Trivial inclusion to ensure header compiles on its own. +#include "flowViewport/selection/fvpSelectionTypes.h" diff --git a/lib/flowViewport/selection/fvpSelectionTypes.h b/lib/flowViewport/selection/fvpSelectionTypes.h new file mode 100644 index 000000000..f39d365f2 --- /dev/null +++ b/lib/flowViewport/selection/fvpSelectionTypes.h @@ -0,0 +1,60 @@ +// +// Copyright 2024 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef FVP_SELECTION_TYPES_H +#define FVP_SELECTION_TYPES_H + +#include "flowViewport/flowViewport.h" + +#include +#include +#include + +namespace FVP_NS_DEF { + +// Based on USD's HdInstanceIndicesSchema : +// https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/instanceIndicesSchema.h +struct InstancesSelection { + PXR_NS::SdfPath instancerPath; + int prototypeIndex; + std::vector instanceIndices; + + inline bool operator==(const InstancesSelection &rhs) const { + return instancerPath == rhs.instancerPath + && prototypeIndex == rhs.prototypeIndex + && instanceIndices == rhs.instanceIndices; + } +}; + +// Based on USD's HdSelectionSchema : +// https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/selectionSchema.h +struct PrimSelection +{ + PXR_NS::SdfPath primPath; + std::vector nestedInstanceIndices; + + inline bool operator==(const PrimSelection &rhs) const { + return primPath == rhs.primPath + && nestedInstanceIndices == rhs.nestedInstanceIndices; + } +}; + +// Using TfSmallVector to optimize for selections that map to a few prims, +// which is likely going to be the bulk of use cases. +using PrimSelections = PXR_NS::TfSmallVector; + +} + +#endif diff --git a/lib/mayaHydra/hydraExtensions/sceneIndex/registration.cpp b/lib/mayaHydra/hydraExtensions/sceneIndex/registration.cpp index 5068ae3cc..baf5d9bdc 100644 --- a/lib/mayaHydra/hydraExtensions/sceneIndex/registration.cpp +++ b/lib/mayaHydra/hydraExtensions/sceneIndex/registration.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include diff --git a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt index 6775756cc..a68bfec5f 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt +++ b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt @@ -81,9 +81,12 @@ set(INTERACTIVE_TEST_SCRIPT_FILES cpp/testGeomSubsetsWireframeHighlight.py ) +# These two test files are identical, except for disabled tests. See +# HYDRA-1245. if (MAYA_HAS_VIEW_SELECTED_OBJECT_API) list(APPEND INTERACTIVE_TEST_SCRIPT_FILES cpp/testIsolateSelect.py + cpp/testUsdNativeInstancingIsolateSelect.py ) endif() diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.cpp b/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.cpp index ceb0e5c1c..8c660f07b 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.cpp +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.cpp @@ -23,6 +23,9 @@ #include #include +#include +#include + #include #include @@ -43,6 +46,60 @@ TEST(TestHydraPrim, isVisible) auto primSelections = Fvp::ufePathToPrimSelections(appPath); + // If the prim is instanced, get the instancer, and determine if the + // prim is being masked out by the instancer mask. + if (!primSelections.empty()) { + const auto& primSelection = primSelections[0]; + auto prim = siRoot->GetPrim(primSelection.primPath); + auto instanceSchema = HdInstanceSchema::GetFromParent(prim.dataSource); + if (instanceSchema.IsDefined()) { + auto instancerPath = instanceSchema.GetInstancer()->GetTypedValue(0); + + // The instancer itself must be visible. If not, none of its + // instances will be. + ASSERT_TRUE(visibility(siRoot, instancerPath)); + + HdSceneIndexPrim instancerPrim = siRoot->GetPrim(instancerPath); + HdInstancerTopologySchema instancerTopologySchema = HdInstancerTopologySchema::GetFromParent(instancerPrim.dataSource); + + // Documentation + // https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/instancerTopologySchema.h#L86 + // says that instanceLocations is only meaningful for native + // instancing, empty for point instancing. + auto instanceLocationsDs = instancerTopologySchema.GetInstanceLocations(); + if (!TF_VERIFY(instanceLocationsDs, "Null instance location data source in isVisible() for instancer %s", instancerPath.GetText())) { + return; + } + + auto instanceLocations = instanceLocationsDs->GetTypedValue(0.0f); + + // Compute the index for the instance location we're concerned with. + // This is O(n) complexity for n instances, which is only + // acceptable because this is test code. + auto found = std::find( + instanceLocations.begin(), instanceLocations.end(), + primSelection.primPath); + + if (!TF_VERIFY(found != instanceLocations.end(), "Instance %s not found in instancer %s", primSelection.primPath.GetText(), instancerPath.GetText())) { + return; + } + + auto ndx = std::distance(instanceLocations.begin(), found); + + auto maskDs = instancerTopologySchema.GetMask(); + + if (!TF_VERIFY(maskDs, "Null mask data source in isVisible() for instancer %s", instancerPath.GetText())) { + return; + } + + auto mask = maskDs->GetTypedValue(0.0f); + + ASSERT_TRUE(mask[ndx]); + + return; + } + } + // If an application path maps to multiple prim selections, all prim // selections must be visible, else we fail. unsigned int primVis = 0; @@ -66,6 +123,63 @@ TEST(TestHydraPrim, notVisible) auto primSelections = Fvp::ufePathToPrimSelections(appPath); + // If the prim is instanced, get the instancer, and determine if the + // prim is being masked out by the instancer mask. + if (!primSelections.empty()) { + const auto& primSelection = primSelections[0]; + auto prim = siRoot->GetPrim(primSelection.primPath); + auto instanceSchema = HdInstanceSchema::GetFromParent(prim.dataSource); + if (instanceSchema.IsDefined()) { + auto instancerPath = instanceSchema.GetInstancer()->GetTypedValue(0); + + // If the instancer itself is not visible, none of its instances + // are, which is what we're asserting. + if (!visibility(siRoot, instancerPath)) { + // Success. + return; + } + + HdSceneIndexPrim instancerPrim = siRoot->GetPrim(instancerPath); + HdInstancerTopologySchema instancerTopologySchema = HdInstancerTopologySchema::GetFromParent(instancerPrim.dataSource); + + // Documentation + // https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hd/instancerTopologySchema.h#L86 + // says that instanceLocations is only meaningful for native + // instancing, empty for point instancing. + auto instanceLocationsDs = instancerTopologySchema.GetInstanceLocations(); + if (!TF_VERIFY(instanceLocationsDs, "Null instance location data source in isVisible() for instancer %s", instancerPath.GetText())) { + return; + } + + auto instanceLocations = instanceLocationsDs->GetTypedValue(0.0f); + + // Compute the index for the instance location we're concerned with. + // This is O(n) complexity for n instances, which is only + // acceptable because this is test code. + auto found = std::find( + instanceLocations.begin(), instanceLocations.end(), + primSelection.primPath); + + if (!TF_VERIFY(found != instanceLocations.end(), "Instance %s not found in instancer %s", primSelection.primPath.GetText(), instancerPath.GetText())) { + return; + } + + auto ndx = std::distance(instanceLocations.begin(), found); + + auto maskDs = instancerTopologySchema.GetMask(); + + if (!TF_VERIFY(maskDs, "Null mask data source in isVisible() for instancer %s", instancerPath.GetText())) { + return; + } + + auto mask = maskDs->GetTypedValue(0.0f); + + ASSERT_FALSE(mask[ndx]); + + return; + } + } + // If an application path maps to multiple prim selections, all prim // selections must be invisible, else we fail. int primVis = 0; diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.py b/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.py index 7492c0b69..59250d47b 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.py +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testIsolateSelect.py @@ -19,7 +19,9 @@ import mayaUsd_createStageWithNewLayer import maya.cmds as cmds import maya.mel as mel +import usdUtils from pxr import UsdGeom +import testUtils def enableIsolateSelect(modelPanel): # Surprisingly @@ -40,6 +42,9 @@ def disableIsolateSelect(modelPanel): cmds.setFocus(modelPanel) mel.eval("enableIsolateSelect %s 0" % modelPanel) +# This test is identical to the one in testUsdNativeInstancingIsolateSelect.py, +# except for disabled tests. See HYDRA-1245. + class TestIsolateSelect(mtohUtils.MayaHydraBaseTestCase): # MayaHydraBaseTestCase.setUpClass requirement. _file = __file__ @@ -158,6 +163,19 @@ def assertVisibility(self, visible, notVisible): self.assertVisible(visible) self.assertNotVisible(notVisible) + def assertIsolateSelect(self, modelPanel, visible, scene): + cmds.select(*visible) + cmds.isolateSelect(modelPanel, loadSelected=True) + + notVisible = scene.copy() + + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + def test_isolateSelectSingleViewport(self): scene = self.setupScene() @@ -370,5 +388,77 @@ def test_isolateSelectMultipleStages(self): # other tests. disableIsolateSelect(modelPanel) + def _test_isolateSelectNativeInstancing(self): + + # Read in a scene with native instancing. + usdScenePath = testUtils.getTestScene('testUsdNativeInstances', 'instancedCubeHierarchies.usda') + + proxyShapePathStr = usdUtils.createStageFromFile(usdScenePath) + + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + + # Add a non-instanced prim to the USD stage + UsdGeom.Cylinder.Define(stage, "/cylinder1") + + # Add a Maya object. + cmds.polySphere() + cmds.move(0, 0, 2, r=True) + + cube1Path = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cubeHierarchies/cubes_1' + cube2Path = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cubeHierarchies/cubes_2' + cylinderPath = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cylinder1' + spherePath = '|pSphere1' + + scene = [cube1Path, cube2Path, cylinderPath, spherePath] + + cmds.select(clear=True) + + # Isolate select not turned on, everything visible. + visible = scene + notVisible = [] + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # Turn isolate select on, nothing selected, nothing visible. + visible = [] + notVisible = scene + + modelPanel = 'modelPanel4' + enableIsolateSelect(modelPanel) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # Select native instanced object, load it into isolate selection. + self.assertIsolateSelect(modelPanel, [cube1Path], scene) + + # Select native instanced object and non-instanced USD object, load + # them into isolate selection. + self.assertIsolateSelect(modelPanel, [cube2Path, cylinderPath], scene) + + # Select native instanced object and Maya object, load them into + # isolate selection. + cmds.select(cube2Path, spherePath) + # cmds.isolateSelect(modelPanel, loadSelected=True) + # does not work: the viewport keeps the previously-selected Maya object + # in the isolate selection. The following command is called by the + # toggleAutoLoad MEL script, which is called by the UI. + cmds.editor(modelPanel, edit=True, updateMainConnection=True) + + visible = [cube2Path, spherePath] + notVisible = scene.copy() + + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + disableIsolateSelect(modelPanel) + if __name__ == '__main__': fixturesUtils.runTests(globals()) diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testUsdNativeInstancingIsolateSelect.py b/test/lib/mayaUsd/render/mayaToHydra/cpp/testUsdNativeInstancingIsolateSelect.py new file mode 100644 index 000000000..9b0017714 --- /dev/null +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testUsdNativeInstancingIsolateSelect.py @@ -0,0 +1,464 @@ +# Copyright 2024 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import fixturesUtils +import mtohUtils +import mayaUsd +import mayaUsd_createStageWithNewLayer +import maya.cmds as cmds +import maya.mel as mel +import usdUtils +from pxr import UsdGeom +import testUtils + +def enableIsolateSelect(modelPanel): + # Surprisingly + # + # cmds.isolateSelect('modelPanel1', state=1) + # + # is insufficient to turn on isolate selection in a viewport, and we must + # use the MEL script used by the menu and Ctrl-1 hotkey. This is because + # the viewport uses the selectionConnection command to filter the selection + # it receives and create its isolate selection, and the the + # mainListConnection, lockMainConnection and unlockMainConnection flags of + # the editor command to suspend changes to its selection connection. See + # the documentation for more details. + cmds.setFocus(modelPanel) + mel.eval("enableIsolateSelect %s 1" % modelPanel) + +def disableIsolateSelect(modelPanel): + cmds.setFocus(modelPanel) + mel.eval("enableIsolateSelect %s 0" % modelPanel) + +# This test is identical to the one in testIsolateSelect.py, except for +# disabled tests. See HYDRA-1245. + +class TestUsdNativeInstancingIsolateSelect(mtohUtils.MayaHydraBaseTestCase): + # MayaHydraBaseTestCase.setUpClass requirement. + _file = __file__ + + # Base class setUp() defines HdStorm as the renderer. + + _pluginsToLoad = ['mayaHydraCppTests', 'mayaHydraFlowViewportAPILocator'] + _pluginsToUnload = [] + + @classmethod + def setUpClass(cls): + super(TestUsdNativeInstancingIsolateSelect, cls).setUpClass() + for p in cls._pluginsToLoad: + if not cmds.pluginInfo(p, q=True, loaded=True): + cls._pluginsToUnload.append(p) + cmds.loadPlugin(p, quiet=True) + + @classmethod + def tearDownClass(cls): + super(TestUsdNativeInstancingIsolateSelect, cls).tearDownClass() + # Clean out the scene to allow all plugins to unload cleanly. + cmds.file(new=True, force=True) + for p in reversed(cls._pluginsToUnload): + if p != 'mayaHydraFlowViewportAPILocator': + cmds.unloadPlugin(p) + + def setupScene(self): + proxyShapePathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + + stage.DefinePrim('/parent1', 'Xform') + stage.DefinePrim('/parent2', 'Xform') + + cylinder1 = UsdGeom.Cylinder.Define(stage, "/parent1/cylinder1") + cone1 = UsdGeom.Cone.Define(stage, "/parent2/cone1") + sphere1 = UsdGeom.Sphere.Define(stage, "/parent2/sphere1") + + # Move objects around so that snapshot comparison is significant. + + # Move USD objects. Can also use undoable Maya cmds.move(), but using + # the USD APIs is simpler. + cylinder1.AddTranslateOp().Set(value=(0, 2, 0)) + cone1.AddTranslateOp().Set(value=(2, 0, 0)) + sphere1.AddTranslateOp().Set(value=(-2, 0, 0)) + + cmds.polyTorus() + cmds.polySphere() + cmds.move(2, 0, 0, r=True) + cmds.polyCube() + cmds.move(-2, 0, 0, r=True) + cmds.polyCone() + cmds.group('pSphere1', 'pCube1') + cmds.move(0, 0, 2, r=True) + cmds.group('pCone1') + cmds.move(0, 0, 2, r=True) + + # Add a Hydra-only data producer. + cmds.createNode("MhFlowViewportAPILocator") + cmds.setAttr("MhFlowViewportAPILocator1.numCubesX", 2) + cmds.setAttr("MhFlowViewportAPILocator1.numCubesY", 2) + cmds.setAttr("MhFlowViewportAPILocator1.cubeHalfSize", 0.25) + cmds.setAttr("MhFlowViewportAPILocator1.cubesDeltaTrans", 1, 1, 1) + + cmds.move(0, 0, 4, "transform1", r=True) + + cmds.refresh() + + return ['|pTorus1', + '|pTorus1|pTorusShape1', + '|stage1|stageShape1,/parent1', + '|stage1|stageShape1,/parent1/cylinder1', + '|stage1|stageShape1,/parent2', + '|stage1|stageShape1,/parent2/cone1', + '|stage1|stageShape1,/parent2/sphere1', + '|group1', + '|group1|pSphere1', + '|group1|pSphere1|pSphereShape1', + '|group1|pCube1', + '|group1|pCube1|pCubeShape1', + '|group2', + '|group2|pCone1', + '|group2|pCone1|pConeShape1'] + + def setupMultiStageScene(self): + scene = self.setupScene() + + proxyShapePathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + + stage.DefinePrim('/parent1', 'Xform') + stage.DefinePrim('/parent2', 'Xform') + + UsdGeom.Cylinder.Define(stage, "/parent1/cylinder1") + UsdGeom.Cone.Define(stage, "/parent2/cone1") + UsdGeom.Sphere.Define(stage, "/parent2/sphere1") + + scene.extend([ + '|stage2|stageShape2,/parent1', + '|stage2|stageShape2,/parent1/cylinder1', + '|stage2|stageShape2,/parent2', + '|stage2|stageShape2,/parent2/cone1', + '|stage2|stageShape2,/parent2/sphere1']) + return scene + + def assertVisible(self, visible): + for v in visible: + self.trace("Testing %s for visibility\n" % v) + cmds.mayaHydraCppTest(v, f="TestHydraPrim.isVisible") + + def assertNotVisible(self, notVisible): + for nv in notVisible: + self.trace("Testing %s for invisibility\n" % nv) + cmds.mayaHydraCppTest(nv, f="TestHydraPrim.notVisible") + + def assertVisibility(self, visible, notVisible): + self.assertVisible(visible) + self.assertNotVisible(notVisible) + + def assertIsolateSelect(self, modelPanel, visible, scene): + cmds.select(*visible) + cmds.isolateSelect(modelPanel, loadSelected=True) + + notVisible = scene.copy() + + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + def _test_isolateSelectSingleViewport(self): + scene = self.setupScene() + + # The default viewport is in the following panel. + modelPanel = 'modelPanel4' + + cmds.select(clear=True) + enableIsolateSelect(modelPanel) + + #============================================================ + # Add + #============================================================ + + # Add a single object to the isolate selection. Only that object, + # its ancestors, and its descendants are visible. + cmds.mayaHydraCppTest(modelPanel, "|pTorus1", f="TestIsolateSelection.add") + + visible = ['|pTorus1', '|pTorus1|pTorusShape1'] + notVisible = scene.copy() + for v in visible: + notVisible.remove(v) + + self.assertVisibility(visible, notVisible) + + # Add a USD object to the isolate selection. + cmds.mayaHydraCppTest(modelPanel, '|stage1|stageShape1,/parent2', f="TestIsolateSelection.add") + + for p in ['|stage1|stageShape1,/parent2', + '|stage1|stageShape1,/parent2/cone1', + '|stage1|stageShape1,/parent2/sphere1']: + visible.append(p) + notVisible.remove(p) + + self.assertVisibility(visible, notVisible) + + #============================================================ + # Remove + #============================================================ + + # Remove the Maya object from the isolate selection. Only the USD + # isolate selected objects are visible. + cmds.mayaHydraCppTest(modelPanel, "|pTorus1", f="TestIsolateSelection.remove") + + visible.clear() + notVisible = scene.copy() + + for p in ['|stage1|stageShape1,/parent2', + '|stage1|stageShape1,/parent2/cone1', + '|stage1|stageShape1,/parent2/sphere1']: + visible.append(p) + notVisible.remove(p) + + self.assertVisibility(visible, notVisible) + + # Remove the USD isolate selected object. The isolate selection + # is empty, isolate selection is enabled, so nothing is now visible. + cmds.mayaHydraCppTest(modelPanel, '|stage1|stageShape1,/parent2', f="TestIsolateSelection.remove") + + visible.clear() + notVisible = scene.copy() + + self.assertVisibility(visible, notVisible) + + #============================================================ + # Clear + #============================================================ + + # Add an object back to the isolate selection. + cmds.mayaHydraCppTest(modelPanel, '|stage1|stageShape1,/parent1/cylinder1', f="TestIsolateSelection.add") + + notVisible = scene.copy() + visible.clear() + + for p in ['|stage1|stageShape1,/parent1', + '|stage1|stageShape1,/parent1/cylinder1']: + visible.append(p) + notVisible.remove(p) + + self.assertVisibility(visible, notVisible) + + # Clear the isolate selection. + cmds.mayaHydraCppTest(modelPanel, f="TestIsolateSelection.clear") + + visible.clear() + notVisible = scene.copy() + + self.assertVisibility(visible, notVisible) + + #============================================================ + # Replace + #============================================================ + + # Add an object back to the isolate selection. + cmds.mayaHydraCppTest(modelPanel, '|group2|pCone1', f="TestIsolateSelection.add") + + notVisible = scene.copy() + visible.clear() + + for p in ['|group2', '|group2|pCone1', '|group2|pCone1|pConeShape1']: + visible.append(p) + notVisible.remove(p) + + self.assertVisibility(visible, notVisible) + + # Replace this isolate selection with a different one. + cmds.mayaHydraCppTest(modelPanel, '|group1|pCube1', '|stage1|stageShape1,/parent2/cone1', f="TestIsolateSelection.replace") + + visible.clear() + notVisible = scene.copy() + + for p in ['|group1', '|group1|pCube1', '|group1|pCube1|pCubeShape1', + '|stage1|stageShape1,/parent2', + '|stage1|stageShape1,/parent2/cone1']: + visible.append(p) + notVisible.remove(p) + + self.assertVisibility(visible, notVisible) + + # Disable the isolate selection to avoid affecting other tests. + disableIsolateSelect(modelPanel) + + # A robust multi-viewport test would have tested scene index prim + # visibility for each viewport, and demonstrated per-viewport visibility. + # Unfortunately, tracing demonstrates that we can't obtain scene index prim + # visibility in one viewport before a draw is performed in another + # viewport. Since visibility is according to the last viewport drawn, and + # don't know of way to control order of viewport draw, this testing + # strategy fails. + # + # Unfortunately performing image comparisons is also not possible, as at + # time of writing playblast doesn't respect isolate select when using + # MayaHydra in a multi-viewport setting. Therefore, the following test is + # weak and does not validate results. + def _test_isolateSelectMultiViewport(self): + scene = self.setupScene() + + # We start in single viewport mode. Set an isolate selection there. + cmds.select('|group1', '|stage1|stageShape1,/parent1/cylinder1') + enableIsolateSelect("modelPanel4") + + notVisible = scene.copy() + visible = ['|group1', + '|group1|pSphere1', + '|group1|pSphere1|pSphereShape1', + '|group1|pCube1', + '|group1|pCube1|pCubeShape1', + '|stage1|stageShape1,/parent1', + '|stage1|stageShape1,/parent1/cylinder1'] + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # Move the camera closer for a view where objects fill in more of the + # viewport. FrameSelectedWithoutChildren() is good, but a manually + # chosen camera position is better. + cmds.setAttr("persp.translate", 4.9, 3.5, 5.7) + cmds.select(clear=1) + + cmds.refresh() + + self.assertSnapshotClose("singleViewportIsolateSelectCylinder1.png", 0.1, 2) + + # Switch to four-up viewport mode. Set the renderer in each new + # viewport to be Hydra Storm. Viewport 4 is already set. + cmds.FourViewLayout() + visible = scene.copy() + notVisible.clear() + for i in range(1, 4): + cmds.setFocus('modelPanel'+str(i)) + self.setHdStormRenderer() + + cmds.select('|pTorus1') + + enableIsolateSelect("modelPanel1") + + cmds.refresh() + + # As a final step disable the isolate selections to avoid affecting + # other tests. + for i in range(1, 5): + disableIsolateSelect('modelPanel'+str(i)) + + def _test_isolateSelectMultipleStages(self): + scene = self.setupMultiStageScene() + + modelPanel = 'modelPanel4' + + cmds.select('|group1|pCube1', '|stage1|stageShape1,/parent2/cone1', + '|stage2|stageShape2,/parent1/cylinder1') + enableIsolateSelect(modelPanel) + + visible = ['|group1', '|group1|pCube1', '|group1|pCube1|pCubeShape1', + '|stage1|stageShape1,/parent2', + '|stage1|stageShape1,/parent2/cone1', + '|stage2|stageShape2,/parent1', + '|stage2|stageShape2,/parent1/cylinder1'] + notVisible = scene.copy() + + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # As a final step disable the isolate selection to avoid affecting + # other tests. + disableIsolateSelect(modelPanel) + + def test_isolateSelectNativeInstancing(self): + + # Read in a scene with native instancing. + usdScenePath = testUtils.getTestScene('testUsdNativeInstances', 'instancedCubeHierarchies.usda') + + proxyShapePathStr = usdUtils.createStageFromFile(usdScenePath) + + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + + # Add a non-instanced prim to the USD stage + UsdGeom.Cylinder.Define(stage, "/cylinder1") + + # Add a Maya object. + cmds.polySphere() + cmds.move(0, 0, 2, r=True) + + cube1Path = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cubeHierarchies/cubes_1' + cube2Path = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cubeHierarchies/cubes_2' + cylinderPath = '|instancedCubeHierarchies|instancedCubeHierarchiesShape,/cylinder1' + spherePath = '|pSphere1' + + scene = [cube1Path, cube2Path, cylinderPath, spherePath] + + cmds.select(clear=True) + + # Isolate select not turned on, everything visible. + visible = scene + notVisible = [] + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # Turn isolate select on, nothing selected, nothing visible. + visible = [] + notVisible = scene + + modelPanel = 'modelPanel4' + enableIsolateSelect(modelPanel) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + # Select native instanced object, load it into isolate selection. + self.assertIsolateSelect(modelPanel, [cube1Path], scene) + + # Select native instanced object and non-instanced USD object, load + # them into isolate selection. + self.assertIsolateSelect(modelPanel, [cube2Path, cylinderPath], scene) + + # Select native instanced object and Maya object, load them into + # isolate selection. + cmds.select(cube2Path, spherePath) + # cmds.isolateSelect(modelPanel, loadSelected=True) + # does not work: the viewport keeps the previously-selected Maya object + # in the isolate selection. The following command is called by the + # toggleAutoLoad MEL script, which is called by the UI. + cmds.editor(modelPanel, edit=True, updateMainConnection=True) + + visible = [cube2Path, spherePath] + notVisible = scene.copy() + + for p in visible: + notVisible.remove(p) + + cmds.refresh() + + self.assertVisibility(visible, notVisible) + + disableIsolateSelect(modelPanel) + +if __name__ == '__main__': + fixturesUtils.runTests(globals())