diff --git a/source/blender/io/usd/intern/usd_reader_material.cc b/source/blender/io/usd/intern/usd_reader_material.cc index d6c7545564e..d98c2626896 100644 --- a/source/blender/io/usd/intern/usd_reader_material.cc +++ b/source/blender/io/usd/intern/usd_reader_material.cc @@ -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()) { + scale = pxr::VtValue::Cast(val).UncheckedGet(); + } + if (bias_input.Get(&val) && val.CanCast()) { + bias = pxr::VtValue::Cast(val).UncheckedGet(); + } + + 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. */ diff --git a/source/blender/io/usd/intern/usd_reader_material.hh b/source/blender/io/usd/intern/usd_reader_material.hh index e8572e2aeff..7133cc5350d 100644 --- a/source/blender/io/usd/intern/usd_reader_material.hh +++ b/source/blender/io/usd/intern/usd_reader_material.hh @@ -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, diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index 00795e283f0..0e665d119fd 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -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(sock_scale->default_value)->value; + const float *bias_value = + static_cast(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(sock_scale->default_value)->value; - const float *bias_value = - static_cast(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()->value; + const float scale_value = sock_scale->default_value_typed()->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( - preview_surface, input_spec, sock->default_value, input_scale); + shader, input_spec, sock->default_value, input_scale); } break; case SOCK_VECTOR: { create_input( - preview_surface, input_spec, sock->default_value, input_scale); + shader, input_spec, sock->default_value, input_scale); } break; case SOCK_RGBA: { create_input( - 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()->value; + const float scale_value = sock_scale->default_value_typed()->value; + const float height_value = sock_height->default_value_typed()->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, diff --git a/source/blender/nodes/shader/nodes/node_shader_displacement.cc b/source/blender/nodes/shader/nodes/node_shader_displacement.cc index 038870c47d1..14024eabd0a 100644 --- a/source/blender/nodes/shader/nodes/node_shader_displacement.cc +++ b/source/blender/nodes/shader/nodes/node_shader_displacement.cc @@ -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; diff --git a/source/blender/nodes/shader/nodes/node_shader_output_material.cc b/source/blender/nodes/shader/nodes/node_shader_output_material.cc index d46ef1f2e05..1a349261ce0 100644 --- a/source/blender/nodes/shader/nodes/node_shader_output_material.cc +++ b/source/blender/nodes/shader/nodes/node_shader_output_material.cc @@ -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 diff --git a/source/blender/nodes/shader/nodes/node_shader_vector_displacement.cc b/source/blender/nodes/shader/nodes/node_shader_vector_displacement.cc index 9d5700a01fc..8968c185c14 100644 --- a/source/blender/nodes/shader/nodes/node_shader_vector_displacement.cc +++ b/source/blender/nodes/shader/nodes/node_shader_vector_displacement.cc @@ -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", diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 25d280d02f2..1a67a4b7c6f 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -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()) diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 8cfb673c7a2..491b54c5c95 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -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"""