Unverified Commit e4a852eb authored by Mayank Mittal's avatar Mayank Mittal Committed by GitHub

Adds replicator event for randomizing colors (#2153)

# Description

This MR adds a replicator event to randomize the color of meshes on an
asset. Currently, this supports only a single mesh.

## Type of change

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

## Screenshots

```bash
python scripts/tutorials/03_envs/create_cube_base_env.py
```


![image](https://github.com/user-attachments/assets/b525b858-bb2d-4f52-a7a8-d65703b2553d)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] 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 324bb8c3
......@@ -192,7 +192,7 @@ class MySceneCfg(InteractiveSceneCfg):
# lights
light = AssetBaseCfg(
prim_path="/World/light",
spawn=sim_utils.DistantLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0),
spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=2000.0),
)
......@@ -261,6 +261,20 @@ class EventCfg:
},
)
# This event term randomizes the visual color of the cube.
# Similar to the scale randomization, this is also a USD-level randomization and requires the flag
# 'replicate_physics' to be set to False.
randomize_color = EventTerm(
func=mdp.randomize_visual_color,
mode="prestartup",
params={
"colors": {"r": (0.0, 1.0), "g": (0.0, 1.0), "b": (0.0, 1.0)},
"asset_cfg": SceneEntityCfg("cube"),
"mesh_name": "geometry/mesh",
"event_name": "rep_cube_randomize_color",
},
)
##
# Environment configuration
......@@ -290,6 +304,9 @@ class CubeEnvCfg(ManagerBasedEnvCfg):
self.sim.dt = 0.01
self.sim.physics_material = self.scene.terrain.physics_material
self.sim.render_interval = 2 # render interval should be a multiple of decimation
# viewer settings
self.viewer.eye = (5.0, 5.0, 5.0)
self.viewer.lookat = (0.0, 0.0, 2.0)
def main():
......
......@@ -276,99 +276,6 @@ 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,
......@@ -1216,6 +1123,198 @@ def reset_scene_to_default(env: ManagerBasedEnv, env_ids: torch.Tensor):
deformable_object.write_nodal_state_to_sim(nodal_state, env_ids=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 replicator extension if not already enabled
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 RuntimeError(
"Unable to randomize visual texture material with scene replication enabled."
" For stable USD-level randomization, please disable scene replication"
" by setting 'replicate_physics' to False in 'InteractiveSceneCfg'."
)
# convert from radians to degrees
texture_rotation = tuple(math.degrees(angle) for angle in texture_rotation)
# obtain the asset entity
asset = 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 affected prim path
# TODO: Remove the hard-coded "/visuals" part.
prim_path = f"{asset.cfg.prim_path}/{body_names_regex}/visuals"
# Create the omni-graph node for the randomization term
def rep_texture_randomization():
prims_group = rep.get.prims(path_pattern=prim_path)
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)
class randomize_visual_color(ManagerTermBase):
"""Randomize the visual color of bodies on an asset using Replicator API.
This function randomizes the visual color of the bodies of the asset using the Replicator API.
The function samples random colors from the given colors and applies them to the bodies
of the asset.
The function assumes that the asset follows the prim naming convention as:
"{asset_prim_path}/{mesh_name}" where the mesh name is the name of the mesh to
which the color is applied. For instance, if the asset has a prim path "/World/asset"
and a mesh named "body_0/mesh", the prim path for the mesh would be
"/World/asset/body_0/mesh".
The colors can be specified as a list of tuples of the form ``(r, g, b)`` or as a dictionary
with the keys ``r``, ``g``, ``b`` and values as tuples of the form ``(low, high)``.
If a dictionary is used, the function will sample random colors from the given ranges.
.. note::
When randomizing the color 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 randomization term."""
super().__init__(cfg, env)
# enable replicator extension if not already enabled
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")
colors = cfg.params.get("colors")
event_name = cfg.params.get("event_name")
mesh_name: str = cfg.params.get("mesh_name", "") # type: ignore
# check to make sure replicate_physics is set to False, else raise warning
if env.cfg.scene.replicate_physics:
raise RuntimeError(
"Unable to randomize visual color with scene replication enabled."
" For stable USD-level randomization, please disable scene replication"
" by setting 'replicate_physics' to False in 'InteractiveSceneCfg'."
)
# obtain the asset entity
asset = env.scene[asset_cfg.name]
# create the affected prim path
if not mesh_name.startswith("/"):
mesh_name = "/" + mesh_name
mesh_prim_path = f"{asset.cfg.prim_path}{mesh_name}"
# TODO: Need to make it work for multiple meshes.
# parse the colors into replicator format
if isinstance(colors, dict):
# (r, g, b) - low, high --> (low_r, low_g, low_b) and (high_r, high_g, high_b)
color_low = [colors[key][0] for key in ["r", "g", "b"]]
color_high = [colors[key][1] for key in ["r", "g", "b"]]
colors = rep.distribution.uniform(color_low, color_high)
else:
colors = list(colors)
# Create the omni-graph node for the randomization term
def rep_texture_randomization():
prims_group = rep.get.prims(path_pattern=mesh_prim_path)
with prims_group:
rep.randomizer.color(colors=colors)
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,
colors: list[tuple[float, float, float]] | dict[str, tuple[float, float]],
mesh_name: str = "",
):
# import replicator
import omni.replicator.core as rep
# only send the event to the replicator
rep.utils.send_og_event(event_name)
"""
Internal helper functions.
"""
......
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