This patch adds a Hydra render delegate to Cycles, allowing Cycles to be used for rendering in applications that provide a Hydra viewport. The implementation was written from scratch against Cycles X, for integration into the Blender repository to make it possible to continue developing it in step with the rest of Cycles. For this purpose it follows the style of the rest of the Cycles code and can be built with a CMake option (`WITH_CYCLES_HYDRA_RENDER_DELEGATE=1`) similar to the existing standalone version of Cycles. Since Hydra render delegates need to be built against the exact USD version and other dependencies as the target application is using, this is intended to be built separate from Blender (`WITH_BLENDER=0` CMake option) and with support for library versions different from what Blender is using. As such the CMake build scripts for Windows had to be modified slightly, so that the Cycles Hydra render delegate can e.g. be built with MSVC 2017 again even though Blender requires MSVC 2019 now, and it's possible to specify custom paths to the USD SDK etc. The codebase supports building against the latest USD release 22.03 and all the way back to USD 20.08 (with some limitations). Reviewed By: brecht, LazyDodo Differential Revision: https://developer.blender.org/D14398
590 lines
18 KiB
C++
590 lines
18 KiB
C++
/* SPDX-License-Identifier: Apache-2.0
|
|
* Copyright 2022 NVIDIA Corporation
|
|
* Copyright 2022 Blender Foundation */
|
|
|
|
#include "hydra/material.h"
|
|
#include "hydra/node_util.h"
|
|
#include "hydra/session.h"
|
|
#include "scene/scene.h"
|
|
#include "scene/shader.h"
|
|
#include "scene/shader_graph.h"
|
|
#include "scene/shader_nodes.h"
|
|
|
|
#include <pxr/imaging/hd/sceneDelegate.h>
|
|
|
|
HDCYCLES_NAMESPACE_OPEN_SCOPE
|
|
|
|
// clang-format off
|
|
TF_DEFINE_PRIVATE_TOKENS(CyclesMaterialTokens,
|
|
((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
|
|
|
|
namespace {
|
|
|
|
// 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) {
|
|
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);
|
|
}
|
|
};
|
|
|
|
class UsdToCycles {
|
|
const UsdToCyclesMapping UsdPreviewSurface = {
|
|
"principled_bsdf",
|
|
{
|
|
{TfToken("diffuseColor"), ustring("base_color")},
|
|
{TfToken("emissiveColor"), ustring("emission")},
|
|
{TfToken("specularColor"), ustring("specular")},
|
|
{TfToken("clearcoatRoughness"), ustring("clearcoat_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
|
|
|
|
struct HdCyclesMaterial::NodeDesc {
|
|
ShaderNode *node;
|
|
const UsdToCyclesMapping *mapping;
|
|
};
|
|
|
|
HdCyclesMaterial::HdCyclesMaterial(const SdfPath &sprimId) : HdMaterial(sprimId)
|
|
{
|
|
}
|
|
|
|
HdCyclesMaterial::~HdCyclesMaterial()
|
|
{
|
|
}
|
|
|
|
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);
|
|
|
|
VtValue value;
|
|
const SdfPath &id = GetId();
|
|
|
|
if (dirtyResource || dirtyParams) {
|
|
value = sceneDelegate->GetMaterialResource(id);
|
|
|
|
#if 1
|
|
const HdMaterialNetwork2 *network = nullptr;
|
|
std::unique_ptr<HdMaterialNetwork2> networkConverted;
|
|
if (value.IsHolding<HdMaterialNetwork2>()) {
|
|
network = &value.UncheckedGet<HdMaterialNetwork2>();
|
|
}
|
|
else if (value.IsHolding<HdMaterialNetworkMap>()) {
|
|
const auto &networkOld = value.UncheckedGet<HdMaterialNetworkMap>();
|
|
// In the case of only parameter updates, there is no need to waste time converting to a
|
|
// HdMaterialNetwork2, as supporting HdMaterialNetworkMap for parameters only is trivial.
|
|
if (!_nodes.empty() && !dirtyResource) {
|
|
for (const auto &networkEntry : networkOld.map) {
|
|
UpdateParameters(networkEntry.second);
|
|
}
|
|
_shader->tag_modified();
|
|
}
|
|
else {
|
|
networkConverted = std::make_unique<HdMaterialNetwork2>();
|
|
HdMaterialNetwork2ConvertFromHdMaterialNetworkMap(networkOld, networkConverted.get());
|
|
network = networkConverted.get();
|
|
}
|
|
}
|
|
else {
|
|
TF_RUNTIME_ERROR("Could not get a HdMaterialNetwork2.");
|
|
}
|
|
|
|
if (network) {
|
|
if (!_nodes.empty() && !dirtyResource) {
|
|
UpdateParameters(*network);
|
|
_shader->tag_modified();
|
|
}
|
|
else {
|
|
PopulateShaderGraph(*network);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (_shader->is_modified()) {
|
|
_shader->tag_update(lock.scene);
|
|
}
|
|
|
|
*dirtyBits = DirtyBits::Clean;
|
|
}
|
|
|
|
void HdCyclesMaterial::UpdateParameters(NodeDesc &nodeDesc,
|
|
const std::map<TfToken, VtValue> ¶meters,
|
|
const SdfPath &nodePath)
|
|
{
|
|
for (const std::pair<TfToken, VtValue> ¶m : parameters) {
|
|
VtValue value = param.second;
|
|
|
|
// 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(param.first, nullptr, &value) :
|
|
param.first.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')",
|
|
param.first.GetText(),
|
|
nodePath.GetText(),
|
|
nodeDesc.node->name.c_str());
|
|
continue;
|
|
}
|
|
|
|
SetNodeValue(nodeDesc.node, *input, value);
|
|
}
|
|
}
|
|
|
|
void HdCyclesMaterial::UpdateParameters(const HdMaterialNetwork &network)
|
|
{
|
|
for (const HdMaterialNode &nodeEntry : network.nodes) {
|
|
const SdfPath &nodePath = nodeEntry.path;
|
|
|
|
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, nodeEntry.parameters, nodePath);
|
|
}
|
|
}
|
|
|
|
void HdCyclesMaterial::UpdateParameters(const HdMaterialNetwork2 &network)
|
|
{
|
|
for (const std::pair<SdfPath, HdMaterialNode2> &nodeEntry : network.nodes) {
|
|
const SdfPath &nodePath = nodeEntry.first;
|
|
|
|
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, nodeEntry.second.parameters, nodePath);
|
|
}
|
|
}
|
|
|
|
void HdCyclesMaterial::UpdateConnections(NodeDesc &nodeDesc,
|
|
const HdMaterialNode2 &matNode,
|
|
const SdfPath &nodePath,
|
|
ShaderGraph *shaderGraph)
|
|
{
|
|
for (const std::pair<TfToken, std::vector<HdMaterialConnection2>> &connection :
|
|
matNode.inputConnections) {
|
|
const TfToken &dstSocketName = connection.first;
|
|
|
|
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;
|
|
}
|
|
|
|
// Now find the output to connect from
|
|
const auto &connectedNodes = connection.second;
|
|
if (connectedNodes.empty()) {
|
|
continue;
|
|
}
|
|
|
|
// TODO: Hydra allows multiple connections of the same input
|
|
// Unsure how to handle this in Cycles, so just use the first
|
|
if (connectedNodes.size() > 1) {
|
|
TF_WARN(
|
|
"Ignoring multiple connections to '%s.%s'", nodePath.GetText(), dstSocketName.GetText());
|
|
}
|
|
|
|
const SdfPath &upstreamNodePath = connectedNodes.front().upstreamNode;
|
|
const TfToken &upstreamOutputName = connectedNodes.front().upstreamOutputName;
|
|
|
|
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(const HdMaterialNetwork2 &networkMap)
|
|
{
|
|
_nodes.clear();
|
|
|
|
auto graph = new ShaderGraph();
|
|
|
|
// Iterate all the nodes first and build a complete but unconnected graph with parameters set
|
|
for (const std::pair<SdfPath, HdMaterialNode2> &nodeEntry : networkMap.nodes) {
|
|
NodeDesc nodeDesc = {};
|
|
const SdfPath &nodePath = nodeEntry.first;
|
|
|
|
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 std::string &nodeTypeId = nodeEntry.second.nodeTypeId.GetString();
|
|
|
|
ustring cyclesType(nodeTypeId);
|
|
// Interpret a node type ID prefixed with cycles_<type> or cycles:<type> as a node of <type>
|
|
if (nodeTypeId.rfind("cycles", 0) == 0) {
|
|
cyclesType = nodeTypeId.substr(7);
|
|
nodeDesc.mapping = sUsdToCyles->findCycles(cyclesType);
|
|
}
|
|
else {
|
|
// Check if any remapping is needed (e.g. for USDPreviewSurface to Cycles nodes)
|
|
nodeDesc.mapping = sUsdToCyles->findUsd(nodeEntry.second.nodeTypeId);
|
|
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 = static_cast<ShaderNode *>(nodeType->create(nodeType));
|
|
nodeDesc.node->set_owner(graph);
|
|
|
|
graph->add(nodeDesc.node);
|
|
|
|
_nodes.emplace(nodePath, nodeDesc);
|
|
}
|
|
else {
|
|
TF_RUNTIME_ERROR("Could not create node '%s'", nodePath.GetText());
|
|
continue;
|
|
}
|
|
}
|
|
|
|
UpdateParameters(nodeDesc, nodeEntry.second.parameters, nodePath);
|
|
}
|
|
|
|
// Now that all nodes have been constructed, iterate the network again and build up any
|
|
// connections between nodes
|
|
for (const std::pair<SdfPath, HdMaterialNode2> &nodeEntry : networkMap.nodes) {
|
|
const SdfPath &nodePath = nodeEntry.first;
|
|
|
|
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, nodeEntry.second, nodePath, graph);
|
|
}
|
|
|
|
// Finally connect the terminals to the graph output (Surface, Volume, Displacement)
|
|
for (const std::pair<TfToken, HdMaterialConnection2> &terminalEntry : networkMap.terminals) {
|
|
const TfToken &terminalName = terminalEntry.first;
|
|
const HdMaterialConnection2 &connection = terminalEntry.second;
|
|
|
|
const auto nodeIt = _nodes.find(connection.upstreamNode);
|
|
if (nodeIt == _nodes.end()) {
|
|
TF_RUNTIME_ERROR("Could not find terminal node '%s'", connection.upstreamNode.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";
|
|
}
|
|
|
|
if (!connection.upstreamOutputName.IsEmpty()) {
|
|
outputName = connection.upstreamOutputName.GetText();
|
|
}
|
|
|
|
ShaderInput *const input = inputName ? graph->output()->input(inputName) : nullptr;
|
|
if (!input) {
|
|
TF_RUNTIME_ERROR("Could not find terminal input '%s.%s'",
|
|
connection.upstreamNode.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'",
|
|
connection.upstreamNode.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);
|
|
graph->add(aovNode);
|
|
|
|
AttributeNode *instanceIdNode = graph->create_node<AttributeNode>();
|
|
instanceIdNode->set_attribute(instanceId);
|
|
graph->add(instanceIdNode);
|
|
|
|
graph->connect(instanceIdNode->output("Fac"), aovNode->input("Value"));
|
|
}
|
|
|
|
_shader->set_graph(graph);
|
|
}
|
|
|
|
void HdCyclesMaterial::Finalize(HdRenderParam *renderParam)
|
|
{
|
|
if (!_shader) {
|
|
return;
|
|
}
|
|
|
|
const SceneLock lock(renderParam);
|
|
|
|
_nodes.clear();
|
|
|
|
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
|