# 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
"""Newton Warp renderer for tiled camera rendering."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, NoReturn
import newton
import torch
import warp as wp
from isaaclab.renderers import BaseRenderer, RenderBufferKind, RenderBufferSpec
from isaaclab.renderers.camera_render_spec import CameraRenderSpec
from isaaclab.sim import SimulationContext
from isaaclab.utils.warp.warp_math import convert_camera_frame_orientation_convention_wp, replace_background_depth_wp
from ..physics.newton_manager import NewtonManager
from .newton_warp_renderer_cfg import NewtonWarpRendererCfg
from .segmentation import NewtonSegmentationMapper, NewtonSegmentationMapping
if TYPE_CHECKING:
from isaaclab_ppisp import PpispPipeline
from isaaclab.sensors.camera.camera_data import CameraData
from isaaclab.utils.warp import ProxyArray
logger = logging.getLogger(__name__)
_PPISP_IMPORT_ERROR_MESSAGE = (
"isaaclab_ppisp is required when CameraCfg.isp_cfg is set. "
"It ships with the Isaac Lab wheel (`pip install isaaclab`); otherwise install the "
"isaaclab-ppisp extension from the Isaac Lab source checkout."
)
def _raise_missing_ppisp_error(exc: ModuleNotFoundError) -> NoReturn:
# Only translate missing isaaclab_ppisp imports into the optional-dependency hint;
# unrelated missing modules should surface unchanged for easier debugging.
if exc.name != "isaaclab_ppisp" and not (exc.name and exc.name.startswith("isaaclab_ppisp.")):
raise exc
raise ModuleNotFoundError(_PPISP_IMPORT_ERROR_MESSAGE, name="isaaclab_ppisp") from exc
class RenderData:
# Back-compat alias for callers of ``RenderData.OutputNames``.
OutputNames = RenderBufferKind
# Maps each supported RenderBufferKind to (CameraOutputs field name, Newton warp dtype).
# Newton reinterprets the allocated buffer memory: e.g. RGBA is allocated as (N,H,W,4) uint8
# but the Newton sensor API consumes it as (world_count,1,H,W) uint32 (same bytes, packed view).
#
# The depth family (``distance_to_camera`` / ``distance_to_image_plane`` / ``depth``) is handled
# separately in :meth:`set_outputs` rather than through this map, because Newton emits a single
# ray-hit-distance buffer that must be reused as the source for the planar-depth conversion.
#
# The segmentation family (``semantic_segmentation`` / ``instance_segmentation``) is likewise
# handled separately: Newton emits a single per-shape index buffer that is remapped into each
# requested segmentation output by
# :class:`~isaaclab_newton.renderers.segmentation.NewtonSegmentationMapper`.
_OUTPUT_MAP: dict[str, tuple[str, type]] = {
str(RenderBufferKind.RGBA): ("color_image", wp.uint32),
str(RenderBufferKind.RGB_HDR): ("hdr_color_image", wp.vec3f),
str(RenderBufferKind.ALBEDO): ("albedo_image", wp.uint32),
str(RenderBufferKind.NORMALS): ("normals_image", wp.vec3f),
}
# Newton's native ``depth_image`` is the ray-hit (euclidean) distance from the camera optical
# center, which is Isaac Lab's ``distance_to_camera``.
_RAY_DEPTH_KIND: str = str(RenderBufferKind.DISTANCE_TO_CAMERA)
# Planar-depth outputs (distance along the camera's forward axis). ``depth`` is Isaac Lab's alias
# for ``distance_to_image_plane``. Both are derived from the ray depth via
# ``convert_ray_depth_to_forward_depth``.
_PLANE_DEPTH_KINDS: frozenset[str] = frozenset(
{
str(RenderBufferKind.DEPTH),
str(RenderBufferKind.DISTANCE_TO_IMAGE_PLANE),
}
)
@dataclass
class CameraOutputs:
color_image: wp.array(dtype=wp.uint32, ndim=4) = None
hdr_color_image: wp.array(dtype=wp.vec3f, ndim=4) = None
albedo_image: wp.array(dtype=wp.uint32, ndim=4) = None
# Buffer Newton fills with ray-hit (euclidean) distance. Bound either to the caller's
# ``distance_to_camera`` output or to an internal scratch buffer (see :meth:`set_outputs`).
depth_image: wp.array(dtype=wp.float32, ndim=4) = None
normals_image: wp.array(dtype=wp.vec3f, ndim=4) = None
# Buffer Newton fills with the per-pixel shape index; the source for all segmentation outputs.
shape_index_image: wp.array(dtype=wp.uint32, ndim=4) = None
def __init__(
self,
newton_sensor: newton.sensors.SensorTiledCamera,
spec: CameraRenderSpec,
seg_mapper: NewtonSegmentationMapper | None = None,
renderer_cfg: NewtonWarpRendererCfg | None = None,
):
self.newton_sensor = newton_sensor
# Shared, scene-static segmentation lookup builder (``None`` until segmentation is requested).
self._seg_mapper = seg_mapper
self._renderer_cfg = renderer_cfg
self.num_cameras = 1
self.camera_rays: wp.array(dtype=wp.vec3f, ndim=4) = None
self.camera_transforms: wp.array(dtype=wp.transformf, ndim=2) = None
self._camera_quat_scratch: wp.array = None
# Name under which this camera's render launch is registered with the
# Newton sensor manager (set on first render).
self.sensor_task_name: str | None = None
self.outputs = RenderData.CameraOutputs()
# Requested depth-family destination views keyed by data-type name. Each view aliases the
# caller's output buffer as ``(world_count, 1, H, W)`` float32.
self._depth_dests: dict[str, wp.array] = {}
# Internal ray-depth buffer allocated only when a planar-depth output is requested without
# ``distance_to_camera``; gives ``convert_ray_depth_to_forward_depth`` a source to read from.
self._ray_depth_scratch: wp.array | None = None
# Requested segmentation outputs keyed by data-type name -> (destination view, mapping). Each view
# aliases the caller's output buffer as ``(world_count, 1, H, W)`` uint32.
self._seg_dests: dict[str, tuple[wp.array, NewtonSegmentationMapping]] = {}
self.width = getattr(spec.cfg, "width", 100)
self.height = getattr(spec.cfg, "height", 100)
# Camera clipping planes [m] from ``spawn.clipping_range`` (``[0]`` near, ``[1]`` far).
# Newton's ray tracer has no near-plane parameter, so only the far plane is enforced (through
# the sensor's ``max_distance``); ``near_clip`` is captured for consumers but not applied.
spawn = getattr(spec.cfg, "spawn", None)
clipping_range = getattr(spawn, "clipping_range", None)
self.near_clip: float | None = float(clipping_range[0]) if clipping_range is not None else None
self.far_clip: float | None = float(clipping_range[1]) if clipping_range is not None else None
# ABGR clear color packed as uint32 — Newton's SensorTiledCamera reads the low byte as R,
# next as G, next as B, high byte as A (little-endian RGBA in memory). Default is 93% gray
# (0xFFEEEEEE), matching the RTX renderer background and improving visibility of dark objects.
background_color = getattr(spec.cfg, "background_color", None)
if background_color is not None:
r, g, b = (max(0, min(255, round(c * 255))) for c in background_color)
self.clear_color: int = (0xFF << 24) | (b << 16) | (g << 8) | r
else:
self.clear_color = 0xFFEEEEEE
# Post-render PPISP pipeline composed when ``spec.cfg.isp_cfg`` is set.
# ``isp_cfg`` is already fully normalized by ``prepare_cameras`` by the time it reaches here.
self.ppisp_pipeline: PpispPipeline | None = None
if spec.cfg.isp_cfg is not None:
try:
from isaaclab_ppisp import PpispPipeline
except ModuleNotFoundError as exc:
_raise_missing_ppisp_error(exc)
self.ppisp_pipeline = PpispPipeline(spec.cfg.isp_cfg)
self._hdr_scratch_wp: wp.array | None = None
"""Internal HDR scratch buffer allocated when PPISP is composed but the
user did not request ``"rgb_hdr"`` in ``data_types``. Also exposed to
the Newton sensor through :attr:`CameraOutputs.hdr_color_image` as a
vec3f reinterpretation of this same backing storage."""
self._ppisp_hdr_source: wp.array | None = None
"""PPISP HDR source bound once in :meth:`set_outputs` from the caller's
``rgb_hdr`` output or :attr:`_hdr_scratch_wp`."""
self._ppisp_rgba_dest: wp.array | None = None
"""PPISP LDR destination bound once in :meth:`set_outputs` from the
caller's ``rgba`` output."""
def _view(self, proxy: ProxyArray, dtype: type, shape: tuple[int, ...]) -> wp.array:
"""Alias the caller's output buffer as a ``(world_count, 1, H, W)`` warp array of ``dtype``.
Newton reinterprets the backing memory in place (no copy), so the sensor writes directly
into the camera's output buffer.
"""
wp_arr = proxy.warp
return wp.array(ptr=wp_arr.ptr, dtype=dtype, shape=shape, device=wp_arr.device, copy=False)
def set_outputs(self, output_data: dict[str, ProxyArray]):
shape = (self.newton_sensor.model.world_count, self.num_cameras, self.height, self.width)
self._depth_dests = {}
self._ray_depth_scratch = None
self._seg_dests = {}
self.outputs.shape_index_image = None
ray_depth_dest: wp.array | None = None
for output_name, proxy in output_data.items():
# Depth family: bind each requested output to a float32 destination view. Newton fills
# only the ray-hit distance; planar outputs are derived from it in :meth:`_convert_plane_depth`.
if output_name == self._RAY_DEPTH_KIND or output_name in self._PLANE_DEPTH_KINDS:
dest = self._view(proxy, wp.float32, shape)
self._depth_dests[output_name] = dest
if output_name == self._RAY_DEPTH_KIND:
ray_depth_dest = dest
continue
# Segmentation family: bind each requested output to a destination view — colorized RGBA
# (uint32 packed) or raw int32 ids (matching the Isaac RTX / OVRTX contract). Newton
# fills only the shape-index scratch (uint32), which is remapped into each output in
# :meth:`_convert_segmentation`.
if output_name == RenderBufferKind.SEMANTIC_SEGMENTATION:
colorize = bool(self._renderer_cfg.colorize_semantic_segmentation)
elif output_name == RenderBufferKind.INSTANCE_SEGMENTATION:
colorize = bool(self._renderer_cfg.colorize_instance_segmentation)
else:
colorize = None
if colorize is not None:
if self._seg_mapper is None:
raise RuntimeError(
f"Output '{output_name}' requires a segmentation mapper, but none was created. "
"Ensure the camera's data_types includes the segmentation output."
)
seg_mapping = self._seg_mapper.get_mapping(output_name, colorize)
dest = self._view(proxy, wp.uint32 if colorize else wp.int32, shape)
self._seg_dests[output_name] = (dest, seg_mapping)
continue
mapping = self._OUTPUT_MAP.get(output_name)
if mapping is None:
if output_name != str(RenderBufferKind.RGB):
logger.warning(f"NewtonWarpRenderer - output type {output_name} is not yet supported")
continue
field_name, dtype = mapping
setattr(self.outputs, field_name, self._view(proxy, dtype, shape))
# Bind the buffer Newton fills with ray-hit distance. Write straight into the
# ``distance_to_camera`` output when requested; otherwise allocate an internal scratch so the
# planar-depth conversion has a source to read from.
if ray_depth_dest is not None:
self.outputs.depth_image = ray_depth_dest
elif any(name in self._PLANE_DEPTH_KINDS for name in self._depth_dests):
self._ray_depth_scratch = wp.zeros(shape, dtype=wp.float32, device=self.newton_sensor.model.device)
self.outputs.depth_image = self._ray_depth_scratch
else:
self.outputs.depth_image = None
# Allocate the shape-index buffer Newton fills when any segmentation output is requested; all
# requested segmentation outputs are remapped from this single buffer in :meth:`_convert_segmentation`.
if self._seg_dests:
self.outputs.shape_index_image = wp.zeros(shape, dtype=wp.uint32, device=self.newton_sensor.model.device)
# When PPISP is composed but the user did not request the raw HDR AOV,
# allocate an internal HDR scratch buffer and route a vec3f-shaped view
# of it as the Newton sensor's ``hdr_color_image`` so the renderer
# fills it directly.
if self.ppisp_pipeline is not None and self.outputs.hdr_color_image is None:
ref_proxy = next(iter(output_data.values()))
self._hdr_scratch_wp = wp.zeros(
(self.newton_sensor.model.world_count, self.height, self.width, 3),
dtype=wp.float32,
device=ref_proxy.device,
)
self.outputs.hdr_color_image = wp.array(
ptr=self._hdr_scratch_wp.ptr,
dtype=wp.vec3f,
shape=shape,
device=self._hdr_scratch_wp.device,
copy=False,
)
# Bind the two warp arrays the per-frame PPISP dispatch needs.
if self.ppisp_pipeline is not None:
if str(RenderBufferKind.RGBA) not in output_data:
raise ValueError(
"Newton renderer ISP requires 'rgba' (or 'rgb', which aliases into rgba) as the"
" LDR output destination, but neither was provided. Add 'rgb' or 'rgba' to"
" Camera.cfg.data_types when isp_cfg is set."
)
hdr_proxy = output_data.get(str(RenderBufferKind.RGB_HDR))
self._ppisp_hdr_source = hdr_proxy.warp if hdr_proxy is not None else self._hdr_scratch_wp
self._ppisp_rgba_dest = output_data[str(RenderBufferKind.RGBA)].warp
def get_output(self, output_name: str) -> wp.array:
if output_name in self._depth_dests:
return self._depth_dests[output_name]
elif output_name in self._seg_dests:
return self._seg_dests[output_name][0]
elif output_name == RenderBufferKind.RGBA:
return self.outputs.color_image
elif output_name == RenderBufferKind.RGB_HDR:
return self.outputs.hdr_color_image
elif output_name == RenderBufferKind.ALBEDO:
return self.outputs.albedo_image
elif output_name == RenderBufferKind.NORMALS:
return self.outputs.normals_image
return None
def _convert_segmentation(self):
"""Remap Newton's shape-index buffer into each requested segmentation output.
Newton emits a single per-pixel shape index (:attr:`CameraOutputs.shape_index_image`);
``semantic_segmentation`` / ``instance_segmentation`` are each derived from it by a
:class:`~isaaclab_newton.renderers.segmentation.NewtonSegmentationMapping`.
No-op when no segmentation output was requested.
"""
if self.outputs.shape_index_image is None:
return
for dest, seg_mapping in self._seg_dests.values():
seg_mapping.convert_shape_index_to_output(self.outputs.shape_index_image, dest)
def segmentation_info(self) -> dict[str, dict]:
"""Per-output ``idToLabels`` / ``idToSemantics`` info for the requested segmentation outputs."""
return {name: seg_mapping.info for name, (_dest, seg_mapping) in self._seg_dests.items()}
def _convert_plane_depth(self):
"""Fill any planar-depth outputs from the ray-hit distance Newton just rendered.
Newton emits ``distance_to_camera`` (euclidean ray distance). ``depth`` and
``distance_to_image_plane`` are the projection of that distance onto the camera's forward
axis, computed by :meth:`newton.sensors.SensorTiledCamera.Utils.convert_ray_depth_to_forward_depth`.
No-op when only ``distance_to_camera`` (or no depth output) was requested.
"""
assert self.outputs.depth_image is not None, "Expected a depth image to convert"
for output_name, dest in self._depth_dests.items():
if output_name in self._PLANE_DEPTH_KINDS:
self.newton_sensor.utils.convert_ray_depth_to_forward_depth(
self.outputs.depth_image,
self.camera_transforms,
self.camera_rays,
out_depth=dest,
)
def _apply_depth_clipping(self, behavior: str):
"""Apply the renderer's depth-clipping behavior to the depth-family outputs.
Newton writes ``0.0`` for rays that miss all geometry or fall beyond the far plane
(``max_distance``), so ``"none"`` and ``"zero"`` both leave that ``0.0`` background. ``"max"``
replaces the background with the far clip [m] to mirror the RTX renderer's
:attr:`~isaaclab_physx.renderers.IsaacRtxRendererCfg.depth_clipping_behavior`. No-op when no
depth output was requested or the camera did not provide a clipping range.
"""
if behavior != "max" or self.far_clip is None:
return
for dest in self._depth_dests.values():
replace_background_depth_wp(dest, self.far_clip, device=dest.device)
def update(self, positions: ProxyArray, orientations: ProxyArray, intrinsics: ProxyArray):
# Buffers are persistent: the sensor manager graph captures the render
# launch against `camera_transforms`, so it must be updated in place.
if self._camera_quat_scratch is None:
self._camera_quat_scratch = wp.empty_like(orientations)
if self.camera_transforms is None:
self.camera_transforms = wp.empty(
(1, self.newton_sensor.model.world_count),
dtype=wp.transformf,
device=self.newton_sensor.model.device,
)
converted_wp = self._camera_quat_scratch
convert_camera_frame_orientation_convention_wp(
src=orientations,
dst=converted_wp,
origin="world",
target="opengl",
device=self.newton_sensor.model.device,
)
wp.launch(
RenderData._update_transforms,
self.newton_sensor.model.world_count,
[positions, converted_wp, self.camera_transforms],
device=self.newton_sensor.model.device,
)
if self.camera_rays is None:
first_focal_length = intrinsics.torch[:, 1, 1][0:1]
fov_radians_all = 2.0 * torch.atan(self.height / (2.0 * first_focal_length))
fov_warp = wp.from_torch(fov_radians_all, dtype=wp.float32)
self.camera_rays = self.newton_sensor.utils.compute_camera_rays_pinhole(
self.width, self.height, camera_fovs=fov_warp
)
@wp.kernel
def _update_transforms(
positions: wp.array(dtype=wp.vec3f),
orientations: wp.array(dtype=wp.quatf),
output: wp.array(dtype=wp.transformf, ndim=2),
):
tid = wp.tid()
output[0, tid] = wp.transformf(positions[tid], orientations[tid])
[docs]
class NewtonWarpRenderer(BaseRenderer):
"""Newton Warp backend for tiled camera rendering."""
RenderData = RenderData
[docs]
def __init__(self, cfg: NewtonWarpRendererCfg):
"""Pre-physics initialization."""
from isaaclab.physics.scene_data_requirements import (
aggregate_requirements,
requirement_for_renderer_type,
)
self.cfg = cfg
self.newton_sensor: newton.sensors.SensorTiledCamera | None = None
# USD stage captured in ``prepare_cameras``; used by the segmentation mapper to read semantics.
self._stage: Any = None
# Shared, scene-static segmentation lookup builder, created lazily in ``create_render_data``.
self._seg_mapper: NewtonSegmentationMapper | None = None
sim = SimulationContext.instance()
current_req = sim.get_scene_data_requirements()
renderer_req = requirement_for_renderer_type("newton_warp")
merged = aggregate_requirements([current_req, renderer_req])
if merged != current_req:
sim.update_scene_data_requirements(merged)
def initialize(self) -> None:
"""Post-physics setup: read the built Newton model and construct the sensor."""
self._newton_model = NewtonManager.get_model()
if self._newton_model is None:
raise RuntimeError(
"NewtonWarpRenderer requires a Newton model but the Newton manager has no model. "
"This usually means the Newton model failed to build from the USD stage "
"(e.g., unsupported PhysX schemas such as tendons). "
"Check the log for earlier Newton model build errors."
)
self.newton_sensor = newton.sensors.SensorTiledCamera(
self._newton_model,
default_render_config=newton.sensors.SensorTiledCamera.RenderConfig(
enable_textures=self.cfg.enable_textures,
enable_shadows=self.cfg.enable_shadows,
enable_ambient_lighting=self.cfg.enable_ambient_lighting,
enable_backface_culling=self.cfg.enable_backface_culling,
max_distance=self.cfg.max_distance,
render_order=newton.sensors.SensorTiledCamera.RenderOrder.TILED,
tile_width=self.cfg.tile_rendering_width,
tile_height=self.cfg.tile_rendering_height,
),
)
if self.cfg.render_order == "pixel_priority":
self.newton_sensor.default_render_config.render_order = (
newton.sensors.SensorTiledCamera.RenderOrder.PIXEL_PRIORITY
)
elif self.cfg.render_order == "view_priority":
self.newton_sensor.default_render_config.render_order = (
newton.sensors.SensorTiledCamera.RenderOrder.VIEW_PRIORITY
)
if self.cfg.create_default_light:
self.newton_sensor.utils.create_default_light(enable_shadows=self.cfg.enable_shadows)
def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]:
"""Publish the per-output layout this Newton Warp backend writes.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.supported_output_types`."""
def seg_spec(colorize: bool) -> RenderBufferSpec:
# Colorized segmentation is RGBA uint8; raw segmentation is a single int32 id channel
# (matching the Isaac RTX / OVRTX contract so backend-independent consumers see the same dtype).
return RenderBufferSpec(4, wp.uint8) if colorize else RenderBufferSpec(1, wp.int32)
return {
RenderBufferKind.RGBA: RenderBufferSpec(4, wp.uint8),
RenderBufferKind.RGB: RenderBufferSpec(3, wp.uint8),
RenderBufferKind.RGB_HDR: RenderBufferSpec(3, wp.float32),
RenderBufferKind.ALBEDO: RenderBufferSpec(4, wp.uint8),
RenderBufferKind.DEPTH: RenderBufferSpec(1, wp.float32),
RenderBufferKind.DISTANCE_TO_CAMERA: RenderBufferSpec(1, wp.float32),
RenderBufferKind.DISTANCE_TO_IMAGE_PLANE: RenderBufferSpec(1, wp.float32),
RenderBufferKind.NORMALS: RenderBufferSpec(3, wp.float32),
RenderBufferKind.SEMANTIC_SEGMENTATION: seg_spec(self.cfg.colorize_semantic_segmentation),
RenderBufferKind.INSTANCE_SEGMENTATION: seg_spec(self.cfg.colorize_instance_segmentation),
}
def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None:
"""Resolve the camera's PPISP cfg before rendering.
:mod:`isaaclab.sensors.camera` does not depend on PPISP; the renderer
owns the sentinel-resolution + cfg-normalization step. Newton has no
USD-side overrides to author beyond this.
Also captures the USD ``stage`` so the segmentation mapper can read the scene's
:class:`UsdSemantics.LabelsAPI` labels when a segmentation output is requested.
"""
self._stage = stage
# NOTE: OpenCV lens distortion (``spawn.distortion``) is not yet applied by the Newton
# renderer. The distortion cfg is renderer-agnostic and could be piped through Newton's warp
# ray-tracing utilities here in the future; for now the camera renders undistorted. This is
# the intended extension point.
spawn = getattr(spec.cfg, "spawn", None)
if getattr(spawn, "distortion", None) is not None:
logger.warning(
"OpenCV lens distortion is set on the camera cfg but is not yet applied by the Newton"
" renderer: it derives a single field of view from fy, so the distortion coefficients,"
" the principal point, and a non-square fx are ignored and the camera renders as a"
" centered, square-pixel pinhole. Use the RTX/OVRTX renderer to apply the full model."
)
if spec.cfg.isp_cfg is None:
return
try:
from isaaclab_ppisp import resolve_and_normalize
except ModuleNotFoundError as exc:
_raise_missing_ppisp_error(exc)
camera_prim_path = spec.camera_prim_paths[0] if spec.camera_prim_paths else None
spec.cfg.isp_cfg = resolve_and_normalize(spec.cfg.isp_cfg, stage, camera_prim_path)
def prepare_stage(self, stage: Any, num_envs: int) -> None:
"""No-op for Newton Warp - uses Newton scene directly without stage export.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.prepare_stage`."""
pass
def create_render_data(self, spec: CameraRenderSpec) -> RenderData:
"""Create render data for the Newton tiled camera.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.create_render_data`."""
# Build the shared segmentation mapper and its per-kind lookup tables up-front for all
# requested segmentation outputs.
if (
RenderBufferKind.SEMANTIC_SEGMENTATION in spec.cfg.data_types
or RenderBufferKind.INSTANCE_SEGMENTATION in spec.cfg.data_types
):
if self._seg_mapper is None:
self._seg_mapper = NewtonSegmentationMapper(self._newton_model, self._stage, self.cfg)
if RenderBufferKind.SEMANTIC_SEGMENTATION in spec.cfg.data_types:
self._seg_mapper.build_mapping(
RenderBufferKind.SEMANTIC_SEGMENTATION, bool(self.cfg.colorize_semantic_segmentation)
)
if RenderBufferKind.INSTANCE_SEGMENTATION in spec.cfg.data_types:
self._seg_mapper.build_mapping(
RenderBufferKind.INSTANCE_SEGMENTATION, bool(self.cfg.colorize_instance_segmentation)
)
render_data = RenderData(self.newton_sensor, spec, seg_mapper=self._seg_mapper, renderer_cfg=self.cfg)
return render_data
def set_outputs(self, render_data: RenderData, output_data: dict[str, ProxyArray]):
"""Store output buffers. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.set_outputs`."""
render_data.set_outputs(output_data)
def update_transforms(self):
"""Sync Newton scene state before rendering.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_transforms`."""
sim = SimulationContext.instance()
sim.physics_manager.forward()
NewtonManager.update_visualization_state()
def update_geometries(self) -> None:
"""No-op for Newton Warp - geometry is read directly from Newton state during render.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_geometries`."""
pass
def update_camera(
self,
render_data: RenderData,
positions: ProxyArray,
orientations: ProxyArray,
intrinsics: ProxyArray,
):
"""Update camera poses and intrinsics.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_camera`."""
render_data.update(positions, orientations, intrinsics)
def render(self, render_data: RenderData):
"""Render and write to output buffers. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.render`."""
# Refresh the shadow state under PhysX before the manager refits the BVH.
NewtonManager.get_state()
if render_data.sensor_task_name is None:
render_data.sensor_task_name = f"newton_warp_render:{id(render_data)}"
NewtonManager._register_sensor_task(render_data.sensor_task_name, lambda: self._launch_render(render_data))
NewtonManager._update_sensor_tasks(render_data.sensor_task_name)
# Post-render PPISP: HDR scene-linear → LDR RGBA. Source/destination
# tensors were bound once in ``set_outputs``.
if render_data.ppisp_pipeline is not None:
render_data.ppisp_pipeline.apply(
render_data._ppisp_hdr_source,
render_data._ppisp_rgba_dest,
)
def _launch_render(self, render_data: RenderData) -> None:
"""Launch the tiled-camera render kernels for sensor graph capture."""
# default_render_config is shared state across all Newton sensors, so set max_distance
# immediately before each render call rather than once in create_render_data.
self.newton_sensor.default_render_config.max_distance = (
render_data.far_clip if render_data.far_clip is not None else self.cfg.max_distance
)
# Use the renderer's clear value to fill distance_to_camera background when it is the only
# depth output requested. This avoids a post-render kernel pass for that common case.
# Planar-depth outputs (depth / distance_to_image_plane) are derived by
# _convert_plane_depth(), which reads the same ray-depth buffer; pre-clearing to far_clip
# would make it compute far_clip * cos(θ) per pixel instead of 0.0, so the <= 0.0
# sentinel that _apply_depth_clipping relies on would no longer identify background pixels.
_depth_kinds = set(render_data._depth_dests)
_use_depth_clear = (
self.cfg.depth_clipping_behavior == "max"
and render_data.far_clip is not None
and render_data._RAY_DEPTH_KIND in _depth_kinds
and not (_depth_kinds & render_data._PLANE_DEPTH_KINDS)
)
self.newton_sensor.update(
NewtonManager.get_state_0(),
render_data.camera_transforms,
render_data.camera_rays,
color_image=render_data.outputs.color_image,
hdr_color_image=render_data.outputs.hdr_color_image,
albedo_image=render_data.outputs.albedo_image,
depth_image=render_data.outputs.depth_image,
normal_image=render_data.outputs.normals_image,
shape_index_image=render_data.outputs.shape_index_image,
# ARGB 93% gray to improve visibility of dark objects and align with RTX renderer background
clear_data=newton.sensors.SensorTiledCamera.ClearData(
clear_color=render_data.clear_color,
**({"clear_depth": render_data.far_clip} if _use_depth_clear else {}),
),
kernel_block_dim=self.cfg.kernel_block_dim,
)
if _depth_kinds & render_data._PLANE_DEPTH_KINDS:
# Derive planar depth from the ray-hit distance, then clip. Deliberately no clear_depth
# here: the ray-depth buffer feeds convert_plane_depth() (see the _use_depth_clear note).
render_data._convert_plane_depth()
render_data._apply_depth_clipping(self.cfg.depth_clipping_behavior)
# Remap the shape-index buffer into the requested segmentation outputs.
render_data._convert_segmentation()
def read_output(self, render_data: RenderData, camera_data: CameraData) -> None:
"""Copy rendered outputs to the camera data buffers.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.read_output`."""
for output_name in camera_data.output:
if output_name == "rgb":
continue
image_data = render_data.get_output(output_name)
if image_data is not None:
output_wp = camera_data.output[output_name].warp
if image_data.ptr != output_wp.ptr:
wp.copy(output_wp, image_data)
# Publish the segmentation id-to-label metadata (idToLabels / idToSemantics) alongside the
# pixel buffers.
for output_name, info in render_data.segmentation_info().items():
camera_data.info[output_name] = info
def cleanup(self, render_data: RenderData | None):
"""Release resources and drop the camera's sensor task.
See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.cleanup`."""
if render_data:
if render_data.sensor_task_name is not None:
NewtonManager._unregister_sensor_task(render_data.sensor_task_name)
render_data.sensor_task_name = None
render_data.sensor = None