USD: Use nodes for alpha-clip behavior instead of material properties

EEVEE-next has removed the MA_BM_CLIP / alpha_threshold material
properties in favor of using nodes for equivalent functionality. This
changes USD to build and traverse node graphs during import and export
accordingly. Indirectly this allows Cycles to correctly render such
materials now too.

A complicating factor is that the UsdPreviewSurface defines its opacity
threshold using greater-than-equals[1], which Blender does not support
(and for which was technically already incorrect as EEVEE-legacy only
used greater-than for its shaders). Due to this we actually need to use
2 nodes: A less-than, followed by a one-minus invert, to arrive at the
proper value. We'll translate UsdPreviewSurface to this form on Import.

For Export we will look for either this 2-node pattern or a Round
node plugged into Alpha. Looking for Round is a result of the glTF
documentation which recommended the use of this node for thresholds of
0.5[2]. It's a tiny addition that seems reasonable to accommodate.

[1] https://openusd.org/release/spec_usdpreviewsurface.html (search for "opacityThreshold")
[2] https://docs.blender.org/manual/en/4.2/addons/import_export/scene_gltf2.html#alpha-modes

See PR for example images

Pull Request: https://projects.blender.org/blender/blender/pulls/122025
This commit is contained in:
Jesse Yurkovich
2024-05-25 23:30:13 +02:00
committed by Jesse Yurkovich
parent 21db0daa4e
commit 9daded5d87
4 changed files with 138 additions and 80 deletions

View File

@@ -546,17 +546,9 @@ void USDMaterialReader::import_usd_preview(Material *mtl,
BKE_ntree_update_main_tree(bmain_, ntree, nullptr);
/* Optionally, set the material blend mode. */
if (params_.set_material_blend) {
if (needs_blend(usd_shader)) {
float opacity_threshold = get_opacity_threshold(usd_shader, 0.0f);
if (opacity_threshold > 0.0f) {
mtl->blend_method = MA_BM_CLIP;
mtl->alpha_threshold = opacity_threshold;
}
else {
mtl->blend_method = MA_BM_BLEND;
}
mtl->surface_render_method = MA_SURFACE_METHOD_FORWARD;
}
}
}
@@ -576,13 +568,17 @@ void USDMaterialReader::set_principled_node_inputs(bNode *principled,
/* Recursively set the principled shader inputs. */
if (pxr::UsdShadeInput diffuse_input = usd_shader.GetInput(usdtokens::diffuseColor)) {
set_node_input(diffuse_input, principled, "Base Color", ntree, column, &context, true);
ExtraLinkInfo extra;
extra.is_color_corrected = true;
set_node_input(diffuse_input, principled, "Base Color", ntree, column, &context, extra);
}
float emission_strength = 0.0f;
if (pxr::UsdShadeInput emissive_input = usd_shader.GetInput(usdtokens::emissiveColor)) {
ExtraLinkInfo extra;
extra.is_color_corrected = true;
if (set_node_input(
emissive_input, principled, "Emission Color", ntree, column, &context, true))
emissive_input, principled, "Emission Color", ntree, column, &context, extra))
{
emission_strength = 1.0f;
}
@@ -593,37 +589,38 @@ void USDMaterialReader::set_principled_node_inputs(bNode *principled,
((bNodeSocketValueFloat *)emission_strength_sock->default_value)->value = emission_strength;
if (pxr::UsdShadeInput specular_input = usd_shader.GetInput(usdtokens::specularColor)) {
set_node_input(specular_input, principled, "Specular Tint", ntree, column, &context, false);
set_node_input(specular_input, principled, "Specular Tint", ntree, column, &context);
}
if (pxr::UsdShadeInput metallic_input = usd_shader.GetInput(usdtokens::metallic)) {
set_node_input(metallic_input, principled, "Metallic", ntree, column, &context, false);
set_node_input(metallic_input, principled, "Metallic", ntree, column, &context);
}
if (pxr::UsdShadeInput roughness_input = usd_shader.GetInput(usdtokens::roughness)) {
set_node_input(roughness_input, principled, "Roughness", ntree, column, &context, false);
set_node_input(roughness_input, principled, "Roughness", ntree, column, &context);
}
if (pxr::UsdShadeInput coat_input = usd_shader.GetInput(usdtokens::clearcoat)) {
set_node_input(coat_input, principled, "Coat Weight", ntree, column, &context, false);
set_node_input(coat_input, principled, "Coat Weight", ntree, column, &context);
}
if (pxr::UsdShadeInput coat_roughness_input = usd_shader.GetInput(usdtokens::clearcoatRoughness))
{
set_node_input(
coat_roughness_input, principled, "Coat Roughness", ntree, column, &context, false);
set_node_input(coat_roughness_input, principled, "Coat Roughness", ntree, column, &context);
}
if (pxr::UsdShadeInput opacity_input = usd_shader.GetInput(usdtokens::opacity)) {
set_node_input(opacity_input, principled, "Alpha", ntree, column, &context, false);
ExtraLinkInfo extra;
extra.opacity_threshold = get_opacity_threshold(usd_shader, 0.0f);
set_node_input(opacity_input, principled, "Alpha", ntree, column, &context, extra);
}
if (pxr::UsdShadeInput ior_input = usd_shader.GetInput(usdtokens::ior)) {
set_node_input(ior_input, principled, "IOR", ntree, column, &context, false);
set_node_input(ior_input, principled, "IOR", ntree, column, &context);
}
if (pxr::UsdShadeInput normal_input = usd_shader.GetInput(usdtokens::normal)) {
set_node_input(normal_input, principled, "Normal", ntree, column, &context, false);
set_node_input(normal_input, principled, "Normal", ntree, column, &context);
}
}
@@ -633,7 +630,7 @@ bool USDMaterialReader::set_node_input(const pxr::UsdShadeInput &usd_input,
bNodeTree *ntree,
const int column,
NodePlacementContext *r_ctx,
bool is_color_corrected) const
const ExtraLinkInfo &extra) const
{
if (!(usd_input && dest_node && r_ctx)) {
return false;
@@ -642,8 +639,7 @@ bool USDMaterialReader::set_node_input(const pxr::UsdShadeInput &usd_input,
if (usd_input.HasConnectedSource()) {
/* The USD shader input has a connected source shader. Follow the connection
* and attempt to convert the connected USD shader to a Blender node. */
return follow_connection(
usd_input, dest_node, dest_socket_name, ntree, column, r_ctx, is_color_corrected);
return follow_connection(usd_input, dest_node, dest_socket_name, ntree, column, r_ctx, extra);
}
else {
/* Set the destination node socket value from the USD shader input value. */
@@ -853,13 +849,53 @@ static IntermediateNode add_separate_color(const pxr::UsdShadeShader &usd_shader
return separate_color;
}
static IntermediateNode add_lessthan(bNodeTree *ntree,
float threshold,
int column,
NodePlacementContext *r_ctx)
{
float locx = 0.0f;
float locy = 0.0f;
compute_node_loc(column, &locx, &locy, r_ctx);
IntermediateNode lessthan{};
lessthan.node = add_node(nullptr, ntree, SH_NODE_MATH, locx, locy);
lessthan.node->custom1 = NODE_MATH_LESS_THAN;
lessthan.sock_input_name = "Value";
lessthan.sock_output_name = "Value";
bNodeSocket *thresh_sock = blender::bke::nodeFindSocket(lessthan.node, SOCK_IN, "Value_001");
((bNodeSocketValueFloat *)thresh_sock->default_value)->value = threshold;
return lessthan;
}
static IntermediateNode add_oneminus(bNodeTree *ntree, int column, NodePlacementContext *r_ctx)
{
float locx = 0.0f;
float locy = 0.0f;
compute_node_loc(column, &locx, &locy, r_ctx);
/* An "invert" node : 1.0f - Value_001 */
IntermediateNode oneminus{};
oneminus.node = add_node(nullptr, ntree, SH_NODE_MATH, locx, locy);
oneminus.node->custom1 = NODE_MATH_SUBTRACT;
oneminus.sock_input_name = "Value_001";
oneminus.sock_output_name = "Value";
bNodeSocket *val_sock = blender::bke::nodeFindSocket(oneminus.node, SOCK_IN, "Value");
((bNodeSocketValueFloat *)val_sock->default_value)->value = 1.0f;
return oneminus;
}
bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
bNode *dest_node,
const char *dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext *r_ctx,
bool is_color_corrected) const
const ExtraLinkInfo &extra) const
{
if (!(usd_input && dest_node && dest_socket_name && ntree && r_ctx)) {
return false;
@@ -974,6 +1010,19 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
target_sock_name = separate_color.sock_input_name;
}
/* Handle opacity threshold if necessary. */
if (source_name == usdtokens::a && extra.opacity_threshold > 0.0f) {
/* USD defines the threshold as >= but Blender does not have that operation. Use < instead
* and then invert it. */
IntermediateNode lessthan = add_lessthan(ntree, extra.opacity_threshold, column + 1, r_ctx);
IntermediateNode invert = add_oneminus(ntree, column + 1, r_ctx);
link_nodes(
ntree, lessthan.node, lessthan.sock_output_name, invert.node, invert.sock_input_name);
link_nodes(ntree, invert.node, invert.sock_output_name, dest_node, dest_socket_name);
target_node = lessthan.node;
target_sock_name = lessthan.sock_input_name;
}
convert_usd_uv_texture(source_shader,
source_name,
target_node,
@@ -981,7 +1030,7 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
ntree,
column + shift,
r_ctx,
is_color_corrected);
extra);
}
else if (shader_id == usdtokens::UsdPrimvarReader_float2) {
convert_usd_primvar_reader_float2(
@@ -1001,7 +1050,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh
bNodeTree *ntree,
const int column,
NodePlacementContext *r_ctx,
bool is_color_corrected) const
const ExtraLinkInfo &extra) const
{
if (!usd_shader || !dest_node || !ntree || !dest_socket_name || !bmain_ || !r_ctx) {
return;
@@ -1026,7 +1075,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh
cache_node(r_ctx->node_cache, usd_shader, tex_image);
/* Load the texture image. */
load_tex_image(usd_shader, tex_image, is_color_corrected);
load_tex_image(usd_shader, tex_image, extra);
}
/* Connect to destination node input. */
@@ -1038,7 +1087,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh
/* Connect the texture image node "Vector" input. */
if (pxr::UsdShadeInput st_input = usd_shader.GetInput(usdtokens::st)) {
set_node_input(st_input, tex_image, "Vector", ntree, column, r_ctx, false);
set_node_input(st_input, tex_image, "Vector", ntree, column, r_ctx);
}
}
@@ -1116,13 +1165,13 @@ void USDMaterialReader::convert_usd_transform_2d(const pxr::UsdShadeShader &usd_
/* Connect the mapping node "Vector" input. */
if (pxr::UsdShadeInput in_input = usd_shader.GetInput(usdtokens::in)) {
set_node_input(in_input, mapping, "Vector", ntree, column, r_ctx, false);
set_node_input(in_input, mapping, "Vector", ntree, column, r_ctx);
}
}
void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
bNode *tex_image,
bool is_color_corrected) const
const ExtraLinkInfo &extra) const
{
if (!(usd_shader && tex_image && tex_image->type == SH_NODE_TEX_IMAGE)) {
return;
@@ -1227,7 +1276,7 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader,
if (color_space == usdtokens::auto_) {
/* If it's auto, determine whether to apply color correction based
* on incoming connection (passed in from outer functions). */
STRNCPY(image->colorspace_settings.name, is_color_corrected ? "sRGB" : "Non-Color");
STRNCPY(image->colorspace_settings.name, extra.is_color_corrected ? "sRGB" : "Non-Color");
}
else if (color_space == usdtokens::sRGB) {

View File

@@ -53,6 +53,15 @@ struct NodePlacementContext {
}
};
/* Helper struct which carries an assortment of optional
* information that is sometimes required when linking
* nodes together. */
struct ExtraLinkInfo {
bool is_color_corrected = false;
float opacity_threshold = 0.0f;
};
/* Converts USD materials to Blender representation. */
/**
@@ -110,7 +119,7 @@ class USDMaterialReader {
bNodeTree *ntree,
int column,
NodePlacementContext *r_ctx,
bool is_color_corrected) const;
const ExtraLinkInfo &extra = {}) const;
/**
* Follow the connected source of the USD input to create corresponding inputs
@@ -122,7 +131,7 @@ class USDMaterialReader {
bNodeTree *ntree,
int column,
NodePlacementContext *r_ctx,
bool is_color_corrected = false) const;
const ExtraLinkInfo &extra = {}) const;
void convert_usd_uv_texture(const pxr::UsdShadeShader &usd_shader,
const pxr::TfToken &usd_source_name,
@@ -131,7 +140,7 @@ class USDMaterialReader {
bNodeTree *ntree,
int column,
NodePlacementContext *r_ctx,
bool is_color_corrected = false) const;
const ExtraLinkInfo &extra = {}) const;
void convert_usd_transform_2d(const pxr::UsdShadeShader &usd_shader,
bNode *dest_node,
@@ -146,7 +155,7 @@ class USDMaterialReader {
*/
void load_tex_image(const pxr::UsdShadeShader &usd_shader,
bNode *tex_image,
bool is_color_corrected = false) const;
const ExtraLinkInfo &extra = {}) const;
/**
* This function creates a Blender UV Map node, under the simplifying assumption that

View File

@@ -298,12 +298,42 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex
}
/* Set opacityThreshold if an alpha cutout is used. */
if ((input_spec.input_name == usdtokens::opacity) &&
(material->blend_method == MA_BM_CLIP) && (material->alpha_threshold > 0.0))
{
pxr::UsdShadeInput opacity_threshold_input = preview_surface.CreateInput(
usdtokens::opacityThreshold, pxr::SdfValueTypeNames->Float);
opacity_threshold_input.GetAttr().Set(pxr::VtValue(material->alpha_threshold));
if (input_spec.input_name == usdtokens::opacity) {
float threshold = 0.0f;
/* The immediate upstream node should either be a Math Round or a Math 1-minus. */
bNodeLink *math_link = traverse_channel(sock, SH_NODE_MATH);
if (math_link && math_link->fromnode) {
bNode *math_node = math_link->fromnode;
if (math_node->custom1 == NODE_MATH_ROUND) {
threshold = 0.5f;
}
else if (math_node->custom1 == NODE_MATH_SUBTRACT) {
/* If this is the 1-minus node, we need to search upstream to find the less-than. */
bNodeSocket *sock = blender::bke::nodeFindSocket(math_node, SOCK_IN, "Value");
if (((bNodeSocketValueFloat *)sock->default_value)->value == 1.0f) {
sock = blender::bke::nodeFindSocket(math_node, SOCK_IN, "Value_001");
math_link = traverse_channel(sock, SH_NODE_MATH);
if (math_link && math_link->fromnode) {
math_node = math_link->fromnode;
if (math_node->custom1 == NODE_MATH_LESS_THAN) {
/* We found the upstream less-than with the threshold value. */
bNodeSocket *threshold_sock = blender::bke::nodeFindSocket(
math_node, SOCK_IN, "Value_001");
threshold = ((bNodeSocketValueFloat *)threshold_sock->default_value)->value;
}
}
}
}
}
if (threshold > 0.0f) {
pxr::UsdShadeInput opacity_threshold_input = preview_surface.CreateInput(
usdtokens::opacityThreshold, pxr::SdfValueTypeNames->Float);
opacity_threshold_input.GetAttr().Set(pxr::VtValue(threshold));
}
}
}
else if (input_spec.set_default_value) {

View File

@@ -148,53 +148,23 @@ class USDExportTest(AbstractUSDTest):
opacity_input = shader.GetInput('opacity')
self.assertEqual(opacity_input.HasConnectedSource(), False,
"Opacity input should not be connected for opaque material")
self.assertAlmostEqual(opacity_input.Get(), 1.0, "Opacity input should be set to 1")
self.assertAlmostEqual(opacity_input.Get(), 1.0, 2, "Opacity input should be set to 1")
# The material already has a texture input to the Base Color.
# Now also link this texture to the Alpha input.
# Set an opacity threshold appropriate for alpha clipping.
mat = bpy.data.materials['Material']
bsdf = mat.node_tree.nodes['Principled BSDF']
tex_output = bsdf.inputs['Base Color'].links[0].from_node.outputs['Color']
alpha_input = bsdf.inputs['Alpha']
mat.node_tree.links.new(tex_output, alpha_input)
bpy.data.materials['Material'].blend_method = 'CLIP'
bpy.data.materials['Material'].alpha_threshold = 0.01
export_path = self.tempdir / "alphaclip_material.usda"
res = bpy.ops.wm.usd_export(
filepath=str(export_path),
export_materials=True,
evaluation_mode="RENDER",
)
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
# Inspect and validate the exported USD for the alpha clip case.
stage = Usd.Stage.Open(str(export_path))
shader_prim = stage.GetPrimAtPath("/root/_materials/Material/Principled_BSDF")
# Inspect and validate the exported USD for the alpha clip w/Round node case.
shader_prim = stage.GetPrimAtPath("/root/_materials/Clip_With_Round/Principled_BSDF")
shader = UsdShade.Shader(shader_prim)
opacity_input = shader.GetInput('opacity')
opacity_thres_input = shader.GetInput('opacityThreshold')
opacity_thresh_input = shader.GetInput('opacityThreshold')
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
self.assertGreater(opacity_thres_input.Get(), 0.0, "Opacity threshold input should be > 0")
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5")
# Modify material again, this time with alpha blend.
bpy.data.materials['Material'].blend_method = 'BLEND'
export_path = self.tempdir / "alphablend_material.usda"
res = bpy.ops.wm.usd_export(
filepath=str(export_path),
export_materials=True,
evaluation_mode="RENDER",
)
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
# Inspect and validate the exported USD for the alpha blend case.
stage = Usd.Stage.Open(str(export_path))
shader_prim = stage.GetPrimAtPath("/root/_materials/Material/Principled_BSDF")
# Inspect and validate the exported USD for the alpha clip w/LessThan+Invert node case.
shader_prim = stage.GetPrimAtPath("/root/_materials/Clip_With_LessThanInvert/Principled_BSDF")
shader = UsdShade.Shader(shader_prim)
opacity_input = shader.GetInput('opacity')
opacity_thres_input = shader.GetInput('opacityThreshold')
opacity_thresh_input = shader.GetInput('opacityThreshold')
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
self.assertEqual(opacity_thres_input.Get(), None, "Opacity threshold should not be specified for alpha blend")
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.2, 2, "Opacity threshold input should be 0.2")
def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len):
pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)