Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion genesis/engine/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def from_trimesh(
must_update_surface = True
roughness_factor = None
color_image = None
color_image_path = None
Comment on lines 234 to +235
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend not supporting specifying both and raise an exception if it happens. Then load image files directly inside this method if provided.

color_factor = None
opacity = 1.0

Expand All @@ -254,6 +255,8 @@ def from_trimesh(
elif isinstance(material, trimesh.visual.material.SimpleMaterial):
if material.image is not None:
color_image = mu.PIL_to_array(material.image)
# Check if the material has stored the original image path
color_image_path = material.kwargs.get("image_path", None)
elif material.diffuse is not None:
color_factor = tuple(np.array(material.diffuse, dtype=np.float32) / 255.0)

Expand Down Expand Up @@ -284,7 +287,7 @@ def from_trimesh(
color_factor = (1.0, 1.0, 1.0, 1.0)

if must_update_surface:
color_texture = mu.create_texture(color_image, color_factor, "srgb")
color_texture = mu.create_texture(color_image, color_factor, "srgb", image_path=color_image_path)
opacity_texture = None
if color_texture is not None:
opacity_texture = color_texture.check_dim(3)
Expand Down
27 changes: 16 additions & 11 deletions genesis/options/textures.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ class Config:
def __init__(self, **data):
super().__init__(**data)

if not (self.image_path is None) ^ (self.image_array is None):
# Allow both image_path and image_array, but require at least one
if self.image_path is None and self.image_array is None:
Comment on lines -115 to +116
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense.

gs.raise_exception("Please set either `image_path` or `image_array`.")

if self.image_path is not None:
Expand All @@ -121,17 +122,21 @@ def __init__(self, **data):
self.image_path = os.path.join(gs.utils.get_assets_dir(), self.image_path)

if not os.path.exists(self.image_path):
gs.raise_exception(
f"File not found in either current directory or assets directory: '{input_image_path}'."
)
if self.image_array is None:
# Only error if we don't have image_array as fallback
gs.raise_exception(
f"File not found in either current directory or assets directory: '{input_image_path}'."
)

# Load image_path as actual image_array, unless for special texture images (e.g. `.hdr` and `.exr`) that are only supported by raytracers
if self.image_path.endswith(HDR_EXTENSIONS):
self.encoding = "linear" # .exr or .hdr images should be encoded with 'linear'
if self.image_path.endswith((".exr")):
self.image_path = mu.check_exr_compression(self.image_path)
else:
self.image_array = np.array(Image.open(self.image_path))
# Only load image if we don't already have image_array
if self.image_array is None:
# Load image_path as actual image_array, unless for special texture images (e.g. `.hdr` and `.exr`) that are only supported by raytracers
if self.image_path.endswith(HDR_EXTENSIONS):
self.encoding = "linear" # .exr or .hdr images should be encoded with 'linear'
if self.image_path.endswith((".exr")):
self.image_path = mu.check_exr_compression(self.image_path)
else:
self.image_array = np.array(Image.open(self.image_path))

elif self.image_array is not None:
if not isinstance(self.image_array, np.ndarray):
Expand Down
17 changes: 12 additions & 5 deletions genesis/utils/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,12 @@ def surface_uvs_to_trimesh_visual(surface, uvs=None, n_verts=None):
uvs = uvs.copy()
uvs[:, 1] = 1.0 - uvs[:, 1]
assert texture.image_array.dtype == np.uint8
material_kwargs = {"image": Image.fromarray(texture.image_array), "diffuse": (1.0, 1.0, 1.0, 1.0)}
if texture.image_path is not None:
material_kwargs["image_path"] = texture.image_path
visual = trimesh.visual.TextureVisuals(
uv=uvs,
material=trimesh.visual.material.SimpleMaterial(
image=Image.fromarray(texture.image_array), diffuse=(1.0, 1.0, 1.0, 1.0)
),
material=trimesh.visual.material.SimpleMaterial(**material_kwargs),
)
else:
# fall back to color texture
Expand Down Expand Up @@ -499,9 +500,14 @@ def tonemapped(image):
return (np.clip(np.power(image / 255 * np.power(2, exposure), 1 / 2.2), 0, 1) * 255).astype(np.uint8)


def create_texture(image, factor, encoding):
def create_texture(image, factor, encoding, image_path=None):
if image is not None:
return gs.textures.ImageTexture(image_array=image, image_color=factor, encoding=encoding)
return gs.textures.ImageTexture(
image_array=image,
image_path=image_path,
image_color=factor,
encoding=encoding,
)
Comment on lines +503 to +510
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. This API is a bit broken I think. What about adding other method called create_texture_from_path or something, that would do the internal plumbing? Because allowing to specify both image and image_path is weird.

if factor is not None:
return gs.textures.ColorTexture(color=factor)
return None
Expand Down Expand Up @@ -928,6 +934,7 @@ def create_plane(normal=(0.0, 0.0, 1.0), plane_size=(1e3, 1e3), tile_size=(1, 1)
),
material=trimesh.visual.material.SimpleMaterial(
image=Image.open(os.path.join(get_assets_dir(), "textures/checker.png")),
image_path="textures/checker.png",
Copy link
Collaborator

@duburcqa duburcqa Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like dark magic. This is not officially supported by Trimesh from what I know, but maybe I'm wrong.

),
)
else:
Expand Down
22 changes: 22 additions & 0 deletions tests/test_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,28 @@ def test_2_channels_luminance_alpha_textures(show_viewer):
)
)

@pytest.mark.required
def test_plane_texture_path_preservation(show_viewer):
"""Test that plane primitives preserve texture paths through the mesh pipeline."""
scene = gs.Scene(show_viewer=show_viewer, show_FPS=False)
plane = scene.add_entity(gs.morphs.Plane())
scene.build()

# Check that the plane's vgeom has a texture with a path
assert len(plane.vgeoms) > 0
vgeom = plane.vgeoms[0]
assert vgeom.vmesh is not None
assert vgeom.vmesh.surface is not None

texture = vgeom.vmesh.surface.diffuse_texture
assert texture is not None
assert isinstance(texture, gs.textures.ImageTexture)
Comment on lines +520 to +528
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. These checks seem to be completely unrelated to what this test is pretending to check. Either broaden the scope of this unit test (test name, docstring, comments...) or remove these checks.


# The texture should have both image_path and image_array
assert texture.image_path is not None
assert texture.image_array is not None
Comment on lines +521 to +532
Copy link
Collaborator

@YilingQiao YilingQiao Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those asserts are not helpful

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Either redundant or out of scope.

assert "checker.png" in texture.image_path


@pytest.mark.required
@pytest.mark.skipif(platform.machine() == "aarch64", reason="Module 'tetgen' is crashing on Linux ARM.")
Expand Down
Loading