Skip to content

Fixed visual prims handling during texture randomization. #2476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "0.40.5"
version = "0.40.6"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
9 changes: 9 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
---------

0.40.6 (2025-06-12)
~~~~~~~~~~~~~~~~~~~

Fixed
^^^^^

* Fixed potential issues in :func:`~isaaclab.envs.mdp.events.randomize_visual_texture_material` related to handling visual prims during texture randomization.


0.40.5 (2025-05-22)
~~~~~~~~~~~~~~~~~~~

Expand Down
22 changes: 19 additions & 3 deletions source/isaaclab/isaaclab/envs/mdp/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,8 +1229,25 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
body_names_regex = ".*"

# create the affected prim path
# TODO: Remove the hard-coded "/visuals" part.
prim_path = f"{asset.cfg.prim_path}/{body_names_regex}/visuals"
# Check if the pattern with '/visuals' yields results when matching `body_names_regex`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please modify the unit test for this function to showcase the cases that's getting handled here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @Mayankm96, Thank you for the suggestion. The change I'm implementing aims to handle cases where the visual prim path does not end with visuals. I've checked the assets used in the test cases, and all of them end with visuals.

In my use case, the table prim path I'm working with is something like /World/envs/env_./Table/.. I haven't found any assets that could show this benefit in the test file. Do you have any suggestions?

Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Mayankm96, Could you please take a look at the test I added? I mocked the find_matching_prim_paths function to return an empty list, allowing it to trigger the fallback behavior. This tests the scenario where the /visuals pattern doesn't match. thanks.

# If not, fall back to a broader pattern without '/visuals'.
asset_main_prim_path = asset.cfg.prim_path
# Try the pattern with '/visuals' first for the generic case
pattern_with_visuals = f"{asset_main_prim_path}/{body_names_regex}/visuals"
# Use sim_utils to check if any prims currently match this pattern
matching_prims = sim_utils.find_matching_prim_paths(pattern_with_visuals)
if matching_prims:
# If matches are found, use the pattern with /visuals
prim_path = pattern_with_visuals
else:
# If no matches found, fall back to the broader pattern without /visuals
# This pattern (e.g., /World/envs/env_.*/Table/.*) should match visual prims
# whether they end in /visuals or have other structures.
prim_path = f"{asset_main_prim_path}/.*"
carb.log_info(
f"Pattern '{pattern_with_visuals}' found no prims. Falling back to '{prim_path}' for texture"
" randomization."
)

# Create the omni-graph node for the randomization term
def rep_texture_randomization():
Expand All @@ -1240,7 +1257,6 @@ def rep_texture_randomization():
rep.randomizer.texture(
textures=texture_paths, project_uvw=True, texture_rotate=rep.distribution.uniform(*texture_rotation)
)

return prims_group.node

# Register the event to the replicator
Expand Down
91 changes: 91 additions & 0 deletions source/isaaclab/test/envs/test_texture_randomization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import math
import torch
import unittest
from unittest.mock import patch

import omni.usd

Expand Down Expand Up @@ -127,6 +128,36 @@ class EventCfg:
)


@configclass
class EventCfgFallback:
"""Configuration for events that tests the fallback mechanism."""

# Test fallback when /visuals pattern doesn't match
test_fallback_texture_randomizer = EventTerm(
func=mdp.randomize_visual_texture_material,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=["slider"]),
"texture_paths": [
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Bamboo_Planks/Bamboo_Planks_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Cherry/Cherry_BaseColor.png",
],
"event_name": "test_fallback_texture_randomizer",
"texture_rotation": (0.0, 0.0),
},
)

reset_cart_position = EventTerm(
func=mdp.reset_joints_by_offset,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]),
"position_range": (-1.0, 1.0),
"velocity_range": (-0.1, 0.1),
},
)


@configclass
class CartpoleEnvCfg(ManagerBasedEnvCfg):
"""Configuration for the cartpole environment."""
Expand All @@ -150,6 +181,29 @@ def __post_init__(self):
self.sim.dt = 0.005 # sim step every 5ms: 200Hz


@configclass
class CartpoleEnvCfgFallback(ManagerBasedEnvCfg):
"""Configuration for the cartpole environment that tests fallback mechanism."""

# Scene settings
scene = CartpoleSceneCfg(env_spacing=2.5)

# Basic settings
actions = ActionsCfg()
observations = ObservationsCfg()
events = EventCfgFallback()

def __post_init__(self):
"""Post initialization."""
# viewer settings
self.viewer.eye = (4.5, 0.0, 6.0)
self.viewer.lookat = (0.0, 0.0, 2.0)
# step settings
self.decimation = 4 # env step every 4 sim steps: 200Hz / 4 = 50Hz
# simulation settings
self.sim.dt = 0.005 # sim step every 5ms: 200Hz


class TestTextureRandomization(unittest.TestCase):
"""Test for texture randomization"""

Expand Down Expand Up @@ -186,6 +240,43 @@ def test_texture_randomization(self):

env.close()

def test_texture_randomization_fallback(self):
"""Test texture randomization fallback mechanism when /visuals pattern doesn't match."""

def mock_find_matching_prim_paths(pattern):
"""Mock function that simulates a case where /visuals pattern doesn't match."""
# If the pattern contains '/visuals', return empty list to trigger fallback
if pattern.endswith('/visuals'):
return []

for device in ["cpu", "cuda"]:
with self.subTest(device=device):
# create a new stage
omni.usd.get_context().new_stage()

# set the arguments - use fallback config
env_cfg = CartpoleEnvCfgFallback()
env_cfg.scene.num_envs = 16
env_cfg.scene.replicate_physics = False
env_cfg.sim.device = device

with patch.object(mdp.events.sim_utils, 'find_matching_prim_paths', side_effect=mock_find_matching_prim_paths):
# This should trigger the fallback mechanism and log the fallback message
env = ManagerBasedEnv(cfg=env_cfg)

# simulate physics
with torch.inference_mode():
for count in range(20): # shorter test for fallback
# reset every few steps to check nothing breaks
if count % 10 == 0:
env.reset()
# sample random actions
joint_efforts = torch.randn_like(env.action_manager.action)
# step the environment
env.step(joint_efforts)

env.close()

def test_texture_randomization_failure_replicate_physics(self):
"""Test texture randomization failure when replicate physics is set to True."""
# create a new stage
Expand Down