USD: enable material displacement support
This enables material displacement for UsdPreviewSurface import and export. Scenarios are limited by what's supported by the preview surface itself. Namely only Object Space displacement can be used (no vector displacement)[1] and the Midlevel and Scale parameters are maintained by adjusting the scale-bias on the image texture controlling the Height (this means that Midlevel and Scale must be constants). Hydra/MaterialX support is more complicated. First, there is a bug which prevents scalar displacment from working correctly and that needs USD 2408+ for the fix[2]. Second, is that there's an open question about which coordinate system to use for MaterialX's vector displacement maps. Lastly, Hydra GL does not render displacement, making verification using only Blender impossible[3]. As a result, this PR only makes MaterialX "ready" for support, but stops short of actually connecting the final piece of the node graph until more of the above can be sorted out. Tests are added which cover: - Variations of Midlevel and Scale values - A constant Height setup - Negative scenarios checking that only Object space is supported and that midlevel and scale need to be constants [1] https://openusd.org/release/spec_usdpreviewsurface.html [2] https://github.com/PixarAnimationStudios/OpenUSD/issues/3325 [3] https://forum.aousd.org/t/materialx-displacement-hydra-storm/1098/2 Pull Request: https://projects.blender.org/blender/blender/pulls/128909
This commit is contained in:
committed by
Jesse Yurkovich
parent
0598db079d
commit
b4c2feea38
@@ -43,6 +43,7 @@ static const pxr::TfToken bias("bias", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken clearcoat("clearcoat", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken clearcoatRoughness("clearcoatRoughness", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken diffuseColor("diffuseColor", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken displacement("displacement", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken emissiveColor("emissiveColor", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken file("file", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken g("g", pxr::TfToken::Immortal);
|
||||
@@ -525,6 +526,10 @@ void USDMaterialReader::import_usd_preview(Material *mtl,
|
||||
/* Recursively create the principled shader input networks. */
|
||||
set_principled_node_inputs(principled, ntree, usd_shader);
|
||||
|
||||
if (set_displacement_node_inputs(ntree, output, usd_shader)) {
|
||||
mtl->displacement_method = MA_DISPLACEMENT_BOTH;
|
||||
}
|
||||
|
||||
blender::bke::node_set_active(ntree, output);
|
||||
|
||||
BKE_ntree_update_main_tree(bmain_, ntree, nullptr);
|
||||
@@ -608,6 +613,53 @@ void USDMaterialReader::set_principled_node_inputs(bNode *principled,
|
||||
}
|
||||
}
|
||||
|
||||
bool USDMaterialReader::set_displacement_node_inputs(bNodeTree *ntree,
|
||||
bNode *output,
|
||||
const pxr::UsdShadeShader &usd_shader) const
|
||||
{
|
||||
/* Only continue if this UsdPreviewSurface has displacement to process. */
|
||||
pxr::UsdShadeInput displacement_input = usd_shader.GetInput(usdtokens::displacement);
|
||||
if (!displacement_input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bNode *displacement_node = add_node(nullptr, ntree, SH_NODE_DISPLACEMENT, 0.0f, -100.0f);
|
||||
if (!displacement_node) {
|
||||
CLOG_ERROR(&LOG,
|
||||
"Couldn't create SH_NODE_DISPLACEMENT node for USD shader %s",
|
||||
usd_shader.GetPath().GetAsString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The context struct keeps track of the locations for adding
|
||||
* input nodes. */
|
||||
NodePlacementContext context(0.0f, -100.0f);
|
||||
|
||||
/* The column index, from right to left relative to the output node. */
|
||||
int column = 0;
|
||||
|
||||
const char *sock_name = "Height";
|
||||
ExtraLinkInfo extra;
|
||||
extra.is_color_corrected = false;
|
||||
set_node_input(displacement_input, displacement_node, sock_name, ntree, column, &context, extra);
|
||||
|
||||
/* If the displacement input is not connected, then this is "constant" displacement.
|
||||
* We need to adjust the Height input by our default Midlevel value of 0.5. */
|
||||
if (!displacement_input.HasConnectedSource()) {
|
||||
bNodeSocket *sock = blender::bke::node_find_socket(displacement_node, SOCK_IN, sock_name);
|
||||
if (!sock) {
|
||||
CLOG_ERROR(&LOG, "Couldn't get destination node socket %s", sock_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
((bNodeSocketValueFloat *)sock->default_value)->value += 0.5f;
|
||||
}
|
||||
|
||||
/* Connect the Displacement node to the output node. */
|
||||
link_nodes(ntree, displacement_node, "Displacement", output, "Displacement");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool USDMaterialReader::set_node_input(const pxr::UsdShadeInput &usd_input,
|
||||
bNode *dest_node,
|
||||
const char *dest_socket_name,
|
||||
@@ -873,6 +925,35 @@ static IntermediateNode add_oneminus(bNodeTree *ntree, int column, NodePlacement
|
||||
return oneminus;
|
||||
}
|
||||
|
||||
static void configure_displacement(const pxr::UsdShadeShader &usd_shader,
|
||||
bNode *displacement_node,
|
||||
int column,
|
||||
NodePlacementContext *r_ctx)
|
||||
{
|
||||
/* Transform the scale-bias values into something that the Displacement node
|
||||
* can understand. */
|
||||
pxr::UsdShadeInput scale_input = usd_shader.GetInput(usdtokens::scale);
|
||||
pxr::UsdShadeInput bias_input = usd_shader.GetInput(usdtokens::bias);
|
||||
pxr::GfVec4f scale(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
pxr::GfVec4f bias(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
pxr::VtValue val;
|
||||
if (scale_input.Get(&val) && val.CanCast<pxr::GfVec4f>()) {
|
||||
scale = pxr::VtValue::Cast<pxr::GfVec4f>(val).UncheckedGet<pxr::GfVec4f>();
|
||||
}
|
||||
if (bias_input.Get(&val) && val.CanCast<pxr::GfVec4f>()) {
|
||||
bias = pxr::VtValue::Cast<pxr::GfVec4f>(val).UncheckedGet<pxr::GfVec4f>();
|
||||
}
|
||||
|
||||
const float scale_avg = (scale[0] + scale[1] + scale[2]) / 3.0f;
|
||||
const float bias_avg = (bias[0] + bias[1] + bias[2]) / 3.0f;
|
||||
|
||||
bNodeSocket *sock_mid = blender::bke::node_find_socket(displacement_node, SOCK_IN, "Midlevel");
|
||||
bNodeSocket *sock_scale = blender::bke::node_find_socket(displacement_node, SOCK_IN, "Scale");
|
||||
((bNodeSocketValueFloat *)sock_mid->default_value)->value = -1.0f * (bias_avg / scale_avg);
|
||||
((bNodeSocketValueFloat *)sock_scale->default_value)->value = scale_avg;
|
||||
}
|
||||
|
||||
bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
|
||||
bNode *dest_node,
|
||||
const char *dest_socket_name,
|
||||
@@ -928,9 +1009,14 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
|
||||
shift++;
|
||||
}
|
||||
|
||||
/* Create a Scale-Bias adjustment node if necessary. */
|
||||
IntermediateNode scale_bias = add_scale_bias(
|
||||
source_shader, ntree, column + shift, is_normal_map, r_ctx);
|
||||
/* Create a Scale-Bias adjustment node or fill in Displacement settings if necessary. */
|
||||
IntermediateNode scale_bias{};
|
||||
if (STREQ(dest_socket_name, "Height")) {
|
||||
configure_displacement(source_shader, dest_node, column + shift, r_ctx);
|
||||
}
|
||||
else {
|
||||
scale_bias = add_scale_bias(source_shader, ntree, column + shift, is_normal_map, r_ctx);
|
||||
}
|
||||
|
||||
/* Wire up any intermediate nodes that are present. Keep track of the
|
||||
* final "target" destination for the Image link. */
|
||||
|
||||
@@ -112,6 +112,10 @@ class USDMaterialReader {
|
||||
bNodeTree *ntree,
|
||||
const pxr::UsdShadeShader &usd_shader) const;
|
||||
|
||||
bool set_displacement_node_inputs(bNodeTree *ntree,
|
||||
bNode *output,
|
||||
const pxr::UsdShadeShader &usd_shader) const;
|
||||
|
||||
/** Convert the given USD shader input to an input on the given Blender node. */
|
||||
bool set_node_input(const pxr::UsdShadeInput &usd_input,
|
||||
bNode *dest_node,
|
||||
|
||||
@@ -67,6 +67,7 @@ static const pxr::TfToken specular("specular", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken opacity("opacity", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken opacityThreshold("opacityThreshold", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken surface("surface", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken displacement("displacement", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken perspective("perspective", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken orthographic("orthographic", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken rgb("rgb", pxr::TfToken::Immortal);
|
||||
@@ -129,6 +130,7 @@ static void create_uv_input(const USDExporterContext &usd_export_context,
|
||||
ReportList *reports);
|
||||
static void export_texture(const USDExporterContext &usd_export_context, bNode *node);
|
||||
static bNode *find_bsdf_node(Material *material);
|
||||
static bNode *find_displacement_node(Material *material);
|
||||
static void get_absolute_path(const Image *ima, char *r_path);
|
||||
static std::string get_tex_image_asset_filepath(const USDExporterContext &usd_export_context,
|
||||
bNode *node);
|
||||
@@ -151,32 +153,33 @@ void create_input(pxr::UsdShadeShader &shader,
|
||||
shader.CreateInput(spec.input_name, spec.input_type).Set(scale * T2(cast_value->value));
|
||||
}
|
||||
|
||||
static void create_usd_preview_surface_material(const USDExporterContext &usd_export_context,
|
||||
Material *material,
|
||||
pxr::UsdShadeMaterial &usd_material,
|
||||
const std::string &active_uvmap_name,
|
||||
ReportList *reports)
|
||||
static void set_scale_bias(pxr::UsdShadeShader &usd_shader,
|
||||
const pxr::GfVec4f scale,
|
||||
const pxr::GfVec4f bias)
|
||||
{
|
||||
if (!material) {
|
||||
return;
|
||||
pxr::UsdShadeInput scale_attr = usd_shader.GetInput(usdtokens::scale);
|
||||
if (!scale_attr) {
|
||||
scale_attr = usd_shader.CreateInput(usdtokens::scale, pxr::SdfValueTypeNames->Float4);
|
||||
}
|
||||
scale_attr.Set(scale);
|
||||
|
||||
/* We only handle the first instance of either principled or
|
||||
* diffuse bsdf nodes in the material's node tree, because
|
||||
* USD Preview Surface has no concept of layering materials. */
|
||||
bNode *node = find_bsdf_node(material);
|
||||
if (!node) {
|
||||
return;
|
||||
pxr::UsdShadeInput bias_attr = usd_shader.GetInput(usdtokens::bias);
|
||||
if (!bias_attr) {
|
||||
bias_attr = usd_shader.CreateInput(usdtokens::bias, pxr::SdfValueTypeNames->Float4);
|
||||
}
|
||||
bias_attr.Set(bias);
|
||||
}
|
||||
|
||||
pxr::UsdShadeShader preview_surface = create_usd_preview_shader(
|
||||
usd_export_context, usd_material, node);
|
||||
|
||||
static void process_inputs(const USDExporterContext &usd_export_context,
|
||||
pxr::UsdShadeMaterial &usd_material,
|
||||
pxr::UsdShadeShader &shader,
|
||||
const bNode *node,
|
||||
const std::string &active_uvmap_name,
|
||||
ReportList *reports)
|
||||
{
|
||||
const InputSpecMap &input_map = preview_surface_input_map();
|
||||
|
||||
/* Set the preview surface inputs. */
|
||||
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
|
||||
|
||||
/* Check if this socket is mapped to a USD preview shader input. */
|
||||
const InputSpec *spec = input_map.lookup_ptr(sock->name);
|
||||
if (spec == nullptr) {
|
||||
@@ -191,15 +194,13 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
|
||||
if (input_spec.input_name == usdtokens::emissive_color) {
|
||||
/* Don't export emission color if strength is zero. */
|
||||
bNodeSocket *emission_strength_sock = bke::node_find_socket(
|
||||
const bNodeSocket *emission_strength_sock = bke::node_find_socket(
|
||||
node, SOCK_IN, "Emission Strength");
|
||||
|
||||
if (!emission_strength_sock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
input_scale = ((bNodeSocketValueFloat *)emission_strength_sock->default_value)->value;
|
||||
|
||||
if (input_scale == 0.0f) {
|
||||
continue;
|
||||
}
|
||||
@@ -243,7 +244,7 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
/* Create the preview surface input and connect it to the shader. */
|
||||
pxr::UsdShadeConnectionSourceInfo source_info(
|
||||
usd_shader.ConnectableAPI(), source_name, pxr::UsdShadeAttributeType::Output);
|
||||
preview_surface.CreateInput(input_spec.input_name, input_spec.input_type)
|
||||
shader.CreateInput(input_spec.input_name, input_spec.input_type)
|
||||
.ConnectToSource(source_info);
|
||||
|
||||
set_normal_texture_range(usd_shader, input_spec);
|
||||
@@ -253,44 +254,52 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
export_texture(usd_export_context, input_node);
|
||||
}
|
||||
|
||||
/* If a Vector Math node was detected ahead of the texture node, and it has
|
||||
* the correct type, NODE_VECTOR_MATH_MULTIPLY_ADD, assume it's meant to be
|
||||
* used for scale-bias. */
|
||||
bNodeLink *scale_link = traverse_channel(sock, SH_NODE_VECTOR_MATH);
|
||||
if (scale_link) {
|
||||
bNode *vector_math_node = scale_link->fromnode;
|
||||
if (vector_math_node->custom1 == NODE_VECTOR_MATH_MULTIPLY_ADD) {
|
||||
/* Attempt one more traversal in case the current node is not the
|
||||
* correct NODE_VECTOR_MATH_MULTIPLY_ADD (see code in usd_reader_material). */
|
||||
bNodeSocket *sock_current = bke::node_find_socket(vector_math_node, SOCK_IN, "Vector");
|
||||
bNodeLink *temp_link = traverse_channel(sock_current, SH_NODE_VECTOR_MATH);
|
||||
if (temp_link && temp_link->fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY_ADD) {
|
||||
vector_math_node = temp_link->fromnode;
|
||||
/* Scale-Bias processing.
|
||||
* Ordinary: If a Vector Math node was detected ahead of the texture node, and it has
|
||||
* the correct type, NODE_VECTOR_MATH_MULTIPLY_ADD, assume it's meant to be
|
||||
* used for scale-bias.
|
||||
* Displacement: The scale-bias values come from the Midlevel and Scale sockets.
|
||||
*/
|
||||
if (input_spec.input_name != usdtokens::displacement) {
|
||||
bNodeLink *scale_link = traverse_channel(sock, SH_NODE_VECTOR_MATH);
|
||||
if (scale_link) {
|
||||
bNode *vector_math_node = scale_link->fromnode;
|
||||
if (vector_math_node->custom1 == NODE_VECTOR_MATH_MULTIPLY_ADD) {
|
||||
/* Attempt one more traversal in case the current node is not the
|
||||
* correct NODE_VECTOR_MATH_MULTIPLY_ADD (see code in usd_reader_material). */
|
||||
bNodeSocket *sock_current = bke::node_find_socket(vector_math_node, SOCK_IN, "Vector");
|
||||
bNodeLink *temp_link = traverse_channel(sock_current, SH_NODE_VECTOR_MATH);
|
||||
if (temp_link && temp_link->fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY_ADD) {
|
||||
vector_math_node = temp_link->fromnode;
|
||||
}
|
||||
|
||||
bNodeSocket *sock_scale = bke::node_find_socket(
|
||||
vector_math_node, SOCK_IN, "Vector_001");
|
||||
bNodeSocket *sock_bias = bke::node_find_socket(
|
||||
vector_math_node, SOCK_IN, "Vector_002");
|
||||
const float *scale_value =
|
||||
static_cast<bNodeSocketValueVector *>(sock_scale->default_value)->value;
|
||||
const float *bias_value =
|
||||
static_cast<bNodeSocketValueVector *>(sock_bias->default_value)->value;
|
||||
|
||||
const pxr::GfVec4f scale(scale_value[0], scale_value[1], scale_value[2], 1.0f);
|
||||
const pxr::GfVec4f bias(bias_value[0], bias_value[1], bias_value[2], 0.0f);
|
||||
set_scale_bias(usd_shader, scale, bias);
|
||||
}
|
||||
|
||||
bNodeSocket *sock_scale = bke::node_find_socket(vector_math_node, SOCK_IN, "Vector_001");
|
||||
bNodeSocket *sock_bias = bke::node_find_socket(vector_math_node, SOCK_IN, "Vector_002");
|
||||
const float *scale_value =
|
||||
static_cast<bNodeSocketValueVector *>(sock_scale->default_value)->value;
|
||||
const float *bias_value =
|
||||
static_cast<bNodeSocketValueVector *>(sock_bias->default_value)->value;
|
||||
|
||||
const pxr::GfVec4f scale(scale_value[0], scale_value[1], scale_value[2], 1.0f);
|
||||
const pxr::GfVec4f bias(bias_value[0], bias_value[1], bias_value[2], 0.0f);
|
||||
|
||||
pxr::UsdShadeInput scale_attr = usd_shader.GetInput(usdtokens::scale);
|
||||
if (!scale_attr) {
|
||||
scale_attr = usd_shader.CreateInput(usdtokens::scale, pxr::SdfValueTypeNames->Float4);
|
||||
}
|
||||
scale_attr.Set(scale);
|
||||
|
||||
pxr::UsdShadeInput bias_attr = usd_shader.GetInput(usdtokens::bias);
|
||||
if (!bias_attr) {
|
||||
bias_attr = usd_shader.CreateInput(usdtokens::bias, pxr::SdfValueTypeNames->Float4);
|
||||
}
|
||||
bias_attr.Set(bias);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const bNodeSocket *sock_midlevel = bke::node_find_socket(node, SOCK_IN, "Midlevel");
|
||||
const bNodeSocket *sock_scale = bke::node_find_socket(node, SOCK_IN, "Scale");
|
||||
const float midlevel_value =
|
||||
sock_midlevel->default_value_typed<bNodeSocketValueFloat>()->value;
|
||||
const float scale_value = sock_scale->default_value_typed<bNodeSocketValueFloat>()->value;
|
||||
|
||||
const float adjusted_bias = -midlevel_value * scale_value;
|
||||
const pxr::GfVec4f scale(scale_value, scale_value, scale_value, 1.0f);
|
||||
const pxr::GfVec4f bias(adjusted_bias, adjusted_bias, adjusted_bias, 0.0f);
|
||||
set_scale_bias(usd_shader, scale, bias);
|
||||
}
|
||||
|
||||
/* Look for a connected uvmap node. */
|
||||
if (bNodeSocket *socket = bke::node_find_socket(input_node, SOCK_IN, "Vector")) {
|
||||
@@ -335,7 +344,7 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
}
|
||||
|
||||
if (threshold > 0.0f) {
|
||||
pxr::UsdShadeInput opacity_threshold_input = preview_surface.CreateInput(
|
||||
pxr::UsdShadeInput opacity_threshold_input = shader.CreateInput(
|
||||
usdtokens::opacityThreshold, pxr::SdfValueTypeNames->Float);
|
||||
opacity_threshold_input.GetAttr().Set(pxr::VtValue(threshold));
|
||||
}
|
||||
@@ -347,15 +356,15 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
switch (sock->type) {
|
||||
case SOCK_FLOAT: {
|
||||
create_input<bNodeSocketValueFloat, float>(
|
||||
preview_surface, input_spec, sock->default_value, input_scale);
|
||||
shader, input_spec, sock->default_value, input_scale);
|
||||
} break;
|
||||
case SOCK_VECTOR: {
|
||||
create_input<bNodeSocketValueVector, pxr::GfVec3f>(
|
||||
preview_surface, input_spec, sock->default_value, input_scale);
|
||||
shader, input_spec, sock->default_value, input_scale);
|
||||
} break;
|
||||
case SOCK_RGBA: {
|
||||
create_input<bNodeSocketValueRGBA, pxr::GfVec3f>(
|
||||
preview_surface, input_spec, sock->default_value, input_scale);
|
||||
shader, input_spec, sock->default_value, input_scale);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
@@ -364,6 +373,73 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
|
||||
}
|
||||
}
|
||||
|
||||
static void create_usd_preview_surface_material(const USDExporterContext &usd_export_context,
|
||||
Material *material,
|
||||
pxr::UsdShadeMaterial &usd_material,
|
||||
const std::string &active_uvmap_name,
|
||||
ReportList *reports)
|
||||
{
|
||||
if (!material) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* We only handle the first instance of either principled or
|
||||
* diffuse bsdf nodes in the material's node tree, because
|
||||
* USD Preview Surface has no concept of layering materials. */
|
||||
bNode *surface_node = find_bsdf_node(material);
|
||||
if (!surface_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::UsdShadeShader preview_surface = create_usd_preview_shader(
|
||||
usd_export_context, usd_material, surface_node);
|
||||
|
||||
/* Handle the primary "surface" output. */
|
||||
process_inputs(
|
||||
usd_export_context, usd_material, preview_surface, surface_node, active_uvmap_name, reports);
|
||||
|
||||
/* Handle the "displacement" output if it meets our requirements. */
|
||||
if (bNode *displacement_node = find_displacement_node(material)) {
|
||||
if (displacement_node->custom1 != SHD_SPACE_OBJECT) {
|
||||
CLOG_WARN(&LOG,
|
||||
"Skipping displacement. Only Object Space displacement is supported by the "
|
||||
"UsdPreviewSurface.");
|
||||
return;
|
||||
}
|
||||
|
||||
bNodeSocket *sock_mid = bke::node_find_socket(displacement_node, SOCK_IN, "Midlevel");
|
||||
bNodeSocket *sock_scale = bke::node_find_socket(displacement_node, SOCK_IN, "Scale");
|
||||
if (sock_mid->link || sock_scale->link) {
|
||||
CLOG_WARN(&LOG, "Skipping displacement. Midlevel and Scale must be constants.");
|
||||
return;
|
||||
}
|
||||
|
||||
usd_material.CreateDisplacementOutput().ConnectToSource(preview_surface.ConnectableAPI(),
|
||||
usdtokens::displacement);
|
||||
|
||||
bNodeSocket *sock_height = bke::node_find_socket(displacement_node, SOCK_IN, "Height");
|
||||
if (sock_height->link) {
|
||||
process_inputs(usd_export_context,
|
||||
usd_material,
|
||||
preview_surface,
|
||||
displacement_node,
|
||||
active_uvmap_name,
|
||||
reports);
|
||||
}
|
||||
else {
|
||||
/* The Height itself was also a constant. Odd but still valid. As there's only 1 value that
|
||||
* can be written to USD, this will be a lossy conversion upon reading back in. The reader
|
||||
* will calculate the node's parameters assuming default values for Midlevel and Scale. */
|
||||
const float mid_value = sock_mid->default_value_typed<bNodeSocketValueFloat>()->value;
|
||||
const float scale_value = sock_scale->default_value_typed<bNodeSocketValueFloat>()->value;
|
||||
const float height_value = sock_height->default_value_typed<bNodeSocketValueFloat>()->value;
|
||||
const float displacement_value = (height_value - mid_value) * scale_value;
|
||||
const InputSpec &spec = preview_surface_input_map().lookup("Height");
|
||||
preview_surface.CreateInput(spec.input_name, spec.input_type).Set(displacement_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void set_normal_texture_range(pxr::UsdShadeShader &usd_shader, const InputSpec &input_spec)
|
||||
{
|
||||
/* Set the scale and bias for normal map textures
|
||||
@@ -441,6 +517,7 @@ static const InputSpecMap &preview_surface_input_map()
|
||||
map.add_new("Coat Weight", {usdtokens::clearcoat, pxr::SdfValueTypeNames->Float, true});
|
||||
map.add_new("Coat Roughness",
|
||||
{usdtokens::clearcoatRoughness, pxr::SdfValueTypeNames->Float, true});
|
||||
map.add_new("Height", {usdtokens::displacement, pxr::SdfValueTypeNames->Float, false});
|
||||
return map;
|
||||
}();
|
||||
|
||||
@@ -802,6 +879,20 @@ static bNode *find_bsdf_node(Material *material)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Returns the first occurrence of a scalar Displacment node found in the given
|
||||
* material's node tree. Vector Displacement is not supported in the UsdPreviewSurface.
|
||||
* Returns null if no instance of either type was found. */
|
||||
static bNode *find_displacement_node(Material *material)
|
||||
{
|
||||
for (bNode *node : material->nodetree->all_nodes()) {
|
||||
if (node->type == SH_NODE_DISPLACEMENT) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Creates a USD Preview Surface shader based on the given cycles node name and type. */
|
||||
static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context,
|
||||
const pxr::UsdShadeMaterial &material,
|
||||
|
||||
@@ -49,6 +49,10 @@ static int gpu_shader_displacement(GPUMaterial *mat,
|
||||
NODE_SHADER_MATERIALX_BEGIN
|
||||
#ifdef WITH_MATERIALX
|
||||
{
|
||||
if (to_type_ != NodeItem::Type::DisplacementShader) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
/* NOTE: Normal input and Space feature don't have an implementation in MaterialX. */
|
||||
NodeItem midlevel = get_input_value("Midlevel", NodeItem::Type::Float);
|
||||
NodeItem height = get_input_value("Height", NodeItem::Type::Float) - midlevel;
|
||||
|
||||
@@ -55,6 +55,15 @@ NODE_SHADER_MATERIALX_BEGIN
|
||||
{{"bsdf", bsdf}, {"edf", edf}, {"opacity", opacity}});
|
||||
}
|
||||
}
|
||||
|
||||
/* Displacement cannot be enabled just yet.
|
||||
* - Verify coordinate system for Tangent Space displacement maps
|
||||
* - Wait on fix for scalar displacement (present in USD 2408+)
|
||||
*/
|
||||
// NodeItem displacement = get_input_link("Displacement", NodeItem::Type::DisplacementShader);
|
||||
// return create_node("surfacematerial",
|
||||
// NodeItem::Type::Material,
|
||||
// {{"surfaceshader", surface}, {"displacementshader", displacement}});
|
||||
return create_node("surfacematerial", NodeItem::Type::Material, {{"surfaceshader", surface}});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,9 +53,13 @@ static int gpu_shader_vector_displacement(GPUMaterial *mat,
|
||||
NODE_SHADER_MATERIALX_BEGIN
|
||||
#ifdef WITH_MATERIALX
|
||||
{
|
||||
/* NOTE: Mid-level input and Space feature don't have an implementation in MaterialX. */
|
||||
// NodeItem midlevel = get_input_value("midlevel", NodeItem::Type::Float);
|
||||
NodeItem vector = get_input_link("Vector", NodeItem::Type::Vector3);
|
||||
if (to_type_ != NodeItem::Type::DisplacementShader) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
/* NOTE: The Space feature doesn't have an implementation in MaterialX. */
|
||||
NodeItem midlevel = get_input_value("Midlevel", NodeItem::Type::Float);
|
||||
NodeItem vector = get_input_link("Vector", NodeItem::Type::Vector3) - midlevel;
|
||||
NodeItem scale = get_input_value("Scale", NodeItem::Type::Float);
|
||||
|
||||
return create_node("displacement",
|
||||
|
||||
@@ -251,6 +251,53 @@ class USDExportTest(AbstractUSDTest):
|
||||
geom_subsets = UsdGeom.Subset.GetGeomSubsets(dynamic_mesh_prim)
|
||||
self.assertEqual(len(geom_subsets), 0)
|
||||
|
||||
def test_export_material_displacement(self):
|
||||
"""Validate correct export of Displacement information for the UsdPreviewSurface"""
|
||||
|
||||
# Use the common materials .blend file
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_displace.blend"))
|
||||
export_path = self.tempdir / "material_displace.usda"
|
||||
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
|
||||
# Verify "constant" displacement
|
||||
shader_surface = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/constant/Principled_BSDF"))
|
||||
self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
|
||||
input_displacement = shader_surface.GetInput('displacement')
|
||||
self.assertEqual(input_displacement.HasConnectedSource(), False, "Displacement input should not be connected")
|
||||
self.assertAlmostEqual(input_displacement.Get(), 0.45, 5)
|
||||
|
||||
# Validate various Midlevel and Scale scenarios
|
||||
def validate_displacement(mat_name, expected_scale, expected_bias):
|
||||
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Principled_BSDF"))
|
||||
shader_image = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Image_Texture"))
|
||||
self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
|
||||
self.assertEqual(shader_image.GetIdAttr().Get(), "UsdUVTexture")
|
||||
input_displacement = shader_surface.GetInput('displacement')
|
||||
input_colorspace = shader_image.GetInput('sourceColorSpace')
|
||||
input_scale = shader_image.GetInput('scale')
|
||||
input_bias = shader_image.GetInput('bias')
|
||||
self.assertEqual(input_displacement.HasConnectedSource(), True, "Displacement input should be connected")
|
||||
self.assertEqual(input_colorspace.Get(), 'raw')
|
||||
self.assertEqual(self.round_vector(input_scale.Get()), expected_scale)
|
||||
self.assertEqual(self.round_vector(input_bias.Get()), expected_bias)
|
||||
|
||||
validate_displacement("mid_0_0", [1.0, 1.0, 1.0, 1.0], [0, 0, 0, 0])
|
||||
validate_displacement("mid_0_5", [1.0, 1.0, 1.0, 1.0], [-0.5, -0.5, -0.5, 0])
|
||||
validate_displacement("mid_1_0", [1.0, 1.0, 1.0, 1.0], [-1, -1, -1, 0])
|
||||
validate_displacement("mid_0_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [0, 0, 0, 0])
|
||||
validate_displacement("mid_0_5_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.15, -0.15, -0.15, 0])
|
||||
validate_displacement("mid_1_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.3, -0.3, -0.3, 0])
|
||||
|
||||
# Validate that no displacement occurs for scenarios USD doesn't support
|
||||
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_wrong_space/Principled_BSDF"))
|
||||
input_displacement = shader_surface.GetInput('displacement')
|
||||
self.assertTrue(input_displacement.Get() is None)
|
||||
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_non_const/Principled_BSDF"))
|
||||
input_displacement = shader_surface.GetInput('displacement')
|
||||
self.assertTrue(input_displacement.Get() is None)
|
||||
|
||||
def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len):
|
||||
pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)
|
||||
self.assertTrue(pv.HasValue())
|
||||
|
||||
@@ -295,6 +295,54 @@ class USDImportTest(AbstractUSDTest):
|
||||
face_indices = [i for i, d in enumerate(material_index_attr.data) if d.value == mat_index]
|
||||
self.assertEqual(len(face_indices), 4, f"Incorrect number of faces with material index {mat_index}")
|
||||
|
||||
def test_import_material_displacement(self):
|
||||
"""Validate correct import of Displacement information for the UsdPreviewSurface"""
|
||||
|
||||
# Use the existing materials test file to create the USD file
|
||||
# for import. It is validated as part of the bl_usd_export test.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_displace.blend"))
|
||||
testfile = str(self.tempdir / "temp_material_displace.usda")
|
||||
res = bpy.ops.wm.usd_export(filepath=str(testfile), export_materials=True)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}")
|
||||
|
||||
# Reload the empty file and import back in
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
||||
res = bpy.ops.wm.usd_import(filepath=testfile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
|
||||
|
||||
# Most shader graph validation should occur through the Hydra render test suite. Here we
|
||||
# will only check some high-level criteria for each expected node graph.
|
||||
|
||||
def assert_displacement(mat, height, midlevel, scale):
|
||||
nodes = mat.node_tree.nodes
|
||||
node_displace_index = nodes.find("Displacement")
|
||||
self.assertTrue(node_displace_index >= 0)
|
||||
|
||||
node_displace = nodes[node_displace_index]
|
||||
if height is not None:
|
||||
self.assertAlmostEqual(node_displace.inputs[0].default_value, height)
|
||||
else:
|
||||
self.assertEqual(len(node_displace.inputs[0].links), 1)
|
||||
self.assertAlmostEqual(node_displace.inputs[1].default_value, midlevel)
|
||||
self.assertAlmostEqual(node_displace.inputs[2].default_value, scale)
|
||||
|
||||
mat = bpy.data.materials["constant"]
|
||||
assert_displacement(mat, 0.95, 0.5, 1.0)
|
||||
|
||||
mat = bpy.data.materials["mid_1_0"]
|
||||
assert_displacement(mat, None, 1.0, 1.0)
|
||||
mat = bpy.data.materials["mid_0_5"]
|
||||
assert_displacement(mat, None, 0.5, 1.0)
|
||||
mat = bpy.data.materials["mid_0_0"]
|
||||
assert_displacement(mat, None, 0.0, 1.0)
|
||||
|
||||
mat = bpy.data.materials["mid_1_0_scale_0_3"]
|
||||
assert_displacement(mat, None, 1.0, 0.3)
|
||||
mat = bpy.data.materials["mid_0_5_scale_0_3"]
|
||||
assert_displacement(mat, None, 0.5, 0.3)
|
||||
mat = bpy.data.materials["mid_0_0_scale_0_3"]
|
||||
assert_displacement(mat, None, 0.0, 0.3)
|
||||
|
||||
def test_import_shader_varname_with_connection(self):
|
||||
"""Test importing USD shader where uv primvar is a connection"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user