Fix #107062: support opacityThreshold when exporting USD

This PR addresses issue “USD export does not respect opacity threshold for clip alpha blend mode #107062”

This commit extends the USD Preview Surface material support to author the opacityThreshold attribute of materials on export, when the Alpha Clip blend mode is selected.

When authoring alpha cutouts in Blender, one sets the Blend Mode to "Alpha Clip", and the Clip Threshold to some value greater than zero.
When this case is detected on export, we now author the opacityThreshold attribute to match the specified clip threshold.

Note that opacityThreshold is already handled correctly on import, so this change allows the feature to be fully round-tripped.

Co-authored-by: Matt McLin <mmclin@apple.com>
Pull Request: https://projects.blender.org/blender/blender/pulls/107149
This commit is contained in:
Michael B Johnson 2023-05-01 17:28:23 +02:00 committed by Michael Kowalski
parent f59fdc40ec
commit 3c74575dac
2 changed files with 84 additions and 0 deletions

View File

@ -47,6 +47,7 @@ static const pxr::TfToken primvar_float2("UsdPrimvarReader_float2", pxr::TfToken
static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal);
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 perspective("perspective", pxr::TfToken::Immortal);
static const pxr::TfToken orthographic("orthographic", pxr::TfToken::Immortal);
@ -144,6 +145,8 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co
const InputSpecMap &input_map = preview_surface_input_map();
bool has_opacity = false;
/* Set the preview surface inputs. */
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
@ -167,6 +170,10 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co
preview_surface.CreateInput(input_spec.input_name, input_spec.input_type)
.ConnectToSource(created_shader.ConnectableAPI(), input_spec.source_name);
set_normal_texture_range(created_shader, input_spec);
if (input_spec.input_name == usdtokens::opacity) {
has_opacity = true;
}
}
else if (input_spec.set_default_value) {
/* Set hardcoded value. */
@ -203,6 +210,14 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co
create_uvmap_shader(
usd_export_context, input_node, usd_material, created_shader, default_uv_sampler);
}
/* Set opacityThreshold if an alpha cutout is used. */
if (has_opacity) {
if ((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));
}
}
}
void set_normal_texture_range(pxr::UsdShadeShader &usd_shader, const InputSpec &input_spec)

View File

@ -8,6 +8,7 @@ import unittest
from pxr import Usd
from pxr import UsdUtils
from pxr import UsdGeom
from pxr import UsdShade
from pxr import Gf
import bpy
@ -124,6 +125,74 @@ class USDExportTest(AbstractUSDTest):
Gf.Vec3d(extent[1]), Gf.Vec3d(0.7515701, 0.5500924, 0.9027928)
)
def test_opacity_threshold(self):
# Note that the scene file used here is shared with a different test.
# Here we assume that it has a Principled BSDF material with
# a texture connected to its Base Color input.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
export_path = self.tempdir / "opaque_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 opaque blend case.
stage = Usd.Stage.Open(str(export_path))
shader_prim = stage.GetPrimAtPath("/_materials/Material/Principled_BSDF")
shader = UsdShade.Shader(shader_prim)
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")
# 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("/_materials/Material/Principled_BSDF")
shader = UsdShade.Shader(shader_prim)
opacity_input = shader.GetInput('opacity')
opacity_thres_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")
# 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("/_materials/Material/Principled_BSDF")
shader = UsdShade.Shader(shader_prim)
opacity_input = shader.GetInput('opacity')
opacity_thres_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")
def main():
global args