Source code for isaaclab.assets.visual_material.visual_material

# 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

"""Runtime-writable visual material asset."""

from __future__ import annotations

from collections.abc import Sequence

import torch

from pxr import Sdf, UsdShade

from isaaclab import cloner
from isaaclab.assets.asset_base import AssetBase
from isaaclab.sim import SimulationContext
from isaaclab.sim.utils import find_matching_prim_paths

from .visual_material_cfg import VisualMaterialCfg

_PREVIEW_CHANNELS = {
    "color": ("diffuseColor", (0.18, 0.18, 0.18)),
    "roughness": ("roughness", 0.5),
    "metallic": ("metallic", 0.0),
    "emissive_color": ("emissiveColor", (0.0, 0.0, 0.0)),
    "opacity": ("opacity", 1.0),
}
_PBR_CHANNELS = {
    "color": ("diffuse_color_constant", (0.18, 0.18, 0.18)),
    "roughness": ("reflection_roughness_constant", 0.5),
    "metallic": ("metallic_constant", 0.0),
    "specular": ("specular_level", 0.5),
    "emissive_color": ("emissive_color", (0.0, 0.0, 0.0)),
    "emissive_intensity": ("emissive_intensity", 0.0),
    "opacity": ("opacity_constant", 1.0),
    "uv_scale": ("texture_scale", (1.0, 1.0)),
    "uv_offset": ("texture_translate", (0.0, 0.0)),
    "uv_rotate": ("texture_rotate", 0.0),
}
_GLASS_CHANNELS = {
    "color": ("glass_color", (1.0, 1.0, 1.0)),
    "roughness": ("frosting_roughness", 0.0),
    "ior": ("glass_ior", 1.491),
}


[docs] class VisualMaterial(AssetBase): """A visual material cloned and finalized through the normal asset lifecycle.""" cfg: VisualMaterialCfg
[docs] def __init__(self, cfg: VisualMaterialCfg): super().__init__(cfg) self._render_context = SimulationContext.instance().render_context source_material_paths = tuple(find_matching_prim_paths(self.cfg.prim_path)) self._source_material_path = source_material_paths[0] self._is_per_env = self._source_material_path != self.cfg.prim_path material = UsdShade.Material(self.stage.GetPrimAtPath(self._source_material_path)) if not material: raise ValueError(f"Visual material {self._source_material_path!r} is not a UsdShade.Material.") outputs = (material.GetSurfaceOutput("mdl"), material.GetSurfaceOutput()) connected = next( (output.GetConnectedSource() for output in outputs if output and output.HasConnectedSource()), None ) if connected is None: raise ValueError(f"Visual material {self._source_material_path!r} has no connected surface shader.") shader = UsdShade.Shader(connected[0].GetPrim()) self._source_shader_path = str(shader.GetPrim().GetPath()) channel_specs = _channel_specs(shader) self._input_names: dict[str, str] = {} self._initial_values: dict[str, torch.Tensor] = {} for channel in self.cfg.channels: if channel not in channel_specs: raise ValueError( f"Material {self._source_material_path!r} does not support channel {channel!r}; " f"available channels are {tuple(channel_specs)}." ) input_name, default = channel_specs[channel] value_type = ( Sdf.ValueTypeNames.Float if isinstance(default, float) else { 2: Sdf.ValueTypeNames.Float2, 3: Sdf.ValueTypeNames.Color3f, }[len(default)] ) shader_input = shader.CreateInput(input_name, value_type) if shader_input.Get() is None: shader_input.Set(default) self._input_names[channel] = input_name self._initial_values[channel] = torch.as_tensor(shader_input.Get(), dtype=torch.float32) self._material_paths: tuple[str, ...] = () self._shader_paths: tuple[str, ...] = () self._values: dict[str, torch.Tensor] = {} self._offsets: dict[str, int] = {}
@property def channels(self) -> tuple[str, ...]: """Runtime-writable channel names.""" return tuple(self._input_names) @property def is_per_env(self) -> bool: """Whether this material owns one clone per environment.""" return self._is_per_env @property def num_instances(self) -> int: return len(self._material_paths) @property def data(self) -> dict[str, torch.Tensor]: return self._values
[docs] @staticmethod def write_channels( materials: Sequence[VisualMaterial], channels: dict[str, torch.Tensor], env_ids: torch.Tensor | None = None, ) -> None: """Write aligned numeric channels to bucket or selected environment rows.""" if materials: materials[0]._render_context.write_visual_materials(materials, channels, env_ids)
[docs] def reset(self, env_ids: Sequence[int] | None = None) -> None: pass
[docs] def write_data_to_sim(self) -> None: pass
[docs] def update(self, dt: float) -> None: pass
def _initialize_impl(self) -> None: plan = SimulationContext.instance().get_clone_plan() if self._is_per_env: assert plan is not None and plan.env_ids is not None plan_env_ids = plan.env_ids.detach().cpu().tolist() columns = {env_id: column for column, env_id in enumerate(plan_env_ids)} material_paths = [""] * len(plan_env_ids) for source_root, destination, source_path, env_ids in cloner.query.iter_sources(plan, self.cfg.prim_path): for env_id in env_ids: material_paths[columns[env_id]] = cloner.path.rebase( source_path, source_root, destination.format(env_id) ) if not all(material_paths): raise ValueError( f"Per-environment material {self._source_material_path!r} must populate every environment." ) self._material_paths = tuple(material_paths) shader_suffix = self._source_shader_path.removeprefix(self._source_material_path) self._shader_paths = tuple(path + shader_suffix for path in self._material_paths) else: self._material_paths = (self._source_material_path,) self._shader_paths = (self._source_shader_path,) if not self._values: for channel, value in self._initial_values.items(): self._values[channel] = value.to(self.device).expand(len(self._material_paths), *value.shape).clone() self._render_context.register_visual_material(self)
def _channel_specs(shader: UsdShade.Shader) -> dict[str, tuple[str, float | tuple[float, ...]]]: if shader.GetShaderId() == "UsdPreviewSurface": return dict(_PREVIEW_CHANNELS) identifier = shader.GetSourceAssetSubIdentifier("mdl") if identifier and identifier.startswith("OmniGlass"): return dict(_GLASS_CHANNELS) if identifier and identifier.startswith("OmniPBR"): channels = dict(_PBR_CHANNELS) texture = shader.GetInput("diffuse_texture") if texture and texture.Get(): channels["color"] = ("diffuse_tint", (1.0, 1.0, 1.0)) return channels raise TypeError("VisualMaterial requires PreviewSurfaceCfg, PbrMdlCfg, or GlassMdlCfg.")