Fix: Improper duplicate material handling in USD import

Part 1/2 for #148569

The map used to de-duplicate incoming materials was filled with the
wrong material names which eventually resulted in a situation where we
were adding 2 different materials with the same name to the map which is
unexpected.

This also contains a fix for Material ID user counts that was
incorrectly changed as part of the unrelated refactor 1b4daf9d2e.

Added further test coverage to validate both situations now.

Pull Request: https://projects.blender.org/blender/blender/pulls/148757
This commit is contained in:
Jesse Yurkovich 2025-10-24 23:41:01 +02:00 • committed by Jesse Yurkovich
parent 0a916b792d
commit fab9993ca8
6 changed files with 442 additions and 7 deletions

View file

@ -458,7 +458,7 @@ Material *USDMaterialReader::add_material(const pxr::UsdShadeMaterial &usd_mater
Material *mtl = BKE_material_add(&bmain_, mtl_name.c_str());
mtl->nodetree = blender::bke::node_tree_add_tree_embedded(
&bmain_, &mtl->id, "USD Material Node Tree", "ShaderNodeTree");
// id_us_min(&mtl->id);
id_us_min(&mtl->id);
if (read_usd_preview) {
import_usd_preview(mtl, usd_material);
@ -1550,8 +1550,7 @@ void build_material_map(const Main *bmain, blender::Map<std::string, Material *>
BLI_assert_msg(r_mat_map.is_empty(), "The incoming material map should be empty");
LISTBASE_FOREACH (Material *, material, &bmain->materials) {
std::string usd_name = make_safe_name(material->id.name + 2, true);
r_mat_map.add_new(usd_name, material);
r_mat_map.add_new(material->id.name + 2, material);
}
}

View file

@ -143,8 +143,7 @@ static void assign_materials(Main *bmain,
continue;
}
const std::string mat_name = make_safe_name(assigned_mat->id.name + 2, true);
settings.mat_name_to_mat.add_new(mat_name, assigned_mat);
settings.mat_name_to_mat.add_new(assigned_mat->id.name + 2, assigned_mat);
if (params.mtl_name_collision_mode == USD_MTL_NAME_COLLISION_MAKE_UNIQUE) {
/* Record the Blender material we created for the USD material with the given path. */

View file

@ -645,8 +645,7 @@ void USDStageReader::import_all_materials(Main *bmain)
Material *new_mtl = mtl_reader.add_material(usd_mtl, !have_import_hook);
BLI_assert_msg(new_mtl, "Failed to create material");
const std::string mtl_name = make_safe_name(new_mtl->id.name + 2, true);
settings_.mat_name_to_mat.add_new(mtl_name, new_mtl);
settings_.mat_name_to_mat.add_new(new_mtl->id.name + 2, new_mtl);
if (params_.mtl_name_collision_mode == USD_MTL_NAME_COLLISION_MAKE_UNIQUE) {
/* Record the Blender material we created for the USD material with the given path.

View file

@ -0,0 +1,215 @@
#usda 1.0
(
defaultPrim = "root"
doc = "HAND CRAFTED!"
metersPerUnit = 1
upAxis = "Z"
)
def Xform "root"
{
def Scope "_materials"
{
def Material "Material"
{
token outputs:surface.connect = </root/_materials/Material/Principled_BSDF.outputs:surface>
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor = (1, 0, 0)
float inputs:ior = 1.45
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
}
def Material "Material_001"
{
token outputs:surface.connect = </root/_materials/Material_001/Principled_BSDF.outputs:surface>
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor = (0, 0, 1)
float inputs:ior = 1.5
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
}
}
def Xform "o1"
{
def Mesh "o1" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o2"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (1, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o2" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material_001>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o3"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 1, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o3" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o4"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (1, 1, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o4" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material_001>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o5"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 2, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o5" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o6"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (1, 2, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o6" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials/Material_001>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
}

View file

@ -0,0 +1,154 @@
#usda 1.0
(
defaultPrim = "root"
doc = "HAND CRAFTED!"
metersPerUnit = 1
upAxis = "Z"
)
def Xform "root"
{
def Scope "_materials1"
{
def Material "MaterialA"
{
token outputs:surface.connect = </root/_materials1/MaterialA/Principled_BSDF.outputs:surface>
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor = (1, 0, 0)
float inputs:ior = 1.45
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
}
}
def Scope "_materials2"
{
def Material "MaterialA"
{
token outputs:surface.connect = </root/_materials2/MaterialA/Principled_BSDF.outputs:surface>
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor = (0, 1, 0)
float inputs:ior = 1.45
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
}
}
def Scope "_materials3"
{
def Material "MaterialA"
{
token outputs:surface.connect = </root/_materials3/MaterialA/Principled_BSDF.outputs:surface>
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor = (0, 0, 1)
float inputs:ior = 1.45
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
}
}
def Xform "o1"
{
def Mesh "o1" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials1/MaterialA>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o2"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (1, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o2" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials2/MaterialA>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
def Xform "o3"
{
float3 xformOp:rotateXYZ = (0, -0, 0)
float3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 1, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "o3" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-0.5, -0.5, 0), (0.5, 0.5, 0)]
int[] faceVertexCounts = [4]
int[] faceVertexIndices = [0, 1, 3, 2]
rel material:binding = </root/_materials3/MaterialA>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] (
interpolation = "faceVarying"
)
point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
}
}
}

View file

@ -594,6 +594,75 @@ class USDImportTest(AbstractUSDTest):
mat = bpy.data.materials["Material"]
self.assert_all_nodes_present(mat, ["Principled BSDF", "Image Texture", "UV Map", "Material Output"])
def check_mat_data(self, mat_name, expected_users, expected_color, ob_names):
if expected_users < 0:
self.assertEqual(bpy.data.materials.find(mat_name), -1)
return
mat = bpy.data.materials[mat_name]
self.assertEqual(mat.users, expected_users)
self.assertEqual(mat.diffuse_color[0:3], expected_color)
for ob_name in ob_names:
mat_slot = bpy.data.objects[ob_name].material_slots[0]
self.assertEqual(mat_slot.name, mat_name, f"Object {ob_name} has incorrect material")
def test_import_material_collisions(self):
"""Validate that material name collisions are properly handled"""
# Variation with multiple objects referencing the same common materials
testfile = str(self.testdir / "usd_materials_collision.usda")
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
bpy.data.materials.new(name="Material").diffuse_color = (1, 0, 1, 1)
res = bpy.ops.wm.usd_import(filepath=testfile, mtl_name_collision_mode='MAKE_UNIQUE')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
self.assertEqual(len(bpy.data.materials), 3)
self.check_mat_data("Material", 0, (1, 0, 1), [])
self.check_mat_data("Material.001", 3, (1, 0, 0), ["o1", "o3", "o5"])
self.check_mat_data("Material_001", 3, (0, 0, 1), ["o2", "o4", "o6"])
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
bpy.data.materials.new(name="Material").diffuse_color = (1, 0, 1, 1)
res = bpy.ops.wm.usd_import(filepath=testfile, mtl_name_collision_mode='REFERENCE_EXISTING')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
self.assertEqual(len(bpy.data.materials), 2)
self.check_mat_data("Material", 3, (1, 0, 1), ["o1", "o3", "o5"])
self.check_mat_data("Material.001", -1, (), [])
self.check_mat_data("Material_001", 3, (0, 0, 1), ["o2", "o4", "o6"])
def test_import_material_collisions2(self):
"""Validate that material name collisions are properly handled"""
# Variation with multiple materials with the same name but under different paths
testfile = str(self.testdir / "usd_materials_collision2.usda")
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=testfile, mtl_name_collision_mode='MAKE_UNIQUE')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
# Due to out of order reading, we know that there should be 3 materials, but we don't know
# which material(name) ended up on each object. The viewport color of the material should
# match what we expect though
self.assertEqual(len(bpy.data.materials), 3)
o1_mat_name = bpy.data.objects["o1"].material_slots[0].name
o2_mat_name = bpy.data.objects["o2"].material_slots[0].name
o3_mat_name = bpy.data.objects["o3"].material_slots[0].name
self.check_mat_data(o1_mat_name, 1, (1, 0, 0), ["o1"])
self.check_mat_data(o2_mat_name, 1, (0, 1, 0), ["o2"])
self.check_mat_data(o3_mat_name, 1, (0, 0, 1), ["o3"])
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=testfile, mtl_name_collision_mode='REFERENCE_EXISTING')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
# Due to out of order reading, we know that there should be 1 material, but we don't know
# which color ended up "winning" during the Import process
self.assertEqual(len(bpy.data.materials), 1)
expected_color = bpy.data.materials["MaterialA"].diffuse_color[0:3]
self.check_mat_data("MaterialA", 3, expected_color, ["o1", "o2", "o3"])
def test_import_shader_varname_with_connection(self):
"""Test importing USD shader where uv primvar is a connection"""