Source code for isaaclab_physx.renderers.isaac_rtx_renderer

# 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

"""Isaac RTX renderer using Omniverse Replicator for tiled camera rendering."""

from __future__ import annotations

import json
import logging
import math
import uuid
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, NoReturn

import numpy as np
import warp as wp
from packaging import version

from pxr import Sdf, Usd, UsdGeom

from isaaclab.app.settings_manager import get_settings_manager
from isaaclab.renderers import BaseRenderer, RenderBufferKind, RenderBufferSpec
from isaaclab.renderers.camera_render_spec import CameraRenderSpec
from isaaclab.sim.utils import enable_extension
from isaaclab.utils.version import get_isaac_sim_version
from isaaclab.utils.warp.kernels import reshape_tiled_image
from isaaclab.utils.warp.warp_math import clamp_depth_to_inf_wp, replace_inf_depth_wp

from .isaac_rtx_renderer_utils import (
    apply_isaac_rtx_determinism_settings,
    apply_isaac_rtx_global_settings,
    ensure_isaac_rtx_render_update,
    ensure_rtx_hydra_engine_attached,
)
from .visual_material import FabricVisualMaterialWriter

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from isaaclab_ppisp import PpispPipeline

    from omni.replicator.core.scripts.utils.viewport_manager import HydraTexture

    from isaaclab.sensors.camera.camera_data import CameraData
    from isaaclab.utils.warp import ProxyArray

from .isaac_rtx_renderer_cfg import IsaacRtxRendererCfg

_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


# RTX simple-shading constants.
#
# Simple shading is driven by Kit's RTX "Minimal" render mode via the
# ``/rtx/minimal/mode`` carb setting (key ``omni:rtx:minimal:mode``), with
# integer values:
#   0 = No Rendering (black output; only other AOVs are produced)
#   1 = Constant Diffuse (single constant color for all surfaces)
#   2 = Texture Diffuse  (diffuse shading using texture colors)
#   3 = Diffuse/Glossy/Emission (full material shading)
#
# The public data-type names we expose (``simple_shading_*``) are kept stable
# for backwards compatibility and map onto the Kit integer values below.
SIMPLE_SHADING_AOV = "SimpleShadingSD"
SIMPLE_SHADING_MODES = {
    "simple_shading_constant_diffuse": 1,
    "simple_shading_diffuse_mdl": 2,
    "simple_shading_full_mdl": 3,
}
SIMPLE_SHADING_MODE_SETTING = "/rtx/minimal/mode"


def _camera_semantic_filter_predicate(semantic_filter: str | list[str]) -> str:
    """Build the instance-mapping semantics predicate from :attr:`isaaclab.sensors.camera.CameraCfg.semantic_filter`.

    Replicator's semantic/instance segmentation annotators consume this via the synthetic-data pipeline.
    """
    if isinstance(semantic_filter, list):
        return ":*; ".join(semantic_filter) + ":*"
    return semantic_filter


@dataclass
class IsaacRtxRenderData:
    """Render data for Isaac RTX renderer."""

    annotators: dict[str, Any]
    render_product: HydraTexture
    output_data: dict[str, ProxyArray] | None = None
    spec: CameraRenderSpec | None = None
    renderer_info: dict[str, Any] = field(default_factory=dict)
    ppisp_pipeline: PpispPipeline | None = None
    """Post-render PPISP pipeline composed when ``spec.cfg.isp_cfg`` is set."""
    _hdr_scratch_wp: wp.array | None = None
    """Internal HDR scratch buffer allocated when the user did not request
    ``"rgb_hdr"`` in ``data_types`` but the PPISP pipeline still needs
    somewhere to receive the HDR AOV before LDR conversion."""


[docs] class IsaacRtxRenderer(BaseRenderer): """Isaac RTX backend using Omniverse Replicator for tiled camera rendering. Requires Isaac Sim. """
[docs] def __init__(self, cfg: IsaacRtxRendererCfg): self.cfg = cfg # Enable Replicator only when the Isaac RTX renderer is selected. Declaring it # in a Kit experience would resolve its bundled omni.warp.core dependency at startup. enable_extension("omni.replicator.core") settings = get_settings_manager() apply_isaac_rtx_global_settings(self.cfg.global_settings, settings) if settings.get("/isaaclab/render/deterministic", False): apply_isaac_rtx_determinism_settings(settings) ensure_rtx_hydra_engine_attached()
# ``/isaaclab/render/rtx_sensors`` is owned by ``Camera.__init__`` (must be set pre-``sim.reset()``). @property def visual_material_writer(self): """Write material channels directly through Fabric.""" return FabricVisualMaterialWriter def prepare_cameras(self, stage: Any, spec: CameraRenderSpec) -> None: """Resolve the camera's PPISP cfg and apply RTX-specific USD overrides. When ``spec.cfg.isp_cfg`` is set, resolves it (sentinel discovery + normalization) via :func:`isaaclab_ppisp.resolve_and_normalize` so :mod:`isaaclab` does not need to know about PPISP. Then pins ``exposure:*`` to neutral and applies ``OmniRtxCameraExposureAPI_1`` so RTX's physical-camera exposure model does not compound on top of the ISP. Without an ISP, the camera prim's authored exposure is left alone. :attr:`~isaaclab.sensors.camera.CameraCfg.background_color` is applied per-render-product in :meth:`create_render_data` via USD attributes. """ if spec.cfg.isp_cfg is None: return try: from isaaclab_ppisp import apply_rtx_exposure_overrides, 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) if spec.cfg.isp_cfg is None or not spec.camera_prim_paths: return apply_rtx_exposure_overrides(stage, list(spec.camera_prim_paths)) def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec]: """Publish the per-output Replicator layout this RTX backend writes. ``ALBEDO`` and the three ``SIMPLE_SHADING_*`` outputs require Isaac Sim 6.0+ and are omitted on older versions. The three segmentation outputs report ``RenderBufferSpec(4, uint8)`` when the matching ``self.cfg.colorize_*`` flag is set, otherwise ``RenderBufferSpec(1, int32)``. """ sim_major = get_isaac_sim_version().major specs: dict[RenderBufferKind, RenderBufferSpec] = { # Replicator's native layout for color output is rgba/uint8; # ``Camera`` aliases ``rgb`` as a view into ``rgba`` storage. RenderBufferKind.RGBA: RenderBufferSpec(4, wp.uint8), RenderBufferKind.RGB: RenderBufferSpec(3, wp.uint8), RenderBufferKind.RGB_HDR: RenderBufferSpec(3, wp.float32), RenderBufferKind.DEPTH: RenderBufferSpec(1, wp.float32), RenderBufferKind.DISTANCE_TO_IMAGE_PLANE: RenderBufferSpec(1, wp.float32), RenderBufferKind.DISTANCE_TO_CAMERA: RenderBufferSpec(1, wp.float32), RenderBufferKind.NORMALS: RenderBufferSpec(3, wp.float32), RenderBufferKind.MOTION_VECTORS: RenderBufferSpec(2, wp.float32), } if sim_major >= 6: specs[RenderBufferKind.ALBEDO] = RenderBufferSpec(4, wp.uint8) for shading_type in SIMPLE_SHADING_MODES: specs[RenderBufferKind(shading_type)] = RenderBufferSpec(3, wp.uint8) seg_specs = ( (RenderBufferKind.SEMANTIC_SEGMENTATION, self.cfg.colorize_semantic_segmentation), (RenderBufferKind.INSTANCE_SEGMENTATION, self.cfg.colorize_instance_segmentation), (RenderBufferKind.INSTANCE_ID_SEGMENTATION_FAST, self.cfg.colorize_instance_id_segmentation), ) for name, colorize in seg_specs: specs[name] = RenderBufferSpec(4, wp.uint8) if colorize else RenderBufferSpec(1, wp.int32) return specs def prepare_stage(self, stage: Usd.Stage, num_envs: int) -> None: """Author per-env ``omni:scenePartition`` attributes for RTX cull-by-env rendering. Authoring is controlled by :attr:`~isaaclab_physx.renderers.IsaacRtxRendererCfg.enable_scene_partitioning`. When disabled, this method is a no-op and writes no ``primvars:omni:scenePartition`` or ``omni:scenePartition`` attributes. When enabled, for each ``/World/envs/env_{i}`` root, writes the inheriting primvar ``primvars:omni:scenePartition`` (token ``env_{i}``) on the root and the matching non-primvar ``omni:scenePartition`` token on every :class:`UsdGeom.Camera` descendant. RTX honors primvar inheritance, so the env-root primvar propagates to all descendant geometry and isolates each env's render tile. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.prepare_stage`.""" if not self.cfg.enable_scene_partitioning: return logger.debug( "Per-environment RTX scene partitioning is enabled. Authoring primvars:omni:scenePartition on %d env(s).", num_envs, ) root_layer = stage.GetRootLayer() token_type = Sdf.ValueTypeNames.Token with Sdf.ChangeBlock(): for env_idx in range(num_envs): env_prim = stage.GetPrimAtPath(f"/World/envs/env_{env_idx}") if not env_prim.IsValid(): continue token = f"env_{env_idx}" for prim in Usd.PrimRange(env_prim): if prim == env_prim: attr_path = prim.GetPath().AppendProperty("primvars:omni:scenePartition") elif prim.IsA(UsdGeom.Camera): attr_path = prim.GetPath().AppendProperty("omni:scenePartition") else: continue # Idempotent: a different renderer backend sharing this stage may have already # authored this attribute. Re-creating an existing spec raises, so only create # it when absent, then (re)assign the per-env token either way. attr_spec = root_layer.GetAttributeAtPath(attr_path) if attr_spec is None: Sdf.JustCreatePrimAttributeInLayer( root_layer, attr_path, token_type, Sdf.VariabilityUniform, True ) attr_spec = root_layer.GetAttributeAtPath(attr_path) attr_spec.default = token def create_render_data(self, spec: CameraRenderSpec) -> IsaacRtxRenderData: """Create render product and annotators for the tiled camera. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.create_render_data`.""" import omni.replicator.core as rep from omni.syntheticdata import SyntheticData from pxr import UsdGeom from isaaclab.sim.utils.stage import get_current_stage settings = get_settings_manager() isaac_sim_version = get_isaac_sim_version() if isaac_sim_version.major >= 6: needs_color_render = any( data_type in spec.cfg.data_types for data_type in ("rgb", "rgba", str(RenderBufferKind.RGB_HDR)) ) if not needs_color_render: settings.set_bool("/rtx/sdg/force/disableColorRender", True) if settings.get("/isaaclab/has_gui"): settings.set_bool("/rtx/sdg/force/disableColorRender", False) else: unsupported = [] if "albedo" in spec.cfg.data_types: unsupported.append("albedo") unsupported.extend(dt for dt in spec.cfg.data_types if dt in SIMPLE_SHADING_MODES) if unsupported: raise ValueError( "Isaac RTX renderer does not support the following requested data types in" " Isaac Sim versions before 6.0:" f" {unsupported}." ) # HACK: Isaac Sim 4.5 has a bug in Camera that breaks segmentation # outputs for instanceable assets. Disable instancing as a workaround. stage = get_current_stage() if isaac_sim_version == version.parse("4.5") and ( "semantic_segmentation" in spec.cfg.data_types or "instance_segmentation" in spec.cfg.data_types ): logger.warning( "Isaac Sim 4.5 introduced a bug in Camera when outputting instance and semantic" " segmentation outputs for instanceable assets. As a workaround, the instanceable flag on assets" " will be disabled in the current workflow and may lead to longer load times and increased memory" " usage." ) with Sdf.ChangeBlock(): for prim in stage.Traverse(): prim.SetInstanceable(False) # Get camera prim paths from sensor view cam_prim_paths = list(spec.camera_prim_paths) for cam_prim_path in cam_prim_paths: cam_prim = stage.GetPrimAtPath(cam_prim_path) if not cam_prim.IsA(UsdGeom.Camera): raise RuntimeError(f"Prim at path '{cam_prim_path}' is not a Camera.") # Unique UUID name so concurrent tiled cameras and sequential env create/destroy # cycles in one Kit process do not reuse a stale Replicator / SyntheticData activation. # ``uuid4().hex`` (no hyphens) prefixed with ``rp_`` is a valid USD identifier. # Collision risk is negligible: uuid4 provides 122 random bits, so the birthday-paradox # chance among n names is ~n^2 / 2^123 (e.g. ~10^-25 for a million names). rp = rep.create.render_product_tiled( cameras=cam_prim_paths, tile_resolution=(spec.cfg.width, spec.cfg.height), name=f"rp_{uuid.uuid4().hex}", ) # Apply background color as per-render-product USD attributes so each render product gets its own # background without touching the process-wide /rtx/background carb settings. background_color = getattr(spec.cfg, "background_color", None) if background_color is not None: r, g, b = background_color rp_prim = stage.GetPrimAtPath(rp.path) if rp_prim is not None and rp_prim.IsValid(): with Sdf.ChangeBlock(): rp_prim.CreateAttribute("omni:rtx:background:source:type", Sdf.ValueTypeNames.Token).Set("color") rp_prim.CreateAttribute("omni:rtx:background:source:color", Sdf.ValueTypeNames.Float3).Set( (r, g, b) ) else: logger.warning( "create_render_data: render product prim at '%s' not found; background_color will not be applied.", rp.path, ) # Synthetic-data instance mapping filter for segmentation; before annotator attach. SyntheticData.Get().set_instance_mapping_semantic_filter( _camera_semantic_filter_predicate(self.cfg.semantic_filter) ) # Register simple shading if needed if any(data_type in SIMPLE_SHADING_MODES for data_type in spec.cfg.data_types): rep.AnnotatorRegistry.register_annotator_from_aov( aov=SIMPLE_SHADING_AOV, output_data_type=np.uint8, output_channels=4 ) # Set simple shading mode (if requested) before rendering simple_shading_mode = self._resolve_simple_shading_mode(spec) if simple_shading_mode is not None: get_settings_manager().set_int(SIMPLE_SHADING_MODE_SETTING, simple_shading_mode) needs_hdr_color = str(RenderBufferKind.RGB_HDR) in spec.cfg.data_types or ( spec.cfg.isp_cfg is not None and any(data_type in ("rgb", "rgba") for data_type in spec.cfg.data_types) ) if needs_hdr_color: rep.AnnotatorRegistry.register_annotator_from_aov( aov="HdrColor", output_data_type=np.float32, output_channels=4 ) # Define annotators based on requested data types annotators = {} for annotator_type in spec.cfg.data_types: if annotator_type == "rgba" or annotator_type == "rgb": if spec.cfg.isp_cfg is not None: if str(RenderBufferKind.RGB_HDR) not in annotators: annotator = rep.AnnotatorRegistry.get_annotator( "HdrColor", device=spec.device, do_array_copy=False ) annotators[str(RenderBufferKind.RGB_HDR)] = annotator else: annotator = rep.AnnotatorRegistry.get_annotator("rgb", device=spec.device, do_array_copy=False) annotators["rgba"] = annotator elif annotator_type == str(RenderBufferKind.RGB_HDR): if str(RenderBufferKind.RGB_HDR) not in annotators: annotator = rep.AnnotatorRegistry.get_annotator("HdrColor", device=spec.device, do_array_copy=False) annotators[str(RenderBufferKind.RGB_HDR)] = annotator elif annotator_type == "albedo": # TODO: this is a temporary solution because replicator has not exposed the annotator yet # once it's exposed, we can remove this rep.AnnotatorRegistry.register_annotator_from_aov( aov="DiffuseAlbedoSD", output_data_type=np.uint8, output_channels=4 ) annotator = rep.AnnotatorRegistry.get_annotator( "DiffuseAlbedoSD", device=spec.device, do_array_copy=False ) annotators["albedo"] = annotator elif annotator_type in SIMPLE_SHADING_MODES: annotator = rep.AnnotatorRegistry.get_annotator( SIMPLE_SHADING_AOV, device=spec.device, do_array_copy=False ) annotators[annotator_type] = annotator elif annotator_type == "depth" or annotator_type == "distance_to_image_plane": # keep depth for backwards compatibility annotator = rep.AnnotatorRegistry.get_annotator( "distance_to_image_plane", device=spec.device, do_array_copy=False ) annotators[annotator_type] = annotator # note: we are verbose here to make it easier to understand the code. # if colorize is true, the data is mapped to colors and a uint8 4 channel image is returned. # if colorize is false, the data is returned as a uint32 image with ids as values. else: init_params = None if annotator_type == "semantic_segmentation": init_params = { "colorize": self.cfg.colorize_semantic_segmentation, "mapping": json.dumps(self.cfg.semantic_segmentation_mapping), } elif annotator_type == "instance_segmentation": init_params = {"colorize": self.cfg.colorize_instance_segmentation} elif annotator_type == "instance_id_segmentation_fast": init_params = {"colorize": self.cfg.colorize_instance_id_segmentation} # Map the user-facing key to the Replicator annotator name when they differ. _REP_ANNOTATOR_NAME = { "instance_segmentation": "instance_segmentation_fast", } rep_annotator_name = _REP_ANNOTATOR_NAME.get(annotator_type, annotator_type) annotator = rep.AnnotatorRegistry.get_annotator( rep_annotator_name, init_params, device=spec.device, do_array_copy=False ) annotators[annotator_type] = annotator # Attach annotators to render product for annotator in annotators.values(): annotator.attach([rp.path]) ppisp_pipeline = None if spec.cfg.isp_cfg is not None: try: from isaaclab_ppisp import PpispPipeline except ModuleNotFoundError as exc: _raise_missing_ppisp_error(exc) ppisp_pipeline = PpispPipeline(spec.cfg.isp_cfg) return IsaacRtxRenderData( annotators=annotators, render_product=rp, spec=spec, ppisp_pipeline=ppisp_pipeline, ) def _resolve_simple_shading_mode(self, spec: CameraRenderSpec) -> int | None: """Resolve the requested simple shading mode from data types.""" requested = [dt for dt in spec.cfg.data_types if dt in SIMPLE_SHADING_MODES] if not requested: return None if len(requested) > 1: logger.warning( "Multiple simple shading modes requested (%s). Using '%s' only.", requested, requested[0], ) return SIMPLE_SHADING_MODES[requested[0]] def set_outputs(self, render_data: IsaacRtxRenderData, output_data: dict[str, ProxyArray]): """Store reference to output buffers for writing during render. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.set_outputs`.""" if render_data.ppisp_pipeline is not None and str(RenderBufferKind.RGBA) not in output_data: raise ValueError( "Isaac RTX 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." ) render_data.output_data = output_data # Allocate an internal HDR scratch buffer when PPISP is composed but # the user did not request the raw HDR AOV in ``data_types`` — the # PPISP kernel still needs somewhere to receive the HDR annotator # output before LDR conversion. if render_data.ppisp_pipeline is not None and str(RenderBufferKind.RGB_HDR) not in output_data: spec = render_data.spec assert spec is not None hdr_spec = self.supported_output_types()[RenderBufferKind.RGB_HDR] assert hdr_spec.dtype is wp.float32 render_data._hdr_scratch_wp = wp.zeros( (spec.num_instances, spec.cfg.height, spec.cfg.width, hdr_spec.channels), dtype=wp.float32, device=spec.device, ) def update_transforms(self) -> None: """No-op for Isaac RTX - uses USD scene directly. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_transforms`.""" pass def update_geometries(self) -> None: """No-op for Isaac RTX - uses USD scene directly. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_geometries`.""" pass def update_camera( self, render_data: IsaacRtxRenderData, positions: ProxyArray, orientations: ProxyArray, intrinsics: ProxyArray, ): """No-op for Replicator - uses USD camera prims directly. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.update_camera`.""" pass def render(self, render_data: IsaacRtxRenderData): """Extract data from annotators and write to output buffers. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.render`.""" spec = render_data.spec output_data = render_data.output_data if output_data is None or spec is None: return # Ensure the RTX renderer has been pumped so annotator buffers are fresh. # This is a no-op if another camera instance already triggered the update # for the current physics step, or if a visualizer already pumped it. ensure_isaac_rtx_render_update() view_count = spec.view_count cfg = spec.cfg device = spec.device def tiling_grid_shape(): cols = math.ceil(math.sqrt(view_count)) rows = math.ceil(view_count / cols) return (cols, rows) num_tiles_x = tiling_grid_shape()[0] # Extract the flattened image buffer for data_type, annotator in render_data.annotators.items(): # check whether returned data is a dict (used for segmentation) output = annotator.get_data() if isinstance(output, dict): tiled_data_buffer = output["data"] render_data.renderer_info[data_type] = output["info"] else: tiled_data_buffer = output # convert data buffer to warp array if isinstance(tiled_data_buffer, np.ndarray): # Let warp infer the dtype from numpy array instead of hardcoding uint8 # Different annotators return different dtypes: RGB(uint8), depth(float32), segmentation(uint32) tiled_data_buffer = wp.array(tiled_data_buffer, device=device) else: tiled_data_buffer = tiled_data_buffer.to(device=device) # process data for different segmentation types # Note: Replicator returns raw buffers of dtype uint32 for segmentation types # so we need to convert them to uint8 4 channel images for colorized types if ( (data_type == "semantic_segmentation" and self.cfg.colorize_semantic_segmentation) or (data_type == "instance_segmentation" and self.cfg.colorize_instance_segmentation) or (data_type == "instance_id_segmentation_fast" and self.cfg.colorize_instance_id_segmentation) ): tiled_data_buffer = wp.array( ptr=tiled_data_buffer.ptr, shape=(*tiled_data_buffer.shape, 4), dtype=wp.uint8, device=device ) # For motion vectors, use specialized kernel that reads 4 channels but only writes 2 # Note: Not doing this breaks the alignment of the data (check: https://github.com/isaac-sim/IsaacLab/issues/2003) if data_type == "motion_vectors": tiled_data_buffer = tiled_data_buffer[:, :, :2].contiguous() # For normals, we only require the first three channels of the tiled buffer # Note: Not doing this breaks the alignment of the data (check: https://github.com/isaac-sim/IsaacLab/issues/4239) if data_type == "normals": tiled_data_buffer = tiled_data_buffer[:, :, :3].contiguous() if data_type in SIMPLE_SHADING_MODES: tiled_data_buffer = tiled_data_buffer[:, :, :3].contiguous() if data_type == str(RenderBufferKind.RGB_HDR): tiled_data_buffer = tiled_data_buffer[:, :, :3].contiguous() # The HDR annotator's destination is the user-visible ``output_data["rgb_hdr"]`` # when they requested it explicitly; otherwise the renderer's internal # scratch buffer that the PPISP pipeline reads. if data_type == str(RenderBufferKind.RGB_HDR) and data_type not in output_data: assert render_data._hdr_scratch_wp is not None buf_wp = render_data._hdr_scratch_wp else: buf_wp = output_data[data_type].warp wp.launch( kernel=reshape_tiled_image, dim=(view_count, cfg.height, cfg.width), inputs=[ tiled_data_buffer.flatten(), buf_wp, *list(buf_wp.shape[1:]), num_tiles_x, ], device=device, ) # rgb is a strided warp view into rgba set up in CameraData.allocate(); # no per-frame alias assignment needed. # NOTE: The `distance_to_camera` annotator returns the distance to the camera optical center. # However, the replicator depth clipping is applied w.r.t. to the image plane which may result # in values larger than the clipping range in the output. We apply an additional clipping to # ensure values are within the clipping range for all the annotators. if data_type == "distance_to_camera": clamp_depth_to_inf_wp(buf_wp, cfg.spawn.clipping_range[1], device=device) # apply defined clipping behavior if ( data_type in ("distance_to_camera", "distance_to_image_plane", "depth") and self.cfg.depth_clipping_behavior != "none" ): replacement = 0.0 if self.cfg.depth_clipping_behavior == "zero" else cfg.spawn.clipping_range[1] replace_inf_depth_wp(buf_wp, replacement, device=device) # Post-render PPISP: HDR scene-linear → LDR RGBA. The camera enforces # that ``rgba`` (or ``rgb`` aliasing into it) is present when an ISP is # configured, so writing to ``output_data["rgba"]`` is safe. if render_data.ppisp_pipeline is not None: hdr_proxy = output_data.get(str(RenderBufferKind.RGB_HDR)) hdr_source = hdr_proxy.warp if hdr_proxy is not None else render_data._hdr_scratch_wp render_data.ppisp_pipeline.apply(hdr_source, output_data[str(RenderBufferKind.RGBA)].warp) def read_output(self, render_data: IsaacRtxRenderData, camera_data: CameraData) -> None: """Populate per-output metadata collected during render(). Pixel data already written in render(). This is a *replace*, not a *merge*: every seeded output key is reset to this frame's metadata, which is ``None`` when its annotator produced none (``renderer_info`` only ever holds a subset of the outputs, so iterating ``camera_data.info`` both preserves its ``output``-mirroring key set and resets any metadata that went away to ``None``). Without this, a stale mapping from a previous frame would linger once an annotator stops emitting one. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.read_output`.""" assert camera_data.info is not None, "CameraData.info should be created in CameraData.allocate" for output_name in camera_data.info: camera_data.info[output_name] = render_data.renderer_info.get(output_name) def cleanup(self, render_data: IsaacRtxRenderData | None): """Detach annotators, destroy the owned tiled render product, and drop held refs. See :meth:`~isaaclab.renderers.base_renderer.BaseRenderer.cleanup`.""" if render_data is None: return for annotator in render_data.annotators.values(): annotator.detach([render_data.render_product.path]) render_data.render_product.destroy() render_data.render_product = None render_data.annotators.clear() render_data.output_data = None render_data.spec = None render_data.renderer_info.clear() render_data.ppisp_pipeline = None render_data._hdr_scratch_wp = None