blender/intern/cycles/hydra/material.cpp
Brecht Van Lommel 51977de03a Cycles: Hydra: Change render delegate to use Hydra 2.0 API
The 1.0 API is deprecated (though not yet removed) in OpenUSD 26.xx
releases. So it's time to migrate.

Use of the HdSceneDelegate is mostly removed, except for retrieving the
prims. Specific properties of prims are retrieved through schemas rather
than querying the scene delegate.

The full replacement of HdSceneDelegate is still ongoing in USD itself.
In the very latest 26.05 only HdPrman has an experimental option for
using the scene index directly, but for example HdStorm does not use it
yet. So removing it's use with the current 25.08 in Blender is not
expected to be stable enough.

Additionally HdRenderer is supposed to replace HdRenderDelegate and its
handling of tasks. However even in 26.05 that is only a stub
implementation so there is no actual alternative to migrate to.

Note that OpenUSD provides a compatibility layer so 1.0 and 2.0 hosts and
render delegates can work together. Which means ideally everything keeps
working the same, but there are likely some differences anyway. Support for
OpenUSD version 23.xx and older was dropped.

Pull Request: https://projects.blender.org/blender/blender/pulls/158456
2026-05-27 15:56:20 +02:00

602 lines
20 KiB
C++

/* 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 <pxr/imaging/hd/material.h>
#include <pxr/imaging/hd/materialConnectionSchema.h>
#include <pxr/imaging/hd/materialNetworkSchema.h>
#include <pxr/imaging/hd/materialNodeParameterSchema.h>
#include <pxr/imaging/hd/materialNodeSchema.h>
#include <pxr/imaging/hd/materialSchema.h>
#include <pxr/imaging/hd/sceneDelegate.h>
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<TfToken, ustring, TfToken::HashFunctor>;
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<std::string>(*value).Get<std::string>();
/* 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<UsdToCycles> 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 &paramName : 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 <switch>, <combine>, 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<ShaderGraph> graph = make_unique<ShaderGraph>();
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 : "<null>");
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 : "<null>");
continue;
}
graph->connect(output, input);
}
/* Create the instanceId AOV output. */
{
const ustring instanceId(HdAovTokens->instanceId.GetString());
OutputAOVNode *aovNode = graph->create_node<OutputAOVNode>();
aovNode->set_name(instanceId);
AttributeNode *instanceIdNode = graph->create_node<AttributeNode>();
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<const HdCyclesSession *>(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<Shader>();
}
HDCYCLES_NAMESPACE_CLOSE_SCOPE