Skip to content

Commit f547744

Browse files
authored
Propagate Newton shape colors before cloning (isaac-sim#6194)
# Description This PR integrates @ooctipus's proposed fix for newton shape color replacement to occur pre-clone. Original PR: rilei-nvidia#1 Replace the post-finalize `replace_newton_shape_colors` pass on the Newton model with `replace_newton_builder_shape_colors`, applied to each ModelBuilder before clone replication. This ensures all cloned environments automatically inherit the correct USD material and displayColor values. This not only improves performance but also addresses the regression that @huidongc discovered last week where only env_0 (the clone source) had the right color for the robot arm, and the other cloned envs got the fallback colors - Add replace_newton_builder_shape_colors to newton_model_utils. - Removed replace_newton_shape_colors and any un-used functionality - Call it in NewtonManager (flat, env, and proto-source builders), newton_clone_utils, and replicate - Update tests to exercise the new replace_newton_builder_shape_colors - Update dexsuite_kuka_hetero golden images for correctness # Validation Validated for correctness in the Newton visualizer with (all robot arms should have the same color): ```bash ./isaaclab.sh train --rl_library rsl_rl \ --task Isaac-Lift-KukaAllegro-Camera \ presets=newton_mjwarp,newton_renderer,rgb64,single_camera \ --seed 42 \ --num_envs 4 \ --max_iterations 2 \ --visualizer newton ``` <img width="1917" height="1159" alt="image" src="https://github.com/user-attachments/assets/fabf9ef4-60f6-4e29-a11a-e266e77d7f61" /> Rendering test golden images for the dexsuite_kuka_hetero kitless suite have also been updated to reflect fix to the regression ```bash ./isaaclab.sh -p -m pytest -v -rA source/isaaclab_tasks/test/core/test_rendering_dexsuite_kuka_hetero_kitless.py::test_rendering_dexsuite_kuka_hetero_kitless[newton-newton_warp-rgb] ``` <img width="742" height="1046" alt="image" src="https://github.com/user-attachments/assets/6e357b06-5cf2-458f-886b-4b283952c94f" /> # Performance Testing on a AMD Ryzen Threadripper PRO 7965WX 24-Cores (48) @ 5.36 GHz For testing the performance impact I used the benchmark script: ```bash ./isaaclab.sh -p scripts/benchmarks/benchmark_non_rl.py \ --task Isaac-Lift-KukaAllegro-Camera \ presets=newton_mjwarp,ovrtx_renderer,rgb64,single_camera \ --seed 42 \ --num_envs "8000" \ --benchmark_backend json \ --output_path "hdc/benchmarks/20260611/heterogeneous_8000" ``` The time of replace_newton_shape_colors has been moved to calls to the new replace_newton_builder_shape_colors pre-clone: | # Env | replace_newton_shape_colors (Before) | replace_newton_builder_shape_colors (After) | | ------ | ----- | ----- | | 4000 | 2 s | 0.0045~ s| | 8000 | 4 s | 0.0045~ s| With @ooctipus's change the time for shape color propagation is not only "free" now, but constant to increasing # of environments. ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] 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
1 parent 832a356 commit f547744

11 files changed

Lines changed: 164 additions & 288 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added
2+
^^^^^
3+
4+
* Added :func:`~isaaclab.sim.utils.newton_model_utils.replace_newton_builder_shape_colors` to
5+
propagate USD material and ``displayColor`` values into a Newton ``ModelBuilder``'s shape colors
6+
before clone replication, so cloned environments inherit correct colors without a separate
7+
post-finalize pass.

source/isaaclab/isaaclab/sim/utils/newton_model_utils.py

Lines changed: 42 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from __future__ import annotations
1414

1515
import logging
16-
import os
1716
import warnings
1817
from typing import Any
1918

@@ -22,7 +21,7 @@
2221

2322
from pxr import Usd, UsdGeom, UsdShade
2423

25-
__all__ = ["replace_newton_shape_colors"]
24+
__all__ = ["replace_newton_builder_shape_colors"]
2625

2726
logger = logging.getLogger(__name__)
2827

@@ -36,39 +35,15 @@
3635
_UNBOUND_DEFAULT_FALLBACK_GRAY = (0.18, 0.18, 0.18)
3736

3837

39-
@wp.func
40-
def _linear_channel_to_srgb_warp(c: float) -> float:
41-
"""Per-channel sRGB OETF on device: linear ``[0, 1]`` to sRGB-encoded ``[0, 1]``."""
38+
def _linear_channel_to_srgb(c: float) -> float:
39+
"""Per-channel sRGB OETF on host: linear ``[0, 1]`` to sRGB-encoded ``[0, 1]``."""
4240
if c <= 0.0:
4341
return 0.0
4442
if c <= 0.0031308:
4543
return 12.92 * c
4644
if c >= 1.0:
4745
return 1.0
48-
return 1.055 * wp.pow(c, 1.0 / 2.4) - 0.055
49-
50-
51-
@wp.func
52-
def _linear_rgb_to_srgb_warp(linear_rgb: wp.vec3) -> wp.vec3:
53-
"""Apply sRGB OETF per channel: linear RGB ``[0, 1]`` to sRGB-encoded ``[0, 1]``."""
54-
return wp.vec3(
55-
_linear_channel_to_srgb_warp(linear_rgb[0]),
56-
_linear_channel_to_srgb_warp(linear_rgb[1]),
57-
_linear_channel_to_srgb_warp(linear_rgb[2]),
58-
)
59-
60-
61-
@wp.kernel
62-
def _scatter_shape_color_rows_kernel(
63-
shape_colors: wp.array(dtype=wp.vec3), # type: ignore
64-
row_indices: wp.array(dtype=wp.int32), # type: ignore
65-
row_colors: wp.array(dtype=wp.vec3), # type: ignore
66-
):
67-
"""Write per-row sRGB colors into ``shape_colors``."""
68-
tid = wp.tid()
69-
index = row_indices[tid]
70-
color = row_colors[tid]
71-
shape_colors[index] = _linear_rgb_to_srgb_warp(color)
46+
return 1.055 * (c ** (1.0 / 2.4)) - 0.055
7247

7348

7449
def _canonical_prim_lookup_key(prim: Usd.Prim) -> str:
@@ -203,7 +178,7 @@ def _resolve_shape_color(
203178
"""Resolve replacement linear RGB for one prim path (sRGB encoding is applied in the scatter kernel).
204179
205180
Returns:
206-
Linear RGB to pass to :func:`_scatter_shape_color_rows_kernel`, or ``None`` to leave the row unchanged.
181+
Linear RGB to pass, or ``None`` to leave the row unchanged.
207182
"""
208183
shape_prim = stage.GetPrimAtPath(prim_path)
209184
if not shape_prim.IsValid():
@@ -231,122 +206,68 @@ def _resolve_shape_color(
231206
return material_color
232207

233208

234-
def replace_newton_shape_colors(model: Any, stage: Usd.Stage | None = None) -> int:
235-
"""Align Newton visualization colors with the USD stage.
209+
def replace_newton_builder_shape_colors(builder: Any, stage: Usd.Stage) -> int:
210+
"""Align a Newton ``ModelBuilder``'s shape colors with the USD stage before clone replication.
211+
212+
Overwrites entries in ``builder.shape_color`` so that colors match the authored USD data:
236213
237-
Newton assigns a per-shape palette to ``shape_color``. This overwrites those rows so rendering matches authored USD
238-
data where supported:
214+
- **No bound material**: use authored ``primvars:displayColor`` (treated as linear RGB), or a
215+
neutral 18% linear gray if ``displayColor`` is not authored.
216+
- **OmniPBR**: use ``diffuse_color_constant`` × ``diffuse_tint`` (linear RGB, with MDL defaults
217+
when inputs are not authored).
218+
- **Other materials**: leave the existing Newton color for that shape unchanged.
219+
- **Guide purpose** prims (``UsdGeom.Tokens.guide``): leave unchanged so guide visualization
220+
stays on the Newton palette.
239221
240-
- **No bound material**: use authored ``primvars:displayColor`` (treated as linear RGB), or a neutral 18% linear
241-
gray if ``displayColor`` is not authored; linear values are encoded to sRGB in the scatter kernel.
242-
- **OmniPBR**: use ``diffuse_color_constant`` times ``diffuse_tint`` (linear RGB, with MDL defaults when inputs are
243-
not authored), encoded to sRGB in the scatter kernel.
244-
- **Other materials**: leave the existing Newton color for that shape.
245-
- **Guide purpose** prims (``UsdGeom.Tokens.guide``): leave unchanged so guide visualization stays on the palette.
222+
Linear RGB values are encoded to sRGB before being written into ``builder.shape_color``.
246223
247224
Args:
248-
model: Object with ``shape_label`` (``list`` of USD prim paths) and ``shape_color`` (``wp.array`` of
249-
``wp.vec3``), typically a finalized Newton model.
250-
stage: USD stage to read from. If ``None``, uses :func:`~isaaclab.sim.utils.stage.get_current_stage`.
225+
builder: Object with ``shape_label`` (``list`` of USD prim paths) and ``shape_color``
226+
(``list`` of ``wp.vec3``), typically a Newton ``ModelBuilder`` before finalization.
227+
stage: USD stage to read material and primvar data from.
251228
252229
Returns:
253230
Number of shapes that had their colors replaced.
254-
255-
Note:
256-
Set ``ISAACLAB_REPLACE_NEWTON_SHAPE_COLORS`` to ``0``, ``false``, ``off``, or ``no`` to skip this pass
257-
entirely (returns ``0``).
258-
259-
This pass exists only while Isaac Lab and Isaac Sim content still relies on NVIDIA-specific MDL and OmniPBR
260-
materials; after migration to neutral USD materials that Newton can consume directly, this path is expected to
261-
be deprecated and removed.
262-
263-
Wall time for USD resolution and the GPU scatter is measured with :class:`~isaaclab.utils.timer.Timer`, which
264-
may print a timing summary when the timer is enabled.
265231
"""
266-
env_val = os.getenv("ISAACLAB_REPLACE_NEWTON_SHAPE_COLORS")
267-
if env_val is not None and env_val.strip().lower() in ["false", "0", "off", "no"]:
268-
logger.debug("Newton shape color replacement is disabled")
269-
return 0
270-
271232
warnings.warn(
272233
"Newton shape color replacement is enabled; this workaround will be deprecated in a future release.",
273234
FutureWarning,
274235
stacklevel=2,
275236
)
276237

277238
# Use duck typing to avoid introducing hard dependencies on newton.
278-
shape_labels = getattr(model, "shape_label", None)
279-
shape_colors = getattr(model, "shape_color", None)
239+
shape_labels = getattr(builder, "shape_label", None)
240+
shape_colors = getattr(builder, "shape_color", None)
280241

281242
if not isinstance(shape_labels, list):
282243
logger.debug("shape_label must be a list, got %s", type(shape_labels))
283244
return 0
284245

285-
if not isinstance(shape_colors, wp.array):
286-
logger.debug("shape_color must be a Warp array, got %s", type(shape_colors))
287-
return 0
288-
289-
num_shapes = len(shape_labels)
290-
if num_shapes == 0:
291-
logger.debug("Found empty list of shape labels")
246+
if not isinstance(shape_colors, list):
247+
logger.debug("shape_color must be a list, got %s", type(shape_colors))
292248
return 0
293249

294-
if num_shapes != len(shape_colors):
295-
logger.debug("Mismatching length of shape_labels and shape_colors: %d != %d", num_shapes, len(shape_colors))
296-
return 0
250+
if len(shape_labels) != len(shape_colors):
251+
raise ValueError(
252+
f"Mismatching length of shape_label and shape_color: {len(shape_labels)} != {len(shape_colors)}"
253+
)
297254

298255
from isaaclab.utils.timer import Timer
299256

300-
num_color_updates = 0
301-
302-
with Timer(f"[INFO]: Time taken for replace_newton_shape_colors for {num_shapes} shapes", enable=False):
303-
if stage is None:
304-
from .stage import get_current_stage
305-
306-
stage = get_current_stage()
307-
308-
shape_keys: list[str] = []
309-
310-
for label in shape_labels:
311-
prim = stage.GetPrimAtPath(label)
312-
shape_keys.append(_canonical_prim_lookup_key(prim) if prim.IsValid() else label)
313-
314-
# shape_keys must stay the same length as shape labels, to guarantee the correctness of
315-
# shape indices that will be used in the scatter kernel.
316-
assert num_shapes == len(shape_keys)
317-
318-
resolved_color_cache: dict[str, tuple[float, float, float] | None] = {}
257+
with Timer(
258+
f"[INFO]: Time taken for replace_newton_builder_shape_colors for {len(shape_labels)} shapes", enable=False
259+
):
260+
num_color_updates = 0
319261
material_color_cache: dict[str, tuple[float, float, float] | None] = {}
320-
321-
unique_keys = dict.fromkeys(shape_keys)
322-
for key in unique_keys:
323-
color = _resolve_shape_color(stage, key, material_color_cache)
324-
resolved_color_cache[key] = color
325-
326-
# Prepare the indices and colors for the scatter kernel:
327-
# - Indices point to the slots in the shape_colors array that should be updated
328-
# - Colors are the new values to write into the slots
329-
indices_np = np.empty(num_shapes, dtype=np.int32)
330-
colors_np = np.empty((num_shapes, 3), dtype=np.float32)
331-
332-
for i, shape_key in enumerate(shape_keys):
333-
if rgb := resolved_color_cache.get(shape_key):
334-
indices_np[num_color_updates] = i
335-
colors_np[num_color_updates] = rgb
262+
for i, label in enumerate(shape_labels):
263+
rgb = _resolve_shape_color(stage, label, material_color_cache)
264+
if rgb is not None:
265+
shape_colors[i] = wp.vec3(
266+
_linear_channel_to_srgb(rgb[0]),
267+
_linear_channel_to_srgb(rgb[1]),
268+
_linear_channel_to_srgb(rgb[2]),
269+
)
336270
num_color_updates += 1
337271

338-
# If there are any color updates, launch the scatter kernel to update the shape_colors array.
339-
if num_color_updates != 0:
340-
indices_wp = wp.from_numpy(indices_np[:num_color_updates], dtype=wp.int32, device=shape_colors.device)
341-
colors_wp = wp.from_numpy(colors_np[:num_color_updates], dtype=wp.vec3, device=shape_colors.device)
342-
343-
wp.launch(
344-
kernel=_scatter_shape_color_rows_kernel,
345-
dim=num_color_updates,
346-
inputs=[shape_colors, indices_wp, colors_wp],
347-
device=shape_colors.device,
348-
)
349-
350-
logger.debug("Replaced colors for %d / %d shapes", num_color_updates, num_shapes)
351-
352-
return num_color_updates
272+
logger.debug("Replaced builder colors for %d / %d shapes", num_color_updates, len(shape_labels))
273+
return num_color_updates

source/isaaclab/test/sim/test_newton_manager_visualization_state.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def test_ensure_visualization_model_builds_from_stage_when_backend_is_physx(monk
100100
monkeypatch.setattr(nm.PhysicsManager, "_sim", None, raising=False)
101101
_set_sim_context(monkeypatch, nm)
102102
monkeypatch.setattr(nm.PhysicsManager, "_device", "cpu", raising=False)
103-
monkeypatch.setattr(nm, "replace_newton_shape_colors", lambda model, *a, **kw: 0)
104103

105104
finalize_calls: list[str] = []
106105

@@ -155,7 +154,6 @@ def test_ensure_visualization_model_populates_num_envs_when_backend_is_physx(mon
155154
monkeypatch.setattr(nm.PhysicsManager, "_sim", None, raising=False)
156155
_set_sim_context(monkeypatch, nm)
157156
monkeypatch.setattr(nm.PhysicsManager, "_device", "cpu", raising=False)
158-
monkeypatch.setattr(nm, "replace_newton_shape_colors", lambda model, *a, **kw: 0)
159157

160158
class _FakeBuilder:
161159
body_count = 3

0 commit comments

Comments
 (0)