From 8c4e9a1ecbec9cbc5323789b8c1bdc1d0021c237 Mon Sep 17 00:00:00 2001 From: LeonLiu4 Date: Tue, 1 Jul 2025 10:51:13 -0700 Subject: [PATCH 1/4] urdf-glb support --- genesis/options/textures.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/genesis/options/textures.py b/genesis/options/textures.py index 691c823a38..4170e1df9f 100644 --- a/genesis/options/textures.py +++ b/genesis/options/textures.py @@ -141,6 +141,30 @@ def __init__(self, **data): elif self.image_array is not None: if not isinstance(self.image_array, np.ndarray): gs.raise_exception("`image_array` needs to be an numpy array.") + if self.image_array.dtype != np.uint8: + if self.image_array.dtype in (np.float32, np.float64): + if self.image_array.max() <= 1.0: + self.image_array = (self.image_array * 255.0).round() + self.image_array = np.clip(self.image_array, 0.0, 255.0).astype(np.uint8) + elif self.image_array.dtype == np.bool_: + self.image_array = self.image_array.astype(np.uint8) * 255 + + elif np.issubdtype(self.image_array.dtype, np.integer): + self.image_array = np.clip(self.image_array, 0, 255).astype(np.uint8) + else: + gs.raise_exception( + f"Unsupported image dtype {self.image_array.dtype}. Only uint8 or float32/64 are supported." + ) + if self.image_array.ndim == 2: + self.image_array = np.stack([self.image_array] * 3, axis=-1) + + elif self.image_array.shape[2] == 1: + self.image_array = np.repeat(self.image_array, 3, axis=2) + + elif self.image_array.shape[2] == 2: + L = self.image_array[..., 0] + A = self.image_array[..., 1] + self.image_array = np.stack([L, L, L, A], axis=-1) # calculate channel if self.image_array is None: @@ -163,8 +187,6 @@ def __init__(self, **data): if self.encoding not in ["srgb", "linear"]: gs.raise_exception(f"Invalid image encoding: {self.encoding}.") - assert self.image_array is None or self.image_array.dtype == np.uint8 - def check_dim(self, dim): if self.image_array is not None: if self._channel > dim: From 5cf6b25f9293e901e68ee8eb569bdb180d9e5644 Mon Sep 17 00:00:00 2001 From: LeonLiu4 Date: Tue, 1 Jul 2025 14:02:31 -0700 Subject: [PATCH 2/4] added unit test --- tests/test_mesh.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 2431935ddc..b514f6be1b 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -3,6 +3,7 @@ import numpy as np import os from huggingface_hub import snapshot_download +from pathlib import Path import genesis as gs import genesis.utils.mesh as mu @@ -292,3 +293,24 @@ def test_usd_parse(usd_filename): check_gs_textures( gs_glb_material.emissive_texture, gs_usd_material.emissive_texture, 0.0, material_name, "emissive" ) + + +@pytest.mark.required +def test_urdf_with_existing_glb(): + assets = Path(gs.utils.get_assets_dir()) + glb_path = assets / "usd" / "sneaker_airforce.glb" + + tmp_path = Path(__file__).parent + urdf_path = tmp_path / "model.urdf" + urdf_path.write_text( + f""" + + + + + + """ + ) + scene = gs.Scene(show_viewer=False) + scene.build() + scene.step() From 471b50537f0fc43347f8f63037e78e481ec70d1a Mon Sep 17 00:00:00 2001 From: LeonLiu4 Date: Wed, 2 Jul 2025 11:26:01 -0700 Subject: [PATCH 3/4] extra unit tests and fixed formatting in textures --- genesis/options/textures.py | 34 ++++++++-------- tests/test_mesh.py | 81 ++++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/genesis/options/textures.py b/genesis/options/textures.py index 4170e1df9f..54b4d5370a 100644 --- a/genesis/options/textures.py +++ b/genesis/options/textures.py @@ -140,27 +140,27 @@ def __init__(self, **data): elif self.image_array is not None: if not isinstance(self.image_array, np.ndarray): - gs.raise_exception("`image_array` needs to be an numpy array.") - if self.image_array.dtype != np.uint8: - if self.image_array.dtype in (np.float32, np.float64): - if self.image_array.max() <= 1.0: - self.image_array = (self.image_array * 255.0).round() - self.image_array = np.clip(self.image_array, 0.0, 255.0).astype(np.uint8) - elif self.image_array.dtype == np.bool_: - self.image_array = self.image_array.astype(np.uint8) * 255 - - elif np.issubdtype(self.image_array.dtype, np.integer): - self.image_array = np.clip(self.image_array, 0, 255).astype(np.uint8) - else: - gs.raise_exception( - f"Unsupported image dtype {self.image_array.dtype}. Only uint8 or float32/64 are supported." - ) + gs.raise_exception("`image_array` needs to be a numpy array.") + if self.image_array.dtype == np.uint8: + pass + elif np.issubdtype(self.image_array.dtype, np.floating): + if self.image_array.max() <= 1.0: + self.image_array = (self.image_array * 255.0).round() + self.image_array = np.clip(self.image_array, 0.0, 255.0).astype(np.uint8) + elif self.image_array.dtype == np.bool_: + self.image_array = self.image_array.astype(np.uint8) * 255 + elif np.issubdtype(self.image_array.dtype, np.integer): + self.image_array = np.clip(self.image_array, 0, 255).astype(np.uint8) + else: + gs.raise_exception( + f"Unsupported image dtype {self.image_array.dtype}. " + "Only uint8, integer, floating-point, or bool types are supported." + ) + if self.image_array.ndim == 2: self.image_array = np.stack([self.image_array] * 3, axis=-1) - elif self.image_array.shape[2] == 1: self.image_array = np.repeat(self.image_array, 3, axis=2) - elif self.image_array.shape[2] == 2: L = self.image_array[..., 0] A = self.image_array[..., 1] diff --git a/tests/test_mesh.py b/tests/test_mesh.py index b514f6be1b..700b1b445b 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -296,11 +296,9 @@ def test_usd_parse(usd_filename): @pytest.mark.required -def test_urdf_with_existing_glb(): +def test_urdf_with_existing_glb(tmp_path, show_viewer): assets = Path(gs.utils.get_assets_dir()) glb_path = assets / "usd" / "sneaker_airforce.glb" - - tmp_path = Path(__file__).parent urdf_path = tmp_path / "model.urdf" urdf_path.write_text( f""" @@ -309,8 +307,81 @@ def test_urdf_with_existing_glb(): - """ + + """ + ) + scene = gs.Scene( + show_viewer=show_viewer, + show_fps=False, + ) + scene.build() + scene.step() + + +@pytest.mark.required +def test_urdf_with_float32_grayscale_glb(tmp_path, show_viewer): + glb_path = tmp_path / "gray.glb" + img = np.random.rand(16, 16).astype(np.float32) + vertices = np.array( + [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.5, 0.5, 0.0], [-0.5, 0.5, 0.0]], + dtype=np.float32, + ) + faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32) + mesh = trimesh.Trimesh(vertices, faces, process=False) + mesh.visual = trimesh.visual.texture.TextureVisuals( + uv=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32), + material=trimesh.visual.material.SimpleMaterial(image=img), + ) + trimesh.Scene([mesh]).export(glb_path) + urdf_path = tmp_path / "model_gray.urdf" + urdf_path.write_text( + f""" + + + + + + + """ + ) + scene = gs.Scene( + show_viewer=show_viewer, + show_fps=False, + ) + scene.build() + scene.step() + + +@pytest.mark.required +def test_urdf_with_float64_two_channel_glb(tmp_path, show_viewer): + glb_path = tmp_path / "rg.glb" + img = np.random.rand(16, 16, 2).astype(np.float64) + vertices = np.array( + [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.5, 0.5, 0.0], [-0.5, 0.5, 0.0]], + dtype=np.float32, + ) + faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32) + mesh = trimesh.Trimesh(vertices, faces, process=False) + mesh.visual = trimesh.visual.texture.TextureVisuals( + uv=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32), + material=trimesh.visual.material.SimpleMaterial(image=img), + ) + trimesh.Scene([mesh]).export(glb_path) + + urdf_path = tmp_path / "model_rg.urdf" + urdf_path.write_text( + f""" + + + + + + + """ + ) + scene = gs.Scene( + show_viewer=show_viewer, + show_fps=False, ) - scene = gs.Scene(show_viewer=False) scene.build() scene.step() From 61f266f569d43b7ea20b4d901dc32ade0484df3d Mon Sep 17 00:00:00 2001 From: LeonLiu4 Date: Wed, 2 Jul 2025 18:26:15 -0700 Subject: [PATCH 4/4] fixed bugs in textures and fixed formatting in test_mesh --- genesis/options/textures.py | 48 ++++++++--------- tests/test_mesh.py | 101 ++++++++++++++---------------------- 2 files changed, 63 insertions(+), 86 deletions(-) diff --git a/genesis/options/textures.py b/genesis/options/textures.py index 54b4d5370a..e5dda5f0b2 100644 --- a/genesis/options/textures.py +++ b/genesis/options/textures.py @@ -141,33 +141,33 @@ def __init__(self, **data): elif self.image_array is not None: if not isinstance(self.image_array, np.ndarray): gs.raise_exception("`image_array` needs to be a numpy array.") - if self.image_array.dtype == np.uint8: - pass - elif np.issubdtype(self.image_array.dtype, np.floating): - if self.image_array.max() <= 1.0: - self.image_array = (self.image_array * 255.0).round() - self.image_array = np.clip(self.image_array, 0.0, 255.0).astype(np.uint8) - elif self.image_array.dtype == np.bool_: - self.image_array = self.image_array.astype(np.uint8) * 255 - elif np.issubdtype(self.image_array.dtype, np.integer): - self.image_array = np.clip(self.image_array, 0, 255).astype(np.uint8) - else: - gs.raise_exception( - f"Unsupported image dtype {self.image_array.dtype}. " - "Only uint8, integer, floating-point, or bool types are supported." - ) - - if self.image_array.ndim == 2: - self.image_array = np.stack([self.image_array] * 3, axis=-1) - elif self.image_array.shape[2] == 1: - self.image_array = np.repeat(self.image_array, 3, axis=2) - elif self.image_array.shape[2] == 2: - L = self.image_array[..., 0] - A = self.image_array[..., 1] - self.image_array = np.stack([L, L, L, A], axis=-1) + arr = self.image_array + if arr.dtype != np.uint8: + if np.issubdtype(arr.dtype, np.floating): + if arr.max() <= 1.0: + arr = (arr * 255.0).round() + arr = np.clip(arr, 0.0, 255.0).astype(np.uint8) + elif arr.dtype == np.bool_: + arr = arr.astype(np.uint8) * 255 + elif np.issubdtype(arr.dtype, np.integer): + arr = np.clip(arr, 0, 255).astype(np.uint8) + else: + gs.raise_exception( + f"Unsupported image dtype {arr.dtype}. " + "Only uint8, integer, floating-point, or bool types are supported." + ) + self.image_array = arr # calculate channel if self.image_array is None: + if isinstance(self.resolution, (tuple, list)): + H, W = self.resolution + else: + H = W = self.resolution + + # Default to 3-channel RGB + white = np.array([255, 255, 255], dtype=np.uint8) + self.image_array = np.full((H, W, 3), white, dtype=np.uint8) self._mean_color = np.array([1.0, 1.0, 1.0], dtype=np.float16) self._channel = 3 else: diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 700b1b445b..2415355051 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -1,16 +1,15 @@ -import pytest -import trimesh -import numpy as np import os -from huggingface_hub import snapshot_download from pathlib import Path +import numpy as np +import pytest +import trimesh import genesis as gs -import genesis.utils.mesh as mu import genesis.utils.gltf as gltf_utils +import genesis.utils.mesh as mu import genesis.utils.usda as usda_utils -from .utils import get_hf_assets, assert_allclose, assert_array_equal +from .utils import assert_allclose, assert_array_equal, get_hf_assets VERTICES_TOL = 1e-05 # Transformation loses a little precision in vertices NORMALS_TOL = 1e-02 # Conversion from .usd to .glb loses a little precision in normals @@ -302,86 +301,64 @@ def test_urdf_with_existing_glb(tmp_path, show_viewer): urdf_path = tmp_path / "model.urdf" urdf_path.write_text( f""" - - - - - - - """ + + + + + + + """ ) scene = gs.Scene( show_viewer=show_viewer, - show_fps=False, + show_FPS=False, ) scene.build() scene.step() @pytest.mark.required -def test_urdf_with_float32_grayscale_glb(tmp_path, show_viewer): - glb_path = tmp_path / "gray.glb" - img = np.random.rand(16, 16).astype(np.float32) +@pytest.mark.parametrize( + "n_channels, float_type", + [ + (1, np.float32), # grayscale → H×W + (2, np.float64), # L+A → H×W×2 + ], +) +def test_urdf_with_float_texture_glb(tmp_path, show_viewer, n_channels, float_type): vertices = np.array( [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.5, 0.5, 0.0], [-0.5, 0.5, 0.0]], dtype=np.float32, ) faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32) + mesh = trimesh.Trimesh(vertices, faces, process=False) - mesh.visual = trimesh.visual.texture.TextureVisuals( - uv=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32), - material=trimesh.visual.material.SimpleMaterial(image=img), - ) - trimesh.Scene([mesh]).export(glb_path) - urdf_path = tmp_path / "model_gray.urdf" - urdf_path.write_text( - f""" - - - - - - - """ - ) - scene = gs.Scene( - show_viewer=show_viewer, - show_fps=False, - ) - scene.build() - scene.step() + H = W = 16 + if n_channels == 1: + img = np.random.rand(H, W).astype(float_type) + else: + img = np.random.rand(H, W, n_channels).astype(float_type) -@pytest.mark.required -def test_urdf_with_float64_two_channel_glb(tmp_path, show_viewer): - glb_path = tmp_path / "rg.glb" - img = np.random.rand(16, 16, 2).astype(np.float64) - vertices = np.array( - [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.5, 0.5, 0.0], [-0.5, 0.5, 0.0]], - dtype=np.float32, - ) - faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32) - mesh = trimesh.Trimesh(vertices, faces, process=False) mesh.visual = trimesh.visual.texture.TextureVisuals( uv=np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32), material=trimesh.visual.material.SimpleMaterial(image=img), ) + + glb_path = tmp_path / f"tex_{n_channels}c.glb" + urdf_path = tmp_path / f"tex_{n_channels}c.urdf" trimesh.Scene([mesh]).export(glb_path) - urdf_path = tmp_path / "model_rg.urdf" urdf_path.write_text( - f""" - - - - - - - """ - ) - scene = gs.Scene( - show_viewer=show_viewer, - show_fps=False, + f""" + + + + + + + """ ) + scene = gs.Scene(show_viewer=show_viewer, show_FPS=False) scene.build() scene.step()