HYDRA-2168 : Generic translation of custom Maya plugin nodes to Hydra#400
HYDRA-2168 : Generic translation of custom Maya plugin nodes to Hydra#400lanierd-adsk wants to merge 8 commits intodevfrom
Conversation
Summary Adds a generic fallback adapter (MayaHydraGenericDagAdapter) that automatically translates any unrecognized Maya plugin DAG node to Hydra as a mayaCustomDagNode prim, without maya-hydra needing knowledge of specific plugin node types. Maya built-in nodes are always skipped. Exposes plugin node data via a custom mayaNode data source containing the Maya type name (mayaTypeName) and a dictionary of non-default attribute values (mayaAttributes), with per-attribute dirty locators (mayaNode.mayaAttributes.<attrName>) for efficient change tracking. Provides an example scene index plugin demonstrating how a render delegate (using Arnold's aiPhotometricLight as an illustration) would consume mayaCustomDagNode prims and re-type/map them to standard Hydra prim types — with zero dependency on maya-hydra headers.
There was a problem hiding this comment.
Pull request overview
Adds a generic fallback path in maya-hydra to translate previously-unhandled plugin-defined Maya DAG nodes into Hydra, exposing their type name + non-default attributes via a dedicated data source, along with an example renderer-side scene index that can re-type/map those prims (illustrated with Arnold’s aiPhotometricLight).
Changes:
- Introduce
MayaHydraGenericDagAdapterto emitmayaCustomDagNodeprims for plugin-registered DAG nodes lacking a dedicated adapter. - Add
mayaNodecontainer data source (mayaTypeName,mayaAttributes) and mixed-utils support for extracting non-default plugin-specific attributes. - Add an Arnold-focused example scene index plugin + new Python/GTest coverage validating prim typing, attribute filtering, and dirty locators.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| test/lib/mayaUsd/render/mayaToHydra/cpp/testGenericDagNodeTranslation.py | Sets up aiPhotometricLight nodes in Maya and dispatches to C++ verification. |
| test/lib/mayaUsd/render/mayaToHydra/cpp/testGenericDagNodeTranslation.cpp | GTest coverage for prim type, data source contents, default filtering, and dirty locators. |
| test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt | Adds new C++ test target source. |
| test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt | Registers the new interactive Python test script. |
| lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.h | Declares generic adapter creation + storage in the scene index. |
| lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp | Wires generic adapter creation into DAG insertion and pending updates. |
| lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraGenericNodeDataSource.h | Declares mayaNode container data source for generic plugin nodes. |
| lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraGenericNodeDataSource.cpp | Implements mayaTypeName/mayaAttributes data source accessors. |
| lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraDataSource.cpp | Exposes mayaNode (and xform/visibility/purpose) for mayaCustomDagNode prims. |
| lib/mayaHydra/hydraExtensions/sceneIndex/CMakeLists.txt | Builds/installs the new generic node data source. |
| lib/mayaHydra/hydraExtensions/plugInfo.json | Registers the new adapter type for plugin discovery/metadata. |
| lib/mayaHydra/hydraExtensions/mixedUtils.h | Declares GetNonDefaultMayaAttributesFromNode() helper API. |
| lib/mayaHydra/hydraExtensions/mixedUtils.cpp | Implements non-default attribute extraction with base-class attribute filtering. |
| lib/mayaHydra/hydraExtensions/adapters/tokens.h | Adds convention tokens (mayaCustomDagNode, mayaNode, etc.). |
| lib/mayaHydra/hydraExtensions/adapters/genericDagAdapter.h | Declares the generic fallback DAG adapter contract. |
| lib/mayaHydra/hydraExtensions/adapters/genericDagAdapter.cpp | Implements the adapter + per-attribute dirtying callback. |
| lib/mayaHydra/hydraExtensions/adapters/CMakeLists.txt | Adds the new adapter to the build. |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/plugInfo.json | Example HdSceneIndexPlugin registration (loads with Arnold). |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/genericNodeTranslationSceneIndexPlugin.h | Declares example scene index plugin wrapper. |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/genericNodeTranslationSceneIndexPlugin.cpp | Registers example plugin via HdSceneIndexPluginRegistry. |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/genericNodeTranslationSceneIndex.h | Declares filtering scene index that re-types/maps generic prims. |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/genericNodeTranslationSceneIndex.cpp | Implements example translation of aiPhotometricLight to sphereLight. |
| lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/CMakeLists.txt | Build/install rules for the example plugin library. |
| doc/genericPluginNodeTranslation.md | Documents the conventions, data sources, locators, and the example flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static const std::set<std::string>& GetBaseClassAttrNames() | ||
| { | ||
| static std::set<std::string> names; | ||
| static bool initialized = false; | ||
| if (!initialized) { | ||
| const char* baseTypes[] = { "locator" }; | ||
| for (const char* typeName : baseTypes) { |
There was a problem hiding this comment.
GetBaseClassAttrNames() uses a manual lazy-init (static bool initialized) to populate a static std::set. This is not thread-safe and can cause a data race if multiple threads call GetNonDefaultMayaAttributesFromNode() concurrently (Hydra scene index evaluation can be multi-threaded, and BUILD_STRICT_MODE enables -Werror but not concurrency checks). Prefer a thread-safe one-time init (e.g., function-local static initialized via a lambda, or std::once_flag + std::call_once) so the set is constructed exactly once without races.
| void _GenericNodeAttrChanged( | ||
| MNodeMessage::AttributeMessage msg, | ||
| MPlug& plug, | ||
| MPlug& otherPlug, |
There was a problem hiding this comment.
_GenericNodeAttrChanged has an unused parameter (otherPlug). With the project's -Wall (and -Werror when BUILD_STRICT_MODE is enabled), this can fail the build on some toolchains. Mark it unused (e.g., TF_UNUSED(otherPlug)) or omit the parameter name in the function signature.
| MPlug& otherPlug, | |
| MPlug& /*otherPlug*/, |
| void HdGenericNodeTranslationSceneIndex::_PrimsAdded( | ||
| const HdSceneIndexBase& sender, | ||
| const HdSceneIndexObserver::AddedPrimEntries& entries) | ||
| { |
There was a problem hiding this comment.
The 'sender' parameter is unused. With the project's -Wall (and -Werror when BUILD_STRICT_MODE is enabled), unused parameters can break strict builds. Consider marking it unused (e.g., TF_UNUSED(sender)) or removing the parameter name in the definition.
| void HdGenericNodeTranslationSceneIndex::_PrimsRemoved( | ||
| const HdSceneIndexBase& sender, | ||
| const HdSceneIndexObserver::RemovedPrimEntries& entries) | ||
| { |
There was a problem hiding this comment.
The 'sender' parameter is unused here as well; consider TF_UNUSED(sender) (or omit the name) to avoid -Wall/-Werror unused-parameter failures in strict builds.
| void HdGenericNodeTranslationSceneIndex::_PrimsDirtied( | ||
| const HdSceneIndexBase& sender, | ||
| const HdSceneIndexObserver::DirtiedPrimEntries& entries) | ||
| { |
There was a problem hiding this comment.
The 'sender' parameter is unused here as well; consider TF_UNUSED(sender) (or omit the name) to avoid -Wall/-Werror unused-parameter failures in strict builds.
| // string ("Arnold") to the target renderer's registered name (e.g., "Prman", | ||
| // "Prman") and handle the relevant Maya node types in the |
There was a problem hiding this comment.
Comment typo: the example list repeats "Prman" twice ("e.g., "Prman", "Prman""). Replace with distinct examples or a single entry (e.g., "Prman").
| // string ("Arnold") to the target renderer's registered name (e.g., "Prman", | |
| // "Prman") and handle the relevant Maya node types in the | |
| // string ("Arnold") to the target renderer's registered name (e.g., "Prman") | |
| // and handle the relevant Maya node types in the |
| if (!(msg & MNodeMessage::kAttributeSet)) { | ||
| return; | ||
| } | ||
| if (plug.isChild()) { | ||
| return; | ||
| } | ||
| // Message attributes are connection-only with no data value; skip them. | ||
| if (plug.attribute().apiType() == MFn::kMessageAttribute) { | ||
| return; | ||
| } | ||
|
|
||
| auto* adapter = reinterpret_cast<MayaHydraGenericDagAdapter*>(clientData); | ||
|
|
||
| MFnAttribute attr(plug.attribute()); | ||
| TfToken attrName(attr.name().asChar()); | ||
| HdDataSourceLocator locator( | ||
| MayaHydraAdapterTokens->mayaNode, | ||
| MayaHydraAdapterTokens->mayaAttributes, | ||
| attrName); | ||
| adapter->GetMayaHydraSceneIndex()->DirtyPrims({{ adapter->GetID(), HdDataSourceLocatorSet(locator) }}); |
There was a problem hiding this comment.
The attribute-changed callback dirties mayaNode.mayaAttributes. for all top-level non-message attributes. However, GetNonDefaultMayaAttributesFromNode() explicitly filters out Maya built-in base-class attributes (visibility, worldPosition, castsShadows, etc.), so changes to those attributes will still emit mayaAttributes dirty locators even though the mayaAttributes dictionary will never include them. Consider filtering these out in the callback as well (e.g., skip attributes that belong to Maya base classes) to avoid noisy/inefficient dirtying and better match the "plugin-specific attributes" intent.
…e value, add fixes from copilot comments from code review
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| MFnAttribute attr(plug.attribute()); | ||
| const char* name = attr.name().asChar(); |
There was a problem hiding this comment.
In _GenericNodeAttrChanged(), const char* name = attr.name().asChar(); stores a pointer into a temporary MString returned by attr.name(). That temporary is destroyed at the end of the statement, so name can be dangling by the time it’s used (e.g., in IsBaseClassAttrName(name) / TfToken attrName(name)). Store the MString in a local variable (or construct TfToken directly from a stable string) before taking asChar().
| const char* name = attr.name().asChar(); | |
| MString attrNameStr = attr.name(); | |
| const char* name = attrNameStr.asChar(); |
|
|
||
| bool MayaHydraGenericDagAdapter::GetVisible() | ||
| { | ||
| return GetDagPath().isVisible(); |
There was a problem hiding this comment.
MayaHydraGenericDagAdapter::MarkDirty() bypasses MayaHydraDagAdapter::MarkDirty(), so the base class’s internal visibility cache (_visibilityDirty / _isVisible) is never updated. However, the dirty-plug callbacks use IsVisible(false) to decide whether to invalidate transforms / emit DirtyTransform, so after any visibility change (or if a node starts hidden) those decisions can become permanently stale. Consider either (a) delegating the visibility bookkeeping to the base class (e.g., call MayaHydraDagAdapter::MarkDirty() for DirtyVisibility/DirtyTransform), or (b) keep _isVisible up to date by calling UpdateVisibility() in GetVisible() and/or avoid IsVisible() in these callbacks.
| return GetDagPath().isVisible(); | |
| // Keep the base-class visibility cache (_isVisible / _visibilityDirty) | |
| // in sync so that IsVisible(false) used by dirty-plug callbacks remains | |
| // accurate even after visibility changes. | |
| UpdateVisibility(); | |
| return IsVisible(false); |
| set(TARGET_NAME mayaHydraGenericNodeTranslationSceneIndex) | ||
|
|
||
| add_library(${TARGET_NAME} SHARED) | ||
|
|
There was a problem hiding this comment.
This example target won’t be built unless its directory is added from lib/mayaHydra/flowViewportAPIExamples/CMakeLists.txt (currently only adds flowViewportAPILocator/footPrintNode/customShadersNode/simpleHydraGenerativeProceduralPlugin). Add an add_subdirectory(genericNodeTranslationSceneIndex) there (optionally gated by whatever flag controls building examples) so the example is actually compiled/installed.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!_genericsToAdd.empty()) { | ||
| for (const auto& obj : _genericsToAdd) { | ||
| if (obj.isNull()) { | ||
| continue; | ||
| } | ||
| MDagPath dag; | ||
| MStatus status = MDagPath::getAPathTo(obj, dag); | ||
| if (!status) { | ||
| continue; | ||
| } | ||
| if (!dag.hasFn(MFn::kTransform)) { | ||
| CreateGenericAdapter(dag); | ||
| } | ||
| } |
There was a problem hiding this comment.
In FlushPendingUpdates(), generic nodes are added via MDagPath::getAPathTo() + CreateGenericAdapter() without the same filtering done in InsertDag() (intermediate objects and instanced paths with instanceNumber > 0). This can cause generic adapters/prims to be created for intermediate objects or for non-zero instances (potential duplicates / inconsistent behavior vs initial population). Consider adding the same checks here (isIntermediateObject, instancing guard) before calling CreateGenericAdapter().
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else if (useMeshAdapter()) { | ||
| _addedNodes.push_back(obj); | ||
| } else { | ||
| _genericsToAdd.push_back(obj); |
There was a problem hiding this comment.
The comment above this if/else block says that when not using the mesh adapter the callback only cares about lights and cameras, but the new logic now also queues non-light/camera DAG nodes into _genericsToAdd. Please update or relocate the comment so it accurately reflects the current behavior (lights/cameras plus generic plugin DAG nodes when mesh adapter is disabled).
| void HdGenericNodeTranslationSceneIndex::_PrimsDirtied( | ||
| const HdSceneIndexBase& /*sender*/, | ||
| const HdSceneIndexObserver::DirtiedPrimEntries& entries) | ||
| { | ||
| if (!_IsObserved()) return; | ||
| _SendPrimsDirtied(entries); | ||
| } |
There was a problem hiding this comment.
This filtering scene index re-types mayaCustomDagNode prims (e.g. to sphereLight) and synthesizes new data source keys (UsdLuxTokens->inputs:*), but _PrimsDirtied forwards the upstream dirty locators unchanged. Downstream consumers that rely on locator intersection for the translated schema may not notice changes (e.g. mayaNode.mayaAttributes.intensity doesn’t intersect inputs:intensity). Consider translating/augmenting dirty locators for translated prims (or conservatively dirtying a parent/root locator) so updates propagate correctly after re-typing.
| HdSceneIndexBaseRefPtr | ||
| HdGenericNodeTranslationSceneIndexPlugin::_AppendSceneIndex( | ||
| const HdSceneIndexBaseRefPtr& inputScene, | ||
| const HdContainerDataSourceHandle& inputArgs) | ||
| { | ||
| return HdGenericNodeTranslationSceneIndex::New(inputScene); | ||
| } |
There was a problem hiding this comment.
_AppendSceneIndex doesn’t use inputArgs. Other scene index plugins in this repo explicitly silence this (e.g. customPrimvarSceneIndexPlugin.cpp uses TF_UNUSED(inputArgs)). Consider adding TF_UNUSED(inputArgs) here as well to avoid unused-parameter warnings in builds that treat warnings as errors.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ppt-adsk
left a comment
There was a problem hiding this comment.
Great work! Just a few minor comments.
| ) | ||
| shapeDefaults = cmds.ls(shapeDefaults, long=True)[0] | ||
|
|
||
| self.setHdStormRenderer() |
There was a problem hiding this comment.
Do we need this? Is this not the default? Isn't it irrelevant which is the render delegate?
There was a problem hiding this comment.
You're right, I removed this. We just need to be in hydra (not VP2)
doc/genericPluginNodeTranslation.md
Outdated
| - Makes no assumption about what the node represents (light, mesh, camera, etc.). | ||
|
|
||
| 3. **Prim insertion**: The adapter inserts a Hydra prim with type `mayaCustomDagNode` into the scene index. The prim carries these data sources: | ||
| - `xform` -- standard Hydra transform from the DAG path. |
There was a problem hiding this comment.
Is this the flattened xform from the complete Dag path?
There was a problem hiding this comment.
Yes, I've moddified the doc to specify that.
lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/plugInfo.json
Show resolved
Hide resolved
| cmds.optionVar(stringValue=("mhGenericShape", shape)) | ||
| cmds.optionVar(stringValue=("mhGenericShapeDefaults", | ||
| shapeDefaults)) |
There was a problem hiding this comment.
What are these optionVar's doing? Would they be needed in an actual deployment?
There was a problem hiding this comment.
The optionVar calls are a test-only mechanism to pass Maya node paths from the Python test setup to the C++ GTest code. They are not needed in any actual deployment -- they're just a way for the Python side to tell the C++ side "here are the node names I created, so you can look them up in the Hydra scene index."
There was a problem hiding this comment.
There's nothing wrong with that, but I believe this requirement is already covered by arguments to the mayaHydraCppTest command, so we don't need to create a new mechanism. See
| // Skip plugin nodes that already provide their own Hydra scene index | ||
| // (e.g. mayaUsdProxyShape contributes USD data through registration.cpp). | ||
| // Translating them generically would create duplicate prims in the scene. | ||
| static const char* kSceneIndexProviderTypes[] = { "mayaUsdProxyShape" }; |
There was a problem hiding this comment.
We need to find a way to make this extensible...
There was a problem hiding this comment.
I actually removed that hardcoded string and I am using the dataProducer registered in flow viewport API as the exclude list as these are already providing their own hydra translation.
The new code is :
// Skip plugin nodes that already provide their own Hydra data through
// the Flow Viewport data producer API. Their MObjectHandle hash code
// is registered in DataProducersNodeHashCodeToSdfPathRegistry when
// they call addDataProducerSceneIndex() with a dccNode pointer.
MObjectHandle nodeHandle(dagPath.node());
if (!Fvp::DataProducersNodeHashCodeToSdfPathRegistry::Instance()
.GetPath(nodeHandle.hashCode()).IsEmpty()) {
return {};
}
lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraGenericNodeDataSource.h
Show resolved
Hide resolved
| auto getFloat = [&](const char* key, float def) -> float { | ||
| auto it = mayaAttrs.find(key); | ||
| if (it != mayaAttrs.end() && it->second.IsHolding<float>()) | ||
| return it->second.UncheckedGet<float>(); | ||
| if (it != mayaAttrs.end() && it->second.IsHolding<double>()) | ||
| return static_cast<float>(it->second.UncheckedGet<double>()); | ||
| return def; | ||
| }; | ||
| auto getVec3f = [&](const char* key, GfVec3f def) -> GfVec3f { | ||
| auto it = mayaAttrs.find(key); | ||
| if (it != mayaAttrs.end() && it->second.IsHolding<GfVec3f>()) | ||
| return it->second.UncheckedGet<GfVec3f>(); | ||
| return def; | ||
| }; | ||
| auto getString = [&](const char* key, std::string def) -> std::string { | ||
| auto it = mayaAttrs.find(key); | ||
| if (it != mayaAttrs.end() && it->second.IsHolding<std::string>()) | ||
| return it->second.UncheckedGet<std::string>(); | ||
| return def; | ||
| }; | ||
| auto getBool = [&](const char* key, bool def) -> bool { | ||
| auto it = mayaAttrs.find(key); | ||
| if (it != mayaAttrs.end() && it->second.IsHolding<bool>()) | ||
| return it->second.UncheckedGet<bool>(); | ||
| return def; | ||
| }; |
There was a problem hiding this comment.
This could be converted into a template, it seems. Also, why are doubles converted to floats?
There was a problem hiding this comment.
The four separate lambdas have been consolidated into a single generic getAttr lambda using auto parameter deduction (C++14) and if constexpr (C++17) for the float specialization. I've added a comment which explains why doubles are narrowed to floats: Maya stores many numeric attributes as doubles internally, but Hydra light schemas expect floats.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * See flowViewportAPIExamples/genericNodeTranslationSceneIndex/ for a complete | ||
| * example that translates aiPhotometricLight to a UsdLux sphereLight for Arnold. |
There was a problem hiding this comment.
The comment references the example path as flowViewportAPIExamples/genericNodeTranslationSceneIndex/, but the example added in this PR lives under lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/. Updating the path (or making it repository-relative in a consistent way) would avoid confusion for readers trying to locate the example.
| * See flowViewportAPIExamples/genericNodeTranslationSceneIndex/ for a complete | |
| * example that translates aiPhotometricLight to a UsdLux sphereLight for Arnold. | |
| * See lib/mayaHydra/flowViewportAPIExamples/genericNodeTranslationSceneIndex/ | |
| * for a complete example that translates aiPhotometricLight to a UsdLux | |
| * sphereLight for Arnold. |
| * delegate's scene index to react to specific attribute changes without | ||
| * recomputing everything. | ||
| */ | ||
| class MayaHydraGenericDagAdapter : public MayaHydraDagAdapter |
There was a problem hiding this comment.
The MayaHydraGenericDagAdapter is a bit confusing, sounds like similar as MayaHydraDagAdapter, would MayaHydraCustomDagAdapter be better?
| // The original xform and visibility data sources from maya-hydra are preserved | ||
| // via HdOverlayContainerDataSource. | ||
| HdSceneIndexPrim | ||
| HdGenericNodeTranslationSceneIndex::_TranslatePhotometricLight( |
There was a problem hiding this comment.
Great to see an example, how the the rendering result looks like with PhotometricLight in HdArnold?
| _shapeAdapters, | ||
| _lightAdapters, | ||
| _cameraAdapters, | ||
| _materialAdapters); |
There was a problem hiding this comment.
Should we also add _genericAdapters here when dealing _adaptersToRebuild?
| // Maya callback invoked when any attribute on the generic node is modified. | ||
| // Fires a per-attribute dirty locator (mayaNode.mayaAttributes.<attrName>) | ||
| // so downstream scene indices can react to specific attribute changes. | ||
| void _GenericNodeAttrChanged( |
There was a problem hiding this comment.
Do we have extension/custom attributes on custom node? If yes, how did we handle this case?
Summary
Adds a generic fallback adapter (MayaHydraGenericDagAdapter) that automatically translates any unrecognized Maya plugin DAG node to Hydra as a mayaCustomDagNode prim, without maya-hydra needing knowledge of specific plugin node types. Maya built-in nodes are always skipped. Exposes plugin node data via a custom mayaNode data source containing the Maya type name (mayaTypeName) and a dictionary of non-default attribute values (mayaAttributes), with per-attribute dirty locators (mayaNode.mayaAttributes.) for efficient change tracking. Provides an example scene index plugin demonstrating how a render delegate (using Arnold's aiPhotometricLight as an illustration) would consume mayaCustomDagNode prims and re-type/map them to standard Hydra prim types — with zero dependency on maya-hydra headers.