diff --git a/documents/Specification/MaterialX.Specification.md b/documents/Specification/MaterialX.Specification.md index c1e92a7138..dc9b82bb47 100644 --- a/documents/Specification/MaterialX.Specification.md +++ b/documents/Specification/MaterialX.Specification.md @@ -958,7 +958,7 @@ Attributes for <nodedef> elements: * `inherit` (string, optional): the `name` of a <nodedef> to inherit node definitions from; the output types of this nodedef and the inherited one must match, and the input/output definitions of this nodedef will be applied on top of those in the inherited-from one. * `nodegroup` (string, optional): an optional group to which this node declaration belongs. Standard MaterialX nodes have `nodegroup` values matching the titles of the section headings in which they are described, e.g. "texture2d", "procedural", "geometric", "application", "math", "adjustment", "compositing", "conditional", "channel", "convolution", or "organization". * `version` (string, optional): a version string for this nodedef, allowing usage of a node to reference a specific version of a node. Version strings should be of the format "_major_[._minor_]", i.e. one or two integer numbers separated by a dot (the minor version is assumed to be "0" if not provided). If there are multiple nodedefs for the same `node` and `target` with the same combination of input and output types, they must each specify a `version`. -* `isdefaultversion` (boolean, optional): If true, then this nodedef should be used for node instances which do not request a specific version. Specifying `isdefaultversion` "true" is only required if there are multiple nodedefs for a node declaring a `version`, and it is not permissible for multiple nodedefs for the same `node` and `target` with the same combination of input and output types to set `isdefaultversion` "true". Defaults to "false". +* `isdefaultversion` (boolean, optional): If true, then this nodedef should be used for node instances that do not request a specific version when the declaration is ambiguous. Ambiguity includes cases where a node instance is declared with insufficient inputs to determine a unique nodedef signature, or where multiple nodedef versions are available and no explicit version is specified on the node instance. In these cases, the nodedef with `isdefaultversion` "true" indicates which definition is instantiated. It is not permissible for multiple nodedefs for the same `node` and `target` with the same combination of input and output types to set `isdefaultversion` "true". Defaults to "false". * `target` (stringarray, optional): the set of targets to which this nodedef is restricted. By default, a nodedef is considered universal, not restricted to any specific targets, but it is possible that certain targets may have different parameter names or usage for the same node. * `uiname` (string, optional): an alternative "node" value for this nodedef to be displayed in the UI. If `uiname` is not provided, then `node` is the presumed UI node value for the nodedef. This is most useful when the <nodedef> defines a namespace, so the user doesn't need to see a full namespaced path for the node. * `internalgeomprops` (stringarray, optional): a list of MaterialX geometric properties (e.g. "position", "normal", "texcoord", etc. or any name defined by a <geompropdef> element) that the node expects to be able to access internally. This metadata hint allows code generators to ensure this data is available and can be used for error checking. `Internalgeomprops` is most useful for nodes whose implementation is defined by external code; it is not necessary for nodegraph-defined nodes, as the list of geometric properties accessed can be determined by examining the nodegraph. diff --git a/source/MaterialXCore/Node.cpp b/source/MaterialXCore/Node.cpp index f5923f337c..a9f2206324 100644 --- a/source/MaterialXCore/Node.cpp +++ b/source/MaterialXCore/Node.cpp @@ -87,6 +87,7 @@ NodeDefPtr Node::getNodeDef(const string& target, bool allowRoughMatch) const } vector nodeDefs = getDocument()->getMatchingNodeDefs(getQualifiedName(getCategory())); vector secondary = getDocument()->getMatchingNodeDefs(getCategory()); + vector exactMatches; vector roughMatches; nodeDefs.insert(nodeDefs.end(), secondary.begin(), secondary.end()); for (NodeDefPtr nodeDef : nodeDefs) @@ -105,10 +106,35 @@ NodeDefPtr Node::getNodeDef(const string& target, bool allowRoughMatch) const } continue; } - return nodeDef; + exactMatches.push_back(nodeDef); + } + + if (!exactMatches.empty()) + { + if (getVersionString().empty()) + { + for (NodeDefPtr nodeDef : exactMatches) + { + if (nodeDef->getDefaultVersion()) + { + return nodeDef; + } + } + } + return exactMatches[0]; } if (!roughMatches.empty()) { + if (getVersionString().empty()) + { + for (NodeDefPtr nodeDef : roughMatches) + { + if (nodeDef->getDefaultVersion()) + { + return nodeDef; + } + } + } return roughMatches[0]; } return NodeDefPtr(); diff --git a/source/MaterialXTest/MaterialXCore/Node.cpp b/source/MaterialXTest/MaterialXCore/Node.cpp index c43156a4ae..c34adf23d0 100644 --- a/source/MaterialXTest/MaterialXCore/Node.cpp +++ b/source/MaterialXTest/MaterialXCore/Node.cpp @@ -334,6 +334,70 @@ TEST_CASE("Inheritance", "[nodedef]") nodedefSpecularInput->getAttribute(mx::ValueElement::VALUE_ATTRIBUTE)); } +TEST_CASE("Default Instance", "[nodedef]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr doc = mx::createDocument(); + mx::loadLibraries({ "libraries" }, searchPath, doc); + REQUIRE(doc->validate()); + + // When a node instance has no explicit version and multiple exact matches + // are available, prefer a single nodedef marked as the default version. + mx::NodeDefPtr ambiguousDefV1 = doc->addNodeDef("ND_ambiguous_a", "float", "ambiguous"); + ambiguousDefV1->setNodeGroup(mx::NodeDef::PROCEDURAL_NODE_GROUP); + ambiguousDefV1->setVersionString("1.0"); + ambiguousDefV1->addInput("value", "float"); + mx::NodePtr ambiguousNodeV1 = doc->addNode("ambiguous", mx::EMPTY_STRING, "float"); + ambiguousNodeV1->setVersionString("1.0"); + REQUIRE(ambiguousNodeV1->getNodeDef() == ambiguousDefV1); + + mx::NodeDefPtr ambiguousDefV2 = doc->addNodeDef("ND_ambiguous_b", "float", "ambiguous"); + ambiguousDefV2->setNodeGroup(mx::NodeDef::PROCEDURAL_NODE_GROUP); + ambiguousDefV2->setDefaultVersion(true); + ambiguousDefV2->setVersionString("2.0"); + ambiguousDefV2->addInput("value", "float"); + + mx::NodePtr ambiguousNodeV2 = doc->addNode("ambiguous", mx::EMPTY_STRING, "float"); + ambiguousNodeV2->addInput("value", "float"); + REQUIRE(ambiguousNodeV2->getNodeDef() == ambiguousDefV2); + + // Modify make second nodedef in the list the default version. + // instead of being order dependent resulting in choosing + // the first one found in Node::getNodeDef(). + mx::NodePtr normalmapNode = doc->addNode("normalmap", mx::EMPTY_STRING, "vector3"); + normalmapNode->addInputsFromNodeDef(); + mx::InputPtr normalmapNodeScaleInput = normalmapNode->getInput("scale"); + const std::string previousScaleType = normalmapNodeScaleInput->getType(); + + std::vector nodeDefs = doc->getMatchingNodeDefs(normalmapNode->getQualifiedName(normalmapNode->getCategory())); + std::string defaultScaleType = mx::EMPTY_STRING; + bool isFirstNodeDef = true; + for (auto nodeDef : nodeDefs) + { + if (isFirstNodeDef) + { + nodeDef->setDefaultVersion(false); + isFirstNodeDef = false; + continue; + } + + nodeDef->setDefaultVersion(true); + mx::InputPtr scaleInput = nodeDef->getInput("scale"); + if (defaultScaleType.empty() && scaleInput) + { + defaultScaleType = scaleInput->getType(); + } + } + + // Check new logic and compare against definition returned with a default being set + mx::NodePtr normalmapNodeDefault = doc->addNode("normalmap", mx::EMPTY_STRING, "vector3"); + normalmapNodeDefault->addInputsFromNodeDef(); + mx::InputPtr normalmapNodeDefaultScaleInput = normalmapNodeDefault->getInput("scale"); + REQUIRE(normalmapNodeDefaultScaleInput->getType() == defaultScaleType); + + REQUIRE(defaultScaleType != previousScaleType); +} + TEST_CASE("Topological sort", "[nodegraph]") { // Create a document.