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:
committed by
Jesse Yurkovich
parent
21db0daa4e
commit
9daded5d87
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user