Skip to content

Commit 68de997

Browse files
authored
OBJ: mesh GeomSubset support (#57)
1 parent 348bdbc commit 68de997

File tree

5 files changed

+240
-17
lines changed

5 files changed

+240
-17
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
newmtl Material_green
2+
Ns 250.000000
3+
Ka 1.000000 1.000000 1.000000
4+
Kd 0.000000 1.000000 0.000000
5+
Ks 0.500000 0.500000 0.500000
6+
Ke 0.000000 0.000000 0.000000
7+
Ni 1.500000
8+
d 1.000000
9+
illum 2
10+
11+
newmtl Material_red
12+
Ns 250.000000
13+
Ka 1.000000 1.000000 1.000000
14+
Kd 1.000000 0.000000 0.000000
15+
Ks 0.500000 0.500000 0.500000
16+
Ke 0.000000 0.000000 0.000000
17+
Ni 1.450000
18+
d 1.000000
19+
illum 2
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
mtllib box_two_materials.mtl
2+
o Cube
3+
v 0.500000 0.500000 -0.500000
4+
v 0.500000 -0.500000 -0.500000
5+
v 0.500000 0.500000 0.500000
6+
v 0.500000 -0.500000 0.500000
7+
v -0.500000 0.500000 -0.500000
8+
v -0.500000 -0.500000 -0.500000
9+
v -0.500000 0.500000 0.500000
10+
v -0.500000 -0.500000 0.500000
11+
vn -0.0000 1.0000 -0.0000
12+
vn -0.0000 -0.0000 1.0000
13+
vn -1.0000 -0.0000 -0.0000
14+
vn -0.0000 -1.0000 -0.0000
15+
vn 1.0000 -0.0000 -0.0000
16+
vn -0.0000 -0.0000 -1.0000
17+
vt 0.625000 0.500000
18+
vt 0.875000 0.500000
19+
vt 0.875000 0.750000
20+
vt 0.625000 0.750000
21+
vt 0.375000 0.750000
22+
vt 0.625000 1.000000
23+
vt 0.375000 1.000000
24+
vt 0.375000 0.000000
25+
vt 0.625000 0.000000
26+
vt 0.625000 0.250000
27+
vt 0.375000 0.250000
28+
vt 0.125000 0.500000
29+
vt 0.375000 0.500000
30+
vt 0.125000 0.750000
31+
s 0
32+
usemtl Material_red
33+
f 4/5/2 3/4/2 7/6/2 8/7/2
34+
f 8/8/3 7/9/3 5/10/3 6/11/3
35+
f 6/12/4 2/13/4 4/5/4 8/14/4
36+
usemtl Material_green
37+
f 1/1/1 5/2/1 7/3/1 3/4/1
38+
f 2/13/5 1/1/5 3/4/5 4/5/5
39+
f 6/11/6 5/10/6 1/1/6 2/13/6

tests/data/mesh_subsets.urdf

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<robot name="mesh_subsets">
3+
<link name="link_mesh_obj">
4+
<visual>
5+
<geometry>
6+
<mesh filename="assets/box_two_materials.obj" />
7+
</geometry>
8+
</visual>
9+
</link>
10+
<link name="link_mesh_dae">
11+
<visual>
12+
<geometry>
13+
<mesh filename="assets/box_two_materials.dae" />
14+
</geometry>
15+
</visual>
16+
</link>
17+
18+
<joint name="joint_mesh_obj_mesh_dae" type="fixed">
19+
<origin rpy="0 0 0" xyz="2 0 0"/>
20+
<parent link="link_mesh_obj"/>
21+
<child link="link_mesh_dae"/>
22+
</joint>
23+
</robot>

tests/testMaterial.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,3 +813,99 @@ def test_dae_materials(self):
813813
self.assertTrue(box_transparent_prim.IsA(UsdGeom.Mesh))
814814
self.assertTrue(box_transparent_prim.HasAuthoredReferences())
815815
self.check_material_binding(box_transparent_prim, material_transparent)
816+
817+
def test_mesh_subsets_materials(self):
818+
input_path = "tests/data/mesh_subsets.urdf"
819+
output_dir = self.tmpDir()
820+
821+
converter = urdf_usd_converter.Converter()
822+
asset_path = converter.convert(input_path, output_dir)
823+
824+
self.assertIsNotNone(asset_path)
825+
self.assertTrue(pathlib.Path(asset_path.path).exists())
826+
827+
stage: Usd.Stage = Usd.Stage.Open(asset_path.path)
828+
self.assertIsValidUsd(stage)
829+
830+
default_prim = stage.GetDefaultPrim()
831+
832+
# Check the materials.
833+
material_scope_prim = default_prim.GetChild("Materials")
834+
self.assertTrue(material_scope_prim.IsValid())
835+
836+
material_green_prim = material_scope_prim.GetChild("Material_green")
837+
self.assertTrue(material_green_prim.IsValid())
838+
self.assertTrue(material_green_prim.IsA(UsdShade.Material))
839+
material_green = UsdShade.Material(material_green_prim)
840+
841+
diffuse_color = self.get_material_diffuse_color(material_green)
842+
self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0, 1, 0), 1e-6))
843+
opacity = self.get_material_opacity(material_green)
844+
self.assertAlmostEqual(opacity, 1.0, places=6)
845+
846+
material_red_prim = material_scope_prim.GetChild("Material_red")
847+
self.assertTrue(material_red_prim.IsValid())
848+
self.assertTrue(material_red_prim.IsA(UsdShade.Material))
849+
material_red = UsdShade.Material(material_red_prim)
850+
851+
diffuse_color = self.get_material_diffuse_color(material_red)
852+
self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(1, 0, 0), 1e-6))
853+
opacity = self.get_material_opacity(material_red)
854+
self.assertAlmostEqual(opacity, 1.0, places=6)
855+
856+
material_red_1_prim = material_scope_prim.GetChild("Material_red_1")
857+
self.assertTrue(material_red_1_prim.IsValid())
858+
self.assertTrue(material_red_1_prim.IsA(UsdShade.Material))
859+
material_red_1 = UsdShade.Material(material_red_1_prim)
860+
861+
diffuse_color = self.get_material_diffuse_color(material_red_1)
862+
self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(1, 0, 0), 1e-6))
863+
opacity = self.get_material_opacity(material_red_1)
864+
self.assertAlmostEqual(opacity, 1.0, places=6)
865+
866+
material_green_1_prim = material_scope_prim.GetChild("Material_green_1")
867+
self.assertTrue(material_green_1_prim.IsValid())
868+
self.assertTrue(material_green_1_prim.IsA(UsdShade.Material))
869+
material_green_1 = UsdShade.Material(material_green_1_prim)
870+
871+
diffuse_color = self.get_material_diffuse_color(material_green_1)
872+
self.assertTrue(Gf.IsClose(diffuse_color, Gf.Vec3f(0, 1, 0), 1e-6))
873+
opacity = self.get_material_opacity(material_green_1)
874+
self.assertAlmostEqual(opacity, 1.0, places=6)
875+
876+
# Check the bindings.
877+
default_prim = stage.GetDefaultPrim()
878+
geometry_scope_prim = default_prim.GetChild("Geometry")
879+
self.assertTrue(geometry_scope_prim.IsValid())
880+
self.assertTrue(geometry_scope_prim.IsA(UsdGeom.Scope))
881+
link_mesh_obj_prim = geometry_scope_prim.GetChild("link_mesh_obj")
882+
box_materials_prim = link_mesh_obj_prim.GetChild("box_two_materials")
883+
self.assertTrue(box_materials_prim.IsValid())
884+
self.assertTrue(box_materials_prim.IsA(UsdGeom.Mesh))
885+
self.assertTrue(box_materials_prim.HasAuthoredReferences())
886+
887+
subset_001_prim = box_materials_prim.GetChild("GeomSubset_001")
888+
self.assertTrue(subset_001_prim.IsValid())
889+
self.assertTrue(subset_001_prim.IsA(UsdGeom.Subset))
890+
self.check_material_binding(subset_001_prim, material_green)
891+
892+
subset_002_prim = box_materials_prim.GetChild("GeomSubset_002")
893+
self.assertTrue(subset_002_prim.IsValid())
894+
self.assertTrue(subset_002_prim.IsA(UsdGeom.Subset))
895+
self.check_material_binding(subset_002_prim, material_red)
896+
897+
link_mesh_dae_prim = link_mesh_obj_prim.GetChild("link_mesh_dae")
898+
box_materials_prim = link_mesh_dae_prim.GetChild("box_two_materials")
899+
self.assertTrue(box_materials_prim.IsValid())
900+
self.assertTrue(box_materials_prim.IsA(UsdGeom.Mesh))
901+
self.assertTrue(box_materials_prim.HasAuthoredReferences())
902+
903+
subset_001_prim = box_materials_prim.GetChild("GeomSubset_001")
904+
self.assertTrue(subset_001_prim.IsValid())
905+
self.assertTrue(subset_001_prim.IsA(UsdGeom.Subset))
906+
self.check_material_binding(subset_001_prim, material_red_1)
907+
908+
subset_002_prim = box_materials_prim.GetChild("GeomSubset_002")
909+
self.assertTrue(subset_002_prim.IsValid())
910+
self.assertTrue(subset_002_prim.IsA(UsdGeom.Subset))
911+
self.check_material_binding(subset_002_prim, material_green_1)

urdf_usd_converter/_impl/mesh.py

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import stl
77
import tinyobjloader
88
import usdex.core
9-
from pxr import Tf, Usd, UsdGeom, Vt
9+
from pxr import Tf, Usd, UsdGeom, UsdShade, Vt
1010

1111
from .conversion_collada import convert_collada
1212
from .data import ConversionData, Tokens
@@ -95,6 +95,63 @@ def convert_stl(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
9595
return usd_mesh
9696

9797

98+
def _mesh_subsets_obj(
99+
mesh: UsdGeom.Mesh,
100+
input_path: pathlib.Path,
101+
reader: tinyobjloader.ObjReader,
102+
obj_mesh: tinyobjloader.mesh_t,
103+
data: ConversionData,
104+
):
105+
"""
106+
Create subsets for the mesh if there are multiple materials.
107+
It also stores the names of the materials assigned to the mesh.
108+
Material binding is done on the Material layer, so no binding is done at this stage.
109+
110+
Args:
111+
mesh: The USD mesh.
112+
input_path: The path to the OBJ file.
113+
reader: The tinyobjloader reader.
114+
obj_mesh: The tinyobjloader mesh.
115+
data: The conversion data.
116+
"""
117+
materials = reader.GetMaterials()
118+
119+
# Get a list of face numbers for each material_id from obj_mesh.material_ids.
120+
# If a material does not exist, the material_id for the face will be set to -1.
121+
face_list_by_material = {}
122+
material_ids_array = np.array(obj_mesh.material_ids)
123+
unique_material_ids = np.unique(material_ids_array)
124+
125+
if len(unique_material_ids) == 1:
126+
# If there is only one material. In this case, no subset is created.
127+
material_id = int(unique_material_ids[0])
128+
material_name = materials[material_id].name if material_id >= 0 else None
129+
if material_name:
130+
store_mesh_material_reference(input_path, mesh.GetPrim().GetName(), [material_name], data)
131+
return
132+
133+
for material_id in unique_material_ids:
134+
face_indices = np.where(material_ids_array == material_id)[0]
135+
face_list_by_material[int(material_id)] = Vt.IntArray.FromNumpy(face_indices)
136+
137+
stage = mesh.GetPrim().GetStage()
138+
139+
# If there are multiple materials. In this case, subsets are created.
140+
material_names = []
141+
for i, (material_id, face_indices) in enumerate(face_list_by_material.items()):
142+
material_name = materials[material_id].name if material_id >= 0 else None
143+
material_names.append(material_name)
144+
subset_name = f"GeomSubset_{(i+1):03d}"
145+
146+
geom_subset = UsdGeom.Subset.Define(stage, mesh.GetPrim().GetPath().AppendChild(subset_name))
147+
geom_subset.GetIndicesAttr().Set(face_indices)
148+
geom_subset.GetElementTypeAttr().Set(UsdGeom.Tokens.face)
149+
geom_subset.GetFamilyNameAttr().Set(UsdShade.Tokens.materialBind)
150+
151+
# Store the material names for the mesh.
152+
store_mesh_material_reference(input_path, mesh.GetPrim().GetName(), material_names, data)
153+
154+
98155
def _convert_single_obj(
99156
prim: Usd.Prim,
100157
input_path: pathlib.Path,
@@ -116,17 +173,10 @@ def _convert_single_obj(
116173
"""
117174
shapes = reader.GetShapes()
118175
attrib = reader.GetAttrib()
119-
materials = reader.GetMaterials()
120176

121177
# This method only deals with a single mesh, so it only considers the first mesh.
122178
obj_mesh = shapes[0].mesh
123179

124-
# Material references are identified by the ID assigned to each face of the mesh.
125-
# This will be a common id for each mesh, so we'll take the first one.
126-
material_id = obj_mesh.material_ids[0]
127-
128-
material_name = materials[material_id].name if material_id >= 0 else None
129-
130180
vertices = attrib.vertices
131181
face_vertex_counts = obj_mesh.num_face_vertices
132182
face_vertex_indices = obj_mesh.vertex_indices()
@@ -157,10 +207,9 @@ def _convert_single_obj(
157207
if not usd_mesh:
158208
Tf.Warn(f'Failed to convert mesh "{prim.GetPath()}" from {input_path}')
159209

160-
# If the mesh has a material, stores the material name for the mesh.
210+
# Create subsets for the mesh if there are multiple materials.
161211
# Material binding is done on the Geometry layer, so no binding is done at this stage.
162-
if material_name:
163-
store_mesh_material_reference(input_path, usd_mesh.GetPrim().GetName(), [material_name], data)
212+
_mesh_subsets_obj(usd_mesh, input_path, reader, obj_mesh, data)
164213

165214
return usd_mesh
166215

@@ -183,7 +232,6 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
183232
return _convert_single_obj(prim, input_path, reader, data)
184233

185234
attrib = reader.GetAttrib()
186-
materials = reader.GetMaterials()
187235

188236
names = []
189237
for shape in shapes:
@@ -193,9 +241,8 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
193241

194242
for shape, name, safe_name in zip(shapes, names, safe_names):
195243
obj_mesh = shape.mesh
244+
196245
face_vertex_counts = Vt.IntArray(obj_mesh.num_face_vertices)
197-
material_ids = obj_mesh.material_ids[0]
198-
material = materials[material_ids] if material_ids >= 0 else None
199246

200247
# Get indices directly as arrays
201248
vertex_indices_in_shape = np.array(obj_mesh.vertex_indices(), dtype=np.int32)
@@ -250,10 +297,9 @@ def convert_obj(prim: Usd.Prim, input_path: pathlib.Path, data: ConversionData)
250297
Tf.Warn(f'Failed to convert mesh "{prim.GetPath()}" from {input_path}')
251298
return None
252299

253-
# If the mesh has a material, stores the material name for the mesh.
300+
# Create subsets for the mesh if there are multiple materials.
254301
# Material binding is done on the Geometry layer, so no binding is done at this stage.
255-
if material and material.name:
256-
store_mesh_material_reference(input_path, usd_mesh.GetPrim().GetName(), [material.name], data)
302+
_mesh_subsets_obj(usd_mesh, input_path, reader, obj_mesh, data)
257303

258304
if name != safe_name:
259305
usdex.core.setDisplayName(usd_mesh.GetPrim(), name)

0 commit comments

Comments
 (0)