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:
Jesse Yurkovich
2024-11-05 20:37:36 +01:00
committed by Jesse Yurkovich
parent 0598db079d
commit b4c2feea38
8 changed files with 360 additions and 67 deletions

View File

@@ -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())

View File

@@ -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"""