diff --git a/cmake/modules/FindMaya.cmake b/cmake/modules/FindMaya.cmake index a315b86cfa..5823b3729a 100644 --- a/cmake/modules/FindMaya.cmake +++ b/cmake/modules/FindMaya.cmake @@ -464,6 +464,12 @@ if (MAYA_API_VERSION VERSION_GREATER_EQUAL 20230200) message(STATUS "Maya has UFE gizmo drawing") endif() +set(MAYA_HAS_SCENE_RENDER_SETTINGS FALSE CACHE INTERNAL "sceneRenderSettings") +if (MAYA_API_VERSION VERSION_GREATER_EQUAL 20270000) + set(MAYA_HAS_SCENE_RENDER_SETTINGS TRUE CACHE INTERNAL "sceneRenderSettings") + message(STATUS "Maya has scene render settings support") +endif() + set(MAYA_LINUX_BUILT_WITH_CXX11_ABI FALSE CACHE INTERNAL "MayaLinuxBuiltWithCxx11ABI") if(IS_LINUX AND MAYA_Foundation_LIBRARY) # Determine if Maya (on Linux) was built using the new CXX11 ABI. diff --git a/lib/mayaUsd/CMakeLists.txt b/lib/mayaUsd/CMakeLists.txt index 28bea18628..10cd96e7f6 100644 --- a/lib/mayaUsd/CMakeLists.txt +++ b/lib/mayaUsd/CMakeLists.txt @@ -41,6 +41,7 @@ target_compile_definitions(${PROJECT_NAME} $<$:GL_GLEXT_PROTOTYPES> $<$:GLX_GLXEXT_PROTOTYPES> $<$:WANT_MATERIALX_BUILD> + $<$:MAYA_HAS_SCENE_RENDER_SETTINGS> $<$:WANT_QT_BUILD> $<$:BUILD_HDMAYA> $<$:WANT_ADSK_USD_ASSET_RESOLVER_BUILD> diff --git a/lib/mayaUsd/fileio/jobs/writeJob.cpp b/lib/mayaUsd/fileio/jobs/writeJob.cpp index e553ebff47..332eb6703d 100644 --- a/lib/mayaUsd/fileio/jobs/writeJob.cpp +++ b/lib/mayaUsd/fileio/jobs/writeJob.cpp @@ -264,8 +264,17 @@ class AutoUpAxisChanger : public MayaUsd::AutoUndoCommands static const char scriptPrefix[] = // Preserve the selection. Grouping and ungrouping changes it. "string $selection[] = `ls -selection`;\n" - // Find all root nodes. "string $rootNodeNames[] = `ls -assemblies`;\n" +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + // Exclude the internal SceneRenderSettings singleton which is not + // scene content and must not be grouped. + "string $filteredRoots[];\n" + "for ($r in $rootNodeNames) {\n" + " string $shapes[] = `listRelatives -shapes -type mayaUsdSceneRenderSettings $r`;\n" + " if (size($shapes) == 0) $filteredRoots[size($filteredRoots)] = $r;\n" + "}\n" + "$rootNodeNames = $filteredRoots;\n" +#endif // Group all root node under a new group: // // - Use -absolute to keep the grouped node world positions diff --git a/lib/mayaUsd/fileio/writeJobContext.cpp b/lib/mayaUsd/fileio/writeJobContext.cpp index 5d98f1254d..33f23aec3a 100644 --- a/lib/mayaUsd/fileio/writeJobContext.cpp +++ b/lib/mayaUsd/fileio/writeJobContext.cpp @@ -24,6 +24,10 @@ #include #include +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS +#include +#endif + #include #include #include @@ -399,6 +403,28 @@ bool UsdMayaWriteJobContext::_NeedToTraverse(const MDagPath& curDag) const } } + // Always skip the SceneRenderSettings singleton node and its parent + // transform – its internal USD stage is not part of the scene content + // that should be exported. +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + { + MFnDependencyNode mfnNode(ob); + if (mfnNode.typeId() == MAYAUSD_NS::UsdSceneRenderSettings::typeId) { + return false; + } + // Also skip the parent transform of the SceneRenderSettings shape. + if (ob.hasFn(MFn::kTransform)) { + MFnDagNode dagNode(curDag); + for (unsigned int i = 0; i < dagNode.childCount(); ++i) { + MFnDependencyNode childFn(dagNode.child(i)); + if (childFn.typeId() == MAYAUSD_NS::UsdSceneRenderSettings::typeId) { + return false; + } + } + } + } +#endif + if (!mArgs.filteredTypeIds.empty()) { MFnDependencyNode mfnNode(ob); if (mArgs.filteredTypeIds.find(mfnNode.typeId().id()) != mArgs.filteredTypeIds.end()) { diff --git a/lib/mayaUsd/nodes/CMakeLists.txt b/lib/mayaUsd/nodes/CMakeLists.txt index 19ec79e6fc..63a620bca8 100644 --- a/lib/mayaUsd/nodes/CMakeLists.txt +++ b/lib/mayaUsd/nodes/CMakeLists.txt @@ -16,6 +16,13 @@ target_sources(${PROJECT_NAME} usdPrimProvider.cpp ) +if(MAYA_HAS_SCENE_RENDER_SETTINGS) + target_sources(${PROJECT_NAME} + PRIVATE + sceneRenderSettings.cpp + ) +endif() + set(HEADERS hdImagingShape.h layerManager.h @@ -31,6 +38,10 @@ set(HEADERS usdPrimProvider.h ) +if(MAYA_HAS_SCENE_RENDER_SETTINGS) + list(APPEND HEADERS sceneRenderSettings.h) +endif() + # ----------------------------------------------------------------------------- # promoted headers # ----------------------------------------------------------------------------- diff --git a/lib/mayaUsd/nodes/proxyShapePlugin.cpp b/lib/mayaUsd/nodes/proxyShapePlugin.cpp index 294a3a89f2..22029d7ab8 100644 --- a/lib/mayaUsd/nodes/proxyShapePlugin.cpp +++ b/lib/mayaUsd/nodes/proxyShapePlugin.cpp @@ -23,6 +23,9 @@ #include #include #include +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS +#include +#endif #include #include #include @@ -171,6 +174,23 @@ MStatus MayaUsdProxyShapePlugin::initialize(MFnPlugin& plugin) status = MayaUsd::MayaUsdProxyShapeStageExtraData::initialize(); CHECK_MSTATUS(status); +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + // Only register and activate the SceneRenderSettings node for mayaUsdPlugin. + if (plugin.name() == "mayaUsdPlugin") { + status = plugin.registerNode( + MayaUsd::UsdSceneRenderSettings::typeName, + MayaUsd::UsdSceneRenderSettings::typeId, + MayaUsd::UsdSceneRenderSettings::creator, + MayaUsd::UsdSceneRenderSettings::initialize, + MPxNode::kLocatorNode); + CHECK_MSTATUS(status); + + MayaUsd::UsdSceneRenderSettings::installCallbacks(); + // Maya doesn't send kAfterNew for the default scene that exists at startup. + MayaUsd::UsdSceneRenderSettings::findOrCreateInstance(); + } +#endif + return status; } @@ -192,6 +212,14 @@ MStatus MayaUsdProxyShapePlugin::finalize(MFnPlugin& plugin) return MS::kSuccess; } +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + if (plugin.name() == "mayaUsdPlugin") { + MayaUsd::UsdSceneRenderSettings::removeCallbacks(); + MStatus srsStatus = plugin.deregisterNode(MayaUsd::UsdSceneRenderSettings::typeId); + CHECK_MSTATUS(srsStatus); + } +#endif + MStatus status = HdVP2ShaderFragments::deregisterFragments(); CHECK_MSTATUS(status); diff --git a/lib/mayaUsd/nodes/sceneRenderSettings.cpp b/lib/mayaUsd/nodes/sceneRenderSettings.cpp new file mode 100644 index 0000000000..a7af0755b9 --- /dev/null +++ b/lib/mayaUsd/nodes/sceneRenderSettings.cpp @@ -0,0 +1,484 @@ +// +// Copyright 2025 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. +// +// WIP – This file is under active development and should not be used in production. + +#include "sceneRenderSettings.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { +// Re-entrancy guard for callback-driven node creation. +bool _isCreatingInstance = false; +} // namespace + +namespace MAYAUSD_NS_DEF { + +const MTypeId UsdSceneRenderSettings::typeId(0x580000A6); +const MString UsdSceneRenderSettings::typeName("mayaUsdSceneRenderSettings"); + +MObject UsdSceneRenderSettings::serializedRootLayerAttr; +MObject UsdSceneRenderSettings::serializedSessionLayerAttr; +MObject UsdSceneRenderSettings::outStageCacheIdAttr; + +MObjectHandle UsdSceneRenderSettings::_cachedInstance; +MCallbackId UsdSceneRenderSettings::_afterNewCbId = 0; +MCallbackId UsdSceneRenderSettings::_afterOpenCbId = 0; +MCallbackId UsdSceneRenderSettings::_beforeSaveCbId = 0; + +/* static */ +void* UsdSceneRenderSettings::creator() { return new UsdSceneRenderSettings(); } + +/* static */ +MStatus UsdSceneRenderSettings::initialize() +{ + MStatus status; + + MFnTypedAttribute typedAttrFn; + MFnStringData stringDataFn; + const MObject defaultStringDataObj = stringDataFn.create(""); + + // Serialized root layer: storable, hidden, internal. + serializedRootLayerAttr = typedAttrFn.create( + "serializedRootLayer", "srl", MFnData::kString, defaultStringDataObj, &status); + CHECK_MSTATUS_AND_RETURN_IT(status); + typedAttrFn.setStorable(true); + typedAttrFn.setHidden(true); + typedAttrFn.setInternal(true); + status = addAttribute(serializedRootLayerAttr); + CHECK_MSTATUS_AND_RETURN_IT(status); + + // Serialized session layer: storable, hidden, internal. + serializedSessionLayerAttr = typedAttrFn.create( + "serializedSessionLayer", "ssl", MFnData::kString, defaultStringDataObj, &status); + CHECK_MSTATUS_AND_RETURN_IT(status); + typedAttrFn.setStorable(true); + typedAttrFn.setHidden(true); + typedAttrFn.setInternal(true); + status = addAttribute(serializedSessionLayerAttr); + CHECK_MSTATUS_AND_RETURN_IT(status); + + // Output stage cache ID: allows downstream consumers (Arnold, Bifrost, ...) + // to discover the stage via UsdUtilsStageCache. + MFnNumericAttribute numericAttrFn; + outStageCacheIdAttr + = numericAttrFn.create("outStageCacheId", "ostcid", MFnNumericData::kInt, -1, &status); + CHECK_MSTATUS_AND_RETURN_IT(status); + numericAttrFn.setStorable(false); + numericAttrFn.setWritable(false); + status = addAttribute(outStageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(status); + + status = attributeAffects(serializedRootLayerAttr, outStageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(status); + status = attributeAffects(serializedSessionLayerAttr, outStageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(status); + + return status; +} + +UsdSceneRenderSettings::UsdSceneRenderSettings() + : MPxLocatorNode() +{ +} + +UsdSceneRenderSettings::~UsdSceneRenderSettings() +{ + // Remove our stage from the global cache so nothing holds it alive + // after the node is destroyed. + if (_stage) { + UsdUtilsStageCache::Get().Erase(_stage); + _stage = nullptr; + } +} + +bool UsdSceneRenderSettings::isBounded() const { return false; } + +MStatus UsdSceneRenderSettings::compute(const MPlug& plug, MDataBlock& dataBlock) +{ + if (plug == outStageCacheIdAttr) { + return computeOutStageCacheId(dataBlock); + } + + return MS::kUnknownParameter; +} + +MStatus UsdSceneRenderSettings::computeOutStageCacheId(MDataBlock& dataBlock) +{ + MStatus status; + + ensureStage(); + + if (!_stage) { + return MS::kFailure; + } + + int cacheId = -1; + auto id = UsdUtilsStageCache::Get().Insert(_stage); + if (id) + cacheId = id.ToLongInt(); + + MDataHandle outHandle = dataBlock.outputValue(outStageCacheIdAttr, &status); + CHECK_MSTATUS_AND_RETURN_IT(status); + + outHandle.set(cacheId); + outHandle.setClean(); + + return MS::kSuccess; +} + +UsdTimeCode UsdSceneRenderSettings::getTime() const { return UsdTimeCode::Default(); } + +UsdStageRefPtr UsdSceneRenderSettings::getUsdStage() const +{ + ensureStage(); + return _stage; +} + +/* static */ +UsdStageRefPtr UsdSceneRenderSettings::getStage() +{ + MObject obj = findOrCreateInstance(); + if (obj.isNull()) { + return nullptr; + } + + MFnDependencyNode depFn(obj); + auto* node = dynamic_cast(depFn.userNode()); + if (!node) { + return nullptr; + } + return node->getUsdStage(); +} + +void UsdSceneRenderSettings::ensureStage() const +{ + if (_stage) { + return; + } + + SdfLayerRefPtr rootLayer = SdfLayer::CreateAnonymous("sceneRenderSettingsRoot"); + SdfLayerRefPtr sessionLayer = SdfLayer::CreateAnonymous("sceneRenderSettingsSession"); + _stage = UsdStage::Open(rootLayer, sessionLayer); + + populateDefaultRenderSettings(); +} + +void UsdSceneRenderSettings::populateDefaultRenderSettings() const +{ + if (!_stage) { + return; + } + + // Create a Scope "Render" to hold all render settings, + // per UsdRender conventions (https://openusd.org/dev/api/usd_render_page_front.html). + const SdfPath renderScopePath("/Render"); + const SdfPath renderSettingsPath("/Render/SceneRenderSettings"); + + UsdGeomScope::Define(_stage, renderScopePath); + // Define the RenderSettings prim, leave the attribute un-authored, + // so it falls back to the default USD RenderSettings schema defaults. + UsdRenderSettings renderSettings = UsdRenderSettings::Define(_stage, renderSettingsPath); + if (!renderSettings) { + return; + } + + // Set stage metadata to point to the default render settings prim. + _stage->SetMetadata(TfToken("renderSettingsPrimPath"), renderSettingsPath.GetString()); +} + +void UsdSceneRenderSettings::serializeToAttributes() +{ + if (!_stage) { + return; + } + + MFnDependencyNode depFn(thisMObject()); + + // Temporarily unlock to allow attribute writes. + bool wasLocked = depFn.isLocked(); + if (wasLocked) { + depFn.setLocked(false); + } + + std::string rootStr; + _stage->GetRootLayer()->ExportToString(&rootStr); + MPlug rootPlug(thisMObject(), serializedRootLayerAttr); + rootPlug.setString(MString(rootStr.c_str())); + + std::string sessionStr; + _stage->GetSessionLayer()->ExportToString(&sessionStr); + MPlug sessionPlug(thisMObject(), serializedSessionLayerAttr); + sessionPlug.setString(MString(sessionStr.c_str())); + + if (wasLocked) { + depFn.setLocked(true); + } +} + +void UsdSceneRenderSettings::deserializeFromAttributes() +{ + MPlug rootPlug(thisMObject(), serializedRootLayerAttr); + MPlug sessionPlug(thisMObject(), serializedSessionLayerAttr); + + MString rootStr = rootPlug.asString(); + MString sessionStr = sessionPlug.asString(); + + SdfLayerRefPtr rootLayer = SdfLayer::CreateAnonymous("sceneRenderSettingsRoot"); + SdfLayerRefPtr sessionLayer = SdfLayer::CreateAnonymous("sceneRenderSettingsSession"); + + if (rootStr.length() > 0) { + rootLayer->ImportFromString(std::string(rootStr.asChar())); + } + + if (sessionStr.length() > 0) { + sessionLayer->ImportFromString(std::string(sessionStr.asChar())); + } + + _stage = UsdStage::Open(rootLayer, sessionLayer); +} + +// --------------------------------------------------------------------------- +// Singleton management +// --------------------------------------------------------------------------- + +/* static */ +MObject UsdSceneRenderSettings::findInstance() +{ + if (_cachedInstance.isValid()) { + MObject cachedObj = _cachedInstance.object(); + MFnDependencyNode depFn(cachedObj); + if (!depFn.isFromReferencedFile()) { + return cachedObj; + } + // Cached instance is from a reference — discard and re-scan. + _cachedInstance = MObjectHandle(); + } + + MItDependencyNodes it(MFn::kPluginLocatorNode); + while (!it.isDone()) { + MObject obj = it.thisNode(); + MFnDependencyNode depFn(obj); + if (depFn.typeId() == typeId && !depFn.isFromReferencedFile()) { + _cachedInstance = MObjectHandle(obj); + return obj; + } + it.next(); + } + return MObject::kNullObj; +} + +/* static */ +MObject UsdSceneRenderSettings::findOrCreateInstance() +{ + MObject existing = findInstance(); + if (!existing.isNull()) { + return existing; + } + + if (_isCreatingInstance) { + return MObject::kNullObj; + } + _isCreatingInstance = true; + + MDagModifier modifier; + MObject transformObj = modifier.createNode(typeName); + modifier.doIt(); + + // The createNode for a DAG node creates a transform + shape. + // Find the shape under the transform. + MObject shapeObj = MObject::kNullObj; + MFnDagNode dagFn(transformObj); + if (dagFn.typeName() == typeName) { + // If createNode returned the shape directly. + shapeObj = transformObj; + } else { + // The transform was returned; find the shape child. + for (unsigned int i = 0; i < dagFn.childCount(); ++i) { + MObject child = dagFn.child(i); + MFnDependencyNode childDepFn(child); + if (childDepFn.typeId() == typeId) { + shapeObj = child; + break; + } + } + } + + if (!shapeObj.isNull()) { + _cachedInstance = MObjectHandle(shapeObj); + + MFnDependencyNode depFn(shapeObj); + depFn.setLocked(true); + + // Name and hide the parent transform. + // Note: the transform is intentionally NOT locked so that generic + // MEL scripts (e.g. the up-axis export helper that groups all + // assemblies) can reparent it without errors. + MFnDagNode shapeDagFn(shapeObj); + MObject parentObj = shapeDagFn.parent(0); + if (!parentObj.isNull()) { + MFnDependencyNode parentDepFn(parentObj); + parentDepFn.setName("SceneRenderSettings"); + MPlug hiddenPlug = parentDepFn.findPlug("hiddenInOutliner", true); + if (!hiddenPlug.isNull()) { + hiddenPlug.setBool(true); + } + } + + // Dirty the UFE stage map so this node's stage is discoverable. + // Unlike MayaUsdProxyShapeBase, this node is not recognized by + // UsdStageMap::processNodeAdded(), so we must trigger a rebuild + // explicitly for the stage map to discover it via + // discoverSceneRenderSettingsNode() during rebuildIfDirty(). + MayaUsd::ufe::setStageMapDirty(); + } + + _isCreatingInstance = false; + return shapeObj; +} + +// --------------------------------------------------------------------------- +// Scene callbacks +// --------------------------------------------------------------------------- + +namespace { + +void afterNewCallback(void* /*clientData*/) { UsdSceneRenderSettings::findOrCreateInstance(); } + +void afterOpenCallback(void* /*clientData*/) +{ + MObject instance = UsdSceneRenderSettings::findInstance(); + if (instance.isNull()) { + UsdSceneRenderSettings::findOrCreateInstance(); + return; + } + + MFnDependencyNode depFn(instance); + auto* node = dynamic_cast(depFn.userNode()); + if (!node) { + return; + } + + // kAfterSceneReadAndRecordEdits fires for File > Open, Import, and + // Reference. Only deserialize when the node was just restored from a + // saved file (stage not yet created). If the stage already exists we + // are handling a reference or import — leave the live stage untouched. + if (!node->hasStage()) { + node->deserializeFromAttributes(); + } + + // Dirty the stage map so the (restored or existing) stage is discoverable + // (see comment in findOrCreateInstance for why this is needed). + MayaUsd::ufe::setStageMapDirty(); +} + +void beforeSaveCallback(void* /*clientData*/) +{ + MObject instance = UsdSceneRenderSettings::findInstance(); + if (instance.isNull()) { + return; + } + + MFnDependencyNode depFn(instance); + auto* node = dynamic_cast(depFn.userNode()); + if (node) { + node->serializeToAttributes(); + } +} + +} // namespace + +/* static */ +void UsdSceneRenderSettings::installCallbacks() +{ + MStatus status; + + _afterNewCbId + = MSceneMessage::addCallback(MSceneMessage::kAfterNew, afterNewCallback, nullptr, &status); + CHECK_MSTATUS(status); + + _afterOpenCbId = MSceneMessage::addCallback( + MSceneMessage::kAfterSceneReadAndRecordEdits, afterOpenCallback, nullptr, &status); + CHECK_MSTATUS(status); + + _beforeSaveCbId = MSceneMessage::addCallback( + MSceneMessage::kBeforeSave, beforeSaveCallback, nullptr, &status); + CHECK_MSTATUS(status); +} + +/* static */ +void UsdSceneRenderSettings::removeCallbacks() +{ + if (_afterNewCbId) { + MSceneMessage::removeCallback(_afterNewCbId); + _afterNewCbId = 0; + } + if (_afterOpenCbId) { + MSceneMessage::removeCallback(_afterOpenCbId); + _afterOpenCbId = 0; + } + if (_beforeSaveCbId) { + MSceneMessage::removeCallback(_beforeSaveCbId); + _beforeSaveCbId = 0; + } + + // Delete the singleton node so the plugin can be unloaded. + // The node (and its parent transform) are locked, so unlock first. + MObject instance = findInstance(); + if (!instance.isNull()) { + MFnDagNode shapeDagFn(instance); + MObject parentObj = shapeDagFn.parent(0); + + MFnDependencyNode depFn(instance); + depFn.setLocked(false); + if (!parentObj.isNull()) { + MFnDependencyNode parentDepFn(parentObj); + parentDepFn.setLocked(false); + } + + MDagModifier modifier; + modifier.deleteNode(instance); + if (!parentObj.isNull()) { + modifier.deleteNode(parentObj); + } + modifier.doIt(); + + _cachedInstance = MObjectHandle(); + } +} + +} // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/nodes/sceneRenderSettings.h b/lib/mayaUsd/nodes/sceneRenderSettings.h new file mode 100644 index 0000000000..162b764a77 --- /dev/null +++ b/lib/mayaUsd/nodes/sceneRenderSettings.h @@ -0,0 +1,111 @@ +// +// Copyright 2026 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 MAYAUSD_SCENE_RENDER_SETTINGS_H +#define MAYAUSD_SCENE_RENDER_SETTINGS_H + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace MAYAUSD_NS_DEF { + +/*! \brief Singleton DAG node that holds an in-memory USD Stage for scene-level render settings. + * + * \warning WIP – This class is under active development and should not be used in production. + * Its API may change without notice. + * + * This node is automatically created on scene new and restored on scene open. + * It is hidden in the outliner by default. Its USD stage data is serialized + * into the Maya scene file so no external USD files are needed. + * + * The node implements ProxyStageProvider so it works with the existing + * MayaUsdAPI::ProxyStage interface. + */ +class MAYAUSD_CORE_PUBLIC UsdSceneRenderSettings + : public MPxLocatorNode + , public PXR_NS::ProxyStageProvider +{ +public: + static const MTypeId typeId; + static const MString typeName; + + // Attributes + static MObject serializedRootLayerAttr; + static MObject serializedSessionLayerAttr; + static MObject outStageCacheIdAttr; + + static void* creator(); + static MStatus initialize(); + + // MPxLocatorNode overrides + MStatus compute(const MPlug& plug, MDataBlock& dataBlock) override; + bool isBounded() const override; + + // ProxyStageProvider interface + PXR_NS::UsdTimeCode getTime() const override; + PXR_NS::UsdStageRefPtr getUsdStage() const override; + + // Singleton management + static MObject findInstance(); + static MObject findOrCreateInstance(); + + //! Get the USD stage from the singleton node, creating it if needed. + static PXR_NS::UsdStageRefPtr getStage(); + + // Scene callback management + static void installCallbacks(); + static void removeCallbacks(); + + //! Return true if the internal stage has already been created. + //! Unlike getUsdStage(), this does not trigger lazy creation. + bool hasStage() const { return _stage != nullptr; } + + // Serialization (called from scene callbacks) + void serializeToAttributes(); + void deserializeFromAttributes(); + +private: + UsdSceneRenderSettings(); + ~UsdSceneRenderSettings() override; + + UsdSceneRenderSettings(const UsdSceneRenderSettings&) = delete; + UsdSceneRenderSettings& operator=(const UsdSceneRenderSettings&) = delete; + + MStatus computeOutStageCacheId(MDataBlock& dataBlock); + + void ensureStage() const; + void populateDefaultRenderSettings() const; + + mutable PXR_NS::UsdStageRefPtr _stage; + + static MObjectHandle _cachedInstance; + static MCallbackId _afterNewCbId; + static MCallbackId _afterOpenCbId; + static MCallbackId _beforeSaveCbId; +}; + +} // namespace MAYAUSD_NS_DEF + +#endif // MAYAUSD_SCENE_RENDER_SETTINGS_H diff --git a/lib/mayaUsd/python/CMakeLists.txt b/lib/mayaUsd/python/CMakeLists.txt index 3f1778e9e5..3a0cf32a41 100644 --- a/lib/mayaUsd/python/CMakeLists.txt +++ b/lib/mayaUsd/python/CMakeLists.txt @@ -60,6 +60,13 @@ target_sources(${PYTHON_TARGET_NAME} wrapShadingMode.cpp ) +if(MAYA_HAS_SCENE_RENDER_SETTINGS) + target_sources(${PYTHON_TARGET_NAME} + PRIVATE + wrapSceneRenderSettings.cpp + ) +endif() + # Edit as Maya requires UFE path mapping. if(CMAKE_UFE_V3_FEATURES_AVAILABLE) target_sources(${PYTHON_TARGET_NAME} @@ -78,6 +85,7 @@ target_compile_definitions(${PYTHON_TARGET_NAME} MFB_PACKAGE_NAME=${PROJECT_NAME} MFB_ALT_PACKAGE_NAME=${PROJECT_NAME} MFB_PACKAGE_MODULE=${PROJECT_NAME} + $<$:MAYA_HAS_SCENE_RENDER_SETTINGS> ) mayaUsd_compile_config(${PYTHON_TARGET_NAME}) diff --git a/lib/mayaUsd/python/module.cpp b/lib/mayaUsd/python/module.cpp index caf83785bb..bdf92c0db9 100644 --- a/lib/mayaUsd/python/module.cpp +++ b/lib/mayaUsd/python/module.cpp @@ -71,4 +71,7 @@ TF_WRAP_MODULE TF_WRAP(SchemaApiAdaptor); TF_WRAP(ShadingUtil); TF_WRAP(ShadingMode); +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + TF_WRAP(SceneRenderSettings); +#endif } diff --git a/lib/mayaUsd/python/wrapSceneRenderSettings.cpp b/lib/mayaUsd/python/wrapSceneRenderSettings.cpp new file mode 100644 index 0000000000..3e61416870 --- /dev/null +++ b/lib/mayaUsd/python/wrapSceneRenderSettings.cpp @@ -0,0 +1,93 @@ +// +// Copyright 2026 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. +// + +// WIP – This file is under active development and should not be used in production. + +#include + +#include +#include +#include + +#include + +namespace { + +// Token for the render settings prim path stage metadata key. +static const PXR_NS::TfToken kRenderSettingsPrimPathToken("renderSettingsPrimPath"); + +// --------------------------------------------------------------------------- +// Singleton discovery / stage access +// --------------------------------------------------------------------------- + +std::string SceneRenderSettings_find() +{ + MObject obj = MayaUsd::UsdSceneRenderSettings::findInstance(); + if (obj.isNull()) { + return {}; + } + MFnDagNode dagFn(obj); + return dagFn.fullPathName().asChar(); +} + +PXR_NS::UsdStageRefPtr SceneRenderSettings_getUsdStage() +{ + return MayaUsd::UsdSceneRenderSettings::getStage(); +} + +PXR_NS::UsdPrim getDefaultRenderSettingsPrim() +{ + auto stage = MayaUsd::UsdSceneRenderSettings::getStage(); + if (!stage) { + return {}; + } + std::string path; + stage->GetMetadata(kRenderSettingsPrimPathToken, &path); + if (path.empty()) { + return {}; + } + return stage->GetPrimAtPath(PXR_NS::SdfPath(path)); +} + +} // namespace + +using namespace PXR_BOOST_PYTHON_NAMESPACE; + +void wrapSceneRenderSettings() +{ + class_( + "SceneRenderSettings", + "WIP - This API is under active development and should not be used in production. " + "It may change without notice.", + no_init) + + // Discovery / stage access + .def( + "find", + &SceneRenderSettings_find, + "[WIP] Find the singleton node. Returns its Maya DAG path, or empty if not found.") + .staticmethod("find") + .def( + "getUsdStage", + &SceneRenderSettings_getUsdStage, + "[WIP] Get the USD stage from the singleton node.") + .staticmethod("getUsdStage") + .def( + "getDefaultRenderSettingsPrim", + &getDefaultRenderSettingsPrim, + "[WIP] Get the default render settings prim from the USD stage.") + .staticmethod("getDefaultRenderSettingsPrim"); +} diff --git a/lib/mayaUsd/ufe/MayaUsdContextOpsHandler.cpp b/lib/mayaUsd/ufe/MayaUsdContextOpsHandler.cpp index e12ca4d212..ee26e2cd9d 100644 --- a/lib/mayaUsd/ufe/MayaUsdContextOpsHandler.cpp +++ b/lib/mayaUsd/ufe/MayaUsdContextOpsHandler.cpp @@ -18,6 +18,11 @@ #include #include +#include + +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS +#include +#endif namespace MAYAUSD_NS_DEF { namespace ufe { @@ -36,6 +41,24 @@ MayaUsdContextOpsHandler::Ptr MayaUsdContextOpsHandler::create() Ufe::ContextOps::Ptr MayaUsdContextOpsHandler::contextOps(const Ufe::SceneItem::Ptr& item) const { + // Suppress context ops for USD prims under the SceneRenderSettings node. +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + if (item) { + const auto& path = item->path(); + if (!path.empty()) { + const Ufe::Path gatewayPath + = path.nbSegments() > 1 ? Ufe::Path(path.getSegments()[0]) : path; + auto dagPath = UsdMayaUtil::nameToDagPath(gatewayPath.popHead().string()); + if (dagPath.isValid()) { + MFnDependencyNode depFn(dagPath.node()); + if (depFn.typeId() == UsdSceneRenderSettings::typeId) { + return nullptr; + } + } + } + } +#endif + auto usdItem = downcast(item); #if !defined(NDEBUG) assert(usdItem); diff --git a/lib/mayaUsd/ufe/ProxyShapeContextOpsHandler.cpp b/lib/mayaUsd/ufe/ProxyShapeContextOpsHandler.cpp index f37ab39ac1..04a8b9707f 100644 --- a/lib/mayaUsd/ufe/ProxyShapeContextOpsHandler.cpp +++ b/lib/mayaUsd/ufe/ProxyShapeContextOpsHandler.cpp @@ -47,7 +47,8 @@ ProxyShapeContextOpsHandler::create(const Ufe::ContextOpsHandler::Ptr& mayaConte Ufe::ContextOps::Ptr ProxyShapeContextOpsHandler::contextOps(const Ufe::SceneItem::Ptr& item) const { - if (isAGatewayType(UsdUfe::getSceneItemNodeType(item))) { + auto nodeType = UsdUfe::getSceneItemNodeType(item); + if (isAGatewayType(nodeType) && !isSceneRenderSettingsNode(nodeType)) { // UsdContextOps expects a UsdSceneItem which wraps a prim, so // create one using the pseudo-root and our own path. PXR_NS::UsdStageWeakPtr stage = getStage(item->path()); diff --git a/lib/mayaUsd/ufe/ProxyShapeHierarchyHandler.cpp b/lib/mayaUsd/ufe/ProxyShapeHierarchyHandler.cpp index 73d1f35893..0154ea1ca7 100644 --- a/lib/mayaUsd/ufe/ProxyShapeHierarchyHandler.cpp +++ b/lib/mayaUsd/ufe/ProxyShapeHierarchyHandler.cpp @@ -46,7 +46,8 @@ ProxyShapeHierarchyHandler::create(const Ufe::HierarchyHandler::Ptr& mayaHierarc Ufe::Hierarchy::Ptr ProxyShapeHierarchyHandler::hierarchy(const Ufe::SceneItem::Ptr& item) const { - if (isAGatewayType(UsdUfe::getSceneItemNodeType(item))) { + auto nodeType = UsdUfe::getSceneItemNodeType(item); + if (isAGatewayType(nodeType) && !isReferencedSceneRenderSettingsNode(nodeType, item->path())) { return ProxyShapeHierarchy::create(_mayaHierarchyHandler, item); } else { return _mayaHierarchyHandler->hierarchy(item); diff --git a/lib/mayaUsd/ufe/UsdStageMap.cpp b/lib/mayaUsd/ufe/UsdStageMap.cpp index 8d82ee13f0..3c77745748 100644 --- a/lib/mayaUsd/ufe/UsdStageMap.cpp +++ b/lib/mayaUsd/ufe/UsdStageMap.cpp @@ -17,12 +17,17 @@ #include #include +#include +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS +#include +#endif #include #include #include #include #include +#include #include #include #include @@ -79,10 +84,34 @@ MayaUsdProxyShapeBase* objToProxyShape(MObject& obj) UsdStageWeakPtr objToStage(MObject& obj) { MayaUsdProxyShapeBase* ps = objToProxyShape(obj); - if (!ps) - return {}; + if (ps) + return ps->getUsdStage(); + + // Fall back to ProxyStageProvider for non-proxy-shape gateway nodes + // (e.g. UsdSceneRenderSettings). + if (!obj.isNull()) { + MFnDependencyNode fn(obj); + ProxyStageProvider* provider = dynamic_cast(fn.userNode()); + if (provider) + return provider->getUsdStage(); + } - return ps->getUsdStage(); + return {}; +} + +// Find the singleton UsdSceneRenderSettings node and add it to the stage map. +void discoverSceneRenderSettingsNode(const std::function& addItemFn) +{ +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + MObject obj = MayaUsd::UsdSceneRenderSettings::findInstance(); + if (!obj.isNull()) { + MFnDagNode dagFn(obj); + MDagPath dagPath; + if (dagFn.getPath(dagPath) == MS::kSuccess) { + addItemFn(MayaUsd::ufe::dagPathToUfe(dagPath)); + } + } +#endif } inline Ufe::Path toPath(const std::string& mayaPathString) @@ -178,9 +207,11 @@ MObject UsdStageMap::proxyShape(const Ufe::Path& path, bool rebuildCacheIfNeeded MProfilingScope profilingScope( kUsdStageMapProfilerCategory, MProfiler::kColorB_L1, "UsdStageMap::proxyShape()"); - // Optimization: if there are not proxy shape instances, - // there is nothing that can be mapped. - if (MayaUsdProxyShapeBase::countProxyShapeInstances() == 0) + // Optimization: if there are no proxy shape instances, the map is not + // dirty, and the map is empty, there is nothing that can be mapped. + // We must still check the map because non-proxy-shape gateway nodes + // (e.g. UsdSceneRenderSettings) may have been added during a previous rebuild. + if (MayaUsdProxyShapeBase::countProxyShapeInstances() == 0 && !_dirty && _pathToObject.empty()) return MObject(); const bool wasRebuilt = rebuildIfDirty(); @@ -198,6 +229,12 @@ MObject UsdStageMap::proxyShape(const Ufe::Path& path, bool rebuildCacheIfNeeded addItem(psPath); } } + // Also try UsdSceneRenderSettings nodes. + discoverSceneRenderSettingsNode([this](const Ufe::Path& path) { + if (_pathToObject.find(path) == std::end(_pathToObject)) { + addItem(path); + } + }); iter = _pathToObject.find(singleSegmentPath); } } @@ -278,9 +315,20 @@ UsdStageMap::StageSet UsdStageMap::allStages() /* no ++it here, we manually move it in the loop */) { const auto& pair = *it; const Ufe::Path& path = pair.first; + MObjectHandle objectHandle = pair.second; // Calling UsdStageMap::stage may erase the entry in fPathToObject at path. // Advance the iterator so that the iterator stays valid if erasure occurs. it++; + +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + // Exclude the SceneRenderSettings internal stage. + if (objectHandle.isValid()) { + MFnDependencyNode depFn(objectHandle.object()); + if (depFn.typeId() == MAYAUSD_NS::UsdSceneRenderSettings::typeId) + continue; + } +#endif + // Now handle path. PXR_NS::UsdStageWeakPtr matchingStage = stage(path); // After this point pair and path cannot be safely used, they may have been erased. @@ -303,6 +351,17 @@ std::vector UsdStageMap::allStagesPaths() for (auto it = _pathToObject.begin(); it != _pathToObject.end(); ++it) { const auto& pair = *it; const Ufe::Path& path = pair.first; + +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + // Exclude the SceneRenderSettings internal stage. + MObjectHandle objectHandle = pair.second; + if (objectHandle.isValid()) { + MFnDependencyNode depFn(objectHandle.object()); + if (depFn.typeId() == MAYAUSD_NS::UsdSceneRenderSettings::typeId) + continue; + } +#endif + _stagePaths.push_back(path); } return _stagePaths; @@ -332,6 +391,9 @@ bool UsdStageMap::rebuildIfDirty() addItem(toPath(psn)); } + // Also discover UsdSceneRenderSettings nodes. + discoverSceneRenderSettingsNode([this](const Ufe::Path& path) { addItem(path); }); + TF_DEBUG(MAYAUSD_STAGEMAP) .Msg("Rebuilt stage map, found %d proxy shapes\n", int(_stageToObject.size())); _dirty = false; diff --git a/lib/mayaUsd/ufe/Utils.cpp b/lib/mayaUsd/ufe/Utils.cpp index 94b26f3a07..8aeefa005c 100644 --- a/lib/mayaUsd/ufe/Utils.cpp +++ b/lib/mayaUsd/ufe/Utils.cpp @@ -15,6 +15,9 @@ // #include #include +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS +#include +#endif #include #include #include @@ -191,6 +194,11 @@ bool isAGatewayType(const std::string& mayaNodeType) return false; } + // Check if this is the UsdSceneRenderSettings node type. + if (isSceneRenderSettingsNode(mayaNodeType)) { + return true; + } + // If we've seen this node type before, return the cached value. auto iter = g_GatewayType.find(mayaNodeType); if (iter != std::end(g_GatewayType)) { @@ -215,6 +223,34 @@ bool isAGatewayType(const std::string& mayaNodeType) return isInherited; } +bool isSceneRenderSettingsNode(const std::string& mayaNodeType) +{ +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + return mayaNodeType == UsdSceneRenderSettings::typeName.asChar(); +#else + return false; +#endif +} + +bool isReferencedSceneRenderSettingsNode(const std::string& mayaNodeType, const Ufe::Path& ufePath) +{ +#ifdef MAYA_HAS_SCENE_RENDER_SETTINGS + if (!isSceneRenderSettingsNode(mayaNodeType)) { + return false; + } + MDagPath dagPath = ufeToDagPath(ufePath); + if (!dagPath.isValid()) { + return false; + } + MFnDependencyNode depFn(dagPath.node()); + return depFn.isFromReferencedFile(); +#else + return false; +#endif +} + +void setStageMapDirty() { UsdStageMap::getInstance().setDirty(); } + Ufe::Path dagPathToUfe(const MDagPath& dagPath) { // This function can only create UFE Maya scene items with a single diff --git a/lib/mayaUsd/ufe/Utils.h b/lib/mayaUsd/ufe/Utils.h index 073ef3f781..d81ddebba1 100644 --- a/lib/mayaUsd/ufe/Utils.h +++ b/lib/mayaUsd/ufe/Utils.h @@ -89,6 +89,20 @@ std::string uniqueChildNameMayaStandard( MAYAUSD_CORE_PUBLIC bool isAGatewayType(const std::string& mayaNodeType); +//! Return true if the Maya node type is the UsdSceneRenderSettings node. +MAYAUSD_CORE_PUBLIC +bool isSceneRenderSettingsNode(const std::string& mayaNodeType); + +//! Return true if the UFE item is a SceneRenderSettings node that comes +//! from a Maya reference. Referenced copies must not be treated as live +//! gateway nodes because they would interfere with the local singleton. +MAYAUSD_CORE_PUBLIC +bool isReferencedSceneRenderSettingsNode(const std::string& mayaNodeType, const Ufe::Path& ufePath); + +//! Mark the UFE stage map as dirty so it will be rebuilt on next access. +MAYAUSD_CORE_PUBLIC +void setStageMapDirty(); + MAYAUSD_CORE_PUBLIC Ufe::Path dagPathToUfe(const MDagPath& dagPath); diff --git a/lib/mayaUsdAPI/utils.cpp b/lib/mayaUsdAPI/utils.cpp index 60009e340d..297af8b1da 100644 --- a/lib/mayaUsdAPI/utils.cpp +++ b/lib/mayaUsdAPI/utils.cpp @@ -147,6 +147,11 @@ bool isAGatewayType(const std::string& mayaNodeType) return MayaUsd::ufe::isAGatewayType(mayaNodeType); } +bool isReferencedSceneRenderSettingsNode(const std::string& mayaNodeType, const Ufe::Path& ufePath) +{ + return MayaUsd::ufe::isReferencedSceneRenderSettingsNode(mayaNodeType, ufePath); +} + bool mergePrims( const PXR_NS::UsdStageRefPtr& srcStage, const PXR_NS::SdfLayerRefPtr& srcLayer, diff --git a/lib/mayaUsdAPI/utils.h b/lib/mayaUsdAPI/utils.h index 6b5cbabaa7..9676898952 100644 --- a/lib/mayaUsdAPI/utils.h +++ b/lib/mayaUsdAPI/utils.h @@ -161,6 +161,10 @@ bool isMaterialsScope(const Ufe::SceneItem::Ptr& item); MAYAUSD_API_PUBLIC bool isAGatewayType(const std::string& mayaNodeType); +//! Return true if the UFE item is a SceneRenderSettings node from a Maya reference. +MAYAUSD_API_PUBLIC +bool isReferencedSceneRenderSettingsNode(const std::string& mayaNodeType, const Ufe::Path& ufePath); + /*! Merges prims starting at a source path from a source layer and stage to a destination, returning * true if it succeeds. */ diff --git a/test/lib/mayaUsd/fileio/testCacheToUsd.py b/test/lib/mayaUsd/fileio/testCacheToUsd.py index 8c9c444acd..9e12126021 100644 --- a/test/lib/mayaUsd/fileio/testCacheToUsd.py +++ b/test/lib/mayaUsd/fileio/testCacheToUsd.py @@ -216,11 +216,15 @@ def runTestCacheToUsd(self, createMayaRefPrimFn, checkCacheParentFn): self.assertEqual(mayaTransformItem.nodeType(), 'transform') # The child of the Maya transform is the top-level transform of the - # referenced Maya scene. + # referenced Maya scene. Find it by name because the referenced file + # may contain additional nodes (e.g. the SceneRenderSettings singleton). mayaTransformHier = createHierarchy(mayaTransformItem) mayaTransformChildren = mayaTransformHier.children() - self.assertTrue(len(mayaTransformChildren), 1) - sphereTransformItem = mayaTransformChildren[0] + sphereTransformItem = next( + (c for c in mayaTransformChildren + if c.nodeName().endswith('pSphere1')), + None) + self.assertIsNotNone(sphereTransformItem) self.assertEqual(sphereTransformItem.nodeType(), 'transform') # As the sphereTransformItem is a pulled node, its path is a pure Maya diff --git a/test/lib/mayaUsd/nodes/CMakeLists.txt b/test/lib/mayaUsd/nodes/CMakeLists.txt index 13ff3cc206..2fd4df8eee 100644 --- a/test/lib/mayaUsd/nodes/CMakeLists.txt +++ b/test/lib/mayaUsd/nodes/CMakeLists.txt @@ -29,3 +29,14 @@ mayaUsd_add_test(testHdImagingShape "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" ) set_property(TEST testHdImagingShape APPEND PROPERTY LABELS nodes) + +if(MAYA_HAS_SCENE_RENDER_SETTINGS) + mayaUsd_get_unittest_target(target testSceneRenderSettings.py) + mayaUsd_add_test(${target} + PYTHON_MODULE ${target} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ENV + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + ) + set_property(TEST ${target} APPEND PROPERTY LABELS nodes) +endif() diff --git a/test/lib/mayaUsd/nodes/testLayerManagerSerialization.py b/test/lib/mayaUsd/nodes/testLayerManagerSerialization.py index e75c5b4715..695a92cab1 100644 --- a/test/lib/mayaUsd/nodes/testLayerManagerSerialization.py +++ b/test/lib/mayaUsd/nodes/testLayerManagerSerialization.py @@ -158,6 +158,22 @@ def testSaveWithoutStage(self): self._tempMayaFile = os.path.join( self._currentTestDir, 'EmptySerializationTest.ma') cmds.file(new=True, force=True) + + # Delete the SceneRenderSettings singleton if present so it doesn't + # create a plugin dependency in an otherwise empty scene. + srsNodes = cmds.ls(type='mayaUsdSceneRenderSettings') + if srsNodes: + cmds.undoInfo(stateWithoutFlush=False) + for node in srsNodes: + parent = cmds.listRelatives(node, parent=True, fullPath=True) + cmds.lockNode(node, lock=False) + if parent: + cmds.lockNode(parent[0], lock=False) + cmds.delete(parent[0]) + else: + cmds.delete(node) + cmds.undoInfo(stateWithoutFlush=True) + cmds.file(rename=self._tempMayaFile) cmds.file(save=True, force=True, type='mayaAscii') cmds.file(new=True, force=True) diff --git a/test/lib/mayaUsd/nodes/testSceneRenderSettings.py b/test/lib/mayaUsd/nodes/testSceneRenderSettings.py new file mode 100644 index 0000000000..30af7745e0 --- /dev/null +++ b/test/lib/mayaUsd/nodes/testSceneRenderSettings.py @@ -0,0 +1,261 @@ +#!/usr/bin/env mayapy +# +# Copyright 2026 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. +# + +from maya import cmds +from maya import standalone + +from mayaUsd.lib import SceneRenderSettings + +from pxr import Usd, UsdGeom, UsdRender, UsdUtils + +import fixturesUtils + +import os +import tempfile +import unittest + + +class testSceneRenderSettings(unittest.TestCase): + + @classmethod + def setUpClass(cls): + fixturesUtils.setUpClass(__file__) + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + def setUp(self): + cmds.file(new=True, force=True) + + # ------------------------------------------------------------------ + # Node existence and singleton + # ------------------------------------------------------------------ + + def testNodeExistsOnStartup(self): + '''The singleton node should exist after plugin load.''' + path = SceneRenderSettings.find() + self.assertTrue(len(path) > 0, "SceneRenderSettings node not found") + self.assertTrue(cmds.objExists(path)) + + def testSingleton(self): + '''find called twice should return the same node; only one instance exists.''' + path1 = SceneRenderSettings.find() + path2 = SceneRenderSettings.find() + self.assertEqual(path1, path2) + + nodes = cmds.ls(type='mayaUsdSceneRenderSettings') + self.assertEqual(len(nodes), 1, + "Expected exactly one SceneRenderSettings node, " + "found %d" % len(nodes)) + + # ------------------------------------------------------------------ + # Default stage structure + # ------------------------------------------------------------------ + + def testDefaultStageStructure(self): + '''The default stage should have /Render scope and /Render/SceneRenderSettings prim.''' + stage = SceneRenderSettings.getUsdStage() + self.assertIsNotNone(stage) + + renderPrim = stage.GetPrimAtPath('/Render') + self.assertTrue(renderPrim.IsValid(), "/Render prim not found") + self.assertTrue(renderPrim.IsA(UsdGeom.Scope)) + + settingsPrim = stage.GetPrimAtPath('/Render/SceneRenderSettings') + self.assertTrue(settingsPrim.IsValid(), + "/Render/SceneRenderSettings prim not found") + self.assertTrue(settingsPrim.IsA(UsdRender.Settings)) + + def testRenderSettingsPrimPathMetadata(self): + '''Stage metadata should point to the default render settings prim.''' + stage = SceneRenderSettings.getUsdStage() + metadata = stage.GetMetadata('renderSettingsPrimPath') + self.assertEqual(metadata, '/Render/SceneRenderSettings') + + # ------------------------------------------------------------------ + # Node properties + # ------------------------------------------------------------------ + + def testNodeIsLocked(self): + '''The singleton shape should be locked.''' + shapePath = SceneRenderSettings.find() + self.assertTrue(cmds.lockNode(shapePath, query=True, lock=True)[0], + "Shape node should be locked") + + def testNodeHiddenInOutliner(self): + '''The parent transform should be hidden in the outliner.''' + shapePath = SceneRenderSettings.find() + parentPath = cmds.listRelatives(shapePath, parent=True, + fullPath=True)[0] + self.assertTrue(cmds.getAttr(parentPath + '.hiddenInOutliner'), + "Transform should be hidden in outliner") + + # ------------------------------------------------------------------ + # Output attributes + # ------------------------------------------------------------------ + + def testOutStageCacheId(self): + '''outStageCacheId should return a valid stage cache ID.''' + shapePath = SceneRenderSettings.find() + cacheId = cmds.getAttr(shapePath + '.outStageCacheId') + self.assertGreaterEqual(cacheId, 0, + "Expected valid cache ID, got %d" % cacheId) + + # Verify we can retrieve the same stage from the cache. + cachedStage = UsdUtils.StageCache.Get().Find( + Usd.StageCache.Id.FromLongInt(cacheId)) + apiStage = SceneRenderSettings.getUsdStage() + self.assertEqual(cachedStage.GetRootLayer().identifier, + apiStage.GetRootLayer().identifier) + + # ------------------------------------------------------------------ + # Stage access + # ------------------------------------------------------------------ + + def testGetUsdStageConsistency(self): + '''getUsdStage should return the same stage on repeated calls.''' + stage1 = SceneRenderSettings.getUsdStage() + stage2 = SceneRenderSettings.getUsdStage() + self.assertIsNotNone(stage1) + self.assertEqual(stage1.GetRootLayer().identifier, + stage2.GetRootLayer().identifier) + + def testGetDefaultRenderSettingsPrim(self): + '''getDefaultRenderSettingsPrim should return the /Render/SceneRenderSettings prim.''' + prim = SceneRenderSettings.getDefaultRenderSettingsPrim() + self.assertTrue(prim.IsValid()) + self.assertEqual(prim.GetPath().pathString, + '/Render/SceneRenderSettings') + self.assertTrue(prim.IsA(UsdRender.Settings)) + + # ------------------------------------------------------------------ + # Node recreation on file new + # ------------------------------------------------------------------ + + def testNodeRecreatedAfterFileNew(self): + '''The singleton should be recreated after file new.''' + pathBefore = SceneRenderSettings.find() + self.assertTrue(len(pathBefore) > 0) + + cmds.file(new=True, force=True) + + pathAfter = SceneRenderSettings.find() + self.assertTrue(len(pathAfter) > 0, + "Node should be recreated after file new") + + # The new stage should have the default structure. + stage = SceneRenderSettings.getUsdStage() + self.assertTrue( + stage.GetPrimAtPath('/Render/SceneRenderSettings').IsValid()) + + # ------------------------------------------------------------------ + # Serialization round-trip + # ------------------------------------------------------------------ + + def testSerializationRoundTrip(self): + '''Stage content should survive a save/open cycle.''' + stage = SceneRenderSettings.getUsdStage() + + # Author a prim directly on the stage. + UsdGeom.Xform.Define(stage, '/Render/TestContent') + + # Save. + tmpFile = os.path.join(tempfile.mkdtemp(), 'testScene.ma') + cmds.file(rename=tmpFile) + cmds.file(save=True, type='mayaAscii') + + # Re-open. + cmds.file(tmpFile, open=True, force=True) + + # Verify. + stage2 = SceneRenderSettings.getUsdStage() + self.assertIsNotNone(stage2) + + self.assertTrue( + stage2.GetPrimAtPath('/Render/SceneRenderSettings').IsValid(), + "Default render settings prim should survive serialization") + self.assertTrue( + stage2.GetPrimAtPath('/Render/TestContent').IsValid(), + "Authored content should survive serialization") + + # ------------------------------------------------------------------ + # Referencing a scene that contains the singleton + # ------------------------------------------------------------------ + + def testReferencedSceneDoesNotBreakLocalSingleton(self): + '''Referencing a Maya file that contains a SceneRenderSettings node + must not replace or break the current scene's singleton.''' + # Author a marker prim so we can distinguish local vs referenced stage. + localStage = SceneRenderSettings.getUsdStage() + UsdGeom.Xform.Define(localStage, '/Render/LocalMarker') + + localPath = SceneRenderSettings.find() + self.assertTrue(len(localPath) > 0) + + # Save the current scene so we can reference it later. + refDir = tempfile.mkdtemp() + refFile = os.path.join(refDir, 'referenced.ma') + cmds.file(rename=refFile) + cmds.file(save=True, type='mayaAscii') + + # Start a fresh scene (creates a new local singleton). + cmds.file(new=True, force=True) + + localPathNew = SceneRenderSettings.find() + self.assertTrue(len(localPathNew) > 0) + + localStageNew = SceneRenderSettings.getUsdStage() + self.assertIsNotNone(localStageNew) + + # The fresh scene should NOT have the marker from the saved file. + self.assertFalse( + localStageNew.GetPrimAtPath('/Render/LocalMarker').IsValid(), + "Fresh scene should not have marker prim before referencing") + + # Reference the saved scene. + cmds.file(refFile, reference=True, namespace='ref') + + # The local singleton should still be the non-referenced node. + pathAfterRef = SceneRenderSettings.find() + self.assertTrue(len(pathAfterRef) > 0, + "Local singleton should still exist after referencing") + + # The referenced file brings in its own SceneRenderSettings node + # under a namespace; verify both exist but find() returns the local one. + allNodes = cmds.ls(type='mayaUsdSceneRenderSettings', long=True) + localNodes = [n for n in allNodes + if not cmds.referenceQuery(n, isNodeReferenced=True)] + self.assertEqual(len(localNodes), 1, + "Expected exactly one local SceneRenderSettings, " + "found %d" % len(localNodes)) + + # The local stage must still have the default render settings. + stageAfterRef = SceneRenderSettings.getUsdStage() + self.assertIsNotNone(stageAfterRef) + self.assertTrue( + stageAfterRef.GetPrimAtPath('/Render/SceneRenderSettings').IsValid(), + "Local render settings prim should not be broken by referencing") + + # The local stage must NOT have the marker from the referenced file. + self.assertFalse( + stageAfterRef.GetPrimAtPath('/Render/LocalMarker').IsValid(), + "Referenced file's stage data should not leak into local singleton") + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test/lib/mayaUsd/utils/testDiagnosticDelegate.py b/test/lib/mayaUsd/utils/testDiagnosticDelegate.py index 22bd5cb61a..385a660fd1 100644 --- a/test/lib/mayaUsd/utils/testDiagnosticDelegate.py +++ b/test/lib/mayaUsd/utils/testDiagnosticDelegate.py @@ -208,6 +208,24 @@ def testZZZBatching_DelegateRemoved(self): Tf.Warn("this warning won't be lost") Tf.Status("this status won't be lost") + # Delete the SceneRenderSettings singleton node with undo off + # so the undo queue doesn't hold references to plugin data types. + # Guard with allNodeTypes check: the type may not be compiled in + # every build configuration. + srsNodes = (cmds.ls(type='mayaUsdSceneRenderSettings') + if 'mayaUsdSceneRenderSettings' in cmds.allNodeTypes() + else []) + if srsNodes: + cmds.undoInfo(stateWithoutFlush=False) + for node in srsNodes: + parent = cmds.listRelatives(node, parent=True, fullPath=True) + cmds.lockNode(node, lock=False) + if parent: + cmds.lockNode(parent[0], lock=False) + cmds.delete(parent[0]) + else: + cmds.delete(node) + cmds.undoInfo(stateWithoutFlush=True) cmds.unloadPlugin('mayaUsdPlugin', force=True) for i in range(5): diff --git a/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py b/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py index 5b80e4f6a4..cb19d8df07 100644 --- a/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py +++ b/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py @@ -164,6 +164,8 @@ def __testUsdPreviewSurfaceRoundtrip(self, # Check that we have no spurious "Looks" transform expectedTr = set(['front', 'persp', 'side', 'top', 'Test:pSphere1']) allTr = set(cmds.ls(tr=True)) + # The SceneRenderSettings singleton is not scene content; ignore it. + allTr.discard('SceneRenderSettings') self.assertEqual(allTr, expectedTr) # Check connections: diff --git a/test/testUtils/fixturesUtils.py b/test/testUtils/fixturesUtils.py index 5edcd64ac0..e52e522347 100644 --- a/test/testUtils/fixturesUtils.py +++ b/test/testUtils/fixturesUtils.py @@ -87,6 +87,20 @@ def tearDownClass(unloadPlugin=True, pluginName=_defaultPluginName): if unloadPlugin: import maya.cmds as cmds + # Delete the SceneRenderSettings singleton node with undo off + # so the undo queue doesn't hold references to plugin data types. + srsNodes = cmds.ls(type='mayaUsdSceneRenderSettings') + if srsNodes: + cmds.undoInfo(stateWithoutFlush=False) + for node in srsNodes: + parent = cmds.listRelatives(node, parent=True, fullPath=True) + cmds.lockNode(node, lock=False) + if parent: + cmds.lockNode(parent[0], lock=False) + cmds.delete(parent[0]) + else: + cmds.delete(node) + cmds.undoInfo(stateWithoutFlush=True) cmds.unloadPlugin(pluginName, force=True) # Exit into the main test directory