diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index acc9dbab0c..9fb6ff899d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -280,6 +280,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/javascript/MaterialXTest/browser/shaderGenerator.spec.js b/javascript/MaterialXTest/browser/shaderGenerator.spec.js index 51ad354727..977be23d0b 100644 --- a/javascript/MaterialXTest/browser/shaderGenerator.spec.js +++ b/javascript/MaterialXTest/browser/shaderGenerator.spec.js @@ -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(); @@ -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); } diff --git a/python/Scripts/generateshader.py b/python/Scripts/generateshader.py index 46ff359043..7c3bbab2f5 100644 --- a/python/Scripts/generateshader.py +++ b/python/Scripts/generateshader.py @@ -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() @@ -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() @@ -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']: diff --git a/source/JsMaterialX/CMakeLists.txt b/source/JsMaterialX/CMakeLists.txt index 60ecd9048a..330dfe301e 100644 --- a/source/JsMaterialX/CMakeLists.txt +++ b/source/JsMaterialX/CMakeLists.txt @@ -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 ") add_executable(JsMaterialXCore MaterialXLib.cpp ${CORE_DEPS} diff --git a/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp b/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp index 74902d8036..7827d6cb31 100644 --- a/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp +++ b/source/JsMaterialX/JsMaterialXGenShader/JsShader.cpp @@ -3,9 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 // -#include +#include #include +#include #include #include @@ -13,11 +14,19 @@ namespace ems = emscripten; namespace mx = MaterialX; +std::vector getShaderNodes(mx::Shader& self) +{ + const auto& nodes = self.getGraph().getNodes(); + return std::vector(nodes.begin(), nodes.end()); +} + EMSCRIPTEN_BINDINGS(Shader) { ems::class_("Shader") .smart_ptr>("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::Shader::getStage), ems::allow_raw_pointers()) + .function("getStage", static_cast(&mx::Shader::getStage), ems::allow_raw_pointers()) ; } diff --git a/source/JsMaterialX/JsMaterialXGenShader/JsShaderNode.cpp b/source/JsMaterialX/JsMaterialXGenShader/JsShaderNode.cpp index e2268209bc..cd56b6f920 100644 --- a/source/JsMaterialX/JsMaterialXGenShader/JsShaderNode.cpp +++ b/source/JsMaterialX/JsMaterialXGenShader/JsShaderNode.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "../MapHelper.h" +#include #include @@ -16,12 +16,61 @@ EMSCRIPTEN_BINDINGS(ShaderPort) { ems::class_("ShaderPort") .smart_ptr>("ShaderPortPtr") + .function("getNode", static_cast(&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_>("ShaderInput") + .smart_ptr>("ShaderInputPtr") + .function("getConnection", static_cast(&mx::ShaderInput::getConnection), ems::allow_raw_pointers()) + ; + + ems::class_>("ShaderOutput") + .smart_ptr>("ShaderOutputPtr") + .function("getConnections", &mx::ShaderOutput::getConnections) + ; + + ems::class_("ShaderNode") + .smart_ptr>("ShaderNodePtr") + .smart_ptr>("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) ; } diff --git a/source/JsMaterialX/JsMaterialXGenShader/post.js b/source/JsMaterialX/JsMaterialXGenShader/post.js new file mode 100644 index 0000000000..e1270d69a3 --- /dev/null +++ b/source/JsMaterialX/JsMaterialXGenShader/post.js @@ -0,0 +1,195 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +// Wrapping code in an anonymous function to prevent clashes with the main module and other pre / post JS. +(function () { + onModuleReady(function () { + // Generate a Mermaid graph string for a generated Shader using JS-side APIs. + function _mxEscapeMermaidLabel(value) { + return String(value) + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"'); + } + + function _mxGetTextColor(fillColor) { + var color = fillColor.replace("#", ""); + if (color.length !== 6) { + return "#000000"; + } + + var red = parseInt(color.substring(0, 2), 16); + var green = parseInt(color.substring(2, 4), 16); + var blue = parseInt(color.substring(4, 6), 16); + var brightness = (0.299 * red) + (0.587 * green) + (0.114 * blue); + return brightness >= 186 ? "#000000" : "#ffffff"; + } + + function _mxGetClassificationData() { + var n = Module.ShaderNode; + var colorByClass = [ + [n.MATERIAL, "#7ccba2"], + [n.SHADER, "#0e7865"], + [n.SURFACE, "#4f9fc5"], + [n.VOLUME, "#3d84b8"], + [n.LIGHT, "#2f6fa3"], + [n.UNLIT, "#94d2bd"], + [n.CLOSURE, "#f4a261"], + [n.BSDF, "#e76f51"], + [n.BSDF_R, "#f4c06a"], + [n.BSDF_T, "#f2a65a"], + [n.EDF, "#f6bd60"], + [n.VDF, "#f28482"], + [n.LAYER, "#f7a072"], + [n.MIX, "#f5b971"], + [n.TEXTURE, "#c27ff8"], + [n.FILETEXTURE, "#be5ae6"], + [n.SAMPLE2D, "#7a558a"], + [n.SAMPLE3D, "#7a4485"], + [n.CONSTANT, "#888888"], + [n.CONDITIONAL, "#e8400d"], + [n.GEOMETRIC, "#4B0336"], + [n.DOT, "#e9f5db"] + ]; + + var labelByClass = [ + [n.MATERIAL, "MATERIAL"], + [n.SHADER, "SHADER"], + [n.SURFACE, "SURFACE"], + [n.VOLUME, "VOLUME"], + [n.LIGHT, "LIGHT"], + [n.UNLIT, "UNLIT"], + [n.CLOSURE, "CLOSURE"], + [n.BSDF, "BSDF"], + [n.BSDF_R, "BSDF_R"], + [n.BSDF_T, "BSDF_T"], + [n.EDF, "EDF"], + [n.VDF, "VDF"], + [n.LAYER, "LAYER"], + [n.MIX, "MIX"], + [n.TEXTURE, "TEXTURE"], + [n.FILETEXTURE, "FILETEXTURE"], + [n.SAMPLE2D, "SAMPLE2D"], + [n.SAMPLE3D, "SAMPLE3D"], + [n.CONSTANT, "CONSTANT"], + [n.CONDITIONAL, "CONDITIONAL"], + [n.GEOMETRIC, "GEOMETRIC"], + [n.DOT, "DOT"] + ]; + + return { + colorByClass: colorByClass, + labelByClass: labelByClass, + textureClass: n.TEXTURE + }; + } + + function _mxGetClassificationFill(classificationBits, classData) { + var textureColor = "#c27ff8"; + var nonTextureBits = classificationBits & ~classData.textureClass; + for (var i = 0; i < classData.colorByClass.length; ++i) { + var entry = classData.colorByClass[i]; + if (nonTextureBits & entry[0]) { + return entry[1]; + } + } + return textureColor; + } + + function _mxGetClassificationLabel(classificationBits, classData) { + var labels = []; + for (var i = 0; i < classData.labelByClass.length; ++i) { + var entry = classData.labelByClass[i]; + if (classificationBits & entry[0]) { + labels.push(entry[1]); + } + } + if (labels.length) { + return labels.join("|"); + } + return "UNKNOWN"; + } + + Module.createMermaidGraph = function(shader, showInputValues = false) { + if (arguments.length < 1 || arguments.length > 2) { + throw new Error("Function createMermaidGraph called with an invalid number of arguments (" + + arguments.length + ") - expects 1 to 2!"); + } + if (!shader || typeof shader.getNodes !== "function") { + throw new Error("createMermaidGraph expects a Shader instance."); + } + if (typeof Module.ShaderNode === "undefined") { + throw new Error("createMermaidGraph requires JsMaterialXGenShader."); + } + + var classData = _mxGetClassificationData(); + var lines = ["graph TB"]; + var strokeColor = "#222222"; + var nodes = shader.getNodes(); + + for (var ni = 0; ni < nodes.length; ++ni) { + var node = nodes[ni]; + var nodeId = node.getUniqueId(); + var classification = node.getClassification(); + var fill = _mxGetClassificationFill(classification, classData); + var textColor = _mxGetTextColor(fill); + var classLabel = _mxGetClassificationLabel(classification, classData); + + lines.push(" " + nodeId + "[\"" + _mxEscapeMermaidLabel(nodeId + " (" + classLabel + ")") + "\"]"); + lines.push(" style " + nodeId + " fill:" + fill + ",stroke:" + strokeColor + ",stroke-width:1px,color:" + textColor); + } + + for (var n2i = 0; n2i < nodes.length; ++n2i) { + var currentNode = nodes[n2i]; + var currentNodeId = currentNode.getUniqueId(); + var outputs = currentNode.getOutputs(); + + for (var oi = 0; oi < outputs.length; ++oi) { + var output = outputs[oi]; + var connections = output.getConnections(); + for (var ci = 0; ci < connections.length; ++ci) { + var inputPort = connections[ci]; + if (!inputPort) { + continue; + } + + var inputNode = inputPort.getNode(); + if (!inputNode) { + continue; + } + + var upstreamId = inputNode.getUniqueId(); + if (upstreamId === currentNodeId) { + continue; + } + + var connectorLabel = _mxEscapeMermaidLabel(output.getName() + " --> " + inputPort.getName()); + lines.push(" " + currentNodeId + " --\"" + connectorLabel + "\"--> " + upstreamId); + } + } + + if (showInputValues) { + var inputs = currentNode.getInputs(); + for (var ii = 0; ii < inputs.length; ++ii) { + var input = inputs[ii]; + if (!input) { + continue; + } + + var valueString = input.getValueString(); + if (!valueString) { + continue; + } + + var valueNodeId = currentNodeId + "/" + input.getName(); + var valueLabel = _mxEscapeMermaidLabel(input.getName() + " = " + valueString); + lines.push(" " + valueNodeId + "[\"" + valueLabel + "\"] --> " + currentNodeId); + } + } + } + + return lines.join("\n"); + }; + }); +})(); diff --git a/source/MaterialXGenShader/ShaderNode.cpp b/source/MaterialXGenShader/ShaderNode.cpp index 7c6b40ff7d..56cd896668 100644 --- a/source/MaterialXGenShader/ShaderNode.cpp +++ b/source/MaterialXGenShader/ShaderNode.cpp @@ -146,6 +146,29 @@ ShaderNodePtr createEmptyNode() const ShaderNodePtr ShaderNode::NONE = createEmptyNode(); +const uint32_t ShaderNode::Classification::TEXTURE; +const uint32_t ShaderNode::Classification::CLOSURE; +const uint32_t ShaderNode::Classification::SHADER; +const uint32_t ShaderNode::Classification::MATERIAL; +const uint32_t ShaderNode::Classification::FILETEXTURE; +const uint32_t ShaderNode::Classification::CONDITIONAL; +const uint32_t ShaderNode::Classification::CONSTANT; +const uint32_t ShaderNode::Classification::BSDF; +const uint32_t ShaderNode::Classification::BSDF_R; +const uint32_t ShaderNode::Classification::BSDF_T; +const uint32_t ShaderNode::Classification::EDF; +const uint32_t ShaderNode::Classification::VDF; +const uint32_t ShaderNode::Classification::LAYER; +const uint32_t ShaderNode::Classification::MIX; +const uint32_t ShaderNode::Classification::SURFACE; +const uint32_t ShaderNode::Classification::VOLUME; +const uint32_t ShaderNode::Classification::LIGHT; +const uint32_t ShaderNode::Classification::UNLIT; +const uint32_t ShaderNode::Classification::SAMPLE2D; +const uint32_t ShaderNode::Classification::SAMPLE3D; +const uint32_t ShaderNode::Classification::GEOMETRIC; +const uint32_t ShaderNode::Classification::DOT; + const string ShaderNode::CONSTANT = "constant"; const string ShaderNode::DOT = "dot"; const string ShaderNode::IMAGE = "image"; diff --git a/source/MaterialXGenShader/ShaderNode.h b/source/MaterialXGenShader/ShaderNode.h index 7059e0cdba..e8a71c101b 100644 --- a/source/MaterialXGenShader/ShaderNode.h +++ b/source/MaterialXGenShader/ShaderNode.h @@ -123,6 +123,8 @@ class MX_GENSHADER_API ShaderPortFlag class MX_GENSHADER_API ShaderPort : public std::enable_shared_from_this { public: + virtual ~ShaderPort() { } + /// Constructor. ShaderPort(ShaderNode* node, TypeDesc type, const string& name, ValuePtr value = nullptr); diff --git a/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp b/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp index 581964aeac..89be68dbc7 100644 --- a/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp +++ b/source/PyMaterialX/PyMaterialXGenShader/PyShader.cpp @@ -6,12 +6,28 @@ #include #include +#include #include namespace py = pybind11; namespace mx = MaterialX; +const std::vector& getShaderNodes(mx::Shader& self) +{ + return self.getGraph().getNodes(); +} + +const std::vector& getShaderInputs(mx::Shader& self) +{ + return self.getGraph().getInputSockets(); +} + +const std::vector& getShaderOutputs(mx::Shader& self) +{ + return self.getGraph().getOutputSockets(); +} + void bindPyShader(py::module& mod) { // Note: py::return_value_policy::reference was needed because getStage returns a @@ -22,11 +38,43 @@ void bindPyShader(py::module& mod) .def("getName", &mx::Shader::getName) .def("hasStage", &mx::Shader::hasStage) .def("numStages", &mx::Shader::numStages) - .def("getStage", static_cast(&mx::Shader::getStage), py::return_value_policy::reference) - .def("getStage", static_cast(&mx::Shader::getStage), py::return_value_policy::reference) + .def("getStage", static_cast(&mx::Shader::getStage), py::return_value_policy::reference) + .def("getStage", static_cast(&mx::Shader::getStage), py::return_value_policy::reference) + .def("getNodes", &getShaderNodes, py::return_value_policy::reference) + .def("getInputSockets", &getShaderInputs, py::return_value_policy::reference) + .def("getOutputSockets", &getShaderOutputs, py::return_value_policy::reference) .def("getSourceCode", &mx::Shader::getSourceCode) .def("hasAttribute", &mx::Shader::hasAttribute) .def("getAttribute", &mx::Shader::getAttribute) .def("setAttribute", static_cast(&mx::Shader::setAttribute)) .def("setAttribute", static_cast(&mx::Shader::setAttribute)); + + py::class_(mod, "ShaderNode") + .def_readonly_static("TEXTURE", &mx::ShaderNode::Classification::TEXTURE) + .def_readonly_static("CLOSURE", &mx::ShaderNode::Classification::CLOSURE) + .def_readonly_static("SHADER", &mx::ShaderNode::Classification::SHADER) + .def_readonly_static("MATERIAL", &mx::ShaderNode::Classification::MATERIAL) + .def_readonly_static("FILETEXTURE", &mx::ShaderNode::Classification::FILETEXTURE) + .def_readonly_static("CONDITIONAL", &mx::ShaderNode::Classification::CONDITIONAL) + .def_readonly_static("CONSTANT", &mx::ShaderNode::Classification::CONSTANT) + .def_readonly_static("BSDF", &mx::ShaderNode::Classification::BSDF) + .def_readonly_static("BSDF_R", &mx::ShaderNode::Classification::BSDF_R) + .def_readonly_static("BSDF_T", &mx::ShaderNode::Classification::BSDF_T) + .def_readonly_static("EDF", &mx::ShaderNode::Classification::EDF) + .def_readonly_static("VDF", &mx::ShaderNode::Classification::VDF) + .def_readonly_static("LAYER", &mx::ShaderNode::Classification::LAYER) + .def_readonly_static("MIX", &mx::ShaderNode::Classification::MIX) + .def_readonly_static("SURFACE", &mx::ShaderNode::Classification::SURFACE) + .def_readonly_static("VOLUME", &mx::ShaderNode::Classification::VOLUME) + .def_readonly_static("LIGHT", &mx::ShaderNode::Classification::LIGHT) + .def_readonly_static("UNLIT", &mx::ShaderNode::Classification::UNLIT) + .def_readonly_static("SAMPLE2D", &mx::ShaderNode::Classification::SAMPLE2D) + .def_readonly_static("SAMPLE3D", &mx::ShaderNode::Classification::SAMPLE3D) + .def_readonly_static("GEOMETRIC", &mx::ShaderNode::Classification::GEOMETRIC) + .def_readonly_static("DOT", &mx::ShaderNode::Classification::DOT) + .def("getName", &mx::ShaderNode::getName) + .def("getUniqueId", &mx::ShaderNode::getUniqueId) + .def("getClassification", &mx::ShaderNode::getClassification) + .def("getInputs", &mx::ShaderNode::getInputs) + .def("getOutputs", &mx::ShaderNode::getOutputs); } diff --git a/source/PyMaterialX/PyMaterialXGenShader/PyShaderPort.cpp b/source/PyMaterialX/PyMaterialXGenShader/PyShaderPort.cpp index 146674c222..050688ffa0 100644 --- a/source/PyMaterialX/PyMaterialXGenShader/PyShaderPort.cpp +++ b/source/PyMaterialX/PyMaterialXGenShader/PyShaderPort.cpp @@ -18,6 +18,7 @@ void bindPyShaderPort(py::module& mod) .def("setName", &mx::ShaderPort::setName) .def("getName", &mx::ShaderPort::getName) .def("getFullName", &mx::ShaderPort::getFullName) + .def("getNode", static_cast(&mx::ShaderPort::getNode), py::return_value_policy::reference) .def("setVariable", &mx::ShaderPort::setVariable) .def("getVariable", &mx::ShaderPort::getVariable) .def("setSemantic", &mx::ShaderPort::setSemantic) @@ -35,4 +36,10 @@ void bindPyShaderPort(py::module& mod) .def("getColorSpace", &mx::ShaderPort::getColorSpace) .def("isUniform", &mx::ShaderPort::isUniform) .def("isEmitted", &mx::ShaderPort::isEmitted); + + py::class_(mod, "ShaderInput") + .def("getConnection", static_cast(&mx::ShaderInput::getConnection), py::return_value_policy::reference); + + py::class_(mod, "ShaderOutput") + .def("getConnections", &mx::ShaderOutput::getConnections, py::return_value_policy::reference); }