From e40749cdf66ebb7c3889a01a499cf64c18cfb52b Mon Sep 17 00:00:00 2001 From: Jonathan Stone Date: Tue, 17 Mar 2026 08:52:10 -0700 Subject: [PATCH 1/2] Add HW generation support for compound material nodes This changelist adds hardware shader generation support for compound material nodes such as LamaSurface, which are defined as node graphs with material-type outputs. The following specific changes are included: - Add an `HwMaterialCompoundNode` class that extends `HwSurfaceNode` for nodegraphs with material-type outputs, discovering BSDF, EDF, and opacity input name mappings by analyzing the nodegraph's internal surface node connections. - Refactor `HwSurfaceNode` to resolve input names through virtual methods rather than hardcoded strings, and add null-safety for inputs that may not be present on compound material nodes. - Fix the fallback condition in `getShaderNodes` to check whether shader nodes were found, rather than whether the material node has inputs. - Fix `isTransparentSurface` to identify material nodes by output type rather than node category, so that compound material nodes are handled correctly. Fixes https://github.com/AcademySoftwareFoundation/MaterialX/issues/1902 and https://github.com/AcademySoftwareFoundation/MaterialX/issues/2801. --- source/MaterialXCore/Material.cpp | 2 +- source/MaterialXGenHw/HwShaderGenerator.cpp | 9 +- .../Nodes/HwMaterialCompoundNode.cpp | 91 +++++++ .../Nodes/HwMaterialCompoundNode.h | 36 +++ source/MaterialXGenHw/Nodes/HwSurfaceNode.cpp | 233 ++++++++++-------- source/MaterialXGenHw/Nodes/HwSurfaceNode.h | 10 + source/MaterialXGenShader/Util.cpp | 2 +- 7 files changed, 282 insertions(+), 101 deletions(-) create mode 100644 source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp create mode 100644 source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.h diff --git a/source/MaterialXCore/Material.cpp b/source/MaterialXCore/Material.cpp index 947c34ec81..0260025e16 100644 --- a/source/MaterialXCore/Material.cpp +++ b/source/MaterialXCore/Material.cpp @@ -88,7 +88,7 @@ vector getShaderNodes(NodePtr materialNode, const string& nodeType, con } } - if (inputs.empty()) + if (shaderNodeVec.empty()) { // Try to find material nodes in the implementation graph if any. // If a target is specified the nodedef for the given target is searched for. diff --git a/source/MaterialXGenHw/HwShaderGenerator.cpp b/source/MaterialXGenHw/HwShaderGenerator.cpp index 68872ce5d2..3d4d19e2b7 100644 --- a/source/MaterialXGenHw/HwShaderGenerator.cpp +++ b/source/MaterialXGenHw/HwShaderGenerator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -400,11 +401,17 @@ ShaderNodeImplPtr HwShaderGenerator::createShaderNodeImplForNodeGraph(const Node const TypeDesc outputType = _typeSystem->getType(outputs[0]->getType()); - // Use a compound implementation. + // Use specialized implementations for nodes that output light shaders and materials. if (outputType == Type::LIGHTSHADER) { return HwLightCompoundNode::create(); } + if (outputType == Type::MATERIAL) + { + return HwMaterialCompoundNode::create(); + } + + // Use the base implementation for nodes that output other types. return CompoundNode::create(); } diff --git a/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp new file mode 100644 index 0000000000..81ae1d4b0a --- /dev/null +++ b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp @@ -0,0 +1,91 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include + +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace +{ + +const string SURFACE_CATEGORY = "surface"; + +} // anonymous namespace + +ShaderNodeImplPtr HwMaterialCompoundNode::create() +{ + return std::make_shared(); +} + +void HwMaterialCompoundNode::initialize(const InterfaceElement& element, GenContext& context) +{ + ShaderNodeImpl::initialize(element, context); + + if (!element.isA()) + { + return; + } + + const NodeGraph& graph = static_cast(element); + + // Find the first surface node in the compound and trace its + // bsdf/edf/opacity inputs back through interfacename to the + // nodedef's external input names. + for (NodePtr node : graph.getNodes()) + { + if (node->getCategory() == SURFACE_CATEGORY) + { + InputPtr bsdfIn = node->getInput("bsdf"); + if (bsdfIn && !bsdfIn->getInterfaceName().empty()) + { + _bsdfInputName = bsdfIn->getInterfaceName(); + } + + InputPtr edfIn = node->getInput("edf"); + if (edfIn && !edfIn->getInterfaceName().empty()) + { + _edfInputName = edfIn->getInterfaceName(); + } + + InputPtr opacityIn = node->getInput("opacity"); + if (opacityIn && !opacityIn->getInterfaceName().empty()) + { + _opacityInputName = opacityIn->getInterfaceName(); + } + + break; + } + } +} + +void HwMaterialCompoundNode::addClassification(ShaderNode& node) const +{ + // Add SHADER|SURFACE so the top-level graph triggers the + // surface shader code path in the HW pixel shader generators. + node.addClassification(ShaderNode::Classification::SHADER | + ShaderNode::Classification::SURFACE); +} + +const string& HwMaterialCompoundNode::getBsdfInputName() const +{ + return _bsdfInputName; +} + +const string& HwMaterialCompoundNode::getEdfInputName() const +{ + return _edfInputName; +} + +const string& HwMaterialCompoundNode::getOpacityInputName() const +{ + return _opacityInputName; +} + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.h b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.h new file mode 100644 index 0000000000..b30bf042c1 --- /dev/null +++ b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.h @@ -0,0 +1,36 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_HWMATERIALCOMPOUNDNODE_H +#define MATERIALX_HWMATERIALCOMPOUNDNODE_H + +#include + +MATERIALX_NAMESPACE_BEGIN + +/// MaterialCompound node implementation for hardware languages. +/// Used for nodegraphs that output material type, such as LamaSurface. +class MX_GENHW_API HwMaterialCompoundNode : public HwSurfaceNode +{ + public: + static ShaderNodeImplPtr create(); + + void initialize(const InterfaceElement& element, GenContext& context) override; + void addClassification(ShaderNode& node) const override; + + protected: + const string& getBsdfInputName() const override; + const string& getEdfInputName() const override; + const string& getOpacityInputName() const override; + + private: + string _bsdfInputName; + string _edfInputName; + string _opacityInputName; +}; + +MATERIALX_NAMESPACE_END + +#endif diff --git a/source/MaterialXGenHw/Nodes/HwSurfaceNode.cpp b/source/MaterialXGenHw/Nodes/HwSurfaceNode.cpp index d9952dacf6..860e382144 100644 --- a/source/MaterialXGenHw/Nodes/HwSurfaceNode.cpp +++ b/source/MaterialXGenHw/Nodes/HwSurfaceNode.cpp @@ -12,6 +12,15 @@ MATERIALX_NAMESPACE_BEGIN +namespace +{ + +const string INPUT_BSDF = "bsdf"; +const string INPUT_EDF = "edf"; +const string INPUT_OPACITY = "opacity"; + +} // anonymous namespace + HwSurfaceNode::HwSurfaceNode() { } @@ -21,6 +30,21 @@ ShaderNodeImplPtr HwSurfaceNode::create() return std::make_shared(); } +const string& HwSurfaceNode::getBsdfInputName() const +{ + return INPUT_BSDF; +} + +const string& HwSurfaceNode::getEdfInputName() const +{ + return INPUT_EDF; +} + +const string& HwSurfaceNode::getOpacityInputName() const +{ + return INPUT_OPACITY; +} + void HwSurfaceNode::createVariables(const ShaderNode&, GenContext& context, Shader& shader) const { // TODO: @@ -105,128 +129,137 @@ void HwSurfaceNode::emitFunctionCall(const ShaderNode& node, GenContext& context const string outColor = output->getVariable() + ".color"; const string outTransparency = output->getVariable() + ".transparency"; - const ShaderInput* bsdfInput = node.getInput("bsdf"); - if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) + const ShaderInput* bsdfInput = node.getInput(getBsdfInputName()); + if (bsdfInput) { - shadergen.emitLineBegin(stage); - shadergen.emitString("float surfaceOpacity = ", stage); - shadergen.emitInput(node.getInput("opacity"), context, stage); - shadergen.emitLineEnd(stage); - shadergen.emitLineBreak(stage); - - // - // Handle direct lighting - // - shadergen.emitComment("Shadow occlusion", stage); - if (context.getOptions().hwShadowMap) + if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) { - shadergen.emitLine("occlusion = mx_shadow_occlusion(" + HW::T_SHADOW_MAP + ", " + HW::T_SHADOW_MATRIX + ", " + prefix + HW::T_POSITION_WORLD + ")", stage); - } - shadergen.emitLineBreak(stage); + shadergen.emitLineBegin(stage); + shadergen.emitString("float surfaceOpacity = ", stage); + shadergen.emitInput(node.getInput(getOpacityInputName()), context, stage); + shadergen.emitLineEnd(stage); + shadergen.emitLineBreak(stage); - emitLightLoop(node, context, stage, outColor); + // + // Handle direct lighting + // + shadergen.emitComment("Shadow occlusion", stage); + if (context.getOptions().hwShadowMap) + { + shadergen.emitLine("occlusion = mx_shadow_occlusion(" + HW::T_SHADOW_MAP + ", " + HW::T_SHADOW_MATRIX + ", " + prefix + HW::T_POSITION_WORLD + ")", stage); + } + shadergen.emitLineBreak(stage); - // - // Handle indirect lighting. - // - shadergen.emitComment("Ambient occlusion", stage); - if (context.getOptions().hwAmbientOcclusion) - { - ShaderPort* texcoord = vertexData[HW::T_TEXCOORD + "_0"]; - shadergen.emitLine(vec2+" ambOccUv = " + prefix + texcoord->getVariable(), stage); - if (context.getOptions().fileTextureVerticalFlip) + emitLightLoop(node, context, stage, outColor); + + // + // Handle indirect lighting. + // + shadergen.emitComment("Ambient occlusion", stage); + if (context.getOptions().hwAmbientOcclusion) { - shadergen.emitLine("ambOccUv = "+vec2+"(ambOccUv.x, 1.0 - ambOccUv.y)", stage); + ShaderPort* texcoord = vertexData[HW::T_TEXCOORD + "_0"]; + shadergen.emitLine(vec2+" ambOccUv = " + prefix + texcoord->getVariable(), stage); + if (context.getOptions().fileTextureVerticalFlip) + { + shadergen.emitLine("ambOccUv = "+vec2+"(ambOccUv.x, 1.0 - ambOccUv.y)", stage); + } + shadergen.emitLine("occlusion = mix(1.0, texture(" + HW::T_AMB_OCC_MAP + ", ambOccUv).x, " + HW::T_AMB_OCC_GAIN + ")", stage); } - shadergen.emitLine("occlusion = mix(1.0, texture(" + HW::T_AMB_OCC_MAP + ", ambOccUv).x, " + HW::T_AMB_OCC_GAIN + ")", stage); - } - else - { - shadergen.emitLine("occlusion = 1.0", stage); - } - shadergen.emitLineBreak(stage); + else + { + shadergen.emitLine("occlusion = 1.0", stage); + } + shadergen.emitLineBreak(stage); - shadergen.emitComment("Add environment contribution", stage); - shadergen.emitScopeBegin(stage); + shadergen.emitComment("Add environment contribution", stage); + shadergen.emitScopeBegin(stage); - // indirect lighting - if (bsdf->hasClassification(ShaderNode::Classification::BSDF_R)) - { - shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_INDIRECT, L, V, N, P, occlusion)", stage); - shadergen.emitFunctionCall(*bsdf, context, stage); - } - else - { - shadergen.emitLineBegin(stage); - shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); - shadergen.emitLineEnd(stage); - } + // indirect lighting + if (bsdf->hasClassification(ShaderNode::Classification::BSDF_R)) + { + shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_INDIRECT, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*bsdf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } - shadergen.emitLineBreak(stage); - shadergen.emitLine(outColor + " += occlusion * " + bsdf->getOutput()->getVariable() + ".response", stage); - shadergen.emitScopeEnd(stage); - shadergen.emitLineBreak(stage); + shadergen.emitLineBreak(stage); + shadergen.emitLine(outColor + " += occlusion * " + bsdf->getOutput()->getVariable() + ".response", stage); + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + } } // // Handle surface emission. // - const ShaderInput* edfInput = node.getInput("edf"); - if (const ShaderNode* edf = edfInput->getConnectedSibling()) + const ShaderInput* edfInput = node.getInput(getEdfInputName()); + if (edfInput) { - shadergen.emitComment("Add surface emission", stage); - shadergen.emitScopeBegin(stage); - - if (edf->hasClassification(ShaderNode::Classification::EDF)) + if (const ShaderNode* edf = edfInput->getConnectedSibling()) { - shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_EMISSION, L, V, N, P, occlusion)", stage); - shadergen.emitFunctionCall(*edf, context, stage); - } - else - { - shadergen.emitLineBegin(stage); - shadergen.emitOutput(edf->getOutput(), true, true, context, stage); - shadergen.emitLineEnd(stage); - } + shadergen.emitComment("Add surface emission", stage); + shadergen.emitScopeBegin(stage); - shadergen.emitLine(outColor + " += " + edf->getOutput()->getVariable(), stage); - shadergen.emitScopeEnd(stage); - shadergen.emitLineBreak(stage); + if (edf->hasClassification(ShaderNode::Classification::EDF)) + { + shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_EMISSION, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*edf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(edf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } + + shadergen.emitLine(outColor + " += " + edf->getOutput()->getVariable(), stage); + shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + } } // // Handle surface transmission and opacity. // - if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) + if (bsdfInput) { - shadergen.emitComment("Calculate the BSDF transmission for viewing direction", stage); - if (bsdf->hasClassification(ShaderNode::Classification::BSDF_T) || bsdf->hasClassification(ShaderNode::Classification::VDF)) - { - shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_TRANSMISSION, L, V, N, P, occlusion)", stage); - shadergen.emitFunctionCall(*bsdf, context, stage); - } - else + if (const ShaderNode* bsdf = bsdfInput->getConnectedSibling()) { - shadergen.emitLineBegin(stage); - shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); - shadergen.emitLineEnd(stage); - } + shadergen.emitComment("Calculate the BSDF transmission for viewing direction", stage); + if (bsdf->hasClassification(ShaderNode::Classification::BSDF_T) || bsdf->hasClassification(ShaderNode::Classification::VDF)) + { + shadergen.emitLine("ClosureData closureData = makeClosureData(CLOSURE_TYPE_TRANSMISSION, L, V, N, P, occlusion)", stage); + shadergen.emitFunctionCall(*bsdf, context, stage); + } + else + { + shadergen.emitLineBegin(stage); + shadergen.emitOutput(bsdf->getOutput(), true, true, context, stage); + shadergen.emitLineEnd(stage); + } - if (context.getOptions().hwTransmissionRenderMethod == TRANSMISSION_REFRACTION) - { - shadergen.emitLine(outColor + " += " + bsdf->getOutput()->getVariable() + ".response", stage); - } - else - { - shadergen.emitLine(outTransparency + " += " + bsdf->getOutput()->getVariable() + ".response", stage); - } + if (context.getOptions().hwTransmissionRenderMethod == TRANSMISSION_REFRACTION) + { + shadergen.emitLine(outColor + " += " + bsdf->getOutput()->getVariable() + ".response", stage); + } + else + { + shadergen.emitLine(outTransparency + " += " + bsdf->getOutput()->getVariable() + ".response", stage); + } - shadergen.emitLineBreak(stage); - shadergen.emitComment("Compute and apply surface opacity", stage); - shadergen.emitScopeBegin(stage); - shadergen.emitLine(outColor + " *= surfaceOpacity", stage); - shadergen.emitLine(outTransparency + " = mix("+vec3_one+", " + outTransparency + ", surfaceOpacity)", stage); - shadergen.emitScopeEnd(stage); + shadergen.emitLineBreak(stage); + shadergen.emitComment("Compute and apply surface opacity", stage); + shadergen.emitScopeBegin(stage); + shadergen.emitLine(outColor + " *= surfaceOpacity", stage); + shadergen.emitLine(outTransparency + " = mix("+vec3_one+", " + outTransparency + ", surfaceOpacity)", stage); + shadergen.emitScopeEnd(stage); + } } shadergen.emitScopeEnd(stage); @@ -245,8 +278,12 @@ void HwSurfaceNode::emitLightLoop(const ShaderNode& node, GenContext& context, S const VariableBlock& vertexData = stage.getInputBlock(HW::VERTEX_DATA); const string prefix = shadergen.getVertexDataPrefix(vertexData); - const ShaderInput* bsdfInput = node.getInput("bsdf"); - const ShaderNode* bsdf = bsdfInput->getConnectedSibling(); + const ShaderInput* bsdfInput = node.getInput(getBsdfInputName()); + const ShaderNode* bsdf = bsdfInput ? bsdfInput->getConnectedSibling() : nullptr; + if (!bsdf) + { + return; + } shadergen.emitComment("Light loop", stage); shadergen.emitLine("int numLights = numActiveLightSources()", stage); diff --git a/source/MaterialXGenHw/Nodes/HwSurfaceNode.h b/source/MaterialXGenHw/Nodes/HwSurfaceNode.h index cdd79106fc..3e3f5f4f4c 100644 --- a/source/MaterialXGenHw/Nodes/HwSurfaceNode.h +++ b/source/MaterialXGenHw/Nodes/HwSurfaceNode.h @@ -23,6 +23,16 @@ class MX_GENHW_API HwSurfaceNode : public HwImplementation void emitFunctionCall(const ShaderNode& node, GenContext& context, ShaderStage& stage) const override; virtual void emitLightLoop(const ShaderNode& node, GenContext& context, ShaderStage& stage, const string& outColor) const; + + protected: + /// Return the name of the BSDF input on the node. + virtual const string& getBsdfInputName() const; + + /// Return the name of the EDF input on the node. + virtual const string& getEdfInputName() const; + + /// Return the name of the opacity input on the node. + virtual const string& getOpacityInputName() const; }; MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenShader/Util.cpp b/source/MaterialXGenShader/Util.cpp index 3b2f2ff02e..b1325eda5f 100644 --- a/source/MaterialXGenShader/Util.cpp +++ b/source/MaterialXGenShader/Util.cpp @@ -269,7 +269,7 @@ bool isTransparentSurface(ElementPtr element, const string& target) if (node) { // Handle material nodes. - if (node->getCategory() == SURFACE_MATERIAL_NODE_STRING) + if (node->getType() == MATERIAL_TYPE_STRING) { vector shaderNodes = getShaderNodes(node); if (!shaderNodes.empty()) From e0c6b157b50ea5191a283926d3c0d6df731bfbf1 Mon Sep 17 00:00:00 2001 From: Jonathan Stone Date: Tue, 17 Mar 2026 14:49:59 -0700 Subject: [PATCH 2/2] Use graph traversal for robustness Instead of looping over nodes to find the first surface node, use graph traversal from each output, which should be more robust in complex graphs. --- .../Nodes/HwMaterialCompoundNode.cpp | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp index 81ae1d4b0a..d73c7a7314 100644 --- a/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp +++ b/source/MaterialXGenHw/Nodes/HwMaterialCompoundNode.cpp @@ -15,6 +15,7 @@ MATERIALX_NAMESPACE_BEGIN namespace { +const string MATERIAL_TYPE = "material"; const string SURFACE_CATEGORY = "surface"; } // anonymous namespace @@ -35,32 +36,39 @@ void HwMaterialCompoundNode::initialize(const InterfaceElement& element, GenCont const NodeGraph& graph = static_cast(element); - // Find the first surface node in the compound and trace its - // bsdf/edf/opacity inputs back through interfacename to the - // nodedef's external input names. - for (NodePtr node : graph.getNodes()) + // Find the material-type output and traverse upstream to discover the + // surface node, reading its bsdf/edf/opacity interface name mappings. + for (OutputPtr output : graph.getOutputs()) { - if (node->getCategory() == SURFACE_CATEGORY) + if (output->getType() == MATERIAL_TYPE) { - InputPtr bsdfIn = node->getInput("bsdf"); - if (bsdfIn && !bsdfIn->getInterfaceName().empty()) + for (Edge edge : output->traverseGraph()) { - _bsdfInputName = bsdfIn->getInterfaceName(); + ElementPtr upstream = edge.getUpstreamElement(); + NodePtr upstreamNode = upstream ? upstream->asA() : nullptr; + if (upstreamNode && upstreamNode->getCategory() == SURFACE_CATEGORY) + { + InputPtr bsdfIn = upstreamNode->getInput("bsdf"); + if (bsdfIn && !bsdfIn->getInterfaceName().empty()) + { + _bsdfInputName = bsdfIn->getInterfaceName(); + } + + InputPtr edfIn = upstreamNode->getInput("edf"); + if (edfIn && !edfIn->getInterfaceName().empty()) + { + _edfInputName = edfIn->getInterfaceName(); + } + + InputPtr opacityIn = upstreamNode->getInput("opacity"); + if (opacityIn && !opacityIn->getInterfaceName().empty()) + { + _opacityInputName = opacityIn->getInterfaceName(); + } + + return; + } } - - InputPtr edfIn = node->getInput("edf"); - if (edfIn && !edfIn->getInterfaceName().empty()) - { - _edfInputName = edfIn->getInterfaceName(); - } - - InputPtr opacityIn = node->getInput("opacity"); - if (opacityIn && !opacityIn->getInterfaceName().empty()) - { - _opacityInputName = opacityIn->getInterfaceName(); - } - - break; } } }