diff --git a/.gitignore b/.gitignore index f2d3d5dccd..afbfb2cdaa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ build dist .DS_Store +python/MaterialX/__pycache__ +python/MaterialX/*.so diff --git a/source/MaterialXCore/Element.cpp b/source/MaterialXCore/Element.cpp index e36a6fcbdf..b845a76598 100644 --- a/source/MaterialXCore/Element.cpp +++ b/source/MaterialXCore/Element.cpp @@ -12,6 +12,11 @@ MATERIALX_NAMESPACE_BEGIN +namespace +{ +thread_local Element::ValidationErrors* g_validationErrors = nullptr; +} // anonymous namespace + const string Element::NAME_ATTRIBUTE = "name"; const string Element::FILE_PREFIX_ATTRIBUTE = "fileprefix"; const string Element::GEOM_PREFIX_ATTRIBUTE = "geomprefix"; @@ -627,9 +632,87 @@ void Element::validateRequire(bool expression, bool& res, string* message, const res = false; if (message) { - *message += errorDesc + ": " + asString() + "\n"; + string location = getNamePath(); + const string& sourceUri = getActiveSourceUri(); + if (!sourceUri.empty()) + { + location += " (file=" + sourceUri + ")"; + } + *message += errorDesc + " at " + location + ": " + asString() + "\n"; + } + if (g_validationErrors) + { + ValidationError err; + err.message = errorDesc; + err.path = getNamePath(); + err.source = asString(); + err.file = getActiveSourceUri(); + err.severity = ValidationSeverity::ERROR; + g_validationErrors->push_back(err); + } + } +} + +Element::ValidationErrorScope::ValidationErrorScope(ValidationErrors* errors) +{ + _prev = g_validationErrors; + g_validationErrors = errors; +} + +Element::ValidationErrorScope::~ValidationErrorScope() +{ + g_validationErrors = _prev; +} + +const char* Element::validationSeverityToString(ValidationSeverity severity) +{ + switch (severity) + { + case ValidationSeverity::ERROR: return "error"; + case ValidationSeverity::WARNING: return "warning"; + case ValidationSeverity::HINT: return "hint"; + default: return "error"; + } +} + +string Element::formatValidationErrorsJson(const ValidationErrors& errors) +{ + string json = "["; + for (size_t i = 0; i < errors.size(); i++) + { + if (i > 0) + { + json += ","; + } + json += "{\"message\":\"" + escapeJsonString(errors[i].message) + "\""; + json += ",\"path\":\"" + escapeJsonString(errors[i].path) + "\""; + json += ",\"source\":\"" + escapeJsonString(errors[i].source) + "\""; + json += ",\"file\":\"" + escapeJsonString(errors[i].file) + "\""; + json += ",\"severity\":\"" + string(validationSeverityToString(errors[i].severity)) + "\"}"; + } + json += "]"; + return json; +} + +string Element::escapeJsonString(const string& input) +{ + string out; + out.reserve(input.size()); + for (char c : input) + { + switch (c) + { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\b': out += "\\b"; break; + case '\f': out += "\\f"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: out += c; break; } } + return out; } // diff --git a/source/MaterialXCore/Element.h b/source/MaterialXCore/Element.h index 89f5c87152..fe8284b414 100644 --- a/source/MaterialXCore/Element.h +++ b/source/MaterialXCore/Element.h @@ -15,6 +15,8 @@ #include #include +#include + MATERIALX_NAMESPACE_BEGIN class Element; @@ -754,6 +756,40 @@ class MX_CORE_API Element : public std::enable_shared_from_this /// @name Validation /// @{ + enum class ValidationSeverity + { + ERROR, + WARNING, + HINT + }; + + struct ValidationError + { + string message; + string path; + string source; + string file; + ValidationSeverity severity = ValidationSeverity::ERROR; + }; + + using ValidationErrors = vector; + + class MX_CORE_API ValidationErrorScope + { + public: + explicit ValidationErrorScope(ValidationErrors* errors); + ~ValidationErrorScope(); + + private: + ValidationErrors* _prev = nullptr; + }; + + static const char* validationSeverityToString(ValidationSeverity severity); + static string formatValidationErrorsJson(const ValidationErrors& errors); + + /// Escape a string for safe inclusion in JSON output. + static string escapeJsonString(const string& input); + /// Validate that the given element tree, including all descendants, is /// consistent with the MaterialX specification. virtual bool validate(string* message = nullptr) const; diff --git a/source/MaterialXGenShader/Util.cpp b/source/MaterialXGenShader/Util.cpp index 7c300a01bd..fadd5c76b2 100644 --- a/source/MaterialXGenShader/Util.cpp +++ b/source/MaterialXGenShader/Util.cpp @@ -6,6 +6,12 @@ #include #include +#include +#include +#include +#include + +#include MATERIALX_NAMESPACE_BEGIN @@ -20,9 +26,6 @@ const std::array GAUSSIAN_KERNEL_7 = { 0.00598f, 0.060626f, 0.241843f, 0.383103f, 0.241843f, 0.060626f, 0.00598f // Sigma 1 }; -namespace -{ - using OpaqueTestPair = std::pair; using OpaqueTestPairList = vector; @@ -32,6 +35,9 @@ const OpaqueTestPairList DEFAULT_INPUT_PAIR_LIST = { { "opacity", 1.0f }, { "alpha", 1.0f }, { "transmission", 0.0f } }; +namespace +{ + const string MIX_CATEGORY("mix"); const string MIX_FG_INPUT("fg"); const string MIX_BG_INPUT("bg"); @@ -82,28 +88,42 @@ InputPtr getInputInterface(const string& interfaceName, NodePtr node) return interfaceInput; } -bool hasTransparentInputs(const OpaqueTestPairList& opaqueInputList, NodePtr node) +bool hasTransparentInputs(const OpaqueTestPairList& opaqueInputList, NodePtr node, + vector* outInputs = nullptr) { - for (auto opaqueInput : opaqueInputList) + bool found = false; + for (const auto& opaqueInput : opaqueInputList) { InputPtr interfaceInput = node->getInput(opaqueInput.first); if (interfaceInput) { - if (interfaceInput->getConnectedNode()) + NodePtr connectedNode = interfaceInput->getConnectedNode(); + if (connectedNode) { - return true; + if (!outInputs) return true; + outInputs->push_back({ opaqueInput.first, interfaceInput->getType(), + "[connected:" + connectedNode->getCategory() + "]", + opaqueInput.second }); + found = true; } - ValuePtr value = interfaceInput->getValue(); - if (value && !isEqual(value, opaqueInput.second)) + else { - return true; + ValuePtr value = interfaceInput->getValue(); + if (value && !isEqual(value, opaqueInput.second)) + { + if (!outInputs) return true; + outInputs->push_back({ opaqueInput.first, interfaceInput->getType(), + value->getValueString(), opaqueInput.second }); + found = true; + } } } } - return false; + return found; } -bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) +bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode, + vector* outInputs = nullptr) { if (!node || node->getType() != SURFACE_SHADER_TYPE_STRING) { @@ -112,19 +132,22 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) if (node->getCategory() == MIX_CATEGORY) { + bool found = false; const InputPtr fg = node->getInput(MIX_FG_INPUT); const NodePtr fgNode = fg ? fg->getConnectedNode() : nullptr; - if (fgNode && isTransparentShaderNode(fgNode, nullptr)) + if (fgNode && isTransparentShaderNode(fgNode, nullptr, outInputs)) { - return true; + if (!outInputs) return true; + found = true; } const InputPtr bg = node->getInput(MIX_BG_INPUT); const NodePtr bgNode = bg ? bg->getConnectedNode() : nullptr; - if (bgNode && isTransparentShaderNode(bgNode, nullptr)) + if (bgNode && isTransparentShaderNode(bgNode, nullptr, outInputs)) { - return true; + if (!outInputs) return true; + found = true; } - return false; + return found; } // Check against nodedef input hints @@ -144,6 +167,8 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) } } + bool found = false; + // Check against the interface if a node is passed in to check against OpaqueTestPairList interfaceNames; if (interfaceNode) @@ -162,9 +187,10 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) } if (!interfaceNames.empty()) { - if (hasTransparentInputs(interfaceNames, interfaceNode)) + if (hasTransparentInputs(interfaceNames, interfaceNode, outInputs)) { - return true; + if (!outInputs) return true; + found = true; } } } @@ -187,7 +213,8 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) } else { - return false; + if (!outInputs) return false; + continue; } } @@ -200,7 +227,17 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) if (nodeGroup != NodeDef::ADJUSTMENT_NODE_GROUP && nodeGroup != NodeDef::CHANNEL_NODE_GROUP) { - return true; + if (outInputs) + { + outInputs->push_back({ inputPair.first, checkInput->getType(), + "[connected:" + inputNode->getCategory() + "]", + inputPair.second }); + found = true; + } + else + { + return true; + } } } else @@ -208,16 +245,27 @@ bool isTransparentShaderNode(NodePtr node, NodePtr interfaceNode) ValuePtr value = checkInput->getValue(); if (value && !isEqual(value, inputPair.second)) { - return true; + if (outInputs) + { + outInputs->push_back({ inputPair.first, checkInput->getType(), + value->getValueString(), inputPair.second }); + found = true; + } + else + { + return true; + } } } } } - return false; + return found; } -bool isTransparentShaderGraph(OutputPtr output, const string& target, NodePtr interfaceNode) +bool isTransparentShaderGraph(OutputPtr output, const string& target, NodePtr interfaceNode, + vector* outInputs = nullptr) { + bool found = false; for (GraphIterator it = output->traverseGraph().begin(); it != GraphIterator::end(); ++it) { ElementPtr upstreamElem = it.getUpstreamElement(); @@ -230,9 +278,10 @@ bool isTransparentShaderGraph(OutputPtr output, const string& target, NodePtr in { // Handle shader nodes. NodePtr node = upstreamElem->asA(); - if (isTransparentShaderNode(node, interfaceNode)) + if (isTransparentShaderNode(node, interfaceNode, outInputs)) { - return true; + if (!outInputs) return true; + found = true; } // Handle graph definitions. @@ -249,9 +298,10 @@ bool isTransparentShaderGraph(OutputPtr output, const string& target, NodePtr in if (outputs.size() > 0) { const OutputPtr& graphOutput = outputs[0]; - if (isTransparentShaderGraph(graphOutput, target, node)) + if (isTransparentShaderGraph(graphOutput, target, node, outInputs)) { - return true; + if (!outInputs) return true; + found = true; } } } @@ -260,65 +310,106 @@ bool isTransparentShaderGraph(OutputPtr output, const string& target, NodePtr in } } - return false; + return found; } } // anonymous namespace -bool isTransparentSurface(ElementPtr element, const string& target) +NodePtr resolveShaderNode(ElementPtr element, const string& target) { NodePtr node = element->asA(); if (node) { - // Handle material nodes. if (node->getCategory() == SURFACE_MATERIAL_NODE_STRING) { - vector shaderNodes = getShaderNodes(node); + vector shaderNodes = getShaderNodes(node, SURFACE_SHADER_TYPE_STRING, target); if (!shaderNodes.empty()) { - node = shaderNodes[0]; + return shaderNodes[0]; } } - - // Handle shader nodes. - if (isTransparentShaderNode(node, nullptr)) + else if (node->getType() == SURFACE_SHADER_TYPE_STRING) { - return true; + return node; } - - // Handle graph definitions. - NodeDefPtr nodeDef = node->getNodeDef(); - InterfaceElementPtr impl = nodeDef ? nodeDef->getImplementation(target) : nullptr; - if (impl && impl->isA()) + } + else if (element->isA()) + { + OutputPtr output = element->asA(); + NodePtr connectedNode = output->getConnectedNode(); + if (connectedNode && connectedNode->getType() == SURFACE_SHADER_TYPE_STRING) { - NodeGraphPtr graph = impl->asA(); + return connectedNode; + } + } + return nullptr; +} - vector outputs = graph->getActiveOutputs(); - if (!outputs.empty()) +bool isUnlitSurface(ElementPtr element, const string& target) +{ + NodePtr shaderNode = resolveShaderNode(element, target); + if (!shaderNode) + { + return false; + } + // Match the classification logic in ShaderNode::create(): + // ND_surface_unlit -> Classification::SHADER | Classification::SURFACE | Classification::UNLIT + NodeDefPtr nodeDef = shaderNode->getNodeDef(target); + const string& nodeDefName = nodeDef ? nodeDef->getName() : EMPTY_STRING; + return nodeDefName == "ND_surface_unlit"; +} + +bool isTransparentSurface(ElementPtr element, const string& target, vector* outInputs) +{ + NodePtr node = resolveShaderNode(element, target); + if (!node) + { + // Handle output elements whose connected node isn't a direct shader + // (e.g. connects to a non-surfaceshader node that wraps one). + if (element->isA()) + { + OutputPtr output = element->asA(); + NodePtr outputNode = output->getConnectedNode(); + if (outputNode) { - const OutputPtr& output = outputs[0]; - if (output->getType() == SURFACE_SHADER_TYPE_STRING) - { - if (isTransparentShaderGraph(output, target, node)) - { - return true; - } - } + return isTransparentSurface(outputNode, target, outInputs); } } + return false; } - else if (element->isA()) + + bool found = false; + + // Handle shader nodes. + if (isTransparentShaderNode(node, nullptr, outInputs)) { - // Handle output elements. - OutputPtr output = element->asA(); - NodePtr outputNode = output->getConnectedNode(); - if (outputNode) + if (!outInputs) return true; + found = true; + } + + // Handle graph definitions. + NodeDefPtr nodeDef = node->getNodeDef(); + InterfaceElementPtr impl = nodeDef ? nodeDef->getImplementation(target) : nullptr; + if (impl && impl->isA()) + { + NodeGraphPtr graph = impl->asA(); + + vector outputs = graph->getActiveOutputs(); + if (!outputs.empty()) { - return isTransparentSurface(outputNode, target); + const OutputPtr& output = outputs[0]; + if (output->getType() == SURFACE_SHADER_TYPE_STRING) + { + if (isTransparentShaderGraph(output, target, node, outInputs)) + { + if (!outInputs) return true; + found = true; + } + } } } - return false; + return found; } void mapValueToColor(ConstValuePtr value, Color4& color) @@ -450,6 +541,251 @@ vector findRenderableElements(ConstDocumentPtr doc) return renderableElements; } +void getRenderableAnalysis(ConstDocumentPtr doc, + const string& target, + vector& out) +{ + out.clear(); + vector elems = findRenderableElements(doc); + for (TypedElementPtr elem : elems) + { + RenderableAnalysis info; + info.path = elem->getNamePath(); + info.file = elem->getActiveSourceUri(); + info.type = elem->getType(); + info.displacement = (elem->getType() == DISPLACEMENT_SHADER_TYPE_STRING); + + // Use the shared transparency detection — same code path as shader gen. + // The outInputs parameter makes the decision observable without duplication. + info.transparency = isTransparentSurface(elem, target, &info.transparencyInputs); + + // Use the shared unlit detection — matches ShaderNode::create() classification. + info.isUnlit = isUnlitSurface(elem, target); + + // Use the shared shader node resolution. + NodePtr shaderNode = resolveShaderNode(elem, target); + if (shaderNode) + { + info.shaderNode = shaderNode->getCategory(); + NodeDefPtr nodeDef = shaderNode->getNodeDef(target); + if (nodeDef) + { + info.shaderNodeDef = nodeDef->getName(); + if (nodeDef->hasVersionString()) + { + info.shaderNodeDefVersion = nodeDef->getVersionString(); + } + } + + // Infer alpha mode from shader-specific inputs. + // This reads the same inputs that the compiled nodegraph will evaluate, + // predicting the resulting alpha behavior at the graph structure level. + string category = shaderNode->getCategory(); + if (category == "gltf_pbr") + { + int alphaMode = 0; + float alphaCutoff = 0.5f; + InputPtr alphaModeInput = shaderNode->getActiveInput("alpha_mode"); + if (alphaModeInput) + { + ValuePtr val = alphaModeInput->getValue(); + if (val && val->isA()) + { + alphaMode = val->asA(); + } + } + InputPtr alphaCutoffInput = shaderNode->getActiveInput("alpha_cutoff"); + if (alphaCutoffInput) + { + ValuePtr val = alphaCutoffInput->getValue(); + if (val && val->isA()) + { + alphaCutoff = val->asA(); + } + } + + if (alphaMode == 0) + { + info.alphaMode = "opaque"; + } + else if (alphaMode == 1) + { + info.alphaMode = "mask"; + info.alphaCutoff = alphaCutoff; + } + else if (alphaMode == 2) + { + info.alphaMode = "blend"; + } + } + else if (category == "UsdPreviewSurface") + { + // USD uses opacityMode: 0 = transparent (cutout), 1 = presence (cutout with zero-check) + int opacityMode = 0; + float opacityThreshold = 0.0f; + InputPtr opacityModeInput = shaderNode->getActiveInput("opacityMode"); + if (opacityModeInput) + { + ValuePtr val = opacityModeInput->getValue(); + if (val && val->isA()) + { + opacityMode = val->asA(); + } + } + (void)opacityMode; // reserved for richer alpha mode inference + InputPtr opacityThreshInput = shaderNode->getActiveInput("opacityThreshold"); + if (opacityThreshInput) + { + ValuePtr val = opacityThreshInput->getValue(); + if (val && val->isA()) + { + opacityThreshold = val->asA(); + } + } + + if (info.transparency) + { + // USD always uses cutout-style opacity — never smooth blending. + info.alphaMode = "mask"; + info.alphaCutoff = opacityThreshold; + } + else + { + info.alphaMode = "opaque"; + } + } + else + { + // Generic surface shaders (standard_surface, open_pbr_surface, etc.) + // If transparent, classify as "blend" since the HW shader will use + // alpha blending with the transparency channel. + info.alphaMode = info.transparency ? "blend" : "opaque"; + } + } + + out.push_back(info); + } +} + +string TransparencyInput::toJson() const +{ + return "{\"name\":\"" + Element::escapeJsonString(name) + + "\",\"valueType\":\"" + Element::escapeJsonString(valueType) + + "\",\"value\":\"" + Element::escapeJsonString(value) + + "\",\"opaqueAt\":" + Value::createValue(opaqueAt)->getValueString() + "}"; +} + +string RenderableAnalysis::toJson() const +{ + string json = "{\"path\":\"" + Element::escapeJsonString(path) + + "\",\"file\":\"" + Element::escapeJsonString(file) + + "\",\"type\":\"" + Element::escapeJsonString(type) + + "\",\"shaderNode\":\"" + Element::escapeJsonString(shaderNode) + + "\",\"shaderNodeDef\":\"" + Element::escapeJsonString(shaderNodeDef) + "\""; + if (!shaderNodeDefVersion.empty()) + { + json += ",\"shaderNodeDefVersion\":\"" + Element::escapeJsonString(shaderNodeDefVersion) + "\""; + } + json += ",\"isUnlit\":"; + json += isUnlit ? "true" : "false"; + json += ",\"transparency\":"; + json += transparency ? "true" : "false"; + json += ",\"alphaMode\":\"" + Element::escapeJsonString(alphaMode) + "\""; + if (alphaMode == "mask") + { + json += ",\"alphaCutoff\":" + Value::createValue(alphaCutoff)->getValueString(); + } + json += ",\"displacement\":"; + json += displacement ? "true" : "false"; + if (!transparencyInputs.empty()) + { + json += ",\"transparencyInputs\":["; + for (size_t i = 0; i < transparencyInputs.size(); i++) + { + if (i > 0) + { + json += ","; + } + json += transparencyInputs[i].toJson(); + } + json += "]"; + } + json += "}"; + return json; +} + +int runMaterialReport(const string& materialFilename, + const FileSearchPath& searchPath, + const string& reportFormat, + std::ostream& out) +{ + DocumentPtr doc = createDocument(); + Element::ValidationErrors errors; + try + { + readFromXmlFile(doc, materialFilename); + DocumentPtr stdlib = createDocument(); + loadLibraries(FilePathVec{"libraries"}, searchPath, stdlib); + doc->setDataLibrary(stdlib); + } + catch (const Exception& e) + { + Element::ValidationError err; + err.message = "Failed to load document"; + err.source = e.what(); + err.file = materialFilename; + err.severity = Element::ValidationSeverity::ERROR; + errors.push_back(err); + + if (reportFormat == "json") + { + out << "{\"materialXVersion\":\"" << Element::escapeJsonString(getVersionString()) << "\"" + << ",\"valid\": false, \"errors\":" << Element::formatValidationErrorsJson(errors) + << ", \"renderables\": []}" << std::endl; + } + else + { + out << "Failed to load document: " << e.what() << std::endl; + } + return 1; + } + + string message; + bool isValid = true; + { + Element::ValidationErrorScope scope(&errors); + isValid = doc->validate(&message); + } + + if (reportFormat == "json") + { + vector renderables; + getRenderableAnalysis(doc, EMPTY_STRING, renderables); + string renderableJson = "["; + for (size_t i = 0; i < renderables.size(); i++) + { + if (i > 0) + { + renderableJson += ","; + } + renderableJson += renderables[i].toJson(); + } + renderableJson += "]"; + out << "{\"materialXVersion\":\"" << Element::escapeJsonString(getVersionString()) << "\"" + << ",\"valid\": " << (isValid ? "true" : "false") + << ", \"errors\":" << Element::formatValidationErrorsJson(errors) + << ", \"renderables\": " << renderableJson << "}" << std::endl; + } + else + { + if (!isValid) + { + out << message << std::endl; + } + } + return isValid ? 0 : 1; +} + InputPtr getNodeDefInput(InputPtr nodeInput, const string& target) { ElementPtr parent = nodeInput ? nodeInput->getParent() : nullptr; diff --git a/source/MaterialXGenShader/Util.h b/source/MaterialXGenShader/Util.h index 73c36d4404..39ae738c07 100644 --- a/source/MaterialXGenShader/Util.h +++ b/source/MaterialXGenShader/Util.h @@ -12,8 +12,11 @@ #include #include +#include #include +#include +#include MATERIALX_NAMESPACE_BEGIN @@ -25,6 +28,18 @@ extern MX_GENSHADER_API const std::array GAUSSIAN_KERNEL_3; extern MX_GENSHADER_API const std::array GAUSSIAN_KERNEL_5; extern MX_GENSHADER_API const std::array GAUSSIAN_KERNEL_7; +/// Information about a transparency-relevant input that deviates from its opaque default. +struct TransparencyInput +{ + string name; ///< Input name (e.g. "opacity", "transmission", "alpha") + string valueType; ///< Value type (e.g. "float", "color3") + string value; ///< Current value as string, or "[connected]" if driven by a node + float opaqueAt; ///< The value at which this input is considered opaque + + /// Serialize this input to a JSON object string. + MX_GENSHADER_API string toJson() const; +}; + /// Returns true if the given element is a surface shader with the potential /// of being transparent. This can be used by HW shader generators to determine /// if a shader will require transparency handling. @@ -37,7 +52,22 @@ extern MX_GENSHADER_API const std::array GAUSSIAN_KERNEL_7; /// function and transparency for such nodes must be tracked separately by the /// target application. /// -MX_GENSHADER_API bool isTransparentSurface(ElementPtr element, const string& target = EMPTY_STRING); +/// @param outInputs If non-null, populated with the transparency-relevant inputs +/// that caused the surface to be classified as transparent. +/// +MX_GENSHADER_API bool isTransparentSurface(ElementPtr element, const string& target = EMPTY_STRING, + vector* outInputs = nullptr); + +/// Resolve the surface shader node from a renderable element. +/// Handles material nodes (via getShaderNodes), direct shader nodes, +/// and output elements connected to shader nodes. +/// @return The resolved shader node, or nullptr if not found. +MX_GENSHADER_API NodePtr resolveShaderNode(ElementPtr element, const string& target = EMPTY_STRING); + +/// Returns true if the given element is an unlit surface shader +/// (i.e. uses the surface_unlit node, which has no lighting and uses +/// a scalar emission+transmission model). +MX_GENSHADER_API bool isUnlitSurface(ElementPtr element, const string& target = EMPTY_STRING); /// Maps a value to a four channel color if it is of the appropriate type. /// Supported types include float, Vector2, Vector3, Vector4, @@ -62,6 +92,54 @@ MX_GENSHADER_API vector findRenderableMaterialNodes(ConstDocume /// @return A vector of renderable elements MX_GENSHADER_API vector findRenderableElements(ConstDocumentPtr doc); +/// Analysis of a single renderable element's shader generation decisions. +struct RenderableAnalysis +{ + string path; ///< Element name path in the document + string file; ///< Source file URI + string type; ///< Element type (e.g. "material", "surfaceshader") + string shaderNode; ///< Shader node category (e.g. "gltf_pbr", "standard_surface", "open_pbr_surface") + string shaderNodeDef; ///< NodeDef name (e.g. "ND_gltf_pbr_surfaceshader") + string shaderNodeDefVersion; ///< NodeDef version (e.g. "2.0.1" for gltf_pbr, "2.6" for UsdPreviewSurface) + + bool transparency = false; ///< Would HW shader gen enable hwTransparency? + bool displacement = false; ///< Is this a displacement shader? + bool isUnlit = false; ///< True when shader is surface_unlit (no lighting, scalar emission model) + + /// Alpha mode inferred from the shader graph structure. + /// "opaque" - no transparency handling needed (all transparency inputs at opaque defaults) + /// "mask" - binary cutout (e.g. gltf_pbr alpha_mode=1 with alpha_cutoff) + /// "blend" - smooth alpha blending (e.g. gltf_pbr alpha_mode=2, or non-default opacity) + string alphaMode = "opaque"; + + /// Alpha cutoff threshold, meaningful when alphaMode == "mask". + float alphaCutoff = 0.0f; + + /// List of transparency-relevant inputs that deviate from opaque defaults. + vector transparencyInputs; + + /// Serialize this analysis to a JSON object string. + MX_GENSHADER_API string toJson() const; +}; + +/// Find renderable elements and return analysis information for each, +/// including transparency mode detection and shader generation decisions. +MX_GENSHADER_API void getRenderableAnalysis(ConstDocumentPtr doc, + const string& target, + vector& out); + +/// Run the material report pipeline: load the document, validate it, +/// analyze renderables, and write the result to the given output stream. +/// @param materialFilename Path to the MTLX document +/// @param searchPath Search path for resolving libraries and references +/// @param reportFormat Output format: "json" or "text" +/// @param out Output stream (typically std::cerr) +/// @return 0 on success (valid document), 1 on validation failure or load error +MX_GENSHADER_API int runMaterialReport(const string& materialFilename, + const FileSearchPath& searchPath, + const string& reportFormat, + std::ostream& out); + /// Given a node input, return the corresponding input within its matching nodedef. /// The optional target string can be used to guide the selection of nodedef declarations. MX_GENSHADER_API InputPtr getNodeDefInput(InputPtr nodeInput, const string& target); diff --git a/source/MaterialXGraphEditor/Main.cpp b/source/MaterialXGraphEditor/Main.cpp index 6001f222b4..20371721c1 100644 --- a/source/MaterialXGraphEditor/Main.cpp +++ b/source/MaterialXGraphEditor/Main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ const std::string options = " --font [FILENAME] Specify the name of the custom font file to use. If not specified the default font will be used.\n" " --fontSize [SIZE] Specify font size to use for the custom font. If not specified a default of 18 will be used.\n" " --captureFilename [FILENAME] Specify the filename to which the first rendered frame should be written\n" + " --report [FORMAT] Validate and analyze the material document, then exit. Format: text or json (defaults to json). Outputs to stderr.\n" " --help Display the complete list of command-line options\n"; template void parseToken(std::string token, std::string type, T& res) @@ -73,6 +75,8 @@ int main(int argc, char* const argv[]) std::string fontFilename; int fontSize = 18; std::string captureFilename; + bool reportMode = false; + std::string reportFormat = "json"; for (size_t i = 0; i < tokens.size(); i++) { @@ -119,6 +123,15 @@ int main(int argc, char* const argv[]) { parseToken(nextToken, "string", captureFilename); } + else if (token == "--report") + { + reportMode = true; + if (!nextToken.empty() && nextToken[0] != '-') + { + reportFormat = nextToken; + i++; + } + } else if (token == "--help") { std::cout << " MaterialXGraphEditor version " << mx::getVersionString() << std::endl; @@ -142,6 +155,11 @@ int main(int argc, char* const argv[]) } } + if (reportMode) + { + return mx::runMaterialReport(materialFilename, searchPath, reportFormat, std::cerr); + } + // Append the standard library folder, giving it a lower precedence than user-supplied libraries. libraryFolders.push_back("libraries"); diff --git a/source/MaterialXView/Main.cpp b/source/MaterialXView/Main.cpp index 626fe57826..2433335dc2 100644 --- a/source/MaterialXView/Main.cpp +++ b/source/MaterialXView/Main.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include + NANOGUI_FORCE_DISCRETE_GPU(); const std::string options = @@ -46,6 +48,7 @@ const std::string options = " --remap [TOKEN1:TOKEN2] Specify the remapping from one token to another when MaterialX document is loaded\n" " --skip [NAME] Specify to skip elements matching the given name attribute\n" " --terminator [STRING] Specify to enforce the given terminator string for file prefixes\n" + " --report [FORMAT] Validate and analyze the material document, then exit. Format: text or json (defaults to json). Outputs to stderr.\n" " --help Display the complete list of command-line options\n"; template void parseToken(std::string token, std::string type, T& res) @@ -103,6 +106,8 @@ int main(int argc, char* const argv[]) std::string bakeFilename; float refresh = 50.0f; bool frameTiming = false; + bool reportMode = false; + std::string reportFormat = "json"; for (size_t i = 0; i < tokens.size(); i++) { @@ -248,6 +253,15 @@ int main(int argc, char* const argv[]) { modifiers.filePrefixTerminator = nextToken; } + else if (token == "--report") + { + reportMode = true; + if (!nextToken.empty() && nextToken[0] != '-') + { + reportFormat = nextToken; + i++; + } + } else if (token == "--help") { std::cout << " MaterialXView version " << mx::getVersionString() << std::endl; @@ -271,6 +285,11 @@ int main(int argc, char* const argv[]) } } + if (reportMode) + { + return mx::runMaterialReport(materialFilename, searchPath, reportFormat, std::cerr); + } + // Append the standard library folder, giving it a lower precedence than user-supplied libraries. libraryFolders.push_back("libraries");