Source code for isaaclab.envs.mdp.visual_events

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""GPU visual appearance randomization terms."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import torch

from isaaclab.assets import VisualMaterialCfg
from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg
from isaaclab.utils.backend_utils import FactoryBase

if TYPE_CHECKING:
    from isaaclab.envs import ManagerBasedEnv


[docs] class randomize_visual_material(ManagerTermBase): """Sample numeric material channels on device and issue one batched runtime renderer write. This term requires an initialized renderer and therefore does not support ``prestartup`` mode. """
[docs] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) if cfg.mode == "prestartup": raise ValueError("Visual-material writes do not support prestartup mode.") material_cfgs = cfg.params["materials"] material_cfgs = [material_cfgs] if isinstance(material_cfgs, SceneEntityCfg) else material_cfgs self._materials = [env.scene[material_cfg.name] for material_cfg in material_cfgs] if not self._materials: raise ValueError("Visual material randomization requires at least one material.") if not all(isinstance(material.cfg, VisualMaterialCfg) for material in self._materials): raise TypeError("Every material selector must resolve to a VisualMaterial asset.") scopes = {material.is_per_env for material in self._materials} if len(scopes) != 1: raise ValueError("Bucket and per-environment materials require separate event terms.") self._per_env = scopes.pop() self._samplers = { channel: _compile_distribution(spec, env.device) for channel, spec in cfg.params["channels"].items() } for channel in self._samplers: if not all(channel in material.channels for material in self._materials): raise ValueError(f"Channel {channel!r} must be declared by every selected material.")
def __call__( self, env: ManagerBasedEnv, env_ids: torch.Tensor | slice | None, materials: list[SceneEntityCfg] | SceneEntityCfg, channels: dict[str, tuple | dict], ) -> None: del materials, channels if isinstance(env_ids, slice): env_ids = None count = env.scene.num_envs if env_ids is None else len(env_ids) shape = (len(self._materials), count) if self._per_env else (len(self._materials),) sampled = {channel: sampler(shape) for channel, sampler in self._samplers.items()} env.sim.render_context.write_visual_materials(self._materials, sampled, env_ids if self._per_env else None)
[docs] class randomize_visual_shape(FactoryBase, ManagerTermBase): """Randomize visual channels per selected shape on backends that expose shape storage.""" @classmethod def _get_backend(cls, cfg: EventTermCfg, env: ManagerBasedEnv) -> str: consumers = (*env.sim.resolve_visualizer_types(), *env.sim.render_context.renderer_types) supported = ("newton_gl", "newton_rtx", "newton_warp") if consumers and all(name in supported for name in consumers): return "newton" raise NotImplementedError( "This renderer has no per-shape visual storage; use one VisualMaterialCfg per randomized part." )
def _compile_distribution(spec: Any, device: str): """Compile one public distribution spec into a device sampler.""" if isinstance(spec, dict) and "choices" in spec: values = torch.as_tensor(spec["choices"], dtype=torch.float32, device=device) def sample_choices(shape): return values[torch.randint(len(values), shape, device=device)] return sample_choices if isinstance(spec, dict): low = tuple(spec[key][0] for key in ("r", "g", "b")) high = tuple(spec[key][1] for key in ("r", "g", "b")) else: low, high = spec low = torch.as_tensor(low, dtype=torch.float32, device=device) high = torch.as_tensor(high, dtype=torch.float32, device=device) span = high - low trailing = () if low.ndim == 0 else tuple(low.shape) def sample_uniform(shape): return torch.rand((*shape, *trailing), device=device).mul_(span).add_(low) return sample_uniform