Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
set_prim_visibility,
)
from isaaclab.utils.assets import check_file_path, retrieve_file_path
from isaaclab.utils.version import has_kit

if TYPE_CHECKING:
from . import from_files_cfg
Expand Down Expand Up @@ -369,9 +368,6 @@ def _spawn_from_usd_file(

# apply visual material
if cfg.visual_material is not None:
if not has_kit():
logger.warning("Skipping visual material application for '%s' in kitless mode.", prim_path)
return stage.GetPrimAtPath(prim_path)
if not cfg.visual_material_path.startswith("/"):
material_path = f"{prim_path}/{cfg.visual_material_path}"
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
from typing import TYPE_CHECKING

from pxr import Usd, UsdShade
from pxr import Sdf, Usd, UsdShade

from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim
from isaaclab.sim.utils.stage import get_current_stage
Expand Down Expand Up @@ -50,11 +50,6 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa
Raises:
ValueError: If a prim already exists at the given path.
"""
# check if Kit is available (required for shader creation commands)
if not has_kit():
logger.warning("Skipping preview surface material at '%s' — Kit is not available.", prim_path)
return None

# get stage handle
stage = get_current_stage()

Expand All @@ -66,14 +61,28 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa
# handle scene creation on a custom stage.
material_prim = UsdShade.Material.Define(stage, prim_path)
if material_prim:
from omni.usd.commands import CreateShaderPrimFromSdrCommand

shader_prim = CreateShaderPrimFromSdrCommand(
parent_path=prim_path,
identifier="UsdPreviewSurface",
stage_or_context=stage,
prim_name="Shader",
).do()
if has_kit():
from omni.usd.commands import CreateShaderPrimFromSdrCommand

shader_prim = CreateShaderPrimFromSdrCommand(
parent_path=prim_path,
identifier="UsdPreviewSurface",
stage_or_context=stage,
prim_name="Shader",
).do()
else:
shader_prim = UsdShade.Shader.Define(stage, f"{prim_path}/Shader")
shader_prim.CreateIdAttr("UsdPreviewSurface")
shader_prim.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f)
shader_prim.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f)
shader_prim.CreateInput("roughness", Sdf.ValueTypeNames.Float)
shader_prim.CreateInput("metallic", Sdf.ValueTypeNames.Float)
shader_prim.CreateInput("opacity", Sdf.ValueTypeNames.Float)
shader_prim.CreateInput("opacityThreshold", Sdf.ValueTypeNames.Float)
shader_prim.CreateInput("ior", Sdf.ValueTypeNames.Float)
shader_prim.CreateOutput("surface", Sdf.ValueTypeNames.Token)
shader_prim.CreateOutput("displacement", Sdf.ValueTypeNames.Token)

# bind the shader graph to the material
if shader_prim:
surface_out = shader_prim.GetOutput("surface")
Expand Down
41 changes: 27 additions & 14 deletions source/isaaclab/isaaclab/sim/utils/prims.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,6 @@ def bind_visual_material(
Raises:
ValueError: If the provided prim paths do not exist on stage.
"""
if not has_kit():
return None
# get stage handle
if stage is None:
stage = get_current_stage()
Expand All @@ -774,19 +772,34 @@ def bind_visual_material(
binding_strength = "strongerThanDescendants"
else:
binding_strength = "weakerThanDescendants"
# obtain material binding API
# note: we prefer using the command here as it is more robust than the USD API
import omni.kit.commands

success, _ = omni.kit.commands.execute(
"BindMaterialCommand",
prim_path=prim_path,
material_path=material_path,
strength=binding_strength,
stage=stage,

if has_kit():
# obtain material binding API
# note: we prefer using the command here as it is more robust than the USD API
import omni.kit.commands

success, _ = omni.kit.commands.execute(
"BindMaterialCommand",
prim_path=prim_path,
material_path=material_path,
strength=binding_strength,
stage=stage,
)
# return success
return success

prim = stage.GetPrimAtPath(prim_path)
if prim.HasAPI(UsdShade.MaterialBindingAPI):
material_binding_api = UsdShade.MaterialBindingAPI(prim)
else:
material_binding_api = UsdShade.MaterialBindingAPI.Apply(prim)

material = UsdShade.Material(stage.GetPrimAtPath(material_path))
usd_binding_strength = (
UsdShade.Tokens.strongerThanDescendants if stronger_than_descendants else UsdShade.Tokens.weakerThanDescendants
)
# return success
return success
material_binding_api.Bind(material, bindingStrength=usd_binding_strength) # type: ignore
return True


@apply_nested
Expand Down
80 changes: 80 additions & 0 deletions source/isaaclab/test/sim/test_kitless_visual_materials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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

from __future__ import annotations

from unittest import mock

import pytest
from pxr import UsdShade

import isaaclab.sim as sim_utils
from isaaclab.sim.spawners.materials import visual_materials
from isaaclab.sim.utils import prims as prim_utils


def test_spawn_preview_surface_authors_material_in_kitless_mode():
"""PreviewSurface should still be authored with pure USD APIs when Kit is unavailable."""
stage = sim_utils.create_new_stage()
stage.DefinePrim("/World", "Xform")

with sim_utils.use_stage(stage):
with mock.patch.object(visual_materials, "has_kit", return_value=False):
cfg = sim_utils.PreviewSurfaceCfg(diffuse_color=(0.25, 0.5, 0.75), roughness=0.2, metallic=0.4)
shader_prim = cfg.func("/World/Looks/PreviewSurface", cfg)

assert shader_prim.IsValid()
assert str(shader_prim.GetPath()) == "/World/Looks/PreviewSurface/Shader"
assert shader_prim.GetAttribute("info:id").Get() == "UsdPreviewSurface"
assert shader_prim.GetAttribute("inputs:diffuseColor").Get() == cfg.diffuse_color
assert shader_prim.GetAttribute("inputs:roughness").Get() == pytest.approx(cfg.roughness)
assert shader_prim.GetAttribute("inputs:metallic").Get() == pytest.approx(cfg.metallic)

material = UsdShade.Material.Get(stage, "/World/Looks/PreviewSurface")
assert material
assert material.GetPrim().IsValid()
assert material.GetSurfaceOutput().HasConnectedSource()


def test_bind_visual_material_works_in_kitless_mode():
"""Visual material binding should fall back to USD MaterialBindingAPI in kitless mode."""
stage = sim_utils.create_new_stage()
stage.DefinePrim("/World", "Xform")
mesh_prim = stage.DefinePrim("/World/Geom/Cube", "Cube")
material = UsdShade.Material.Define(stage, "/World/Looks/TestMaterial")

with sim_utils.use_stage(stage):
with mock.patch.object(prim_utils, "has_kit", return_value=False):
sim_utils.bind_visual_material("/World/Geom/Cube", "/World/Looks/TestMaterial", stage=stage)

binding_api = UsdShade.MaterialBindingAPI(mesh_prim)
direct_binding = binding_api.GetDirectBinding()
assert direct_binding.GetMaterialPath() == material.GetPath()
assert direct_binding.GetMaterialPurpose() == ""


@pytest.mark.parametrize("stronger_than_descendants", [True, False])
def test_bind_visual_material_preserves_requested_binding_strength_in_kitless_mode(stronger_than_descendants):
stage = sim_utils.create_new_stage()
stage.DefinePrim("/World", "Xform")
mesh_prim = stage.DefinePrim("/World/Geom/Cube", "Cube")
UsdShade.Material.Define(stage, "/World/Looks/TestMaterial")

with sim_utils.use_stage(stage):
with mock.patch.object(prim_utils, "has_kit", return_value=False):
sim_utils.bind_visual_material(
"/World/Geom/Cube",
"/World/Looks/TestMaterial",
stage=stage,
stronger_than_descendants=stronger_than_descendants,
)

relationship = mesh_prim.GetRelationship("material:binding")
assert relationship.GetTargets() == [stage.GetPrimAtPath("/World/Looks/TestMaterial").GetPath()]
assert relationship.GetMetadata("bindMaterialAs") == (
UsdShade.Tokens.strongerThanDescendants
if stronger_than_descendants
else UsdShade.Tokens.weakerThanDescendants
)
23 changes: 23 additions & 0 deletions source/isaaclab/test/sim/test_spawn_from_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""Rest everything follows."""

import pytest
from pxr import UsdShade

import omni.kit.app

Expand Down Expand Up @@ -62,6 +63,28 @@ def test_spawn_usd_fails(sim):
cfg.func("/World/Franka", cfg)


@pytest.mark.isaacsim_ci
def test_spawn_usd_applies_visual_material(sim):
"""Test loading prim from USD file with a PreviewSurface override applied."""
cfg = sim_utils.UsdFileCfg(
usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd",
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.1, 0.2, 0.3), roughness=0.4),
)

prim = cfg.func("/World/Franka", cfg)

assert prim.IsValid()
material = UsdShade.Material.Get(sim.stage, "/World/Franka/material")
assert material.GetPrim().IsValid()

shader = sim.stage.GetPrimAtPath("/World/Franka/material/Shader")
assert shader.IsValid()
assert shader.GetAttribute("inputs:diffuseColor").Get() == (0.1, 0.2, 0.3)

root_binding = UsdShade.MaterialBindingAPI(prim).GetDirectBinding()
assert root_binding.GetMaterialPath() == material.GetPath()


@pytest.mark.isaacsim_ci
def test_spawn_urdf(sim):
"""Test loading prim from URDF file."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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

from __future__ import annotations

from unittest import mock

import pytest
from pxr import UsdGeom, UsdShade

import isaaclab.sim as sim_utils
from isaaclab.sim.spawners.materials import visual_materials
from isaaclab.sim.utils import prims as prim_utils


def test_spawn_shape_authors_preview_surface_and_display_color_in_kitless_mode():
"""Primitive shape spawners should still author PreviewSurface metadata in kitless mode."""
stage = sim_utils.create_new_stage()
stage.DefinePrim("/World", "Xform")

cfg = sim_utils.CuboidCfg(
size=(0.4, 0.5, 0.6),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), opacity=0.8),
)

with sim_utils.use_stage(stage):
with (
mock.patch.object(visual_materials, "has_kit", return_value=False),
mock.patch.object(prim_utils, "has_kit", return_value=False),
):
prim = cfg.func("/World/Cube", cfg)

assert prim.IsValid()

material = UsdShade.Material.Get(stage, "/World/Cube/geometry/material")
assert material
assert material.GetPrim().IsValid()

mesh_prim = stage.GetPrimAtPath("/World/Cube/geometry/mesh")
shader_prim = stage.GetPrimAtPath("/World/Cube/geometry/material/Shader")
assert shader_prim.IsValid()
assert shader_prim.GetAttribute("inputs:diffuseColor").Get() == (1.0, 0.0, 0.0)

binding_api = UsdShade.MaterialBindingAPI(mesh_prim)
assert binding_api.GetDirectBinding().GetMaterialPath() == material.GetPath()

shader = UsdShade.Shader(shader_prim)
assert shader.GetInput("opacity").Get() == pytest.approx(0.8)

mesh = UsdGeom.Gprim(mesh_prim)
assert not mesh.GetDisplayColorPrimvar().HasAuthoredValue()
assert not mesh.GetDisplayOpacityPrimvar().HasAuthoredValue()