Skip to content

Commit ae7dcdc

Browse files
authored
[FEATURE] URDF-GLB import: support non-uint8 + 1/2-channel textures (#1343)
1 parent 9e1980d commit ae7dcdc

File tree

2 files changed

+100
-8
lines changed

2 files changed

+100
-8
lines changed

genesis/options/textures.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,34 @@ def __init__(self, **data):
140140

141141
elif self.image_array is not None:
142142
if not isinstance(self.image_array, np.ndarray):
143-
gs.raise_exception("`image_array` needs to be an numpy array.")
143+
gs.raise_exception("`image_array` needs to be a numpy array.")
144+
arr = self.image_array
145+
if arr.dtype != np.uint8:
146+
if np.issubdtype(arr.dtype, np.floating):
147+
if arr.max() <= 1.0:
148+
arr = (arr * 255.0).round()
149+
arr = np.clip(arr, 0.0, 255.0).astype(np.uint8)
150+
elif arr.dtype == np.bool_:
151+
arr = arr.astype(np.uint8) * 255
152+
elif np.issubdtype(arr.dtype, np.integer):
153+
arr = np.clip(arr, 0, 255).astype(np.uint8)
154+
else:
155+
gs.raise_exception(
156+
f"Unsupported image dtype {arr.dtype}. "
157+
"Only uint8, integer, floating-point, or bool types are supported."
158+
)
159+
self.image_array = arr
144160

145161
# calculate channel
146162
if self.image_array is None:
163+
if isinstance(self.resolution, (tuple, list)):
164+
H, W = self.resolution
165+
else:
166+
H = W = self.resolution
167+
168+
# Default to 3-channel RGB
169+
white = np.array([255, 255, 255], dtype=np.uint8)
170+
self.image_array = np.full((H, W, 3), white, dtype=np.uint8)
147171
self._mean_color = np.array([1.0, 1.0, 1.0], dtype=np.float16)
148172
self._channel = 3
149173
else:
@@ -163,8 +187,6 @@ def __init__(self, **data):
163187
if self.encoding not in ["srgb", "linear"]:
164188
gs.raise_exception(f"Invalid image encoding: {self.encoding}.")
165189

166-
assert self.image_array is None or self.image_array.dtype == np.uint8
167-
168190
def check_dim(self, dim):
169191
if self.image_array is not None:
170192
if self._channel > dim:

tests/test_mesh.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
import os
2+
from pathlib import Path
3+
import numpy as np
14
import pytest
25
import trimesh
3-
import numpy as np
4-
import os
5-
from huggingface_hub import snapshot_download
66

77
import genesis as gs
8-
import genesis.utils.mesh as mu
98
import genesis.utils.gltf as gltf_utils
9+
import genesis.utils.mesh as mu
1010
import genesis.utils.usda as usda_utils
1111

12-
from .utils import get_hf_assets, assert_allclose, assert_array_equal
12+
from .utils import assert_allclose, assert_array_equal, get_hf_assets
1313

1414
VERTICES_TOL = 1e-05 # Transformation loses a little precision in vertices
1515
NORMALS_TOL = 1e-02 # Conversion from .usd to .glb loses a little precision in normals
@@ -292,3 +292,73 @@ def test_usd_parse(usd_filename):
292292
check_gs_textures(
293293
gs_glb_material.emissive_texture, gs_usd_material.emissive_texture, 0.0, material_name, "emissive"
294294
)
295+
296+
297+
@pytest.mark.required
298+
def test_urdf_with_existing_glb(tmp_path, show_viewer):
299+
assets = Path(gs.utils.get_assets_dir())
300+
glb_path = assets / "usd" / "sneaker_airforce.glb"
301+
urdf_path = tmp_path / "model.urdf"
302+
urdf_path.write_text(
303+
f"""<robot name="shoe">
304+
<link name="base">
305+
<visual>
306+
<geometry><mesh filename="{glb_path}"/></geometry>
307+
</visual>
308+
</link>
309+
</robot>
310+
"""
311+
)
312+
scene = gs.Scene(
313+
show_viewer=show_viewer,
314+
show_FPS=False,
315+
)
316+
scene.build()
317+
scene.step()
318+
319+
320+
@pytest.mark.required
321+
@pytest.mark.parametrize(
322+
"n_channels, float_type",
323+
[
324+
(1, np.float32), # grayscale → H×W
325+
(2, np.float64), # L+A → H×W×2
326+
],
327+
)
328+
def test_urdf_with_float_texture_glb(tmp_path, show_viewer, n_channels, float_type):
329+
vertices = np.array(
330+
[[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.5, 0.5, 0.0], [-0.5, 0.5, 0.0]],
331+
dtype=np.float32,
332+
)
333+
faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32)
334+
335+
mesh = trimesh.Trimesh(vertices, faces, process=False)
336+
337+
H = W = 16
338+
if n_channels == 1:
339+
img = np.random.rand(H, W).astype(float_type)
340+
else:
341+
img = np.random.rand(H, W, n_channels).astype(float_type)
342+
343+
mesh.visual = trimesh.visual.texture.TextureVisuals(
344+
uv=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32),
345+
material=trimesh.visual.material.SimpleMaterial(image=img),
346+
)
347+
348+
glb_path = tmp_path / f"tex_{n_channels}c.glb"
349+
urdf_path = tmp_path / f"tex_{n_channels}c.urdf"
350+
trimesh.Scene([mesh]).export(glb_path)
351+
352+
urdf_path.write_text(
353+
f"""<robot name="tex{n_channels}c">
354+
<link name="base">
355+
<visual>
356+
<geometry><mesh filename="{glb_path}"/></geometry>
357+
</visual>
358+
</link>
359+
</robot>
360+
"""
361+
)
362+
scene = gs.Scene(show_viewer=show_viewer, show_FPS=False)
363+
scene.build()
364+
scene.step()

0 commit comments

Comments
 (0)