/* SPDX-FileCopyrightText: 2022 NVIDIA Corporation * SPDX-FileCopyrightText: 2022 Blender Foundation * * SPDX-License-Identifier: Apache-2.0 */ #include "hydra/material.h" #include "hydra/node_util.h" #include "hydra/session.h" #include "hydra/util.h" #include "scene/scene.h" #include "scene/shader.h" #include "scene/shader_graph.h" #include "scene/shader_nodes.h" #include #include #include #include #include #include #include HDCYCLES_NAMESPACE_OPEN_SCOPE /* Normalize a material network node name to a full SdfPath. The schema may * provide either a full path or a bare identifier. */ static SdfPath MaterialNodeNameToSdfPath(const TfToken &nodeName) { const std::string &s = nodeName.GetString(); if (s.empty()) { return SdfPath::EmptyPath(); } if (s[0] == '/' && SdfPath::IsValidPathString(s)) { return SdfPath(s); } return SdfPath::AbsoluteRootPath().AppendChild(nodeName); } // clang-format off TF_DEFINE_PRIVATE_TOKENS(CyclesMaterialTokens, (cycles) ((cyclesSurface, "cycles:surface")) ((cyclesDisplacement, "cycles:displacement")) ((cyclesVolume, "cycles:volume")) (UsdPreviewSurface) (UsdUVTexture) (UsdPrimvarReader_float) (UsdPrimvarReader_float2) (UsdPrimvarReader_float3) (UsdPrimvarReader_float4) (UsdPrimvarReader_int) (UsdTransform2d) (a) (rgb) (r) (g) (b) (result) (st) (wrapS) (wrapT) (periodic) ); // clang-format on /* Simple class to handle remapping of USDPreviewSurface nodes and parameters to Cycles * equivalents. */ class UsdToCyclesMapping { using ParamMap = std::unordered_map; public: UsdToCyclesMapping(const char *nodeType, ParamMap paramMap) : _nodeType(nodeType), _paramMap(std::move(paramMap)) { } ustring nodeType() const { return _nodeType; } virtual std::string parameterName(const TfToken &name, const ShaderInput *inputConnection, VtValue * /*value*/ = nullptr) const { /* UsdNode.name -> Node.input. These all follow a simple pattern that we can just * remap based on the name or 'Node.input' type. */ if (inputConnection) { if (name == CyclesMaterialTokens->a) { return "alpha"; } if (name == CyclesMaterialTokens->rgb) { return "color"; } /* TODO: Is there a better mapping than 'color'? */ if (name == CyclesMaterialTokens->r || name == CyclesMaterialTokens->g || name == CyclesMaterialTokens->b) { return "color"; } if (name == CyclesMaterialTokens->result) { switch (inputConnection->socket_type.type) { case SocketType::BOOLEAN: case SocketType::FLOAT: case SocketType::INT: case SocketType::UINT: return "alpha"; case SocketType::COLOR: case SocketType::VECTOR: case SocketType::POINT: case SocketType::NORMAL: default: return "color"; } } } /* Simple mapping case */ const auto it = _paramMap.find(name); return it != _paramMap.end() ? it->second.string() : name.GetString(); } private: const ustring _nodeType; ParamMap _paramMap; }; class UsdToCyclesTexture : public UsdToCyclesMapping { public: using UsdToCyclesMapping::UsdToCyclesMapping; std::string parameterName(const TfToken &name, const ShaderInput *inputConnection, VtValue *value) const override { if (value) { /* Remap UsdUVTexture.wrapS and UsdUVTexture.wrapT to cycles_image_texture.extension. */ if (name == CyclesMaterialTokens->wrapS || name == CyclesMaterialTokens->wrapT) { const std::string valueString = VtValue::Cast(*value).Get(); /* A value of 'repeat' in USD is equivalent to 'periodic' in Cycles. */ if (valueString == "repeat") { *value = VtValue(CyclesMaterialTokens->periodic); } return "extension"; } } return UsdToCyclesMapping::parameterName(name, inputConnection, value); } }; namespace { class UsdToCycles { const UsdToCyclesMapping UsdPreviewSurface = { "principled_bsdf", { {TfToken("diffuseColor"), ustring("base_color")}, {TfToken("emissiveColor"), ustring("emission")}, {TfToken("specularColor"), ustring("specular")}, {TfToken("clearcoatRoughness"), ustring("coat_roughness")}, {TfToken("opacity"), ustring("alpha")}, /* opacityThreshold */ /* occlusion */ /* displacement */ }}; const UsdToCyclesTexture UsdUVTexture = { "image_texture", { {CyclesMaterialTokens->st, ustring("vector")}, {CyclesMaterialTokens->wrapS, ustring("extension")}, {CyclesMaterialTokens->wrapT, ustring("extension")}, {TfToken("file"), ustring("filename")}, {TfToken("sourceColorSpace"), ustring("colorspace")}, }}; const UsdToCyclesMapping UsdPrimvarReader = {"attribute", {{TfToken("varname"), ustring("attribute")}}}; public: const UsdToCyclesMapping *findUsd(const TfToken &usdNodeType) { if (usdNodeType == CyclesMaterialTokens->UsdPreviewSurface) { return &UsdPreviewSurface; } if (usdNodeType == CyclesMaterialTokens->UsdUVTexture) { return &UsdUVTexture; } if (usdNodeType == CyclesMaterialTokens->UsdPrimvarReader_float || usdNodeType == CyclesMaterialTokens->UsdPrimvarReader_float2 || usdNodeType == CyclesMaterialTokens->UsdPrimvarReader_float3 || usdNodeType == CyclesMaterialTokens->UsdPrimvarReader_float4 || usdNodeType == CyclesMaterialTokens->UsdPrimvarReader_int) { return &UsdPrimvarReader; } return nullptr; } const UsdToCyclesMapping *findCycles(const ustring & /*cyclesNodeType*/) { return nullptr; } }; TfStaticData sUsdToCyles; } // namespace HdCyclesMaterial::HdCyclesMaterial(const SdfPath &sprimId) : HdMaterial(sprimId) {} HdCyclesMaterial::~HdCyclesMaterial() = default; HdDirtyBits HdCyclesMaterial::GetInitialDirtyBitsMask() const { return DirtyBits::DirtyResource | DirtyBits::DirtyParams; } void HdCyclesMaterial::Sync(HdSceneDelegate *sceneDelegate, HdRenderParam *renderParam, HdDirtyBits *dirtyBits) { if (*dirtyBits == DirtyBits::Clean) { return; } Initialize(renderParam); const SceneLock lock(renderParam); const bool dirtyParams = (*dirtyBits & DirtyBits::DirtyParams); const bool dirtyResource = (*dirtyBits & DirtyBits::DirtyResource); const SdfPath &id = GetId(); if (dirtyResource || dirtyParams) { const HdSceneIndexPrim prim = GetPrim(sceneDelegate, id); const HdContainerDataSourceHandle &primDs = prim.dataSource; HdMaterialSchema matSchema = HdMaterialSchema::GetFromParent(primDs); /* Prefer cycles network if it exists, otherwise use universal network. */ HdMaterialNetworkSchema network = matSchema.GetMaterialNetwork(CyclesMaterialTokens->cycles); if (!network) { network = matSchema.GetMaterialNetwork(); } if (network) { if (!_nodes.empty() && !dirtyResource) { UpdateParameters(network); _shader->tag_modified(); } else { PopulateShaderGraph(network); } } else { TF_RUNTIME_ERROR("Could not get a material network for %s.", id.GetText()); } } if (_shader->is_modified()) { _shader->tag_update(lock.scene); } *dirtyBits = DirtyBits::Clean; } void HdCyclesMaterial::UpdateParameters(NodeDesc &nodeDesc, HdMaterialNodeParameterContainerSchema params, const SdfPath &nodePath) { for (const TfToken ¶mName : params.GetNames()) { auto valueDs = params.Get(paramName).GetValue(); if (!valueDs) { continue; } VtValue value = valueDs->GetValue(0.0f); /* See if the parameter name is in USDPreviewSurface terms, and needs to be converted .*/ const UsdToCyclesMapping *inputMapping = nodeDesc.mapping; const std::string inputName = inputMapping ? inputMapping->parameterName(paramName, nullptr, &value) : paramName.GetString(); /* Find the input to write the parameter value to. */ const SocketType *input = nullptr; for (const SocketType &socket : nodeDesc.node->type->inputs) { if (string_iequals(socket.name.string(), inputName) || socket.ui_name == inputName) { input = &socket; break; } } if (!input) { TF_WARN("Could not find parameter '%s' on node '%s' ('%s')", paramName.GetText(), nodePath.GetText(), nodeDesc.node->name.c_str()); continue; } SetNodeValue(nodeDesc.node, *input, value); } } void HdCyclesMaterial::UpdateParameters(HdMaterialNetworkSchema network) { HdMaterialNodeContainerSchema nodes = network.GetNodes(); for (const TfToken &nodeName : nodes.GetNames()) { const SdfPath nodePath = MaterialNodeNameToSdfPath(nodeName); const auto nodeIt = _nodes.find(nodePath); if (nodeIt == _nodes.end()) { TF_RUNTIME_ERROR("Could not update parameters on missing node '%s'", nodePath.GetText()); continue; } UpdateParameters(nodeIt->second, nodes.Get(nodeName).GetParameters(), nodePath); } } void HdCyclesMaterial::UpdateConnections(NodeDesc &nodeDesc, HdMaterialNodeSchema nodeSchema, const SdfPath &nodePath, ShaderGraph *shaderGraph) { HdMaterialConnectionVectorContainerSchema conns = nodeSchema.GetInputConnections(); for (const TfToken &dstSocketName : conns.GetNames()) { HdMaterialConnectionVectorSchema connVec = conns.Get(dstSocketName); const size_t count = connVec.GetNumElements(); if (count == 0) { continue; } const UsdToCyclesMapping *inputMapping = nodeDesc.mapping; const std::string inputName = inputMapping ? inputMapping->parameterName(dstSocketName, nullptr) : dstSocketName.GetString(); /* Find the input to connect to on the passed in node. */ ShaderInput *input = nullptr; for (ShaderInput *in : nodeDesc.node->inputs) { if (string_iequals(in->socket_type.name.string(), inputName)) { input = in; break; } } if (!input) { TF_WARN("Ignoring connection on '%s.%s', input '%s' was not found", nodePath.GetText(), dstSocketName.GetText(), dstSocketName.GetText()); continue; } /* USD allows N connections per input (MaterialX , , struct * inputs etc). Cycles inputs are single-connection, and the right lowering * depends on the node type, so just take the first and warn. */ if (count > 1) { TF_WARN( "Ignoring multiple connections to '%s.%s'", nodePath.GetText(), dstSocketName.GetText()); } HdMaterialConnectionSchema connSchema = connVec.GetElement(0); const SdfPath upstreamNodePath = connSchema.GetUpstreamNodePath() ? MaterialNodeNameToSdfPath(connSchema.GetUpstreamNodePath()->GetTypedValue(0.0f)) : SdfPath(); const TfToken upstreamOutputName = connSchema.GetUpstreamNodeOutputName() ? connSchema.GetUpstreamNodeOutputName()->GetTypedValue( 0.0f) : TfToken(); const auto srcNodeIt = _nodes.find(upstreamNodePath); if (srcNodeIt == _nodes.end()) { TF_WARN("Ignoring connection from '%s.%s' to '%s.%s', node '%s' was not found", upstreamNodePath.GetText(), upstreamOutputName.GetText(), nodePath.GetText(), dstSocketName.GetText(), upstreamNodePath.GetText()); continue; } const UsdToCyclesMapping *outputMapping = srcNodeIt->second.mapping; const std::string outputName = outputMapping ? outputMapping->parameterName(upstreamOutputName, input) : upstreamOutputName.GetString(); ShaderOutput *output = nullptr; for (ShaderOutput *out : srcNodeIt->second.node->outputs) { if (string_iequals(out->socket_type.name.string(), outputName)) { output = out; break; } } if (!output) { TF_WARN("Ignoring connection from '%s.%s' to '%s.%s', output '%s' was not found", upstreamNodePath.GetText(), upstreamOutputName.GetText(), nodePath.GetText(), dstSocketName.GetText(), upstreamOutputName.GetText()); continue; } shaderGraph->connect(output, input); } } void HdCyclesMaterial::PopulateShaderGraph(HdMaterialNetworkSchema network) { _nodes.clear(); unique_ptr graph = make_unique(); HdMaterialNodeContainerSchema nodes = network.GetNodes(); /* Iterate all the nodes first and build a complete but unconnected graph with parameters set. */ for (const TfToken &nodeName : nodes.GetNames()) { HdMaterialNodeSchema nodeSchema = nodes.Get(nodeName); const SdfPath nodePath = MaterialNodeNameToSdfPath(nodeName); NodeDesc nodeDesc = {}; const auto nodeIt = _nodes.find(nodePath); /* Create new node only if it does not exist yet. */ if (nodeIt != _nodes.end()) { nodeDesc = nodeIt->second; } else { /* E.g. cycles_principled_bsdf or UsdPreviewSurface. */ const TfToken nodeTypeIdToken = nodeSchema.GetNodeIdentifier() ? nodeSchema.GetNodeIdentifier()->GetTypedValue(0.0f) : TfToken(); const std::string &nodeTypeId = nodeTypeIdToken.GetString(); ustring cyclesType(nodeTypeId); if (nodeTypeId.starts_with("cycles_") || nodeTypeId.starts_with("cycles:")) { /* Native Cycles note embedded in USDShade. */ cyclesType = nodeTypeId.substr(strlen("cycles_")); nodeDesc.mapping = sUsdToCyles->findCycles(cyclesType); } else { /* Check if any remapping is needed (e.g. for USDPreviewSurface to Cycles nodes). */ nodeDesc.mapping = sUsdToCyles->findUsd(nodeTypeIdToken); if (nodeDesc.mapping) { cyclesType = nodeDesc.mapping->nodeType(); } } /* If it's a native Cycles' node-type, just do the lookup now. */ if (const NodeType *nodeType = NodeType::find(cyclesType)) { nodeDesc.node = graph->create_node(nodeType); _nodes.emplace(nodePath, nodeDesc); } else { TF_RUNTIME_ERROR("Could not create node '%s'", nodePath.GetText()); continue; } } UpdateParameters(nodeDesc, nodeSchema.GetParameters(), nodePath); } /* Now that all nodes have been constructed, iterate the network again and build up any * connections between nodes. */ for (const TfToken &nodeName : nodes.GetNames()) { const SdfPath nodePath = MaterialNodeNameToSdfPath(nodeName); const auto nodeIt = _nodes.find(nodePath); if (nodeIt == _nodes.end()) { TF_RUNTIME_ERROR("Could not find node '%s' to connect", nodePath.GetText()); continue; } UpdateConnections(nodeIt->second, nodes.Get(nodeName), nodePath, graph.get()); } /* Finally connect the terminals to the graph output (Surface, Volume, Displacement). */ HdMaterialConnectionContainerSchema terminals = network.GetTerminals(); for (const TfToken &terminalName : terminals.GetNames()) { HdMaterialConnectionSchema termSchema = terminals.Get(terminalName); const SdfPath upstreamNodePath = termSchema.GetUpstreamNodePath() ? MaterialNodeNameToSdfPath(termSchema.GetUpstreamNodePath()->GetTypedValue(0.0f)) : SdfPath(); const TfToken upstreamOutputName = termSchema.GetUpstreamNodeOutputName() ? termSchema.GetUpstreamNodeOutputName()->GetTypedValue( 0.0f) : TfToken(); const auto nodeIt = _nodes.find(upstreamNodePath); if (nodeIt == _nodes.end()) { TF_RUNTIME_ERROR("Could not find terminal node '%s'", upstreamNodePath.GetText()); continue; } ShaderNode *const node = nodeIt->second.node; const char *inputName = nullptr; const char *outputName = nullptr; if (terminalName == HdMaterialTerminalTokens->surface || terminalName == CyclesMaterialTokens->cyclesSurface) { inputName = "Surface"; /* Find default output name based on the node if none is provided. */ if (node->type->name == "add_closure" || node->type->name == "mix_closure") { outputName = "Closure"; } else if (node->type->name == "emission") { outputName = "Emission"; } else { outputName = "BSDF"; } } else if (terminalName == HdMaterialTerminalTokens->displacement || terminalName == CyclesMaterialTokens->cyclesDisplacement) { inputName = outputName = "Displacement"; } else if (terminalName == HdMaterialTerminalTokens->volume || terminalName == CyclesMaterialTokens->cyclesVolume) { inputName = outputName = "Volume"; } /* For native Cycles nodes we use the upstream output name as is, for * mapping from e.g. UsdPreviewSurface we need to use the default output * name that is known to exist. */ if (!upstreamOutputName.IsEmpty() && nodeIt->second.mapping == nullptr) { outputName = upstreamOutputName.GetText(); } ShaderInput *const input = inputName ? graph->output()->input(inputName) : nullptr; if (!input) { TF_RUNTIME_ERROR("Could not find terminal input '%s.%s'", upstreamNodePath.GetText(), inputName ? inputName : ""); continue; } ShaderOutput *const output = outputName ? node->output(outputName) : nullptr; if (!output) { TF_RUNTIME_ERROR("Could not find terminal output '%s.%s'", upstreamNodePath.GetText(), outputName ? outputName : ""); continue; } graph->connect(output, input); } /* Create the instanceId AOV output. */ { const ustring instanceId(HdAovTokens->instanceId.GetString()); OutputAOVNode *aovNode = graph->create_node(); aovNode->set_name(instanceId); AttributeNode *instanceIdNode = graph->create_node(); instanceIdNode->set_attribute(instanceId); graph->connect(instanceIdNode->output("Fac"), aovNode->input("Value")); } _shader->set_graph(std::move(graph)); } void HdCyclesMaterial::Finalize(HdRenderParam *renderParam) { if (!_shader) { return; } const SceneLock lock(renderParam); const bool keep_nodes = static_cast(renderParam)->keep_nodes; _nodes.clear(); if (!keep_nodes) { lock.scene->delete_node(_shader); } _shader = nullptr; } void HdCyclesMaterial::Initialize(HdRenderParam *renderParam) { if (_shader) { return; } const SceneLock lock(renderParam); _shader = lock.scene->create_node(); } HDCYCLES_NAMESPACE_CLOSE_SCOPE