diff --git a/documents/Specification/MaterialX.Specification.md b/documents/Specification/MaterialX.Specification.md index 29049205c2..afa69b006b 100644 --- a/documents/Specification/MaterialX.Specification.md +++ b/documents/Specification/MaterialX.Specification.md @@ -581,14 +581,14 @@ Nodes are individual data generation or processing "blocks". Node functionality Individual node elements have the form: ```xml - ...additional input or token elements... - + ``` -where _nodecategory_ is the general "category" of the node (e.g. "image", "add" or "mix"), `name` (string, required) defines the name of this instance of the node, which must be unique within the scope it appears in, and `type` (string, required) specifies the MaterialX type (typically float, colorN, or vectorN) of the output of that node. If the application uses a different name for this instance of the node in the user interface, a `uiname` attribute may be added to the <_nodecategory_> element to indicate the name of the node as it appears to the user. +where _nodecategory_ is the general "category" of the node (e.g. "image", "add" or "mix"), `name` (string, required) defines the name of this instance of the node, which must be unique within the scope it appears in, and `type` (string, required) specifies the MaterialX type (typically float, colorN, or vectorN) of the output of that node. If there are more than outputs for the node the output type is `multioutput`. If the application uses a different name for this instance of the node in the user interface, a `uiname` attribute may be added to the <_node_> element to indicate the name of the node as it appears to the user. Node elements may optionally specify a `version` string attribute in "_major_[._minor_]" format, requesting that a specific version of that node's definition be used instead of the default version. Normally, the types of a node's inputs and outputs are sufficient to disambiguate which signature of the applicable version of a node is intended, but if necessary, a node instantiation may also declare a specific nodedef name to precisely define exactly which node signature is desired. Please refer to the [Custom Node Declaration NodeDef Elements](#custom-node-declaration-nodedef-elements) section below for further details. diff --git a/python/Scripts/mxformat.py b/python/Scripts/mxformat.py index c9c036769c..b5ad9d5017 100644 --- a/python/Scripts/mxformat.py +++ b/python/Scripts/mxformat.py @@ -24,6 +24,7 @@ def main(): parser.add_argument('-u', '--upgrade', dest='upgrade', action="store_true", help='Upgrade documents to the latest version of the standard.') parser.add_argument('-v', '--validate', dest='validate', action="store_true", help='Perform MaterialX validation on documents after reformatting.') parser.add_argument('-x', '--xml_syntax', dest='xml_syntax', action="store_true", help='Check XML syntax after reformatting.') + parser.add_argument('-n', '--node_types', dest='node_types', action="store_true", help='Write nodes instances using "node" type.') parser.add_argument(dest="inputFolder", help="An input folder to scan for MaterialX documents.") opts = parser.parse_args() @@ -70,9 +71,14 @@ def main(): xml_syntax = opts.xml_syntax if xml_syntax: print(f'- Check XML syntax') + node_types = opts.node_types + writeOptions = mx.XmlWriteOptions() + if node_types: + writeOptions.writeNodeInstanceAsNode = True + print(f'- Write nodes using "node" type') for (filename, doc) in validDocs.items(): if xml_syntax: - xml_string = mx.writeToXmlString(doc) + xml_string = mx.writeToXmlString(doc, writeOptions) errors = is_well_formed(xml_string) if errors: print(f'- Warning: Document {filename} is not well-formed XML: {errors}') @@ -80,7 +86,8 @@ def main(): is_valid, errors = doc.validate() if not is_valid: print(f'- Warning: Document {filename} is invalid. Errors {errors}.') - mx.writeToXmlFile(doc, filename) + + mx.writeToXmlFile(doc, filename, writeOptions) if opts.upgrade: print('Upgraded %i documents to MaterialX v%i.%i' % (len(validDocs), mxVersion[0], mxVersion[1])) diff --git a/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid_node.mtlx b/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid_node.mtlx new file mode 100644 index 0000000000..4a26b39be2 --- /dev/null +++ b/resources/Materials/Examples/StandardSurface/standard_surface_marble_solid_node.mtlx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled_node.mtlx b/resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled_node.mtlx new file mode 100644 index 0000000000..af36a04471 --- /dev/null +++ b/resources/Materials/Examples/StandardSurface/standard_surface_onyx_hextiled_node.mtlx @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/Materials/TestSuite/pbrlib/multioutput/multishaderoutput_node.mtlx b/resources/Materials/TestSuite/pbrlib/multioutput/multishaderoutput_node.mtlx new file mode 100644 index 0000000000..ef9f4878b9 --- /dev/null +++ b/resources/Materials/TestSuite/pbrlib/multioutput/multishaderoutput_node.mtlx @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/MaterialXFormat/XmlIo.cpp b/source/MaterialXFormat/XmlIo.cpp index 83e4721dc1..63839eec13 100644 --- a/source/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXFormat/XmlIo.cpp @@ -42,6 +42,7 @@ void documentToXml(DocumentPtr doc, xml_node& xmlRoot, const XmlWriteOptions* wr elemStack.pop_back(); bool writeXIncludeEnable = writeOptions ? writeOptions->writeXIncludeEnable : true; + bool writeNodeInstanceAsNode = writeOptions ? writeOptions->writeNodeInstanceAsNode : false; ElementPredicate elementPredicate = writeOptions ? writeOptions->elementPredicate : nullptr; // Store attributes in XML. @@ -49,6 +50,16 @@ void documentToXml(DocumentPtr doc, xml_node& xmlRoot, const XmlWriteOptions* wr { xmlNode.append_attribute(Element::NAME_ATTRIBUTE.c_str()) = elem->getName().c_str(); } + + bool isNode = writeNodeInstanceAsNode && elem->isA(); + if (isNode) + { + const string& category = elem->getCategory(); + const string NODE_TYPE_ATTRIBUTE = "nodetype"; + xml_attribute xmlAttr = xmlNode.append_attribute(NODE_TYPE_ATTRIBUTE.c_str()); + xmlAttr.set_value(category.c_str()); + } + for (const string& attrName : elem->getAttributeNames()) { xml_attribute xmlAttr = xmlNode.append_attribute(attrName.c_str()); @@ -91,33 +102,49 @@ void documentToXml(DocumentPtr doc, xml_node& xmlRoot, const XmlWriteOptions* wr } } - // Handle the interpretation of comments and newlines. - if (child->getCategory() == CommentElement::CATEGORY) + isNode = writeNodeInstanceAsNode && child->isA(); + if (isNode) { - xml_node xmlChild = xmlNode.append_child(node_comment); - xmlChild.set_value(child->getAttribute(Element::DOC_ATTRIBUTE).c_str()); - continue; + // Push child data onto the stack. + const string NODE_CATEGORY = "node"; + xml_node xmlChild = xmlNode.append_child(NODE_CATEGORY.c_str()); + elemStack.emplace_back(child, xmlChild); } - if (child->getCategory() == NewlineElement::CATEGORY) + else { - xml_node xmlChild = xmlNode.append_child(node_newline); - xmlChild.set_value("\n"); - continue; - } + string category = child->getCategory(); - // Push child data onto the stack. - xml_node xmlChild = xmlNode.append_child(child->getCategory().c_str()); - elemStack.emplace_back(child, xmlChild); + // Handle the interpretation of comments and newlines. + if (category == CommentElement::CATEGORY) + { + xml_node xmlChild = xmlNode.append_child(node_comment); + xmlChild.set_value(child->getAttribute(Element::DOC_ATTRIBUTE).c_str()); + } + else if (category == NewlineElement::CATEGORY) + { + xml_node xmlChild = xmlNode.append_child(node_newline); + xmlChild.set_value("\n"); + } + else + { + // Push child data onto the stack. + xml_node xmlChild = xmlNode.append_child(category.c_str()); + elemStack.emplace_back(child, xmlChild); + } + } } } } void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptions* readOptions, int depth = 1) { + const string NODETYPE_ATTRIBUTE = "nodetype"; + // Store attributes in element. for (const xml_attribute& xmlAttr : xmlNode.attributes()) { - if (xmlAttr.name() != Element::NAME_ATTRIBUTE) + if (xmlAttr.name() != Element::NAME_ATTRIBUTE && + xmlAttr.name() != NODETYPE_ATTRIBUTE) { elem->setAttribute(xmlAttr.name(), xmlAttr.value()); } @@ -147,6 +174,12 @@ void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptio throw ExceptionParseError("Maximum tree depth exceeded."); } + // If name is "node" get category from attribute. + if (category == "node") + { + category = xmlChild.attribute(NODETYPE_ATTRIBUTE.c_str()).value(); + } + // Create the child element. ElementPtr child = elem->addChildOfCategory(category, name); elementFromXml(xmlChild, child, readOptions, depth + 1); @@ -280,7 +313,8 @@ XmlReadOptions::XmlReadOptions() : // XmlWriteOptions::XmlWriteOptions() : - writeXIncludeEnable(true) + writeXIncludeEnable(true), + writeNodeInstanceAsNode(false) { } diff --git a/source/MaterialXFormat/XmlIo.h b/source/MaterialXFormat/XmlIo.h index 5c80ae8005..575b81383c 100644 --- a/source/MaterialXFormat/XmlIo.h +++ b/source/MaterialXFormat/XmlIo.h @@ -70,6 +70,10 @@ class MX_FORMAT_API XmlWriteOptions /// XIncludes rather than explicit data. Defaults to true. bool writeXIncludeEnable; + /// If true, node instances are written as "node" elements rather than + /// elements based on category. Defaults to false. + bool writeNodeInstanceAsNode; + /// If provided, this function will be used to exclude specific elements /// (those returning false) from the write operation. Defaults to nullptr. ElementPredicate elementPredicate; diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 9f505375fa..9249bcfe51 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -4973,6 +4973,7 @@ void Graph::saveDocument(mx::FilePath filePath) } mx::XmlWriteOptions writeOptions; + writeOptions.writeNodeInstanceAsNode = true; writeOptions.elementPredicate = getElementPredicate(); mx::writeToXmlFile(writeDoc, filePath, &writeOptions); } diff --git a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp index 9b4b32cf6a..b6bb671b13 100644 --- a/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp +++ b/source/PyMaterialX/PyMaterialXFormat/PyXmlIo.cpp @@ -24,6 +24,7 @@ void bindPyXmlIo(py::module& mod) py::class_(mod, "XmlWriteOptions") .def(py::init()) .def_readwrite("writeXIncludeEnable", &mx::XmlWriteOptions::writeXIncludeEnable) + .def_readwrite("writeNodeInstanceAsNode", &mx::XmlWriteOptions::writeNodeInstanceAsNode) .def_readwrite("elementPredicate", &mx::XmlWriteOptions::elementPredicate); mod.def("readFromXmlFileBase", &mx::readFromXmlFile,