diff --git a/tests/data/assets/box_specular_workflow.mtl b/tests/data/assets/box_specular_workflow.mtl
new file mode 100644
index 0000000..ddc9bab
--- /dev/null
+++ b/tests/data/assets/box_specular_workflow.mtl
@@ -0,0 +1,9 @@
+newmtl specular_workflow_mat
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Ks 0.500000 0.200000 0.100000
+Ke 0.000000 0.000000 0.000000
+Ni 1.450000
+d 1.000000
+illum 2
+map_Kd ./grid.png
diff --git a/tests/data/assets/box_specular_workflow.obj b/tests/data/assets/box_specular_workflow.obj
new file mode 100644
index 0000000..5cbb25b
--- /dev/null
+++ b/tests/data/assets/box_specular_workflow.obj
@@ -0,0 +1,38 @@
+mtllib box_specular_workflow.mtl
+o Cube
+v 0.500000 0.500000 -0.500000
+v 0.500000 -0.500000 -0.500000
+v 0.500000 0.500000 0.500000
+v 0.500000 -0.500000 0.500000
+v -0.500000 0.500000 -0.500000
+v -0.500000 -0.500000 -0.500000
+v -0.500000 0.500000 0.500000
+v -0.500000 -0.500000 0.500000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vt 0.625000 0.500000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.125000 0.500000
+vt 0.375000 0.500000
+vt 0.125000 0.750000
+s 0
+usemtl specular_workflow_mat
+f 1/1/1 5/2/1 7/3/1 3/4/1
+f 4/5/2 3/4/2 7/6/2 8/7/2
+f 8/8/3 7/9/3 5/10/3 6/11/3
+f 6/12/4 2/13/4 4/5/4 8/14/4
+f 2/13/5 1/1/5 3/4/5 4/5/5
+f 6/11/6 5/10/6 1/1/6 2/13/6
diff --git a/tests/data/assets/box_specular_workflow_with_texture.mtl b/tests/data/assets/box_specular_workflow_with_texture.mtl
new file mode 100644
index 0000000..cefe818
--- /dev/null
+++ b/tests/data/assets/box_specular_workflow_with_texture.mtl
@@ -0,0 +1,10 @@
+newmtl specular_workflow_with_texture_mat
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.400000 0.400000 0.400000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.450000
+d 1.000000
+illum 2
+map_Ks ./specular.png
diff --git a/tests/data/assets/box_specular_workflow_with_texture.obj b/tests/data/assets/box_specular_workflow_with_texture.obj
new file mode 100644
index 0000000..7e8504e
--- /dev/null
+++ b/tests/data/assets/box_specular_workflow_with_texture.obj
@@ -0,0 +1,38 @@
+mtllib box_specular_workflow_with_texture.mtl
+o Cube
+v 0.500000 0.500000 -0.500000
+v 0.500000 -0.500000 -0.500000
+v 0.500000 0.500000 0.500000
+v 0.500000 -0.500000 0.500000
+v -0.500000 0.500000 -0.500000
+v -0.500000 -0.500000 -0.500000
+v -0.500000 0.500000 0.500000
+v -0.500000 -0.500000 0.500000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vt 0.625000 0.500000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.125000 0.500000
+vt 0.375000 0.500000
+vt 0.125000 0.750000
+s 0
+usemtl specular_workflow_with_texture_mat
+f 1/1/1 5/2/1 7/3/1 3/4/1
+f 4/5/2 3/4/2 7/6/2 8/7/2
+f 8/8/3 7/9/3 5/10/3 6/11/3
+f 6/12/4 2/13/4 4/5/4 8/14/4
+f 2/13/5 1/1/5 3/4/5 4/5/5
+f 6/11/6 5/10/6 1/1/6 2/13/6
diff --git a/tests/data/assets/box_with_texture.mtl b/tests/data/assets/box_with_texture.mtl
new file mode 100644
index 0000000..990dc8a
--- /dev/null
+++ b/tests/data/assets/box_with_texture.mtl
@@ -0,0 +1,12 @@
+newmtl texture_mat
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Ks 0.000000 0.000000 0.000000
+Ke 0.000000 0.000000 0.000000
+Ni 1.450000
+d 1.000000
+illum 2
+map_Kd ./grid.png
+norm ./normal.png
+map_Pr ./roughness.png
+map_Pm ./metallic.png
diff --git a/tests/data/assets/box_with_texture.obj b/tests/data/assets/box_with_texture.obj
new file mode 100644
index 0000000..934fd9f
--- /dev/null
+++ b/tests/data/assets/box_with_texture.obj
@@ -0,0 +1,38 @@
+mtllib box_with_texture.mtl
+o Cube
+v 0.500000 0.500000 -0.500000
+v 0.500000 -0.500000 -0.500000
+v 0.500000 0.500000 0.500000
+v 0.500000 -0.500000 0.500000
+v -0.500000 0.500000 -0.500000
+v -0.500000 -0.500000 -0.500000
+v -0.500000 0.500000 0.500000
+v -0.500000 -0.500000 0.500000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vt 0.625000 0.500000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.125000 0.500000
+vt 0.375000 0.500000
+vt 0.125000 0.750000
+s 0
+usemtl texture_mat
+f 1/1/1 5/2/1 7/3/1 3/4/1
+f 4/5/2 3/4/2 7/6/2 8/7/2
+f 8/8/3 7/9/3 5/10/3 6/11/3
+f 6/12/4 2/13/4 4/5/4 8/14/4
+f 2/13/5 1/1/5 3/4/5 4/5/5
+f 6/11/6 5/10/6 1/1/6 2/13/6
diff --git a/tests/data/assets/box_with_texture_opacity.mtl b/tests/data/assets/box_with_texture_opacity.mtl
new file mode 100644
index 0000000..8debd58
--- /dev/null
+++ b/tests/data/assets/box_with_texture_opacity.mtl
@@ -0,0 +1,10 @@
+newmtl texture_opacity_mat
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Ks 0.000000 0.000000 0.000000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+map_Kd ./grid.png
+map_d ./opacity.png
diff --git a/tests/data/assets/box_with_texture_opacity.obj b/tests/data/assets/box_with_texture_opacity.obj
new file mode 100644
index 0000000..b1528c6
--- /dev/null
+++ b/tests/data/assets/box_with_texture_opacity.obj
@@ -0,0 +1,38 @@
+mtllib box_with_texture_opacity.mtl
+o Cube
+v 0.500000 0.500000 -0.500000
+v 0.500000 -0.500000 -0.500000
+v 0.500000 0.500000 0.500000
+v 0.500000 -0.500000 0.500000
+v -0.500000 0.500000 -0.500000
+v -0.500000 -0.500000 -0.500000
+v -0.500000 0.500000 0.500000
+v -0.500000 -0.500000 0.500000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -0.0000 1.0000
+vn -1.0000 -0.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vn 1.0000 -0.0000 -0.0000
+vn -0.0000 -0.0000 -1.0000
+vt 0.625000 0.500000
+vt 0.875000 0.500000
+vt 0.875000 0.750000
+vt 0.625000 0.750000
+vt 0.375000 0.750000
+vt 0.625000 1.000000
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.625000 0.000000
+vt 0.625000 0.250000
+vt 0.375000 0.250000
+vt 0.125000 0.500000
+vt 0.375000 0.500000
+vt 0.125000 0.750000
+s 0
+usemtl texture_opacity_mat
+f 1/1/1 5/2/1 7/3/1 3/4/1
+f 4/5/2 3/4/2 7/6/2 8/7/2
+f 8/8/3 7/9/3 5/10/3 6/11/3
+f 6/12/4 2/13/4 4/5/4 8/14/4
+f 2/13/5 1/1/5 3/4/5 4/5/5
+f 6/11/6 5/10/6 1/1/6 2/13/6
diff --git a/tests/data/assets/metallic.png b/tests/data/assets/metallic.png
new file mode 100644
index 0000000..fb59433
Binary files /dev/null and b/tests/data/assets/metallic.png differ
diff --git a/tests/data/assets/normal.png b/tests/data/assets/normal.png
new file mode 100644
index 0000000..ab56f10
Binary files /dev/null and b/tests/data/assets/normal.png differ
diff --git a/tests/data/assets/opacity.png b/tests/data/assets/opacity.png
new file mode 100644
index 0000000..09a5800
Binary files /dev/null and b/tests/data/assets/opacity.png differ
diff --git a/tests/data/assets/roughness.png b/tests/data/assets/roughness.png
new file mode 100644
index 0000000..6a1e62c
Binary files /dev/null and b/tests/data/assets/roughness.png differ
diff --git a/tests/data/assets/specular.png b/tests/data/assets/specular.png
new file mode 100644
index 0000000..feb17a9
Binary files /dev/null and b/tests/data/assets/specular.png differ
diff --git a/tests/data/assets/two_boxes.mtl b/tests/data/assets/two_boxes.mtl
index 3024c78..94451ea 100644
--- a/tests/data/assets/two_boxes.mtl
+++ b/tests/data/assets/two_boxes.mtl
@@ -2,7 +2,7 @@ newmtl green_mat
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.000000 1.000000 0.000000
-Ks 0.500000 0.500000 0.500000
+Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
@@ -12,8 +12,10 @@ newmtl red_mat
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 1.000000 0.000000 0.000000
-Ks 0.500000 0.500000 0.500000
+Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
+Pr 0.3
+Pm 0.05
illum 2
diff --git a/tests/data/material_color.urdf b/tests/data/material_color.urdf
new file mode 100644
index 0000000..9383c71
--- /dev/null
+++ b/tests/data/material_color.urdf
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/material_mesh_color.urdf b/tests/data/material_mesh_color.urdf
new file mode 100644
index 0000000..b4c8d1a
--- /dev/null
+++ b/tests/data/material_mesh_color.urdf
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/material_mesh_override.urdf b/tests/data/material_mesh_override.urdf
new file mode 100644
index 0000000..23a4c5e
--- /dev/null
+++ b/tests/data/material_mesh_override.urdf
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/material_mesh_texture.urdf b/tests/data/material_mesh_texture.urdf
new file mode 100644
index 0000000..af565dc
--- /dev/null
+++ b/tests/data/material_mesh_texture.urdf
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/material_texture.urdf b/tests/data/material_texture.urdf
new file mode 100644
index 0000000..76d5bdd
--- /dev/null
+++ b/tests/data/material_texture.urdf
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/material_texture_name_duplication.urdf b/tests/data/material_texture_name_duplication.urdf
new file mode 100644
index 0000000..e129061
--- /dev/null
+++ b/tests/data/material_texture_name_duplication.urdf
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/test_displayname.urdf b/tests/data/test_displayname.urdf
index 0fe257b..4ab4f1b 100644
--- a/tests/data/test_displayname.urdf
+++ b/tests/data/test_displayname.urdf
@@ -1,6 +1,9 @@
+
+
+
@@ -21,6 +24,7 @@
+
diff --git a/tests/data/verifying_elements.urdf b/tests/data/verifying_elements.urdf
index 00f0603..0a692fa 100644
--- a/tests/data/verifying_elements.urdf
+++ b/tests/data/verifying_elements.urdf
@@ -9,7 +9,7 @@
-
+
diff --git a/tests/testCli.py b/tests/testCli.py
index 1dc52a0..be222fd 100644
--- a/tests/testCli.py
+++ b/tests/testCli.py
@@ -30,13 +30,22 @@ def test_run(self):
self.assertTrue((pathlib.Path(self.tmpDir()) / "simple-primitives.usda").exists())
def test_no_layer_structure(self):
- model = "tests/data/simple_box.urdf"
+ model = "tests/data/material_mesh_texture.urdf"
robot_name = pathlib.Path(model).stem
+
+ # This is the process to check whether an existing folder will be removed.
+ textures_dir = pathlib.Path(self.tmpDir()) / "Textures"
+ if not textures_dir.exists():
+ textures_dir.mkdir(parents=True, exist_ok=True)
+ shutil.copy("tests/data/assets/grid.png", textures_dir / "foo.png")
+
with patch("sys.argv", ["urdf_usd_converter", model, self.tmpDir(), "--no-layer-structure"]):
self.assertEqual(run(), 0, f"Failed to convert {model}")
self.assertFalse((pathlib.Path(self.tmpDir()) / "Payload").exists())
self.assertFalse((pathlib.Path(self.tmpDir()) / f"{robot_name}.usda").exists())
self.assertTrue((pathlib.Path(self.tmpDir()) / f"{robot_name}.usdc").exists())
+ self.assertTrue((textures_dir / "grid.png").exists())
+ self.assertFalse((textures_dir / "foo.png").exists())
def test_no_physics_scene(self):
model = "tests/data/simple_box.urdf"
@@ -211,7 +220,10 @@ def test_conversion_warning_multiple_ros_packages_invalid(self):
patch("sys.argv", ["urdf_usd_converter", robot, str(output_dir), "--package", package_1]),
usdex.test.ScopedDiagnosticChecker(
self,
- [(Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Failed to convert mesh:.*")],
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Failed to convert mesh:.*"),
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
level=usdex.core.DiagnosticsLevel.eWarning,
),
):
diff --git a/tests/testConverter.py b/tests/testConverter.py
index b4e5adc..33ad574 100644
--- a/tests/testConverter.py
+++ b/tests/testConverter.py
@@ -4,7 +4,7 @@
import shutil
import usdex.test
-from pxr import Tf, Usd, UsdGeom
+from pxr import Tf, Usd, UsdGeom, UsdShade
import urdf_usd_converter
from tests.util.ConverterTestCase import ConverterTestCase
@@ -124,7 +124,15 @@ def test_ros_packages(self):
{"name": "test_texture_package", "path": test_texture_package_dir},
]
converter = urdf_usd_converter.Converter(ros_packages=packages)
- asset_path = converter.convert(input_path, output_dir)
+ with usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ):
+ asset_path = converter.convert(input_path, output_dir)
+
self.assertIsNotNone(asset_path)
self.assertTrue(pathlib.Path(asset_path.path).exists())
@@ -152,7 +160,24 @@ def test_ros_packages(self):
self.assertTrue(stl_mesh_prim.HasAuthoredReferences())
# Check material texture.
- # TODO: Here we need to make sure that the reference to the usd file is correct after the texture is loaded.
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+ self.assertTrue(material_scope_prim.IsA(UsdGeom.Scope))
+
+ texture_material_prim = material_scope_prim.GetChild("texture_material")
+ self.assertTrue(texture_material_prim.IsValid())
+ self.assertTrue(texture_material_prim.IsA(UsdShade.Material))
+
+ texture_material = UsdShade.Material(texture_material_prim)
+ self.assertTrue(texture_material)
+ self.assertTrue(texture_material.GetPrim().HasAuthoredReferences())
+
+ texture_path = self.get_material_texture_path(texture_material, "diffuseColor")
+ self.assertEqual(texture_path, pathlib.Path("./Textures/grid.png"))
+ diffuse_color = self.get_material_diffuse_color(texture_material)
+ self.assertEqual(diffuse_color, None)
+ opacity = self.get_material_opacity(texture_material)
+ self.assertAlmostEqual(opacity, 1.0, places=6)
def test_asset_identifer(self):
model = pathlib.Path("tests/data/prismatic_joints.urdf")
diff --git a/tests/testDisplayName.py b/tests/testDisplayName.py
index 14db52a..5125d74 100644
--- a/tests/testDisplayName.py
+++ b/tests/testDisplayName.py
@@ -3,7 +3,7 @@
import pathlib
import usdex.core
-from pxr import Usd, UsdGeom, UsdPhysics
+from pxr import Usd, UsdGeom, UsdPhysics, UsdShade
import urdf_usd_converter
from tests.util.ConverterTestCase import ConverterTestCase
@@ -74,3 +74,12 @@ def test_display_name(self):
self.assertTrue(joint_root_prim.IsValid())
self.assertTrue(joint_root_prim.IsA(UsdPhysics.FixedJoint))
self.assertEqual(usdex.core.getDisplayName(joint_root_prim), "joint:root")
+
+ # Check for materials.
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+
+ material_red_prim = material_scope_prim.GetChild("tn__materialred_rL")
+ self.assertTrue(material_red_prim.IsValid())
+ self.assertTrue(material_red_prim.IsA(UsdShade.Material))
+ self.assertEqual(usdex.core.getDisplayName(material_red_prim), "material:red")
diff --git a/tests/testMaterial.py b/tests/testMaterial.py
new file mode 100644
index 0000000..6407066
--- /dev/null
+++ b/tests/testMaterial.py
@@ -0,0 +1,552 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
+# SPDX-License-Identifier: Apache-2.0
+import pathlib
+import shutil
+
+import usdex.test
+from pxr import Gf, Tf, Usd, UsdGeom, UsdShade
+
+import urdf_usd_converter
+from tests.util.ConverterTestCase import ConverterTestCase
+
+
+class TestMaterial(ConverterTestCase):
+ def test_material_color(self):
+ input_path = "tests/data/material_color.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check materials.
+ default_prim = stage.GetDefaultPrim()
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+ self.assertTrue(material_scope_prim.IsA(UsdGeom.Scope))
+
+ red_material_prim = material_scope_prim.GetChild("red")
+ self.assertTrue(red_material_prim.IsValid())
+ self.assertTrue(red_material_prim.IsA(UsdShade.Material))
+
+ red_material = UsdShade.Material(red_material_prim)
+ self.assertTrue(red_material)
+ self.assertTrue(red_material.GetPrim().HasAuthoredReferences())
+
+ diffuse_color = self.get_material_diffuse_color(red_material)
+ self.assertEqual(diffuse_color, Gf.Vec3f(1, 0, 0))
+ opacity = self.get_material_opacity(red_material)
+ self.assertEqual(opacity, 1.0)
+
+ green_material_prim = material_scope_prim.GetChild("green")
+ self.assertTrue(green_material_prim.IsValid())
+ self.assertTrue(green_material_prim.IsA(UsdShade.Material))
+
+ green_material = UsdShade.Material(green_material_prim)
+ self.assertTrue(green_material)
+ self.assertTrue(green_material.GetPrim().HasAuthoredReferences())
+
+ diffuse_color = self.get_material_diffuse_color(green_material)
+ self.assertEqual(diffuse_color, Gf.Vec3f(0, 1, 0))
+ opacity = self.get_material_opacity(green_material)
+ self.assertEqual(opacity, 1.0)
+
+ opacity_half_material_prim = material_scope_prim.GetChild("opacity_half")
+ self.assertTrue(opacity_half_material_prim.IsValid())
+ self.assertTrue(opacity_half_material_prim.IsA(UsdShade.Material))
+
+ opacity_half_material = UsdShade.Material(opacity_half_material_prim)
+ self.assertTrue(opacity_half_material)
+ self.assertTrue(opacity_half_material.GetPrim().HasAuthoredReferences())
+
+ # Diffuse Color is stored in Linear format, so it is converted from Linear to sRGB.
+ diffuse_color = self.get_material_diffuse_color(opacity_half_material)
+ diffuse_color = usdex.core.linearToSrgb(diffuse_color)
+ self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0.2, 0.5, 1), 1e-6))
+ opacity = self.get_material_opacity(opacity_half_material)
+ self.assertEqual(opacity, 0.5)
+
+ # Check the material bindings.
+ geometry_scope_prim = default_prim.GetChild("Geometry")
+ self.assertTrue(geometry_scope_prim.IsValid())
+ self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
+
+ link_box_red_prim = geometry_scope_prim.GetChild("link_box_red")
+ self.assertTrue(link_box_red_prim.IsValid())
+ self.assertTrue(link_box_red_prim.IsA(UsdGeom.Xform))
+
+ box_prim = link_box_red_prim.GetChild("box")
+ self.assertTrue(box_prim.IsValid())
+ self.assertTrue(box_prim.IsA(UsdGeom.Cube))
+ self.check_material_binding(box_prim, red_material)
+
+ link_box_green_prim = link_box_red_prim.GetChild("link_box_green")
+ self.assertTrue(link_box_green_prim.IsValid())
+ self.assertTrue(link_box_green_prim.IsA(UsdGeom.Xform))
+
+ box_prim = link_box_green_prim.GetChild("box")
+ self.assertTrue(box_prim.IsValid())
+ self.assertTrue(box_prim.IsA(UsdGeom.Cube))
+ self.check_material_binding(box_prim, green_material)
+
+ link_box_opacity_half_prim = link_box_green_prim.GetChild("link_box_opacity_half")
+ self.assertTrue(link_box_opacity_half_prim.IsValid())
+ self.assertTrue(link_box_opacity_half_prim.IsA(UsdGeom.Xform))
+
+ box_prim = link_box_opacity_half_prim.GetChild("box")
+ self.assertTrue(box_prim.IsValid())
+ self.assertTrue(box_prim.IsA(UsdGeom.Cube))
+ self.check_material_binding(box_prim, opacity_half_material)
+
+ def test_material_texture(self):
+ input_path = "tests/data/material_texture.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+
+ # A warning will appear when performing texture mapping on cubes, spheres, and cylinders.
+ with usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ):
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check texture.
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid.png"
+ self.assertTrue(output_texture_path.exists())
+
+ # Check materials.
+ default_prim = stage.GetDefaultPrim()
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+ self.assertTrue(material_scope_prim.IsA(UsdGeom.Scope))
+
+ texture_material_prim = material_scope_prim.GetChild("texture_material")
+ self.assertTrue(texture_material_prim.IsValid())
+ self.assertTrue(texture_material_prim.IsA(UsdShade.Material))
+
+ texture_material = UsdShade.Material(texture_material_prim)
+ self.assertTrue(texture_material)
+ self.assertTrue(texture_material.GetPrim().HasAuthoredReferences())
+
+ diffuse_color = self.get_material_diffuse_color(texture_material)
+ self.assertEqual(diffuse_color, None)
+ opacity = self.get_material_opacity(texture_material)
+ self.assertEqual(opacity, 1.0)
+ diffuse_color_texture_path = self.get_material_texture_path(texture_material, "diffuseColor")
+ self.assertEqual(diffuse_color_texture_path, pathlib.Path("./Textures/grid.png"))
+
+ color_texture_material_prim = material_scope_prim.GetChild("color_texture_material")
+ self.assertTrue(color_texture_material_prim.IsValid())
+ self.assertTrue(color_texture_material_prim.IsA(UsdShade.Material))
+
+ color_texture_material = UsdShade.Material(color_texture_material_prim)
+ self.assertTrue(color_texture_material)
+ self.assertTrue(color_texture_material.GetPrim().HasAuthoredReferences())
+
+ opacity = self.get_material_opacity(color_texture_material)
+ self.assertEqual(opacity, 1.0)
+
+ # This texture is multiplied, so the color value is obtained from the fallback.
+ diffuse_color = self.get_material_diffuse_color_texture_fallback(color_texture_material)
+ diffuse_color = Gf.Vec3f(*diffuse_color[:3])
+ diffuse_color = usdex.core.linearToSrgb(diffuse_color)
+ self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0.5, 0.2, 0.5), 1e-6))
+
+ geometry_scope_prim = default_prim.GetChild("Geometry")
+ self.assertTrue(geometry_scope_prim.IsValid())
+ self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
+
+ link_box_prim = geometry_scope_prim.GetChild("link_box")
+ self.assertTrue(link_box_prim.IsValid())
+ self.assertTrue(link_box_prim.IsA(UsdGeom.Xform))
+
+ box_prim = link_box_prim.GetChild("box")
+ self.assertTrue(box_prim.IsValid())
+ self.assertTrue(box_prim.IsA(UsdGeom.Cube))
+ self.check_material_binding(box_prim, texture_material)
+
+ link_sphere_prim = link_box_prim.GetChild("link_sphere")
+ self.assertTrue(link_sphere_prim.IsValid())
+ self.assertTrue(link_sphere_prim.IsA(UsdGeom.Xform))
+
+ sphere_prim = link_sphere_prim.GetChild("sphere")
+ self.assertTrue(sphere_prim.IsValid())
+ self.assertTrue(sphere_prim.IsA(UsdGeom.Sphere))
+ self.check_material_binding(sphere_prim, texture_material)
+
+ link_cylinder_prim = link_sphere_prim.GetChild("link_cylinder")
+ self.assertTrue(link_cylinder_prim.IsValid())
+ self.assertTrue(link_cylinder_prim.IsA(UsdGeom.Xform))
+
+ cylinder_prim = link_cylinder_prim.GetChild("cylinder")
+ self.assertTrue(cylinder_prim.IsValid())
+ self.assertTrue(cylinder_prim.IsA(UsdGeom.Cylinder))
+ self.check_material_binding(cylinder_prim, texture_material)
+
+ link_obj_texture_prim = link_cylinder_prim.GetChild("link_obj_texture")
+ self.assertTrue(link_obj_texture_prim.IsValid())
+ self.assertTrue(link_obj_texture_prim.IsA(UsdGeom.Xform))
+
+ obj_prim = link_obj_texture_prim.GetChild("box")
+ self.assertTrue(obj_prim.IsValid())
+ self.assertTrue(obj_prim.IsA(UsdGeom.Mesh))
+ self.check_material_binding(obj_prim, texture_material)
+
+ link_obj_color_texture_prim = link_obj_texture_prim.GetChild("link_obj_color_texture")
+ self.assertTrue(link_obj_color_texture_prim.IsValid())
+ self.assertTrue(link_obj_color_texture_prim.IsA(UsdGeom.Xform))
+
+ obj_prim = link_obj_color_texture_prim.GetChild("box")
+ self.assertTrue(obj_prim.IsValid())
+ self.assertTrue(obj_prim.IsA(UsdGeom.Mesh))
+ self.check_material_binding(obj_prim, color_texture_material)
+
+ def test_material_texture_name_duplication_missing_texture(self):
+ input_path = "tests/data/material_texture_name_duplication.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+ with usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Texture file not found:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ):
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check texture.
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid.png"
+ self.assertTrue(output_texture_path.exists())
+
+ # Confirm that this non-existent texture is not being output.
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid_1.png"
+ self.assertFalse(output_texture_path.exists())
+
+ def test_material_texture_name_duplication(self):
+ """
+ Place a structure containing the same-named texture "grid.png" in the temporary directory.
+ In this case, the converted textures will be placed as "grid.png" and "grid_1.png" in the "Textures" directory.
+
+ [temp]
+ material_texture_name_duplication.urdf
+ [assets]
+ box.obj
+ grid.png
+ [textures]
+ grid.png
+ """
+ temp_path = pathlib.Path(self.tmpDir())
+ input_path = temp_path / "material_texture_name_duplication.urdf"
+ assets_dir = temp_path / "assets"
+ assets_textures_dir = temp_path / "assets" / "textures"
+ output_dir = temp_path / "output"
+
+ assets_dir.mkdir(parents=True, exist_ok=True)
+ assets_textures_dir.mkdir(parents=True, exist_ok=True)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ shutil.copy("tests/data/material_texture_name_duplication.urdf", temp_path)
+ shutil.copy("tests/data/assets/box.obj", assets_dir)
+ shutil.copy("tests/data/assets/grid.png", assets_dir)
+ shutil.copy("tests/data/assets/grid.png", assets_textures_dir)
+
+ converter = urdf_usd_converter.Converter()
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check texture.
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid.png"
+ self.assertTrue(output_texture_path.exists())
+
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid_1.png"
+ self.assertTrue(output_texture_path.exists())
+
+ def test_material_mesh_color(self):
+ input_path = "tests/data/material_mesh_color.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ default_prim = stage.GetDefaultPrim()
+ geometry_scope_prim = default_prim.GetChild("Geometry")
+ self.assertTrue(geometry_scope_prim.IsValid())
+ self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
+
+ link_box_prim = geometry_scope_prim.GetChild("link_box")
+ self.assertTrue(link_box_prim.IsValid())
+ self.assertTrue(link_box_prim.IsA(UsdGeom.Xform))
+
+ link_obj_prim = link_box_prim.GetChild("link_obj")
+ self.assertTrue(link_obj_prim.IsValid())
+ self.assertTrue(link_obj_prim.IsA(UsdGeom.Xform))
+
+ two_boxes_prim = link_obj_prim.GetChild("two_boxes")
+ self.assertTrue(two_boxes_prim.IsValid())
+ self.assertTrue(two_boxes_prim.IsA(UsdGeom.Xform))
+ self.assertTrue(two_boxes_prim.HasAuthoredReferences())
+
+ link_obj_specular_workflow_prim = link_obj_prim.GetChild("link_obj_specular_workflow")
+ self.assertTrue(link_obj_specular_workflow_prim.IsValid())
+ self.assertTrue(link_obj_specular_workflow_prim.IsA(UsdGeom.Xform))
+
+ # Check the materials.
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+
+ green_material_prim = material_scope_prim.GetChild("green_mat")
+ self.assertTrue(green_material_prim.IsValid())
+ self.assertTrue(green_material_prim.IsA(UsdShade.Material))
+ green_material = UsdShade.Material(green_material_prim)
+ self.assertTrue(green_material)
+
+ diffuse_color = self.get_material_diffuse_color(green_material)
+ diffuse_color = usdex.core.linearToSrgb(diffuse_color)
+ self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0, 1, 0), 1e-6))
+ opacity = self.get_material_opacity(green_material)
+ self.assertEqual(opacity, 1.0)
+ ior = self.get_material_ior(green_material)
+ self.assertAlmostEqual(ior, 1.5, places=6)
+ specular_workflow = self.get_material_specular_workflow(green_material)
+ self.assertFalse(specular_workflow)
+
+ red_material_prim = material_scope_prim.GetChild("red_mat")
+ self.assertTrue(red_material_prim.IsValid())
+ self.assertTrue(red_material_prim.IsA(UsdShade.Material))
+ red_material = UsdShade.Material(red_material_prim)
+ self.assertTrue(red_material)
+ specular_workflow = self.get_material_specular_workflow(red_material)
+ self.assertFalse(specular_workflow)
+
+ diffuse_color = self.get_material_diffuse_color(red_material)
+ diffuse_color = usdex.core.linearToSrgb(diffuse_color)
+ self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(1, 0, 0), 1e-6))
+ opacity = self.get_material_opacity(red_material)
+ self.assertAlmostEqual(opacity, 1.0, places=6)
+ ior = self.get_material_ior(red_material)
+ self.assertAlmostEqual(ior, 1.45, places=6)
+ roughness = self.get_material_roughness(red_material)
+ self.assertAlmostEqual(roughness, 0.3, places=6)
+ metallic = self.get_material_metallic(red_material)
+ self.assertAlmostEqual(metallic, 0.05, places=6)
+
+ box_specular_workflow_prim = material_scope_prim.GetChild("specular_workflow_mat")
+ self.assertTrue(box_specular_workflow_prim.IsValid())
+ self.assertTrue(box_specular_workflow_prim.IsA(UsdShade.Material))
+ box_specular_workflow_material = UsdShade.Material(box_specular_workflow_prim)
+ self.assertTrue(box_specular_workflow_material)
+
+ ior = self.get_material_ior(box_specular_workflow_material)
+ self.assertAlmostEqual(ior, 1.45, places=6)
+ specular_workflow = self.get_material_specular_workflow(box_specular_workflow_material)
+ self.assertTrue(specular_workflow)
+ specular_color = self.get_material_specular_color(box_specular_workflow_material)
+ specular_color = usdex.core.linearToSrgb(specular_color)
+ self.assertTrue(Gf.IsClose(specular_color, Gf.Vec3f(0.5, 0.2, 0.1), 1e-6))
+
+ mesh_prim = two_boxes_prim.GetChild("Cube_Green")
+ self.assertTrue(mesh_prim.IsValid())
+ self.assertTrue(mesh_prim.IsA(UsdGeom.Mesh))
+ self.check_material_binding(mesh_prim, green_material)
+
+ mesh_prim = two_boxes_prim.GetChild("Cube_Red")
+ self.assertTrue(mesh_prim.IsValid())
+ self.assertTrue(mesh_prim.IsA(UsdGeom.Mesh))
+ self.check_material_binding(mesh_prim, red_material)
+
+ box_specular_workflow_prim = link_obj_specular_workflow_prim.GetChild("box_specular_workflow")
+ self.assertTrue(box_specular_workflow_prim.IsValid())
+ self.assertTrue(box_specular_workflow_prim.IsA(UsdGeom.Mesh))
+ self.assertTrue(box_specular_workflow_prim.HasAuthoredReferences())
+ self.check_material_binding(box_specular_workflow_prim, box_specular_workflow_material)
+
+ def test_material_mesh_texture(self):
+ input_path = "tests/data/material_mesh_texture.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check texture.
+ output_texture_path = pathlib.Path(output_dir) / "Payload" / "Textures" / "grid.png"
+ self.assertTrue(output_texture_path.exists())
+
+ default_prim = stage.GetDefaultPrim()
+ geometry_scope_prim = default_prim.GetChild("Geometry")
+ self.assertTrue(geometry_scope_prim.IsValid())
+ self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
+
+ link_box_prim = geometry_scope_prim.GetChild("link_box")
+ self.assertTrue(link_box_prim.IsValid())
+ self.assertTrue(link_box_prim.IsA(UsdGeom.Xform))
+
+ link_obj_prim = link_box_prim.GetChild("link_obj")
+ self.assertTrue(link_obj_prim.IsValid())
+ self.assertTrue(link_obj_prim.IsA(UsdGeom.Xform))
+
+ box_with_texture_prim = link_obj_prim.GetChild("box_with_texture")
+ self.assertTrue(box_with_texture_prim.IsValid())
+ self.assertTrue(box_with_texture_prim.IsA(UsdGeom.Mesh))
+ self.assertTrue(box_with_texture_prim.HasAuthoredReferences())
+
+ link_obj_opacity_prim = link_obj_prim.GetChild("link_obj_opacity")
+ self.assertTrue(link_obj_opacity_prim.IsValid())
+ self.assertTrue(link_obj_opacity_prim.IsA(UsdGeom.Xform))
+
+ box_with_texture_opacity_prim = link_obj_opacity_prim.GetChild("box_with_texture_opacity")
+ self.assertTrue(box_with_texture_opacity_prim.IsValid())
+ self.assertTrue(box_with_texture_opacity_prim.IsA(UsdGeom.Mesh))
+ self.assertTrue(box_with_texture_opacity_prim.HasAuthoredReferences())
+
+ link_obj_specular_workflow_with_texture_prim = link_obj_opacity_prim.GetChild("link_obj_specular_workflow_with_texture")
+ self.assertTrue(link_obj_specular_workflow_with_texture_prim.IsValid())
+ self.assertTrue(link_obj_specular_workflow_with_texture_prim.IsA(UsdGeom.Xform))
+
+ box_specular_workflow_with_texture_prim = link_obj_specular_workflow_with_texture_prim.GetChild("box_specular_workflow_with_texture")
+ self.assertTrue(box_specular_workflow_with_texture_prim.IsValid())
+ self.assertTrue(box_specular_workflow_with_texture_prim.IsA(UsdGeom.Mesh))
+ self.assertTrue(box_specular_workflow_with_texture_prim.HasAuthoredReferences())
+
+ # Check the materials.
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+
+ texture_material_prim = material_scope_prim.GetChild("texture_mat")
+ self.assertTrue(texture_material_prim.IsValid())
+ self.assertTrue(texture_material_prim.IsA(UsdShade.Material))
+ texture_material = UsdShade.Material(texture_material_prim)
+ self.assertTrue(texture_material)
+
+ diffuse_color = self.get_material_diffuse_color(texture_material)
+ self.assertIsNone(diffuse_color)
+ opacity = self.get_material_opacity(texture_material)
+ self.assertAlmostEqual(opacity, 1.0, places=6)
+ ior = self.get_material_ior(texture_material)
+ self.assertAlmostEqual(ior, 1.45, places=6)
+ diffuse_color_texture_path = self.get_material_texture_path(texture_material, "diffuseColor")
+ self.assertEqual(diffuse_color_texture_path, pathlib.Path("./Textures/grid.png"))
+ normal_texture_path = self.get_material_texture_path(texture_material, "normal")
+ self.assertEqual(normal_texture_path, pathlib.Path("./Textures/normal.png"))
+ roughness_texture_path = self.get_material_texture_path(texture_material, "roughness")
+ self.assertEqual(roughness_texture_path, pathlib.Path("./Textures/roughness.png"))
+ metallic_texture_path = self.get_material_texture_path(texture_material, "metallic")
+ self.assertEqual(metallic_texture_path, pathlib.Path("./Textures/metallic.png"))
+
+ texture_opacity_material_prim = material_scope_prim.GetChild("texture_opacity_mat")
+ self.assertTrue(texture_opacity_material_prim.IsValid())
+ self.assertTrue(texture_opacity_material_prim.IsA(UsdShade.Material))
+ texture_opacity_material = UsdShade.Material(texture_opacity_material_prim)
+ self.assertTrue(texture_opacity_material)
+
+ diffuse_color = self.get_material_diffuse_color(texture_opacity_material)
+ self.assertIsNone(diffuse_color)
+ ior = self.get_material_ior(texture_opacity_material)
+ self.assertAlmostEqual(ior, 1.0, places=6)
+ opacity_texture_path = self.get_material_texture_path(texture_opacity_material, "opacity")
+ self.assertEqual(opacity_texture_path, pathlib.Path("./Textures/opacity.png"))
+
+ texture_specular_workflow_material_prim = material_scope_prim.GetChild("specular_workflow_with_texture_mat")
+ self.assertTrue(texture_specular_workflow_material_prim.IsValid())
+ self.assertTrue(texture_specular_workflow_material_prim.IsA(UsdShade.Material))
+ texture_specular_workflow_material = UsdShade.Material(texture_specular_workflow_material_prim)
+ self.assertTrue(texture_specular_workflow_material)
+
+ diffuse_color = self.get_material_diffuse_color(texture_specular_workflow_material)
+ diffuse_color = usdex.core.linearToSrgb(diffuse_color)
+ self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0.4, 0.4, 0.4), 1e-6))
+ ior = self.get_material_ior(texture_specular_workflow_material)
+ self.assertAlmostEqual(ior, 1.45, places=6)
+ specular_workflow = self.get_material_specular_workflow(texture_specular_workflow_material)
+ self.assertTrue(specular_workflow)
+ specular_texture_path = self.get_material_texture_path(texture_specular_workflow_material, "specularColor")
+ self.assertEqual(specular_texture_path, pathlib.Path("./Textures/specular.png"))
+
+ self.check_material_binding(box_with_texture_prim, texture_material)
+ self.check_material_binding(box_with_texture_opacity_prim, texture_opacity_material)
+ self.check_material_binding(box_specular_workflow_with_texture_prim, texture_specular_workflow_material)
+
+ def test_material_mesh_override(self):
+ input_path = "tests/data/material_mesh_override.urdf"
+ output_dir = self.tmpDir()
+
+ converter = urdf_usd_converter.Converter()
+ asset_path = converter.convert(input_path, output_dir)
+
+ self.assertIsNotNone(asset_path)
+ self.assertTrue(pathlib.Path(asset_path.path).exists())
+
+ stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
+ self.assertIsValidUsd(stage)
+
+ # Check materials.
+ default_prim = stage.GetDefaultPrim()
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+ self.assertTrue(material_scope_prim.IsA(UsdGeom.Scope))
+
+ blue_material_prim = material_scope_prim.GetChild("blue")
+ self.assertTrue(blue_material_prim.IsValid())
+ self.assertTrue(blue_material_prim.IsA(UsdShade.Material))
+
+ blue_material = UsdShade.Material(blue_material_prim)
+ self.assertTrue(blue_material)
+ self.assertTrue(blue_material.GetPrim().HasAuthoredReferences())
+
+ diffuse_color = self.get_material_diffuse_color(blue_material)
+ self.assertEqual(diffuse_color, Gf.Vec3f(0, 0, 1))
+ opacity = self.get_material_opacity(blue_material)
+ self.assertEqual(opacity, 1.0)
+
+ default_prim = stage.GetDefaultPrim()
+ geometry_scope_prim = default_prim.GetChild("Geometry")
+ self.assertTrue(geometry_scope_prim.IsValid())
+ self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
+
+ two_boxes_prim = geometry_scope_prim.GetChild("link_box").GetChild("link_obj").GetChild("two_boxes")
+ self.assertTrue(two_boxes_prim.IsValid())
+ self.assertTrue(two_boxes_prim.IsA(UsdGeom.Xform))
+ self.assertTrue(two_boxes_prim.HasAuthoredReferences())
+
+ # Check that the material bind is overwritten with blue_material.
+ self.check_material_binding(two_boxes_prim, blue_material)
diff --git a/tests/testROSPackagesCli.py b/tests/testROSPackagesCli.py
index 7a87d04..a126075 100644
--- a/tests/testROSPackagesCli.py
+++ b/tests/testROSPackagesCli.py
@@ -4,7 +4,8 @@
import shutil
from unittest.mock import patch
-from pxr import Usd, UsdGeom
+import usdex.test
+from pxr import Tf, Usd, UsdGeom, UsdShade
from tests.util.ConverterTestCase import ConverterTestCase
from urdf_usd_converter._impl.cli import run
@@ -20,7 +21,16 @@ def test_do_not_specify_ros_package_name(self):
input_path = "tests/data/ros_packages.urdf"
output_dir = self.tmpDir()
- with patch("sys.argv", ["urdf_usd_converter", input_path, output_dir]):
+ with (
+ patch("sys.argv", ["urdf_usd_converter", input_path, output_dir]),
+ usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ),
+ ):
self.assertEqual(run(), 0, f"Failed to convert {input_path}")
# Check the USD file after converting ros_packages.urdf.
@@ -50,17 +60,26 @@ def test_specify_ros_package_names(self):
self.assertTrue(pathlib.Path(temp_stl_file_path).exists())
self.assertTrue(pathlib.Path(temp_texture_file_path).exists())
- with patch(
- "sys.argv",
- [
- "urdf_usd_converter",
- input_path,
- output_dir,
- "--package",
- "test_package=" + test_package_dir,
- "--package",
- "test_texture_package=" + test_texture_package_dir,
- ],
+ with (
+ patch(
+ "sys.argv",
+ [
+ "urdf_usd_converter",
+ input_path,
+ output_dir,
+ "--package",
+ "test_package=" + test_package_dir,
+ "--package",
+ "test_texture_package=" + test_texture_package_dir,
+ ],
+ ),
+ usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ),
):
self.assertEqual(run(), 0, f"Failed to convert {input_path}")
@@ -99,13 +118,22 @@ def test_do_not_specify_ros_package_with_relative_path(self):
shutil.copy("tests/data/assets/box.stl", mesh_dir)
shutil.copy("tests/data/assets/grid.png", texture_dir)
- with patch(
- "sys.argv",
- [
- "urdf_usd_converter",
- str(input_path),
- str(output_dir),
- ],
+ with (
+ patch(
+ "sys.argv",
+ [
+ "urdf_usd_converter",
+ str(input_path),
+ str(output_dir),
+ ],
+ ),
+ usdex.test.ScopedDiagnosticChecker(
+ self,
+ [
+ (Tf.TF_DIAGNOSTIC_WARNING_TYPE, ".*Textures are not projection mapped for Cube, Sphere, and Cylinder:.*"),
+ ],
+ level=usdex.core.DiagnosticsLevel.eWarning,
+ ),
):
self.assertEqual(run(), 0, f"Failed to convert {input_path}")
@@ -141,4 +169,21 @@ def check_usd_converted_from_urdf(self, usd_path: pathlib.Path):
self.assertTrue(stl_mesh_prim.HasAuthoredReferences())
# Check material texture.
- # TODO: Here we need to make sure that the reference to the usd file is correct after the texture is loaded.
+ material_scope_prim = default_prim.GetChild("Materials")
+ self.assertTrue(material_scope_prim.IsValid())
+ self.assertTrue(material_scope_prim.IsA(UsdGeom.Scope))
+
+ texture_material_prim = material_scope_prim.GetChild("texture_material")
+ self.assertTrue(texture_material_prim.IsValid())
+ self.assertTrue(texture_material_prim.IsA(UsdShade.Material))
+
+ texture_material = UsdShade.Material(texture_material_prim)
+ self.assertTrue(texture_material)
+ self.assertTrue(texture_material.GetPrim().HasAuthoredReferences())
+
+ texture_path = self.get_material_texture_path(texture_material, "diffuseColor")
+ self.assertEqual(texture_path, pathlib.Path("./Textures/grid.png"))
+ diffuse_color = self.get_material_diffuse_color(texture_material)
+ self.assertEqual(diffuse_color, None)
+ opacity = self.get_material_opacity(texture_material)
+ self.assertAlmostEqual(opacity, 1.0, places=6)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 22ffee3..565e2bf 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -299,7 +299,7 @@ def test_find_materials(self):
self.assertEqual(materials[0].name, "red")
self.assertEqual(materials[1].name, "green")
self.assertEqual(materials[2].name, "blue")
- self.assertEqual(materials[3].name, "black")
+ self.assertEqual(materials[3].name, "default")
self.assertEqual(materials[4].name, "texture")
# Find materials by name.
@@ -324,10 +324,10 @@ def test_find_materials(self):
self.assertIsNone(texture_material.color)
self.assertEqual(texture_material.texture.get_with_default("filename"), "assets/grid.png")
- black_material = self.parser.find_material_by_name("black")
- self.assertTrue(black_material)
- self.assertEqual(black_material.name, "black")
- self.assertEqual(black_material.color.get_with_default("rgba"), (0.0, 0.0, 0.0, 0.0))
+ default_material = self.parser.find_material_by_name("default")
+ self.assertTrue(default_material)
+ self.assertEqual(default_material.name, "default")
+ self.assertEqual(default_material.color.get_with_default("rgba"), (1.0, 1.0, 1.0, 1.0))
# Get non-existent material.
non_existent_material = self.parser.find_material_by_name("non_existent_material")
@@ -530,13 +530,13 @@ def test_get_materials(self):
self.assertEqual(material[2], None)
material = materials[3]
- self.assertEqual(material[0], "black")
- self.assertEqual(material[1], (0.0, 0.0, 0.0, 0.0))
+ self.assertEqual(material[0], "default")
+ self.assertEqual(material[1], (1.0, 1.0, 1.0, 1.0))
self.assertEqual(material[2], None)
material = materials[4]
self.assertEqual(material[0], "texture")
- self.assertEqual(material[1], (0.0, 0.0, 0.0, 0.0))
+ self.assertEqual(material[1], (1.0, 1.0, 1.0, 1.0))
self.assertEqual(material[2], "assets/grid.png")
material = materials[5]
diff --git a/tests/util/ConverterTestCase.py b/tests/util/ConverterTestCase.py
index b4760ce..442b760 100644
--- a/tests/util/ConverterTestCase.py
+++ b/tests/util/ConverterTestCase.py
@@ -1,9 +1,10 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
+import pathlib
import omni.asset_validator
import usdex.test
-from pxr import UsdGeom
+from pxr import Gf, Usd, UsdGeom, UsdShade
class ConverterTestCase(usdex.test.TestCase):
@@ -15,3 +16,75 @@ def setUp(self):
# All conversion results should be valid atomic assets
self.validationEngine.enable_rule(omni.asset_validator.AnchoredAssetPathsChecker)
self.validationEngine.enable_rule(omni.asset_validator.SupportedFileTypesChecker)
+
+ def check_material_binding(self, prim: Usd.Prim, material: UsdShade.Material):
+ material_binding = UsdShade.MaterialBindingAPI(prim)
+ self.assertTrue(material_binding)
+ self.assertTrue(material_binding.GetDirectBindingRel())
+ self.assertEqual(len(material_binding.GetDirectBindingRel().GetTargets()), 1)
+ bound_material = material_binding.GetDirectBindingRel().GetTargets()[0]
+ self.assertEqual(bound_material, material.GetPrim().GetPath())
+
+ def _get_input_value(self, shader: UsdShade.Shader, input_name: str):
+ value_attrs = UsdShade.Utils.GetValueProducingAttributes(shader.GetInput(input_name))
+
+ # If no value is set, returns None.
+ if not value_attrs or len(value_attrs) == 0:
+ return None
+
+ return value_attrs[0].Get()
+
+ def _get_material_input_value(self, material: UsdShade.Material, input_name: str):
+ shader: UsdShade.Shader = usdex.core.computeEffectivePreviewSurfaceShader(material)
+ return self._get_input_value(shader, input_name)
+
+ def get_material_diffuse_color(self, material: UsdShade.Material) -> Gf.Vec3f | None:
+ return self._get_material_input_value(material, "diffuseColor")
+
+ def get_material_specular_color(self, material: UsdShade.Material) -> Gf.Vec3f | None:
+ return self._get_material_input_value(material, "specularColor")
+
+ def get_material_specular_workflow(self, material: UsdShade.Material) -> bool:
+ return self._get_material_input_value(material, "useSpecularWorkflow") == 1
+
+ def get_material_opacity(self, material: UsdShade.Material) -> float:
+ return self._get_material_input_value(material, "opacity")
+
+ def get_material_roughness(self, material: UsdShade.Material) -> float:
+ return self._get_material_input_value(material, "roughness")
+
+ def get_material_metallic(self, material: UsdShade.Material) -> float:
+ return self._get_material_input_value(material, "metallic")
+
+ def get_material_ior(self, material: UsdShade.Material) -> float:
+ return self._get_material_input_value(material, "ior")
+
+ def get_material_texture_path(self, material: UsdShade.Material, texture_type: str = "diffuseColor") -> pathlib.Path:
+ """
+ Get the texture path for the given texture type.
+
+ Args:
+ material: The material.
+ texture_type: The texture type. Valid values are "diffuseColor", "normal", "roughness" and "metallic".
+
+ Returns:
+ The texture path.
+ """
+ shader: UsdShade.Shader = usdex.core.computeEffectivePreviewSurfaceShader(material)
+ texture_input: UsdShade.Input = shader.GetInput(texture_type)
+ self.assertTrue(texture_input.HasConnectedSource())
+
+ connected_source = texture_input.GetConnectedSource()
+ texture_shader = UsdShade.Shader(connected_source[0].GetPrim())
+ texture_file_value = self._get_input_value(texture_shader, "file")
+ return pathlib.Path(texture_file_value.path)
+
+ def get_material_diffuse_color_texture_fallback(self, material: UsdShade.Material) -> Gf.Vec4f | None:
+ shader: UsdShade.Shader = usdex.core.computeEffectivePreviewSurfaceShader(material)
+ diffuse_color_input = shader.GetInput("diffuseColor")
+ if diffuse_color_input.HasConnectedSource():
+ source = diffuse_color_input.GetConnectedSource()
+ if len(source) > 0 and isinstance(source[0], UsdShade.ConnectableAPI) and source[0].GetPrim().IsA(UsdShade.Shader):
+ diffuse_texture_shader = UsdShade.Shader(source[0].GetPrim())
+ return self._get_input_value(diffuse_texture_shader, "fallback")
+ return None
diff --git a/urdf_usd_converter/_impl/_flatten.py b/urdf_usd_converter/_impl/_flatten.py
index 8842df2..97c20fb 100644
--- a/urdf_usd_converter/_impl/_flatten.py
+++ b/urdf_usd_converter/_impl/_flatten.py
@@ -17,18 +17,18 @@ def export_flattened(asset_stage: Usd.Stage, output_dir: str, asset_dir: str, as
asset_identifier = f"{output_path.absolute().as_posix()}/{asset_stem}.{asset_format}"
usdex.core.exportLayer(layer, asset_identifier, get_authoring_metadata(), comment)
- # fix all PreviewMaterial inputs:file to ./Textures/xxx
+ # fix all PreviewMaterial material interface asset inputs from abs to rel paths (./Textures/xxx)
stage = Usd.Stage.Open(asset_identifier)
for prim in stage.Traverse():
- if prim.IsA(UsdShade.Shader):
- shader = UsdShade.Shader(prim)
- file_input = shader.GetInput("file")
- if file_input and file_input.Get() is not None:
- file_path = pathlib.Path(file_input.Get().path if hasattr(file_input.Get(), "path") else file_input.Get())
- tmpdir = pathlib.Path(tempfile.gettempdir())
- if file_path.is_relative_to(tmpdir):
- new_path = f"./{Tokens.Textures}/{file_path.name}"
- file_input.Set(Sdf.AssetPath(new_path))
+ if prim.IsA(UsdShade.Material):
+ material = UsdShade.Material(prim)
+ for input in material.GetInputs(onlyAuthored=True):
+ if input.GetTypeName() == Sdf.ValueTypeNames.Asset:
+ file_path = pathlib.Path(input.Get().path)
+ tmpdir = pathlib.Path(tempfile.gettempdir())
+ if file_path.is_relative_to(tmpdir):
+ new_path = f"./{Tokens.Textures}/{file_path.name}"
+ input.Set(Sdf.AssetPath(new_path))
stage.Save()
# copy texture to output dir
temp_textures_dir = pathlib.Path(asset_dir) / Tokens.Payload / Tokens.Textures
diff --git a/urdf_usd_converter/_impl/convert.py b/urdf_usd_converter/_impl/convert.py
index fef1b24..ead8aef 100644
--- a/urdf_usd_converter/_impl/convert.py
+++ b/urdf_usd_converter/_impl/convert.py
@@ -89,6 +89,9 @@ def convert(self, input_file: str, output_dir: str) -> Sdf.AssetPath:
link_hierarchy=LinkHierarchy(parser.get_root_element()),
mesh_cache=MeshCache(),
ros_packages=ros_packages,
+ resolved_file_paths={},
+ material_data_list=[],
+ mesh_material_references={},
)
# setup the main output layer (which will become an asset interface later)
@@ -120,14 +123,17 @@ def convert(self, input_file: str, output_dir: str) -> Sdf.AssetPath:
# setup the root layer of the payload
data.content[Tokens.Contents] = usdex.core.createAssetPayload(asset_stage)
- # author the mesh library
+ # author the mesh library.
+ # Here, the material data referenced by each mesh is retrieved and stored in data.material_data_list.
convert_meshes(data)
- # setup a content layer for referenced meshes
- data.content[Tokens.Geometry] = usdex.core.addAssetContent(data.content[Tokens.Contents], Tokens.Geometry, format="usda")
- # author the material library and setup the content layer for materials only if there are materials
+ # Convert the materials.
+ # Here, all materials referenced by the URDF's global materials and meshes are scanned and stored.
convert_materials(data)
+ # setup a content layer for referenced meshes
+ data.content[Tokens.Geometry] = usdex.core.addAssetContent(data.content[Tokens.Contents], Tokens.Geometry, format="usda")
+
# setup a content layer for physics
data.content[Tokens.Physics] = usdex.core.addAssetContent(data.content[Tokens.Contents], Tokens.Physics, format="usda")
data.content[Tokens.Physics].SetMetadata(UsdPhysics.Tokens.kilogramsPerUnit, 1)
diff --git a/urdf_usd_converter/_impl/data.py b/urdf_usd_converter/_impl/data.py
index 804bbe3..0ea8601 100644
--- a/urdf_usd_converter/_impl/data.py
+++ b/urdf_usd_converter/_impl/data.py
@@ -1,11 +1,13 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
+import pathlib
from dataclasses import dataclass
import usdex.core
from pxr import Usd
from .link_hierarchy import LinkHierarchy
+from .material_data import MaterialData
from .mesh_cache import MeshCache
from .urdf_parser.parser import URDFParser
@@ -35,3 +37,6 @@ class ConversionData:
link_hierarchy: LinkHierarchy
mesh_cache: MeshCache
ros_packages: list[dict[str, str]]
+ resolved_file_paths: dict[str, pathlib.Path] # [mesh_file_name, resolved_file_path]
+ material_data_list: list[MaterialData] # Store all material parameters.
+ mesh_material_references: dict[pathlib.Path, dict[str, str]] # [mesh_file_path, [mesh_safe_name, material_name]]
diff --git a/urdf_usd_converter/_impl/geometry.py b/urdf_usd_converter/_impl/geometry.py
index 22855b1..9d04863 100644
--- a/urdf_usd_converter/_impl/geometry.py
+++ b/urdf_usd_converter/_impl/geometry.py
@@ -4,6 +4,7 @@
from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics
from .data import ConversionData, Tokens
+from .material import bind_material, bind_mesh_material
from .urdf_parser.elements import (
ElementBox,
ElementCollision,
@@ -44,6 +45,14 @@ def convert_geometry(parent: Usd.Prim, name: str, safe_name: str, geometry: Elem
# Apply CollisionAPI to collision geometry
apply_physics_collision(prim.GetPrim(), data)
+ # If the visual has a material, bind the material.
+ # If the Visual element does not have a material, bind a material per mesh.
+ if isinstance(geometry, ElementVisual):
+ if geometry.material and geometry.material.name:
+ bind_material(prim.GetPrim(), None, geometry.material.name, data)
+ elif isinstance(geometry.geometry.shape, ElementMesh):
+ bind_mesh_material(prim.GetPrim(), geometry.geometry.shape.filename, data)
+
return prim
diff --git a/urdf_usd_converter/_impl/material.py b/urdf_usd_converter/_impl/material.py
index 63d5782..3e68194 100644
--- a/urdf_usd_converter/_impl/material.py
+++ b/urdf_usd_converter/_impl/material.py
@@ -1,18 +1,408 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
+import pathlib
+import shutil
+
+import tinyobjloader
import usdex.core
+from pxr import Gf, Sdf, Tf, Usd, UsdGeom, UsdShade, UsdUtils
from .data import ConversionData, Tokens
+from .material_cache import MaterialCache
+from .material_data import MaterialData
+from .ros_package import resolve_ros_package_paths
-__all__ = ["convert_materials"]
+__all__ = [
+ "bind_material",
+ "bind_mesh_material",
+ "convert_materials",
+ "store_mesh_material_reference",
+ "store_obj_material_data",
+]
def convert_materials(data: ConversionData):
- materials = data.urdf_parser.get_materials()
- if not len(materials):
+ # Acquire the global material data for URDF and the material data for obj/dae files.
+ material_cache = MaterialCache(data)
+ if not len(data.material_data_list):
return
+ # Copy the textures to the payload directory.
+ _copy_textures(material_cache, data)
+
data.libraries[Tokens.Materials] = usdex.core.addAssetLibrary(data.content[Tokens.Contents], Tokens.Materials, format="usdc")
data.references[Tokens.Materials] = {}
- # TODO: Implement
+ materials_scope = data.libraries[Tokens.Materials].GetDefaultPrim()
+
+ # Set the safe names of the material data list.
+ material_cache.store_safe_names(data)
+
+ # Convert the material data to USD.
+ for material_data in data.material_data_list:
+ material_prim = _convert_material(
+ materials_scope,
+ material_data.safe_name,
+ material_data.diffuse_color,
+ material_data.specular_color,
+ material_data.opacity,
+ material_data.roughness,
+ material_data.metallic,
+ material_data.ior,
+ material_data.diffuse_texture_path,
+ material_data.specular_texture_path,
+ material_data.normal_texture_path,
+ material_data.roughness_texture_path,
+ material_data.metallic_texture_path,
+ material_data.opacity_texture_path,
+ material_cache.texture_paths,
+ data,
+ )
+ data.references[Tokens.Materials][material_data.safe_name] = material_prim
+ if material_data.name != material_data.safe_name:
+ usdex.core.setDisplayName(material_prim.GetPrim(), material_data.name)
+
+ robot_name = data.urdf_parser.get_robot_name()
+ usdex.core.saveStage(data.libraries[Tokens.Materials], comment=f"Material Library for {robot_name}. {data.comment}")
+
+ # setup a content layer for referenced materials
+ data.content[Tokens.Materials] = usdex.core.addAssetContent(data.content[Tokens.Contents], Tokens.Materials, format="usda")
+
+
+def _copy_textures(material_cache: MaterialCache, data: ConversionData):
+ """
+ Copy the textures to the payload directory.
+
+ Args:
+ material_cache: The material cache.
+ data: The conversion data.
+ """
+ if not len(material_cache.texture_paths):
+ return
+
+ # copy the texture to the payload directory
+ local_texture_dir = pathlib.Path(data.content[Tokens.Contents].GetRootLayer().identifier).parent / Tokens.Textures
+ if not local_texture_dir.exists():
+ local_texture_dir.mkdir(parents=True)
+
+ for texture_path in material_cache.texture_paths:
+ # At this stage, the existence has already been checked.
+ if texture_path.exists():
+ unique_file_name = material_cache.texture_paths[texture_path]
+
+ local_texture_path = local_texture_dir / unique_file_name
+ shutil.copyfile(texture_path, local_texture_path)
+ Tf.Status(f"Copied texture {texture_path} to {local_texture_path}")
+
+
+def _convert_material(
+ parent: Usd.Prim,
+ safe_name: str,
+ diffuse_color: Gf.Vec3f,
+ specular_color: Gf.Vec3f,
+ opacity: float,
+ roughness: float,
+ metallic: float,
+ ior: float,
+ diffuse_texture_path: pathlib.Path | None,
+ specular_texture_path: pathlib.Path | None,
+ normal_texture_path: pathlib.Path | None,
+ roughness_texture_path: pathlib.Path | None,
+ metallic_texture_path: pathlib.Path | None,
+ opacity_texture_path: pathlib.Path | None,
+ texture_paths: dict[pathlib.Path, str],
+ data: ConversionData,
+) -> UsdShade.Material:
+ """
+ Convert a material to USD.
+ This is used for both URDF global materials and materials in obj/dae files.
+
+ Args:
+ parent: The parent prim.
+ safe_name: The safe name of the material. This is a unique name that does not overlap with other material names.
+ diffuse_color: The diffuse color of the material.
+ specular_color: The specular color of the material.
+ opacity: The opacity of the material.
+ roughness: The roughness of the material.
+ metallic: The metallic of the material.
+ ior: The ior of the material.
+ diffuse_texture_path: The path to the diffuse texture.
+ specular_texture_path: The path to the specular texture.
+ normal_texture_path: The path to the normal texture.
+ roughness_texture_path: The path to the roughness texture.
+ metallic_texture_path: The path to the metallic texture.
+ opacity_texture_path: The path to the opacity texture.
+ texture_paths: A dictionary of texture paths and unique names.
+ data: The conversion data.
+
+ Returns:
+ The material prim.
+ """
+ diffuse_color = usdex.core.sRgbToLinear(diffuse_color)
+ specular_color = usdex.core.sRgbToLinear(specular_color)
+
+ # Build kwargs for material properties
+ material_kwargs = {
+ "color": diffuse_color,
+ "opacity": opacity,
+ "roughness": roughness,
+ "metallic": metallic,
+ }
+
+ # Define the material.
+ material_prim = usdex.core.definePreviewMaterial(parent, safe_name, **material_kwargs)
+ if not material_prim:
+ Tf.RaiseRuntimeError(f'Failed to convert material "{safe_name}"')
+
+ surface_shader: UsdShade.Shader = usdex.core.computeEffectivePreviewSurfaceShader(material_prim)
+ if ior != 0.0:
+ surface_shader.CreateInput("ior", Sdf.ValueTypeNames.Float).Set(ior)
+
+ if diffuse_texture_path:
+ usdex.core.addDiffuseTextureToPreviewMaterial(material_prim, _get_texture_asset_path(diffuse_texture_path, texture_paths, data))
+
+ if normal_texture_path:
+ usdex.core.addNormalTextureToPreviewMaterial(material_prim, _get_texture_asset_path(normal_texture_path, texture_paths, data))
+
+ if roughness_texture_path:
+ usdex.core.addRoughnessTextureToPreviewMaterial(material_prim, _get_texture_asset_path(roughness_texture_path, texture_paths, data))
+
+ if metallic_texture_path:
+ usdex.core.addMetallicTextureToPreviewMaterial(material_prim, _get_texture_asset_path(metallic_texture_path, texture_paths, data))
+
+ if opacity_texture_path:
+ usdex.core.addOpacityTextureToPreviewMaterial(material_prim, _get_texture_asset_path(opacity_texture_path, texture_paths, data))
+
+ # If the specular color is not black or the specular texture exists, use the specular workflow.
+ if specular_color != [0, 0, 0] or specular_texture_path:
+ surface_shader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
+ surface_shader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(specular_color)
+ if specular_texture_path:
+ _add_specular_texture_to_preview_material(material_prim, _get_texture_asset_path(specular_texture_path, texture_paths, data))
+
+ result = usdex.core.addPreviewMaterialInterface(material_prim)
+ if not result:
+ Tf.RaiseRuntimeError(f'Failed to add material instance to material prim "{material_prim.GetPath()}"')
+
+ material_prim.GetPrim().SetInstanceable(True)
+
+ return material_prim
+
+
+def _add_specular_texture_to_preview_material(material_prim: UsdShade.Material, specular_texture_path: Sdf.AssetPath):
+ """
+ Add the specular texture to the preview material.
+
+ Args:
+ material_prim: The material prim.
+ specular_texture_path: The path to the specular texture.
+ """
+ surface: UsdShade.Shader = usdex.core.computeEffectivePreviewSurfaceShader(material_prim)
+
+ specular_color = Gf.Vec3f(0.0, 0.0, 0.0)
+ specular_color_input = surface.GetInput("specularColor")
+ if specular_color_input:
+ value_attrs = specular_color_input.GetValueProducingAttributes()
+ if value_attrs and len(value_attrs) > 0:
+ specular_color = value_attrs[0].Get()
+ specular_color_input.GetAttr().Clear()
+ else:
+ specular_color_input = surface.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f)
+ fallback = Gf.Vec4f(specular_color[0], specular_color[1], specular_color[2], 1.0)
+
+ # Acquire the texture reader.
+ texture_reader: UsdShade.Shader = _acquire_texture_reader(
+ material_prim, "SpecularTexture", specular_texture_path, usdex.core.ColorSpace.eAuto, fallback
+ )
+
+ # Connect the PreviewSurface shader "specularColor" to the specular texture shader output
+ specular_color_input.ConnectToSource(texture_reader.CreateOutput("rgb", Sdf.ValueTypeNames.Float3))
+
+
+def _acquire_texture_reader(
+ material_prim: UsdShade.Material,
+ shader_name: str,
+ texture_path: pathlib.Path,
+ color_space: usdex.core.ColorSpace,
+ fallback: Gf.Vec4f,
+) -> UsdShade.Shader:
+ """
+ Acquire the texture reader.
+
+ Args:
+ material_prim: The material prim.
+ shader_name: The name of the shader.
+ texture_path: The path to the texture.
+ color_space: The color space of the texture.
+ fallback: The fallback value for the texture.
+
+ Returns:
+ The texture reader.
+ """
+ shader_path = material_prim.GetPath().AppendChild(shader_name)
+ tex_shader = UsdShade.Shader.Define(material_prim.GetPrim().GetStage(), shader_path)
+ tex_shader.SetShaderId("UsdUVTexture")
+ tex_shader.CreateInput("fallback", Sdf.ValueTypeNames.Float4).Set(fallback)
+ tex_shader.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(texture_path)
+ tex_shader.CreateInput("sourceColorSpace", Sdf.ValueTypeNames.Token).Set(usdex.core.getColorSpaceToken(color_space))
+ st_input = tex_shader.CreateInput("st", Sdf.ValueTypeNames.Float2)
+ connected = usdex.core.connectPrimvarShader(st_input, UsdUtils.GetPrimaryUVSetName())
+ if not connected:
+ return UsdShade.Shader()
+
+ return tex_shader
+
+
+def _get_texture_asset_path(texture_path: pathlib.Path, texture_paths: dict[pathlib.Path, str], data: ConversionData) -> Sdf.AssetPath:
+ """
+ Get the asset path for the texture.
+
+ Args:
+ texture_path: The path to the texture.
+ texture_paths: A dictionary of texture paths and unique names.
+
+ Returns:
+ The asset path for the texture.
+ """
+ # The path to the texture to reference. If None, the texture does not exist.
+ unique_file_name = texture_paths.get(texture_path)
+
+ # If the texture exists, add the texture to the material.
+ payload_dir = pathlib.Path(data.content[Tokens.Contents].GetRootLayer().identifier).parent
+ local_texture_dir = payload_dir / Tokens.Textures
+ local_texture_path = local_texture_dir / unique_file_name
+ if local_texture_path.exists():
+ relative_texture_path = local_texture_path.relative_to(payload_dir)
+ return Sdf.AssetPath(f"./{relative_texture_path.as_posix()}")
+ else:
+ return Sdf.AssetPath("")
+
+
+def store_obj_material_data(mesh_file_path: pathlib.Path, reader: tinyobjloader.ObjReader, data: ConversionData):
+ """
+ Store the material data from the OBJ file.
+ This is used to temporarily cache material parameters when loading an OBJ mesh.
+
+ Args:
+ mesh_file_path: The path to the mesh file.
+ reader: The tinyobjloader reader.
+ data: The conversion data.
+ """
+ materials = reader.GetMaterials()
+ for material in materials:
+ material_data = MaterialData()
+ material_data.mesh_file_path = mesh_file_path
+ material_data.name = material.name
+ material_data.diffuse_color = Gf.Vec3f(material.diffuse[0], material.diffuse[1], material.diffuse[2])
+ material_data.specular_color = Gf.Vec3f(material.specular[0], material.specular[1], material.specular[2])
+ material_data.opacity = material.dissolve
+ material_data.ior = material.ior if material.ior else 0.0
+
+ # The following is the extended specification of obj.
+ material_data.roughness = material.roughness if material.roughness else 0.5
+ material_data.metallic = material.metallic if material.metallic else 0.0
+
+ material_data.diffuse_texture_path = (mesh_file_path.parent / material.diffuse_texname) if material.diffuse_texname else None
+ material_data.specular_texture_path = (mesh_file_path.parent / material.specular_texname) if material.specular_texname else None
+ material_data.normal_texture_path = (mesh_file_path.parent / material.normal_texname) if material.normal_texname else None
+ material_data.roughness_texture_path = (mesh_file_path.parent / material.roughness_texname) if material.roughness_texname else None
+ material_data.metallic_texture_path = (mesh_file_path.parent / material.metallic_texname) if material.metallic_texname else None
+ material_data.opacity_texture_path = (mesh_file_path.parent / material.alpha_texname) if material.alpha_texname else None
+
+ # If the normal texture is not specified, use the bump texture.
+ if material_data.normal_texture_path is None:
+ material_data.normal_texture_path = (mesh_file_path.parent / material.bump_texname) if material.bump_texname else None
+
+ data.material_data_list.append(material_data)
+
+
+def store_mesh_material_reference(mesh_file_path: pathlib.Path, mesh_safe_name: str, material_name: str, data: ConversionData):
+ """
+ Store the per-mesh material reference.
+
+ Args:
+ mesh_file_path: The path to the source file.
+ mesh_safe_name: The safe name of the mesh.
+ material_name: The name of the material.
+ data: The conversion data.
+ """
+ if mesh_file_path not in data.mesh_material_references:
+ data.mesh_material_references[mesh_file_path] = {}
+ data.mesh_material_references[mesh_file_path][mesh_safe_name] = material_name
+
+
+def _get_material_by_name(mesh_file_path: pathlib.Path | None, material_name: str, data: ConversionData) -> UsdShade.Material:
+ """
+ Get the material by the mesh path and material name.
+
+ Args:
+ mesh_file_path: The path to the mesh file. If None, the material is a global material.
+ material_name: The name of the material.
+ data: The conversion data.
+
+ Returns:
+ The material if found, otherwise None.
+ """
+ for material_data in data.material_data_list:
+ if material_data.name == material_name and material_data.mesh_file_path == mesh_file_path:
+ return data.references[Tokens.Materials][material_data.safe_name]
+ return None
+
+
+def bind_material(geom_prim: Usd.Prim, mesh_file_path: pathlib.Path | None, material_name: str, data: ConversionData):
+ """
+ Bind the material to the geometries.
+ If there are meshes in the Xform, it will traverse the meshes and assign materials to them.
+
+ Args:
+ geom_prim: The geometry prim.
+ mesh_file_path: The path to the mesh file. If None, the material is a global material.
+ material_name: The name of the material.
+ data: The conversion data.
+ """
+ local_materials = data.content[Tokens.Materials].GetDefaultPrim().GetChild(Tokens.Materials)
+
+ # Get the material by the mesh path and material name.
+ ref_material = _get_material_by_name(mesh_file_path, material_name, data)
+ if not ref_material:
+ Tf.Warn(f"Material '{material_name}' not found in Material Library {data.libraries[Tokens.Materials].GetRootLayer().identifier}")
+ return
+
+ # If the material does not exist in the Material layer, define the reference.
+ material_prim = UsdShade.Material(local_materials.GetChild(ref_material.GetPrim().GetName()))
+ if not material_prim:
+ material_prim = UsdShade.Material(usdex.core.defineReference(local_materials, ref_material.GetPrim(), ref_material.GetPrim().GetName()))
+
+ # If the geometry is a cube, sphere, or cylinder, check if the material has a texture.
+ if mesh_file_path is None and (geom_prim.IsA(UsdGeom.Cube) or geom_prim.IsA(UsdGeom.Sphere) or geom_prim.IsA(UsdGeom.Cylinder)):
+ # Get the texture from the material.
+ materials = data.urdf_parser.get_materials()
+ for material in materials:
+ if material[0] == material_name:
+ if material[2]:
+ Tf.Warn(f"Textures are not projection mapped for Cube, Sphere, and Cylinder: {geom_prim.GetPath()}")
+ break
+
+ # Bind the material to the geometry.
+ geom_over = data.content[Tokens.Materials].OverridePrim(geom_prim.GetPath())
+ usdex.core.bindMaterial(geom_over, material_prim)
+
+
+def bind_mesh_material(geom_prim: Usd.Prim, mesh_file_name: str, data: ConversionData):
+ """
+ Bind the material to the meshes in the geometry.
+ Each mesh references a mesh within the GeometryLibrary,
+ and if a material exists for the prim name at that time, it searches for and binds it.
+
+ Args:
+ geom_prim: The geometry prim.
+ mesh_file_name: The name of the mesh file.
+ data: The conversion data.
+ """
+ resolved_file_path = resolve_ros_package_paths(mesh_file_name, data)
+ for prim in Usd.PrimRange(geom_prim):
+ if prim.IsA(UsdGeom.Mesh):
+ mesh_name = prim.GetName()
+ if resolved_file_path in data.mesh_material_references and mesh_name in data.mesh_material_references[resolved_file_path]:
+ material_name = data.mesh_material_references[resolved_file_path][mesh_name]
+ bind_material(prim, resolved_file_path, material_name, data)
diff --git a/urdf_usd_converter/_impl/material_cache.py b/urdf_usd_converter/_impl/material_cache.py
new file mode 100644
index 0000000..48fca51
--- /dev/null
+++ b/urdf_usd_converter/_impl/material_cache.py
@@ -0,0 +1,132 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
+# SPDX-License-Identifier: Apache-2.0
+import pathlib
+
+from pxr import Gf, Tf
+
+from .data import ConversionData, Tokens
+from .material_data import MaterialData
+from .ros_package import resolve_ros_package_paths
+
+__all__ = ["MaterialCache"]
+
+
+class MaterialCache:
+ def __init__(self, data: ConversionData):
+ # A dictionary of texture paths and unique names.
+ self.texture_paths: dict[pathlib.Path, str] = {}
+
+ # Store the material data.
+ self._store_materials(data)
+
+ def store_safe_names(self, data: ConversionData):
+ """
+ Store the safe names of the material data list.
+
+ Args:
+ material_data_list: The list of material data.
+ data: The conversion data.
+ """
+ materials_scope = data.libraries[Tokens.Materials].GetDefaultPrim()
+ material_names = [material_data.name for material_data in data.material_data_list]
+ safe_names = data.name_cache.getPrimNames(materials_scope, material_names)
+
+ for material_data, safe_name in zip(data.material_data_list, safe_names):
+ material_data.safe_name = safe_name
+
+ def _store_materials(self, data: ConversionData):
+ """
+ Get the material data from the URDF file and the OBJ/DAE files.
+ Material data is stored in `data.material_data_list`.
+ If the material is stored in an obj or dae file,
+ the material data will already be stored when this method is called.
+ Here, global materials in URDF are added, and a list of texture paths is created.
+
+ Args:
+ data: The conversion data.
+ """
+ # Get the material data from the URDF file.
+ data.material_data_list.extend(self._get_urdf_material_data_list(data))
+
+ # Get a dictionary of resolved texture paths and unique names.
+ # It stores all the texture file paths referenced by urdf materials and each mesh.
+ self.texture_paths = self._get_material_texture_paths(data)
+
+ def _get_material_texture_paths(self, data: ConversionData) -> dict[pathlib.Path, str]:
+ """
+ Create a dictionary of resolved texture paths and unique names.
+ These include all global materials and the textures of materials referenced by meshes.
+
+ Args:
+ data: The conversion data.
+
+ Returns:
+ A dictionary of texture paths and unique names.
+ """
+ # Get the texture paths from the materials.
+ texture_paths_list: list[pathlib.Path] = []
+ for material_data in data.material_data_list:
+ texture_paths = [
+ material_data.diffuse_texture_path,
+ material_data.specular_texture_path,
+ material_data.normal_texture_path,
+ material_data.roughness_texture_path,
+ material_data.metallic_texture_path,
+ material_data.opacity_texture_path,
+ ]
+ for texture_path in texture_paths:
+ if texture_path and texture_path not in texture_paths_list:
+ texture_paths_list.append(texture_path)
+ if not texture_path.exists():
+ Tf.Warn(f"Texture file not found: {texture_path}")
+
+ # Create a list of texture filenames.
+ names = [texture_path.name for texture_path in texture_paths_list]
+
+ # Rename the list of image filenames to unique names.
+ unique_file_names = []
+ name_counts = {}
+ for name in names:
+ if name not in name_counts:
+ name_counts[name] = 0
+ unique_file_names.append(name)
+ else:
+ name_counts[name] += 1
+ stem = pathlib.Path(name).stem
+ suffix = pathlib.Path(name).suffix
+ unique_name = f"{stem}_{name_counts[name]}{suffix}"
+ unique_file_names.append(unique_name)
+
+ texture_paths = dict(zip(texture_paths_list, unique_file_names))
+
+ return texture_paths
+
+ def _get_urdf_material_data_list(self, data: ConversionData) -> list[MaterialData]:
+ """
+ Get the material data from the URDF file (Global Materials).
+
+ Args:
+ data: The conversion data.
+
+ Returns:
+ A list of material data.
+ """
+ material_data_list = []
+
+ materials = data.urdf_parser.get_materials()
+ for material in materials:
+ material_data = MaterialData()
+ material_data.name = material[0]
+ material_data.diffuse_color = Gf.Vec3f(*material[1][:3])
+ material_data.opacity = material[1][3]
+
+ # material[2] is the path to the texture file.
+ if material[2]:
+ # Resolve the ROS package paths.
+ # If the path is not a ROS package, it will return the original path.
+ # It also converts the path to a relative path based on the urdf file.
+ material_data.diffuse_texture_path = resolve_ros_package_paths(material[2], data)
+
+ material_data_list.append(material_data)
+
+ return material_data_list
diff --git a/urdf_usd_converter/_impl/material_data.py b/urdf_usd_converter/_impl/material_data.py
new file mode 100644
index 0000000..4986451
--- /dev/null
+++ b/urdf_usd_converter/_impl/material_data.py
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
+# SPDX-License-Identifier: Apache-2.0
+import pathlib
+
+from pxr import Gf
+
+__all__ = ["MaterialData"]
+
+
+class MaterialData:
+ """
+ Temporary data when storing material.
+ """
+
+ def __init__(self):
+ # The path to the mesh file. For global materials used in URDF, None is entered.
+ self.mesh_file_path: pathlib.Path | None = None
+
+ # The name of the material.
+ self.name: str | None = None
+
+ # The safe name of the material.
+ # This is a unique name that does not overlap with other material names.
+ self.safe_name: str | None = None
+
+ # The material properties.
+ self.diffuse_color: Gf.Vec3f = Gf.Vec3f(1.0, 1.0, 1.0)
+ self.specular_color: Gf.Vec3f = Gf.Vec3f(0.0, 0.0, 0.0)
+ self.opacity: float = 1.0
+ self.roughness: float = 0.5
+ self.metallic: float = 0.0
+ self.ior: float = 0.0
+
+ self.diffuse_texture_path: pathlib.Path | None = None
+ self.specular_texture_path: pathlib.Path | None = None
+ self.normal_texture_path: pathlib.Path | None = None
+ self.roughness_texture_path: pathlib.Path | None = None
+ self.metallic_texture_path: pathlib.Path | None = None
+ self.opacity_texture_path: pathlib.Path | None = None
diff --git a/urdf_usd_converter/_impl/mesh.py b/urdf_usd_converter/_impl/mesh.py
index 88fcb49..819c8a6 100644
--- a/urdf_usd_converter/_impl/mesh.py
+++ b/urdf_usd_converter/_impl/mesh.py
@@ -9,6 +9,7 @@
from pxr import Gf, Tf, Usd, UsdGeom, Vt
from .data import ConversionData, Tokens
+from .material import store_mesh_material_reference, store_obj_material_data
from .numpy import convert_vec3f_array
from .ros_package import resolve_ros_package_paths
@@ -94,14 +95,38 @@ def convert_stl(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
return usd_mesh
-def _convert_single_obj(prim: Usd.Prim, input_path: pathlib.Path, reader: tinyobjloader.ObjReader) -> UsdGeom.Mesh:
+def _convert_single_obj(
+ prim: Usd.Prim,
+ input_path: pathlib.Path,
+ reader: tinyobjloader.ObjReader,
+ data: ConversionData,
+) -> UsdGeom.Mesh:
"""
Convert a single OBJ mesh to a USD mesh.
+
+ Args:
+ prim: The prim to convert the mesh to.
+ input_path: The path to the OBJ file.
+ reader: The tinyobjloader reader.
+ materials_prims: The dictionary of material names and their prims.
+ data: The conversion data.
+
+ Returns:
+ The USD mesh.
"""
shapes = reader.GetShapes()
attrib = reader.GetAttrib()
+ materials = reader.GetMaterials()
+
+ # This method only deals with a single mesh, so it only considers the first mesh.
obj_mesh = shapes[0].mesh
+ # Material references are identified by the ID assigned to each face of the mesh.
+ # This will be a common id for each mesh, so we'll take the first one.
+ material_id = obj_mesh.material_ids[0]
+
+ material_name = materials[material_id].name if material_id >= 0 else None
+
vertices = attrib.vertices
face_vertex_counts = obj_mesh.num_face_vertices
face_vertex_indices = obj_mesh.vertex_indices()
@@ -133,6 +158,12 @@ def _convert_single_obj(prim: Usd.Prim, input_path: pathlib.Path, reader: tinyob
)
if not usd_mesh:
Tf.Warn(f'Failed to convert mesh "{prim.GetPath()}" from {input_path}')
+
+ # If the mesh has a material, stores the material name for the mesh.
+ # Material binding is done on the Geometry layer, so no binding is done at this stage.
+ if material_name:
+ store_mesh_material_reference(input_path, usd_mesh.GetPrim().GetName(), material_name, data)
+
return usd_mesh
@@ -142,14 +173,19 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
Tf.Warn(f'Invalid input_path: "{input_path}" could not be parsed. {reader.Error()}')
return None
+ # Store the material data from the OBJ file.
+ store_obj_material_data(input_path, reader, data)
+
shapes = reader.GetShapes()
if len(shapes) == 0:
Tf.Warn(f'Invalid input_path: "{input_path}" contains no meshes')
return None
elif len(shapes) == 1:
- return _convert_single_obj(prim, input_path, reader)
+ # If there is only one shape, convert the single shape.
+ return _convert_single_obj(prim, input_path, reader, data)
attrib = reader.GetAttrib()
+ materials = reader.GetMaterials()
names = []
for shape in shapes:
@@ -160,6 +196,8 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
for shape, name, safe_name in zip(shapes, names, safe_names):
obj_mesh = shape.mesh
face_vertex_counts = obj_mesh.num_face_vertices
+ material_ids = obj_mesh.material_ids[0]
+ material = materials[material_ids] if material_ids >= 0 else None
# Get indices directly as arrays
vertex_indices_in_shape = np.array(obj_mesh.vertex_indices(), dtype=np.int32)
@@ -216,6 +254,11 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
Tf.Warn(f'Failed to convert mesh "{prim.GetPath()}" from {input_path}')
return None
+ # If the mesh has a material, stores the material name for the mesh.
+ # Material binding is done on the Geometry layer, so no binding is done at this stage.
+ if material and material.name:
+ store_mesh_material_reference(input_path, usd_mesh.GetPrim().GetName(), material.name, data)
+
if name != safe_name:
usdex.core.setDisplayName(usd_mesh.GetPrim(), name)
diff --git a/urdf_usd_converter/_impl/ros_package.py b/urdf_usd_converter/_impl/ros_package.py
index c996156..e772261 100644
--- a/urdf_usd_converter/_impl/ros_package.py
+++ b/urdf_usd_converter/_impl/ros_package.py
@@ -22,6 +22,9 @@ def resolve_ros_package_paths(uri: str, data: ConversionData) -> pathlib.Path:
Returns:
The resolved path.
"""
+ if uri in data.resolved_file_paths:
+ return data.resolved_file_paths[uri]
+
if "://" in uri and not uri.startswith("package://"):
protocol = uri.partition("://")[0]
Tf.Warn(f"'{protocol}' is not supported: {uri}")
@@ -45,7 +48,8 @@ def resolve_ros_package_paths(uri: str, data: ConversionData) -> pathlib.Path:
urdf_dir = data.urdf_parser.input_file.parent
# Convert the path to a relative path based on the urdf file.
- return resolved_path if resolved_path.is_absolute() else urdf_dir / resolved_path
+ data.resolved_file_paths[uri] = resolved_path if resolved_path.is_absolute() else urdf_dir / resolved_path
+ return data.resolved_file_paths[uri]
def search_ros_packages(urdf_parser: URDFParser) -> dict[str]:
diff --git a/urdf_usd_converter/_impl/urdf_parser/elements.py b/urdf_usd_converter/_impl/urdf_parser/elements.py
index 48b9000..26e993e 100644
--- a/urdf_usd_converter/_impl/urdf_parser/elements.py
+++ b/urdf_usd_converter/_impl/urdf_parser/elements.py
@@ -107,7 +107,7 @@ class ElementColor(ElementBase):
available_tag_names: ClassVar[list[str]] = ["color"]
_defaults: ClassVar[dict[str, Any]] = {
- "rgba": (0.0, 0.0, 0.0, 0.0),
+ "rgba": (1.0, 1.0, 1.0, 1.0),
}
def __init__(self):
diff --git a/urdf_usd_converter/_impl/urdf_parser/parser.py b/urdf_usd_converter/_impl/urdf_parser/parser.py
index 5202d64..85fdd2c 100644
--- a/urdf_usd_converter/_impl/urdf_parser/parser.py
+++ b/urdf_usd_converter/_impl/urdf_parser/parser.py
@@ -576,7 +576,7 @@ def _store_materials(self):
"""
# Global material names are unique, so they are stored as is.
for material in self.root_element.materials:
- color = material.color.get_with_default("rgba") if material.color else (0.0, 0.0, 0.0, 0.0)
+ color = material.color.get_with_default("rgba") if material.color else (1.0, 1.0, 1.0, 1.0)
texture = material.texture.get_with_default("filename") if material.texture else None
self.materials.append((material.name, color, texture))