From d92197b24568ea696e175351bd3fbd0fe10b525a Mon Sep 17 00:00:00 2001 From: Bernard Kwok Date: Mon, 23 Feb 2026 14:50:45 -0500 Subject: [PATCH] Mermaid graphing util for ShaderGraphs. --- .github/workflows/main.yml | 1 + python/Scripts/generateshader.py | 13 +++++ .../JsMaterialXGenShader/JsShader.cpp | 1 + source/MaterialXGenShader/Shader.cpp | 5 ++ source/MaterialXGenShader/Shader.h | 5 ++ source/MaterialXGenShader/ShaderGraph.cpp | 52 +++++++++++++++++++ source/MaterialXGenShader/ShaderGraph.h | 5 ++ .../PyMaterialXGenShader/PyShader.cpp | 3 +- 8 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9de29a53b..37f537a680 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -247,6 +247,7 @@ jobs: python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target mdl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target msl python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --target slang + python Scripts/generateshader.py ../resources/Materials/Examples/StandardSurface --graph --graphValues working-directory: python - name: Shader Validation Tests (Windows) diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py index 46ff359043..1f7a8ba0c5 100644 --- a/python/Scripts/generateshader.py +++ b/python/Scripts/generateshader.py @@ -53,6 +53,8 @@ def main(): parser.add_argument('--validatorArgs', dest='validatorArgs', nargs='?', const=' ', type=str, help='Optional arguments for code validator.') parser.add_argument('--vulkanGlsl', dest='vulkanCompliantGlsl', default=False, type=bool, help='Set to True to generate Vulkan-compliant GLSL when using the genglsl target.') parser.add_argument('--shaderInterfaceType', dest='shaderInterfaceType', default=0, type=int, help='Set the type of shader interface to be generated') + parser.add_argument('--graph', dest='graph', action='store_true', help='Set to True to generate a Mermaid graph of the shader graph for each shader.') + parser.add_argument('--graphValues', dest='graphValues', action='store_true', help='Set to True to output input values for Mermaid graphs.') parser.add_argument(dest='inputFilename', help='Path to input document or folder containing input documents.') opts = parser.parse_args() @@ -156,6 +158,17 @@ def main(): elemName = mx.createValidName(elemName) shader = shadergen.generate(elemName, elem, context) if shader: + # Generate a Mermaid graph of the shader graph if requested + if opts.graph: + graphValues = opts.graphValues == True + mermaidGraph = shader.createMermaidGraph(graphValues) + mermaidGraph = "```mermaid\n" + mermaidGraph + "\n```" + filename = pathPrefix + "/" + shader.getName() + "." + gentarget + ".md" + print('--- Wrote Mermaid graph to: ' + filename) + file = open(filename, 'w+') + file.write(mermaidGraph) + file.close() + # Use extension of .vert and .frag as it's type is # recognized by glslangValidator if gentarget in ['glsl', 'essl', 'vulkan', 'msl', 'wgsl']: diff --git a/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp b/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp index 74902d8036..f401be372d 100644 --- a/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp +++ b/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp @@ -19,5 +19,6 @@ EMSCRIPTEN_BINDINGS(Shader) .smart_ptr>("ShaderPtr") .function("getSourceCode", &mx::Shader::getSourceCode) .function("getStage", PTR_RETURN_OVERLOAD(mx::ShaderStage& (mx::Shader::*)(const std::string&), &mx::Shader::getStage), ems::allow_raw_pointers()) + .function("createMermaidGraph", &mx::Shader::createMermaidGraph) ; } diff --git a/source/MaterialXGenShader/Shader.cpp b/source/MaterialXGenShader/Shader.cpp index ab22d45389..5ae8f02720 100644 --- a/source/MaterialXGenShader/Shader.cpp +++ b/source/MaterialXGenShader/Shader.cpp @@ -67,4 +67,9 @@ ShaderStagePtr Shader::createStage(const string& name, ConstSyntaxPtr syntax) return s; } +string Shader::createMermaidGraph(bool showInputValues) const +{ + return _graph ? _graph->createMermaidGraph(showInputValues) : string(); +} + MATERIALX_NAMESPACE_END diff --git a/source/MaterialXGenShader/Shader.h b/source/MaterialXGenShader/Shader.h index 070741c22a..e435d664c3 100644 --- a/source/MaterialXGenShader/Shader.h +++ b/source/MaterialXGenShader/Shader.h @@ -100,6 +100,11 @@ class MX_GENSHADER_API Shader /// Return the shader source code for a given shader stage. const string& getSourceCode(const string& stage = Stage::PIXEL) const { return getStage(stage).getSourceCode(); } + /// Generate a Mermaid graph representing the shader graph and its nodes. + /// @param showInputValues If true, the graph will include the values of any unconnected input sockets + /// @return Mermaid graph as a string. + string createMermaidGraph(bool showInputValues) const; + protected: /// Create a new stage in the shader. ShaderStagePtr createStage(const string& name, ConstSyntaxPtr syntax); diff --git a/source/MaterialXGenShader/ShaderGraph.cpp b/source/MaterialXGenShader/ShaderGraph.cpp index bfb15e9281..3a3f777d41 100644 --- a/source/MaterialXGenShader/ShaderGraph.cpp +++ b/source/MaterialXGenShader/ShaderGraph.cpp @@ -1308,6 +1308,58 @@ void ShaderGraph::populateUnitTransformMap(UnitSystemPtr unitSystem, ShaderPort* } } +string ShaderGraph::createMermaidGraph(bool showInputValues) const +{ + std::ostringstream oss; + oss << "graph LR\n"; + + // Emit nodes + for (const ShaderNode* node : getNodes()) + { + const string& id = node->getUniqueId(); + oss << " " << id << "[\"" << id << "\"]\n"; + } + + for (const ShaderNode* node : getNodes()) + { + const string& nodeId = node->getUniqueId(); + + // Emit connections + for (const ShaderOutput* output : node->getOutputs()) + { + const ShaderInputVec& connections = output->getConnections(); + for (const ShaderInput* input : connections) + { + if (input && input->getNode()) + { + const string connector = " --\"" + output->getName() + " --> " + input->getName() + "\"--> "; + oss << " " << nodeId << connector << input->getNode()->getUniqueId() << "\n"; + } + } + } + + // Optionally emit input values + if (showInputValues) + { + for (const ShaderInput* input : node->getInputs()) + { + if (input) + { + const string& valueString = input->getValueString(); + if (!valueString.empty()) + { + oss << " " << node->getUniqueId() << "/" << input->getName(); + oss << "[\"" << input->getName() << " = " << valueString << "\"]"; + oss << " --> " << nodeId << "\n"; + } + } + } + } + } + + return oss.str(); +} + namespace { static const ShaderGraphEdgeIterator NULL_EDGE_ITERATOR(nullptr); diff --git a/source/MaterialXGenShader/ShaderGraph.h b/source/MaterialXGenShader/ShaderGraph.h index 0fe9a93124..65c52fef85 100644 --- a/source/MaterialXGenShader/ShaderGraph.h +++ b/source/MaterialXGenShader/ShaderGraph.h @@ -70,6 +70,11 @@ class MX_GENSHADER_API ShaderGraph : public ShaderNode /// Get a vector of all nodes in order const vector& getNodes() const { return _nodeOrder; } + /// Generate a Mermaid graph representing this shader graph. + /// @param showInputValues If true, the graph will include the values of any unconnected input sockets + /// @return Mermaid graph as a string. + string createMermaidGraph(bool showInputValues) const; + /// Get number of input sockets size_t numInputSockets() const { return numOutputs(); } diff --git a/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp b/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp index 581964aeac..434385cfbd 100644 --- a/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp +++ b/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp @@ -28,5 +28,6 @@ void bindPyShader(py::module& mod) .def("hasAttribute", &mx::Shader::hasAttribute) .def("getAttribute", &mx::Shader::getAttribute) .def("setAttribute", static_cast(&mx::Shader::setAttribute)) - .def("setAttribute", static_cast(&mx::Shader::setAttribute)); + .def("setAttribute", static_cast(&mx::Shader::setAttribute)) + .def("createMermaidGraph", &mx::Shader::createMermaidGraph); }