From 1c43c917667b511b5f2e6017410af6f48948bf14 Mon Sep 17 00:00:00 2001 From: Adrien Herubel Date: Tue, 5 May 2026 16:58:54 +0100 Subject: [PATCH] Initial commit --- libraries/bxdf/open_pbr_surface.mtlx | 876 +++++++++++++++++- .../surfaceshader/open_pbr_specular_haze.mtlx | 21 + .../pbrlib/surfaceshader/open_pbr_v1_1_1.mtlx | 13 + .../surfaceshader/open_pbr_version_test.mtlx | 34 + source/MaterialXCore/Document.cpp | 69 +- source/MaterialXFormat/XmlIo.cpp | 55 +- source/MaterialXGraphEditor/Graph.cpp | 157 ++++ source/MaterialXGraphEditor/Graph.h | 3 + 8 files changed, 1213 insertions(+), 15 deletions(-) create mode 100644 resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_specular_haze.mtlx create mode 100644 resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_v1_1_1.mtlx create mode 100644 resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_version_test.mtlx diff --git a/libraries/bxdf/open_pbr_surface.mtlx b/libraries/bxdf/open_pbr_surface.mtlx index 5fe0236d08..90b3891758 100644 --- a/libraries/bxdf/open_pbr_surface.mtlx +++ b/libraries/bxdf/open_pbr_surface.mtlx @@ -1,9 +1,868 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -90,9 +949,6 @@ - @@ -619,9 +1475,10 @@ - + + + + @@ -629,9 +1486,6 @@ - diff --git a/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_specular_haze.mtlx b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_specular_haze.mtlx new file mode 100644 index 0000000000..60c47c7a4b --- /dev/null +++ b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_specular_haze.mtlx @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_v1_1_1.mtlx b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_v1_1_1.mtlx new file mode 100644 index 0000000000..2cf7cb2502 --- /dev/null +++ b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_v1_1_1.mtlx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_version_test.mtlx b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_version_test.mtlx new file mode 100644 index 0000000000..66067bfa15 --- /dev/null +++ b/resources/Materials/TestSuite/pbrlib/surfaceshader/open_pbr_version_test.mtlx @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/MaterialXCore/Document.cpp b/source/MaterialXCore/Document.cpp index ec8bb54e84..5145b51b3d 100644 --- a/source/MaterialXCore/Document.cpp +++ b/source/MaterialXCore/Document.cpp @@ -5,6 +5,8 @@ #include +#include +#include #include #include @@ -265,6 +267,52 @@ void Document::importLibrary(const ConstDocumentPtr& library) return; } + // Phase 1: Build a rename map for versioned nodedef conflicts. + // When an incoming nodedef has the same name as an existing one but a + // different version, auto-rename it to _ (dots→underscores) + // so both can coexist in the merged document. + std::map renames; + for (const auto& child : library->getChildren()) + { + ConstNodeDefPtr newNodeDef = child->asA(); + if (!newNodeDef || newNodeDef->getVersionString().empty()) + continue; + const string childName = child->getQualifiedName(child->getName()); + ConstElementPtr previous = getChild(childName); + if (!previous) + continue; + ConstNodeDefPtr prevNodeDef = previous->asA(); + if (!prevNodeDef) + continue; + if (newNodeDef->getVersionString() != prevNodeDef->getVersionString()) + { + string vSuffix = newNodeDef->getVersionString(); + std::replace(vSuffix.begin(), vSuffix.end(), '.', '_'); + renames[childName] = childName + "_" + vSuffix; + } + } + + // Phase 2: Extend renames to nodegraphs that implement a renamed nodedef. + if (!renames.empty()) + { + for (const auto& child : library->getChildren()) + { + if (!child->asA()) + continue; + const string ndAttr = child->getAttribute(InterfaceElement::NODE_DEF_ATTRIBUTE); + if (ndAttr.empty()) + continue; + const string qualNd = child->getQualifiedName(ndAttr); + auto it = renames.find(qualNd); + if (it == renames.end()) + continue; + const string ngName = child->getQualifiedName(child->getName()); + const string suffix = it->second.substr(it->first.size()); + renames[ngName] = ngName + suffix; + } + } + + // Phase 3: Import all children, applying renames and updating references. for (auto child : library->getChildren()) { if (child->getCategory().empty()) @@ -272,9 +320,11 @@ void Document::importLibrary(const ConstDocumentPtr& library) throw Exception("Trying to import child without a category: " + child->getName()); } - const string childName = child->getQualifiedName(child->getName()); + const string originalName = child->getQualifiedName(child->getName()); + auto renameIt = renames.find(originalName); + const string childName = (renameIt != renames.end()) ? renameIt->second : originalName; - // Check for duplicate elements. + // Check for duplicate elements (after applying any rename). ConstElementPtr previous = getChild(childName); if (previous) { @@ -284,6 +334,21 @@ void Document::importLibrary(const ConstDocumentPtr& library) // Create the imported element. ElementPtr childCopy = addChildOfCategory(child->getCategory(), childName); childCopy->copyContentFrom(child); + + // If this nodegraph's nodedef reference was renamed, update it. + if (!renames.empty() && child->asA()) + { + const string ndAttr = childCopy->getAttribute(InterfaceElement::NODE_DEF_ATTRIBUTE); + if (!ndAttr.empty()) + { + const string qualNd = child->getQualifiedName(ndAttr); + auto ndRename = renames.find(qualNd); + if (ndRename != renames.end()) + childCopy->setAttribute(InterfaceElement::NODE_DEF_ATTRIBUTE, ndRename->second); + + } + } + if (!childCopy->hasFilePrefix() && library->hasFilePrefix()) { childCopy->setFilePrefix(library->getFilePrefix()); diff --git a/source/MaterialXFormat/XmlIo.cpp b/source/MaterialXFormat/XmlIo.cpp index 83e4721dc1..0a2d2f3f84 100644 --- a/source/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXFormat/XmlIo.cpp @@ -9,8 +9,10 @@ #include +#include #include #include +#include #include using namespace pugi; @@ -123,6 +125,9 @@ void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptio } } + // Track auto-renames for versioned nodedef conflicts within this element's children. + std::map versionedRenames; + // Create child elements and recurse. for (const xml_node& xmlChild : xmlNode.children()) { @@ -133,12 +138,47 @@ void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptio continue; } - // Get child name and skip duplicates. + // Get child name and handle duplicates. string name = xmlChild.attribute(Element::NAME_ATTRIBUTE.c_str()).value(); ConstElementPtr previous = elem->getChild(name); if (previous) { - continue; + // Auto-rename versioned nodedef conflicts instead of skipping. + if (category == NodeDef::CATEGORY) + { + string newVersion = xmlChild.attribute(InterfaceElement::VERSION_ATTRIBUTE.c_str()).value(); + ConstNodeDefPtr prevNodeDef = previous->asA(); + if (prevNodeDef && !newVersion.empty() && newVersion != prevNodeDef->getVersionString()) + { + string vSuffix = newVersion; + std::replace(vSuffix.begin(), vSuffix.end(), '.', '_'); + name = name + "_" + vSuffix; + versionedRenames[xmlChild.attribute(Element::NAME_ATTRIBUTE.c_str()).value()] = name; + } + else + { + continue; + } + } + else if (category == NodeGraph::CATEGORY && !versionedRenames.empty()) + { + // Auto-rename nodegraphs that implement a renamed nodedef. + string ndAttr = xmlChild.attribute(InterfaceElement::NODE_DEF_ATTRIBUTE.c_str()).value(); + auto it = versionedRenames.find(ndAttr); + if (it != versionedRenames.end()) + { + string suffix = it->second.substr(it->first.size()); + name = name + suffix; + } + else + { + continue; + } + } + else + { + continue; + } } // Enforce maximum tree depth. @@ -151,6 +191,17 @@ void elementFromXml(const xml_node& xmlNode, ElementPtr elem, const XmlReadOptio ElementPtr child = elem->addChildOfCategory(category, name); elementFromXml(xmlChild, child, readOptions, depth + 1); + // Update nodedef reference on auto-renamed nodegraphs. + if (category == NodeGraph::CATEGORY && !versionedRenames.empty()) + { + string ndAttr = child->getAttribute(InterfaceElement::NODE_DEF_ATTRIBUTE); + auto it = versionedRenames.find(ndAttr); + if (it != versionedRenames.end()) + { + child->setAttribute(InterfaceElement::NODE_DEF_ATTRIBUTE, it->second); + } + } + // Handle the interpretation of XML comments and newlines. if (readOptions && category.empty()) { diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 8149e394aa..67d8f35ec4 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -12,8 +12,10 @@ #include #include +#include #include #include +#include namespace { @@ -138,6 +140,7 @@ Graph::Graph(const std::string& materialFilename, _popup(false), _shaderPopup(false), _searchNodeId(-1), + _contextMenuNodeId(-1), _addNewNode(false), _ctrlClick(false), _isCut(false), @@ -1129,6 +1132,11 @@ void Graph::createNodeUIList(mx::DocumentPtr doc) for (const auto& nodeDef : nodeDefs) { + // Skip non-default versions from the add-node menu. + // Users create the default version, then switch via right-click context menu. + if (!nodeDef->getVersionString().empty() && !nodeDef->getDefaultVersion()) + continue; + std::string group = nodeDef->getNodeGroup(); if (group.empty()) { @@ -3975,6 +3983,144 @@ void Graph::readOnlyPopup() } } +void Graph::nodeVersionPopup() +{ + if (ImGui::BeginPopup("node version")) + { + int pos = findNode(_contextMenuNodeId); + if (pos >= 0) + { + UiNodePtr uiNode = _state.nodes[pos]; + mx::NodePtr mxNode = uiNode->getNode(); + if (mxNode) + { + std::string category = mxNode->getCategory(); + std::vector matchingNodeDefs = _graphDoc->getMatchingNodeDefs(category); + + if (matchingNodeDefs.size() > 1) + { + ImGui::Text("Switch Version: %s", category.c_str()); + ImGui::Separator(); + mx::NodeDefPtr currentNodeDef = mxNode->getNodeDef(); + for (mx::NodeDefPtr nodeDef : matchingNodeDefs) + { + std::string version = nodeDef->getVersionString(); + std::string label = version.empty() ? getUserNodeDefName(nodeDef->getName()) : "v" + version; + bool isCurrent = currentNodeDef && (currentNodeDef->getName() == nodeDef->getName()); + if (isCurrent) + { + ImGui::Text("[current] %s", label.c_str()); + } + else if (!readOnly() && ImGui::MenuItem(label.c_str())) + { + switchNodeVersion(uiNode, nodeDef); + ImGui::CloseCurrentPopup(); + } + } + } + else + { + ImGui::Text("No other versions available"); + } + } + } + ImGui::EndPopup(); + } +} + +void Graph::switchNodeVersion(UiNodePtr uiNode, mx::NodeDefPtr newNodeDef) +{ + mx::NodePtr mxNode = uiNode->getNode(); + if (!mxNode || !newNodeDef) + return; + + // Bind the node to the new version using the version attribute (spec-compliant). + // This keeps saved files clean — no internal auto-renamed nodedef strings are written out. + mxNode->removeAttribute(mx::InterfaceElement::NODE_DEF_ATTRIBUTE); + if (!newNodeDef->getVersionString().empty() && !newNodeDef->getDefaultVersion()) + mxNode->setVersionString(newNodeDef->getVersionString()); + else + mxNode->removeAttribute(mx::InterfaceElement::VERSION_ATTRIBUTE); + + // Collect input names defined by the new nodedef + std::set newInputNames; + for (mx::InputPtr input : newNodeDef->getActiveInputs()) + newInputNames.insert(input->getName()); + + // Remove inputs from the MX node that are not in the new nodedef + std::vector inputsToRemove; + for (mx::InputPtr input : mxNode->getInputs()) + { + if (newInputNames.find(input->getName()) == newInputNames.end()) + inputsToRemove.push_back(input->getName()); + } + for (const std::string& name : inputsToRemove) + mxNode->removeInput(name); + + // Disconnect UI edges for input pins that are being removed + for (UiPinPtr pin : uiNode->getInputPins()) + { + if (newInputNames.find(pin->getName()) == newInputNames.end()) + { + UiNodePtr upNode = uiNode->getConnectedNode(pin->getName()); + if (upNode) + { + upNode->removeOutputConnection(uiNode->getName()); + uiNode->eraseEdge(upNode->getId(), pin); + } + } + } + + // Remove obsolete edges from the global edge list + for (size_t i = _state.edges.size(); i > 0; --i) + { + const UiEdge& edge = _state.edges[i - 1]; + if (edge.getDown()->getId() == uiNode->getId()) + { + mx::InputPtr edgeInput = edge.getInput(); + if (edgeInput && newInputNames.find(edgeInput->getName()) == newInputNames.end()) + _state.edges.erase(_state.edges.begin() + (i - 1)); + } + } + + // Remove all existing pins for this node from the global pin list + for (UiPinPtr pin : uiNode->getInputPins()) + _state.pins.erase(std::remove(_state.pins.begin(), _state.pins.end(), pin), _state.pins.end()); + for (UiPinPtr pin : uiNode->getOutputPins()) + _state.pins.erase(std::remove(_state.pins.begin(), _state.pins.end(), pin), _state.pins.end()); + + // Clear pin collections on the UiNode + uiNode->getInputPins().clear(); + uiNode->getOutputPins().clear(); + + // Rebuild input pins from the new nodedef + for (mx::InputPtr input : newNodeDef->getActiveInputs()) + { + // Use the node's actual input element if it exists (preserves connected/valued inputs) + if (mxNode->getInput(input->getName())) + input = mxNode->getInput(input->getName()); + UiPinPtr inPin = std::make_shared(_state.nextUiId, uiNode, ax::NodeEditor::PinKind::Input, input); + uiNode->getInputPins().push_back(inPin); + _state.pins.push_back(inPin); + ++_state.nextUiId; + } + + // Rebuild output pins from the new nodedef + for (mx::OutputPtr output : newNodeDef->getActiveOutputs()) + { + if (mxNode->getOutput(output->getName())) + output = mxNode->getOutput(output->getName()); + UiPinPtr outPin = std::make_shared(_state.nextUiId, uiNode, ax::NodeEditor::PinKind::Output, output); + uiNode->getOutputPins().push_back(outPin); + _state.pins.push_back(outPin); + ++_state.nextUiId; + } + + // Rebuild UI links and trigger material recompilation + linkGraph(); + updateMaterials(); +} + void Graph::shaderPopup() { if (_renderer->getMaterialCompilation()) @@ -4115,6 +4261,16 @@ void Graph::drawGraph(ImVec2 mousePos) { ed::Suspend(); + // Detect right-click on node for version switch context menu + { + ed::NodeId contextNodeId; + if (ed::ShowNodeContextMenu(&contextNodeId)) + { + _contextMenuNodeId = (int)contextNodeId.Get(); + ImGui::OpenPopup("node version"); + } + } + // Set up popups for adding a node when tab is pressed ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); ImGui::SetNextWindowSizeConstraints(ImVec2(250.0f, 300.0f), ImVec2(-1.0f, 500.0f)); @@ -4122,6 +4278,7 @@ void Graph::drawGraph(ImVec2 mousePos) searchNodePopup(TextCursor); addPinPopup(); readOnlyPopup(); + nodeVersionPopup(); ImGui::PopStyleVar(); ed::Resume(); diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 0bbeb652ed..c5a30770a0 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -250,6 +250,8 @@ class Graph void addPinPopup(); bool readOnly(); void readOnlyPopup(); + void nodeVersionPopup(); + void switchNodeVersion(UiNodePtr node, mx::NodeDefPtr newNodeDef); // Compiling shaders message void shaderPopup(); @@ -329,6 +331,7 @@ class Graph bool _popup; bool _shaderPopup; int _searchNodeId; + int _contextMenuNodeId; bool _addNewNode; bool _ctrlClick; bool _isCut;