Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d92197b
Mermaid graphing util for ShaderGraphs.
kwokcb Feb 23, 2026
bc1175d
Merge remote-tracking branch 'upstream/main' into graph_genshader_graphs
kwokcb Mar 2, 2026
bc5b62c
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Apr 1, 2026
7ac6533
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Apr 8, 2026
e80f30e
Merge remote-tracking branch 'aswf/main' into graph_genshader_graphs
kwokcb Apr 10, 2026
120932c
Merge remote-tracking branch 'upstream/main' into graph_genshader_graphs
kwokcb Apr 20, 2026
2bb110d
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb May 28, 2026
a71d1c5
Merge remote-tracking branch 'aswf/main' into graph_genshader_graphs
kwokcb Jun 22, 2026
0419edd
Merge remote-tracking branch 'aswf/main' into graph_genshader_graphs
kwokcb Jun 25, 2026
116b614
Merge remote-tracking branch 'aswf/main' into graph_genshader_graphs
kwokcb Jul 6, 2026
66ca7dc
Rework to expose generic API for shader graph nodes and connections.
kwokcb Jul 8, 2026
91e1bf5
Add JS test.
kwokcb Jul 8, 2026
d65026f
Merge remote-tracking branch 'aswf/main' into graph_genshader_graphs
kwokcb Jul 8, 2026
ddf42c9
Expose input and output sockets == input uniforms.
kwokcb Jul 8, 2026
965878d
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Jul 15, 2026
fdf0576
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Jul 23, 2026
d52e122
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Aug 4, 2026
81f35fc
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Aug 11, 2026
266a72b
Merge branch 'main' into graph_genshader_graphs
jstone-lucasfilm Sep 20, 2026
04f6131
Merge branch 'AcademySoftwareFoundation:main' into graph_genshader_gr…
kwokcb Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,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)
Expand Down
20 changes: 20 additions & 0 deletions javascript/MaterialXTest/browser/shaderGenerator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ test.describe('Generate Shaders', () =>
shaderInput.setType('surfaceshader');
shaderInput.setNodeName(ssName);

docString = mx.writeToXmlString(doc);
console.log('Document XML:\n' + docString);

const valid = doc.validate();
shaderInput.delete();
smNode.delete();
Expand Down Expand Up @@ -149,6 +152,23 @@ test.describe('Generate Shaders', () =>
errors.push('Fragment shader: ' + gl.getShaderInfoLog(glFS));
}

if (typeof mx.createMermaidGraph === 'function')
{
const mermaidGraph = mx.createMermaidGraph(mxShader, true);
if (!mermaidGraph || !mermaidGraph.includes('graph '))
{
errors.push('Failed to generate Mermaid graph');
}
else
{
console.log('Shader graph diagram generated.');
}
}
else
{
errors.push('createMermaidGraph is not available on JsMaterialXGenShader module');
}

gl.deleteShader(glVS);
gl.deleteShader(glFS);
}
Expand Down
175 changes: 175 additions & 0 deletions python/Scripts/generateshader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,168 @@
import MaterialX.PyMaterialXGenSlang as mx_gen_slang
import MaterialX.PyMaterialXGenShader as mx_gen_shader


def createMermaidGraph(shader, orientation='TB', showInputValues=False):
'''
Generate a Mermaid graph of the shader graph for a given shader.
@param shader: The shader to generate a graph for.
@param orientation: The orientation of the graph. Can be 'TB' (top to bottom), 'LR' (left to right), 'RL' (right to left), or 'BT' (bottom to top).
@param showInputValues: If True, include input values in the graph.
@return: A string containing the Mermaid graph.
'''
lines = ['graph ' + orientation]
nodes = shader.getNodes()

# Get input socket list
inputSockets = shader.getInputSockets()
inputSocketNames = []
for inputSocket in inputSockets:
inputSocketNames.append(mx.createValidName(inputSocket.getName()))

# Color mapper for node classifications
classificationMap = {
# Material / shader family (green-blue)
mx_gen_shader.ShaderNode.MATERIAL: '#7ccba2',
mx_gen_shader.ShaderNode.SHADER: "#0e7865",
mx_gen_shader.ShaderNode.SURFACE: '#4f9fc5',
mx_gen_shader.ShaderNode.VOLUME: '#3d84b8',
mx_gen_shader.ShaderNode.LIGHT: '#2f6fa3',
mx_gen_shader.ShaderNode.UNLIT: '#94d2bd',

# Closure family
mx_gen_shader.ShaderNode.CLOSURE: '#f4a261',
mx_gen_shader.ShaderNode.BSDF: '#e76f51',
mx_gen_shader.ShaderNode.BSDF_R: '#f4c06a',
mx_gen_shader.ShaderNode.BSDF_T: '#f2a65a',
mx_gen_shader.ShaderNode.EDF: '#f6bd60',
mx_gen_shader.ShaderNode.VDF: '#f28482',
mx_gen_shader.ShaderNode.LAYER: '#f7a072',
mx_gen_shader.ShaderNode.MIX: '#f5b971',

# Texture sampling family
mx_gen_shader.ShaderNode.TEXTURE: "#6b5e76",
mx_gen_shader.ShaderNode.FILETEXTURE: "#3d174c",
mx_gen_shader.ShaderNode.SAMPLE2D: "#7a558a",
mx_gen_shader.ShaderNode.SAMPLE3D: "#7a4485",

# Other utility classes
mx_gen_shader.ShaderNode.CONSTANT: "#787878",
mx_gen_shader.ShaderNode.CONDITIONAL: "#e8400d",
mx_gen_shader.ShaderNode.GEOMETRIC: "#4B0349",
mx_gen_shader.ShaderNode.DOT: '#e9f5db'
}

# Label mapper for node classifications
classificationLabelMap = {
mx_gen_shader.ShaderNode.MATERIAL: 'MATERIAL',
mx_gen_shader.ShaderNode.SHADER: 'SHADER',
mx_gen_shader.ShaderNode.SURFACE: 'SURFACE',
mx_gen_shader.ShaderNode.VOLUME: 'VOLUME',
mx_gen_shader.ShaderNode.LIGHT: 'LIGHT',
mx_gen_shader.ShaderNode.UNLIT: 'UNLIT',
mx_gen_shader.ShaderNode.CLOSURE: 'CLOSURE',
mx_gen_shader.ShaderNode.BSDF: 'BSDF',
mx_gen_shader.ShaderNode.BSDF_R: 'BSDF_R',
mx_gen_shader.ShaderNode.BSDF_T: 'BSDF_T',
mx_gen_shader.ShaderNode.EDF: 'EDF',
mx_gen_shader.ShaderNode.VDF: 'VDF',
mx_gen_shader.ShaderNode.LAYER: 'LAYER',
mx_gen_shader.ShaderNode.MIX: 'MIX',
mx_gen_shader.ShaderNode.TEXTURE: 'TEXTURE',
mx_gen_shader.ShaderNode.FILETEXTURE: 'FILETEXTURE',
mx_gen_shader.ShaderNode.SAMPLE2D: 'SAMPLE2D',
mx_gen_shader.ShaderNode.SAMPLE3D: 'SAMPLE3D',
mx_gen_shader.ShaderNode.CONSTANT: 'CONSTANT',
mx_gen_shader.ShaderNode.CONDITIONAL: 'CONDITIONAL',
mx_gen_shader.ShaderNode.GEOMETRIC: 'GEOMETRIC',
mx_gen_shader.ShaderNode.DOT: 'DOT'
}

strokeColor = "#222222"
socketFillColor = "#053333"

def getTextColor(fillColor):
color = fillColor.lstrip('#')
if len(color) != 6:
return '#000000'

red = int(color[0:2], 16)
green = int(color[2:4], 16)
blue = int(color[4:6], 16)

# Relative luminance approximation for contrast selection.
brightness = (0.299 * red) + (0.587 * green) + (0.114 * blue)
return '#000000' if brightness >= 186 else '#ffffff'

def getClassificationFill(classificationBits):
# Strip out texture
classificationBits = classificationBits & ~mx_gen_shader.ShaderNode.TEXTURE
returnFill = classificationMap[mx_gen_shader.ShaderNode.TEXTURE]
for classification, fillColor in classificationMap.items():
if classificationBits & classification:# and (classificationBits & mx_gen_shader.ShaderNode.TEXTURE) == 0:
return fillColor
return returnFill

def getClassificationLabel(classificationBits):
labels = []
for classification, label in classificationLabelMap.items():
if classificationBits & classification:
labels.append(label)
if labels:
return '|'.join(labels)
return 'UNKNOWN'

# Output nodes with formatting
for node in nodes:
nodeId = node.getUniqueId()
nodeClassification = node.getClassification()
classificationFill = getClassificationFill(nodeClassification)
classificationLabel = getClassificationLabel(nodeClassification)
textColor = getTextColor(classificationFill)
lines.append(f' {nodeId}["{nodeId} ({classificationLabel})"]')
lines.append(f' style {nodeId} fill:{classificationFill},stroke:{strokeColor},stroke-width:1px,color:{textColor}')

# Output connections
for node in nodes:
nodeId = node.getUniqueId()

for output in node.getOutputs():
for inputPort in output.getConnections():
if inputPort:
inputNode = inputPort.getNode()
if inputNode.getUniqueId() != nodeId:
connector = f' --"{output.getName()} --> {inputPort.getName()}"--> '
lines.append(f' {nodeId}{connector}{inputPort.getNode().getUniqueId()}')

if showInputValues:
for inputPort in node.getInputs():
if inputPort:
connectedOutput = inputPort.getConnection()
fullInputPortName = inputPort.getName()
isInputSocket = False
if connectedOutput:
outputName = connectedOutput.getName()
# Test if it's an input socket
if inputSocketNames and outputName in inputSocketNames:
fullInputPortName = f'{outputName}'
isInputSocket = True
else:
fullInputPortName = connectedOutput.getFullName()
valueString = inputPort.getValueString()
if valueString:
if isInputSocket:
# Fill input sockets (exposed uniforms) with a different color to distinguish them from internal hard-coded shader values
lines.append(
f' {node.getUniqueId()}/{inputPort.getName()}["{fullInputPortName} = {valueString} [SOCKET]"] --"{inputPort.getName()}"--> {nodeId}'
)
lines.append(f' style {node.getUniqueId()}/{inputPort.getName()} fill:{socketFillColor},stroke:{strokeColor},stroke-width:2px,color:#FFFFFF')
else:
lines.append(
f' {node.getUniqueId()}/{inputPort.getName()}["{fullInputPortName} = {valueString}"] --"{inputPort.getName()}"--> {nodeId}'
)

return '\n'.join(lines)

def validateCode(sourceCodeFile, codevalidator, codevalidatorArgs):
if codevalidator:
cmd = codevalidator.split()
Expand Down Expand Up @@ -53,6 +215,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()

Expand Down Expand Up @@ -156,6 +320,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 = createMermaidGraph(shader, 'LR', 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']:
Expand Down
2 changes: 1 addition & 1 deletion source/JsMaterialX/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ else()
endif()


set(JS_LINK_FLAGS_GENSHADER "${JS_LINK_FLAGS_CORE} --preload-file ${PROJECT_SOURCE_DIR}/libraries@libraries ")
set(JS_LINK_FLAGS_GENSHADER "${JS_LINK_FLAGS_CORE} --post-js ${GENSHADER}post.js --preload-file ${PROJECT_SOURCE_DIR}/libraries@libraries ")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look for all post.js files.


add_executable(JsMaterialXCore MaterialXLib.cpp
${CORE_DEPS}
Expand Down
13 changes: 11 additions & 2 deletions source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@
// SPDX-License-Identifier: Apache-2.0
//

#include <JsMaterialX/Helpers.h>
#include <JsMaterialX/VectorHelper.h>

#include <MaterialXGenShader/Shader.h>
#include <MaterialXGenShader/ShaderNode.h>
#include <MaterialXGenShader/ShaderStage.h>

#include <emscripten/bind.h>

namespace ems = emscripten;
namespace mx = MaterialX;

std::vector<mx::ShaderNode*> getShaderNodes(mx::Shader& self)
{
const auto& nodes = self.getGraph().getNodes();
return std::vector<mx::ShaderNode*>(nodes.begin(), nodes.end());
}

EMSCRIPTEN_BINDINGS(Shader)
{
ems::class_<mx::Shader>("Shader")
.smart_ptr<std::shared_ptr<mx::Shader>>("ShaderPtr")
.function("getNodes", &getShaderNodes, ems::allow_raw_pointers())
.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("getStage", static_cast<mx::ShaderStage& (mx::Shader::*)(size_t)>(&mx::Shader::getStage), ems::allow_raw_pointers())
.function("getStage", static_cast<mx::ShaderStage& (mx::Shader::*)(const std::string&)>(&mx::Shader::getStage), ems::allow_raw_pointers())
;
}
55 changes: 52 additions & 3 deletions source/JsMaterialX/JsMaterialXGenShader/JsShaderNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//

#include "../MapHelper.h"
#include <JsMaterialX/VectorHelper.h>

#include <MaterialXGenShader/ShaderNode.h>

Expand All @@ -16,12 +16,61 @@ EMSCRIPTEN_BINDINGS(ShaderPort)
{
ems::class_<mx::ShaderPort>("ShaderPort")
.smart_ptr<std::shared_ptr<mx::ShaderPort>>("ShaderPortPtr")
.function("getNode", static_cast<mx::ShaderNode* (mx::ShaderPort::*)()>(&mx::ShaderPort::getNode), ems::allow_raw_pointers())
.function("getName", &mx::ShaderPort::getName)
.function("getFullName", &mx::ShaderPort::getFullName)
.function("getVariable", &mx::ShaderPort::getVariable)
.function("getType", &mx::ShaderPort::getType, ems::allow_raw_pointers())
.function("getType", &mx::ShaderPort::getType)
.function("getValue", &mx::ShaderPort::getValue)
.function("getValueString", &mx::ShaderPort::getValueString)
.function("getPath", &mx::ShaderPort::getPath)
.function("getUnit", &mx::ShaderPort::getUnit)
.function("getColorSpace", &mx::ShaderPort::getColorSpace)
.function("setGeomProp", &mx::ShaderPort::setGeomProp)
.function("getGeomProp", &mx::ShaderPort::getGeomProp)
.function("getFlags", &mx::ShaderPort::getFlags)
.function("hasAuthoredValue", &mx::ShaderPort::hasAuthoredValue)
.function("setGeomProp", &mx::ShaderPort::setGeomProp)
;

ems::class_<mx::ShaderInput, ems::base<mx::ShaderPort>>("ShaderInput")
.smart_ptr<std::shared_ptr<mx::ShaderInput>>("ShaderInputPtr")
.function("getConnection", static_cast<const mx::ShaderOutput* (mx::ShaderInput::*)() const>(&mx::ShaderInput::getConnection), ems::allow_raw_pointers())
;

ems::class_<mx::ShaderOutput, ems::base<mx::ShaderPort>>("ShaderOutput")
.smart_ptr<std::shared_ptr<mx::ShaderOutput>>("ShaderOutputPtr")
.function("getConnections", &mx::ShaderOutput::getConnections)
;

ems::class_<mx::ShaderNode>("ShaderNode")
.smart_ptr<std::shared_ptr<mx::ShaderNode>>("ShaderNodePtr")
.smart_ptr<std::shared_ptr<const mx::ShaderNode>>("ShaderNode")
.class_property("TEXTURE", &mx::ShaderNode::Classification::TEXTURE)
.class_property("CLOSURE", &mx::ShaderNode::Classification::CLOSURE)
.class_property("SHADER", &mx::ShaderNode::Classification::SHADER)
.class_property("MATERIAL", &mx::ShaderNode::Classification::MATERIAL)
.class_property("FILETEXTURE", &mx::ShaderNode::Classification::FILETEXTURE)
.class_property("CONDITIONAL", &mx::ShaderNode::Classification::CONDITIONAL)
.class_property("CONSTANT", &mx::ShaderNode::Classification::CONSTANT)
.class_property("BSDF", &mx::ShaderNode::Classification::BSDF)
.class_property("BSDF_R", &mx::ShaderNode::Classification::BSDF_R)
.class_property("BSDF_T", &mx::ShaderNode::Classification::BSDF_T)
.class_property("EDF", &mx::ShaderNode::Classification::EDF)
.class_property("VDF", &mx::ShaderNode::Classification::VDF)
.class_property("LAYER", &mx::ShaderNode::Classification::LAYER)
.class_property("MIX", &mx::ShaderNode::Classification::MIX)
.class_property("SURFACE", &mx::ShaderNode::Classification::SURFACE)
.class_property("VOLUME", &mx::ShaderNode::Classification::VOLUME)
.class_property("LIGHT", &mx::ShaderNode::Classification::LIGHT)
.class_property("UNLIT", &mx::ShaderNode::Classification::UNLIT)
.class_property("SAMPLE2D", &mx::ShaderNode::Classification::SAMPLE2D)
.class_property("SAMPLE3D", &mx::ShaderNode::Classification::SAMPLE3D)
.class_property("GEOMETRIC", &mx::ShaderNode::Classification::GEOMETRIC)
.class_property("DOT", &mx::ShaderNode::Classification::DOT)
.function("getName", &mx::ShaderNode::getName)
.function("getUniqueId", &mx::ShaderNode::getUniqueId)
.function("getClassification", &mx::ShaderNode::getClassification)
.function("getInputs", &mx::ShaderNode::getInputs)
.function("getOutputs", &mx::ShaderNode::getOutputs)
;
}
Loading
Loading