Unverified Commit 324bb8c3 authored by Harsh Patel's avatar Harsh Patel Committed by GitHub

Adds texture and scale randomization event terms (#2121)

# Description

- Added back texture randomization event term
- Carrying over texture randomization and scale randomization unit tests
from previous PRs
- Added additional replicate physics failure case test

## Type of change

- New feature (non-breaking change which adds functionality)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
parent 8f7f2b8e
......@@ -14,12 +14,14 @@ the event introduced by the function.
from __future__ import annotations
import math
import torch
from typing import TYPE_CHECKING, Literal
import carb
import omni.physics.tensors.impl.api as physx
import omni.usd
from isaacsim.core.utils.extensions import enable_extension
from pxr import Gf, Sdf, UsdGeom, Vt
import isaaclab.sim as sim_utils
......@@ -274,6 +276,99 @@ class randomize_rigid_body_material(ManagerTermBase):
self.asset.root_physx_view.set_material_properties(materials, env_ids)
class randomize_visual_texture_material(ManagerTermBase):
"""Randomize the visual texture of bodies on an asset using Replicator API.
This function randomizes the visual texture of the bodies of the asset using the Replicator API.
The function samples random textures from the given texture paths and applies them to the bodies
of the asset. The textures are projected onto the bodies and rotated by the given angles.
.. note::
The function assumes that the asset follows the prim naming convention as:
"{asset_prim_path}/{body_name}/visuals" where the body name is the name of the body to
which the texture is applied. This is the default prim ordering when importing assets
from the asset converters in Isaac Lab.
.. note::
When randomizing the texture of individual assets, please make sure to set
:attr:`isaaclab.scene.InteractiveSceneCfg.replicate_physics` to False. This ensures that physics
parser will parse the individual asset properties separately.
"""
def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
"""Initialize the term.
Args:
cfg: The configuration of the event term.
env: The environment instance.
"""
super().__init__(cfg, env)
enable_extension("omni.replicator.core")
# we import the module here since we may not always need the replicator
import omni.replicator.core as rep
# read parameters from the configuration
asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg")
texture_paths = cfg.params.get("texture_paths")
event_name = cfg.params.get("event_name")
texture_rotation = cfg.params.get("texture_rotation", (0.0, 0.0))
# check to make sure replicate_physics is set to False, else raise warning
if env.cfg.scene.replicate_physics:
raise ValueError(
"Unable to randomize visual texture material - ensure InteractiveSceneCfg's replicate_physics parameter"
" is set to False."
)
# convert from radians to degrees
texture_rotation = tuple(math.degrees(angle) for angle in texture_rotation)
# obtain the asset entity
asset_entity = env.scene[asset_cfg.name]
# join all bodies in the asset
body_names = asset_cfg.body_names
if isinstance(body_names, str):
body_names_regex = body_names
elif isinstance(body_names, list):
body_names_regex = "|".join(body_names)
else:
body_names_regex = ".*"
# Create the omni-graph node for the randomization term
def rep_texture_randomization():
prims_group = rep.get.prims(path_pattern=f"{asset_entity.cfg.prim_path}/{body_names_regex}/visuals")
with prims_group:
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
with rep.trigger.on_custom_event(event_name=event_name):
rep_texture_randomization()
def __call__(
self,
env: ManagerBasedEnv,
env_ids: torch.Tensor,
event_name: str,
asset_cfg: SceneEntityCfg,
texture_paths: list[str],
texture_rotation: tuple[float, float] = (0.0, 0.0),
):
# import replicator
import omni.replicator.core as rep
# only send the event to the replicator
# note: This triggers the nodes for all the environments.
# We need to investigate how to make it happen only for a subset based on env_ids.
rep.utils.send_og_event(event_name)
def randomize_rigid_body_mass(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
......
This diff is collapsed.
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
This script tests the functionality of texture randomization applied to the cartpole scene.
"""
"""Launch Isaac Sim Simulator first."""
from isaaclab.app import AppLauncher, run_tests
# launch omniverse app
app_launcher = AppLauncher(headless=True, enable_cameras=True)
simulation_app = app_launcher.app
"""Rest everything follows."""
import math
import torch
import unittest
import isaaclab.envs.mdp as mdp
from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.utils import configclass
from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR
from isaaclab_tasks.manager_based.classic.cartpole.cartpole_env_cfg import CartpoleSceneCfg
@configclass
class ActionsCfg:
"""Action specifications for the environment."""
joint_efforts = mdp.JointEffortActionCfg(asset_name="robot", joint_names=["slider_to_cart"], scale=5.0)
@configclass
class ObservationsCfg:
"""Observation specifications for the environment."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel)
joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel)
def __post_init__(self) -> None:
self.enable_corruption = False
self.concatenate_terms = True
# observation groups
policy: PolicyCfg = PolicyCfg()
@configclass
class EventCfg:
"""Configuration for events."""
# on reset apply a new set of textures
cart_texture_randomizer = EventTerm(
func=mdp.randomize_visual_texture_material,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=["cart"]),
"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",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Oak/Oak_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Timber/Timber_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Timber_Cladding/Timber_Cladding_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Walnut_Planks/Walnut_Planks_BaseColor.png",
],
"event_name": "cart_texture_randomizer",
"texture_rotation": (math.pi / 2, math.pi / 2),
},
)
pole_texture_randomizer = EventTerm(
func=mdp.randomize_visual_texture_material,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=["pole"]),
"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",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Oak/Oak_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Timber/Timber_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Timber_Cladding/Timber_Cladding_BaseColor.png",
f"{NVIDIA_NUCLEUS_DIR}/Materials/Base/Wood/Walnut_Planks/Walnut_Planks_BaseColor.png",
],
"event_name": "pole_texture_randomizer",
"texture_rotation": (math.pi / 2, math.pi / 2),
},
)
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),
},
)
reset_pole_position = EventTerm(
func=mdp.reset_joints_by_offset,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]),
"position_range": (-0.125 * math.pi, 0.125 * math.pi),
"velocity_range": (-0.01 * math.pi, 0.01 * math.pi),
},
)
@configclass
class CartpoleEnvCfg(ManagerBasedEnvCfg):
"""Configuration for the cartpole environment."""
# Scene settings
scene = CartpoleSceneCfg(env_spacing=2.5)
# Basic settings
actions = ActionsCfg()
observations = ObservationsCfg()
events = EventCfg()
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"""
"""
Tests
"""
def test_texture_randomization(self):
# set the arguments
env_cfg = CartpoleEnvCfg()
env_cfg.scene.num_envs = 16
env_cfg.scene.replicate_physics = False
# setup base environment
env = ManagerBasedEnv(cfg=env_cfg)
# simulate physics
with torch.inference_mode():
for count in range(50):
# 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):
with self.assertRaises(ValueError):
cfg_failure = CartpoleEnvCfg()
cfg_failure.scene.num_envs = 16
cfg_failure.scene.replicate_physics = True
env = ManagerBasedEnv(cfg_failure)
env.close()
if __name__ == "__main__":
# run the main function
run_tests()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment