Skip to content

Commit 73a68df

Browse files
committed
Add camera PPISP HDR renderer integration
Apply camera PPISP from renderer HDR buffers and verify RTX, OVRTX, and Newton HDR output plumbing for the camera pipeline.
1 parent 7361ece commit 73a68df

17 files changed

Lines changed: 1081 additions & 4 deletions

File tree

source/isaaclab/isaaclab/renderers/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
__all__ = [
77
"BaseRenderer",
88
"CameraRenderSpec",
9+
"CameraPPISPCfg",
910
"RenderBufferKind",
1011
"RenderBufferSpec",
1112
"Renderer",
@@ -16,6 +17,7 @@ __all__ = [
1617
from .base_renderer import BaseRenderer
1718
from .camera_render_spec import CameraRenderSpec
1819
from .output_contract import RenderBufferKind, RenderBufferSpec
20+
from .camera_ppisp import CameraPPISPCfg
1921
from .renderer import Renderer
2022
from .renderer_cfg import RendererCfg
2123
from .render_context import RenderContext
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Camera PPISP configuration and USD/SPG parsing helpers.
7+
8+
The implementation follows the physically plausible ISP model described in
9+
https://arxiv.org/abs/2601.18336.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from dataclasses import dataclass, field
15+
from typing import Any
16+
17+
from isaaclab.utils import configclass
18+
19+
20+
CAMERA_PPISP_SHADER_NAME = "PPISP"
21+
22+
CAMERA_PPISP_FLOAT2_INPUTS = {
23+
"vignettingCenterR",
24+
"vignettingCenterG",
25+
"vignettingCenterB",
26+
"colorLatentBlue",
27+
"colorLatentRed",
28+
"colorLatentGreen",
29+
"colorLatentNeutral",
30+
}
31+
32+
CAMERA_PPISP_DEFAULT_INPUTS: dict[str, float | tuple[float, float]] = {
33+
"exposureOffset": 0.0,
34+
"vignettingCenterR": (0.0, 0.0),
35+
"vignettingAlpha1R": 0.0,
36+
"vignettingAlpha2R": 0.0,
37+
"vignettingAlpha3R": 0.0,
38+
"vignettingCenterG": (0.0, 0.0),
39+
"vignettingAlpha1G": 0.0,
40+
"vignettingAlpha2G": 0.0,
41+
"vignettingAlpha3G": 0.0,
42+
"vignettingCenterB": (0.0, 0.0),
43+
"vignettingAlpha1B": 0.0,
44+
"vignettingAlpha2B": 0.0,
45+
"vignettingAlpha3B": 0.0,
46+
"colorLatentBlue": (0.0, 0.0),
47+
"colorLatentRed": (0.0, 0.0),
48+
"colorLatentGreen": (0.0, 0.0),
49+
"colorLatentNeutral": (0.0, 0.0),
50+
"crfToeR": 0.013659,
51+
"crfShoulderR": 0.013659,
52+
"crfGammaR": 0.378165,
53+
"crfCenterR": 0.0,
54+
"crfToeG": 0.013659,
55+
"crfShoulderG": 0.013659,
56+
"crfGammaG": 0.378165,
57+
"crfCenterG": 0.0,
58+
"crfToeB": 0.013659,
59+
"crfShoulderB": 0.013659,
60+
"crfGammaB": 0.378165,
61+
"crfCenterB": 0.0,
62+
}
63+
64+
65+
def default_camera_ppisp_inputs() -> dict[str, float | tuple[float, float]]:
66+
"""Return a copy of the PPISP identity/default input dictionary."""
67+
return dict(CAMERA_PPISP_DEFAULT_INPUTS)
68+
69+
70+
@configclass
71+
class CameraPPISPCfg:
72+
"""Configuration for PPISP post-processing.
73+
74+
PPISP inputs are static in IsaacLab. If imported from animated USD shader inputs,
75+
the first authored time sample is used and later samples are ignored.
76+
"""
77+
78+
shader_prim_path: str | None = None
79+
"""Optional source USD shader prim path used to populate :attr:`inputs`."""
80+
81+
inputs: dict[str, float | tuple[float, float]] = field(default_factory=default_camera_ppisp_inputs)
82+
"""Flat PPISP shader input values keyed by USD input name."""
83+
84+
85+
@dataclass
86+
class RenderProductInfo:
87+
"""Parsed USD RenderProduct information used for PPISP validation."""
88+
89+
render_product_path: str
90+
camera_paths: list[str]
91+
resolution: tuple[int, int] | None
92+
ordered_vars: list[str]
93+
ppisp: CameraPPISPCfg | None
94+
camera_xform_time_samples: list[float]
95+
96+
97+
def normalize_camera_ppisp_cfg(
98+
ppisp_cfg: CameraPPISPCfg | dict[str, Any] | None,
99+
stage: Any | None = None,
100+
) -> CameraPPISPCfg | None:
101+
"""Convert supported user PPISP representations to :class:`CameraPPISPCfg`."""
102+
if ppisp_cfg is None:
103+
return None
104+
if isinstance(ppisp_cfg, CameraPPISPCfg):
105+
input_overrides = dict(ppisp_cfg.inputs)
106+
if ppisp_cfg.shader_prim_path and stage is not None:
107+
ppisp_cfg = _merge_shader_inputs_with_cfg(ppisp_cfg, stage, input_overrides)
108+
else:
109+
ppisp_cfg.inputs = _normalized_inputs(input_overrides)
110+
return ppisp_cfg
111+
if isinstance(ppisp_cfg, dict):
112+
input_overrides = ppisp_cfg.get(
113+
"inputs", {key: value for key, value in ppisp_cfg.items() if key in CAMERA_PPISP_DEFAULT_INPUTS}
114+
)
115+
cfg = CameraPPISPCfg()
116+
cfg.inputs = _normalized_inputs(input_overrides)
117+
shader_prim_path = ppisp_cfg.get("shader_prim_path")
118+
if shader_prim_path is not None:
119+
cfg.shader_prim_path = str(shader_prim_path)
120+
if stage is not None:
121+
cfg = _merge_shader_inputs_with_cfg(cfg, stage, input_overrides)
122+
return cfg
123+
raise TypeError(f"Unsupported PPISP configuration type: {type(ppisp_cfg)!r}")
124+
125+
126+
def camera_ppisp_cfg_from_usd_shader(shader: Any) -> CameraPPISPCfg:
127+
"""Create :class:`CameraPPISPCfg` from a ``UsdShade.Shader`` prim.
128+
129+
Animated inputs are collapsed to their first authored time sample.
130+
"""
131+
cfg = CameraPPISPCfg(shader_prim_path=str(shader.GetPath()))
132+
values = default_camera_ppisp_inputs()
133+
for input_name in values:
134+
shader_input = shader.GetInput(input_name)
135+
if not shader_input:
136+
continue
137+
attr = shader_input.GetAttr()
138+
value = _read_first_authored_value(attr)
139+
if value is not None:
140+
values[input_name] = _normalize_input_value(input_name, value)
141+
cfg.inputs = values
142+
return cfg
143+
144+
145+
def camera_ppisp_cfg_from_usd_stage(stage: Any, shader_prim_path: str) -> CameraPPISPCfg:
146+
"""Create :class:`CameraPPISPCfg` from a shader prim path in a USD stage."""
147+
from pxr import UsdShade
148+
149+
shader = UsdShade.Shader(stage.GetPrimAtPath(shader_prim_path))
150+
if not shader:
151+
raise ValueError(f"PPISP shader prim not found at path: {shader_prim_path}")
152+
return camera_ppisp_cfg_from_usd_shader(shader)
153+
154+
155+
def parse_render_product(stage: Any, render_product_path: str) -> RenderProductInfo:
156+
"""Parse a USD RenderProduct and optional PPISP shader configuration."""
157+
render_product = stage.GetPrimAtPath(render_product_path)
158+
if not render_product.IsValid() or render_product.GetTypeName() != "RenderProduct":
159+
raise ValueError(f"RenderProduct not found at path: {render_product_path}")
160+
161+
camera_rel = render_product.GetRelationship("camera")
162+
camera_paths = [str(path) for path in camera_rel.GetTargets()] if camera_rel else []
163+
if not camera_paths:
164+
raise ValueError(f"RenderProduct at path '{render_product_path}' has no camera relationship targets.")
165+
166+
resolution = None
167+
resolution_attr = render_product.GetAttribute("resolution")
168+
if resolution_attr:
169+
resolution_value = resolution_attr.Get()
170+
if resolution_value is not None:
171+
resolution = (int(resolution_value[0]), int(resolution_value[1]))
172+
173+
ordered_vars_rel = render_product.GetRelationship("orderedVars")
174+
ordered_vars = [str(path) for path in ordered_vars_rel.GetTargets()] if ordered_vars_rel else []
175+
176+
ppisp = None
177+
ppisp_prim = stage.GetPrimAtPath(f"{render_product_path}/{CAMERA_PPISP_SHADER_NAME}")
178+
if ppisp_prim.IsValid():
179+
from pxr import UsdShade
180+
181+
ppisp = camera_ppisp_cfg_from_usd_shader(UsdShade.Shader(ppisp_prim))
182+
183+
return RenderProductInfo(
184+
render_product_path=render_product_path,
185+
camera_paths=camera_paths,
186+
resolution=resolution,
187+
ordered_vars=ordered_vars,
188+
ppisp=ppisp,
189+
camera_xform_time_samples=collect_camera_xform_time_samples(stage, camera_paths),
190+
)
191+
192+
193+
def parse_render_product_file(usd_path: str, render_product_path: str) -> RenderProductInfo:
194+
"""Open a USD file and parse a RenderProduct."""
195+
from pxr import Usd
196+
197+
stage = Usd.Stage.Open(usd_path)
198+
if stage is None:
199+
raise RuntimeError(f"Failed to open USD stage at path: {usd_path}")
200+
return parse_render_product(stage, render_product_path)
201+
202+
203+
def collect_camera_xform_time_samples(stage: Any, camera_paths: list[str]) -> list[float]:
204+
"""Collect authored xform time samples from cameras and inherited source cameras."""
205+
time_samples = set()
206+
for camera_path in camera_paths:
207+
prim = stage.GetPrimAtPath(camera_path)
208+
if not prim.IsValid():
209+
continue
210+
_collect_xform_attr_time_samples(prim, time_samples)
211+
for inherited_path in prim.GetInherits().GetAllDirectInherits():
212+
inherited_prim = stage.GetPrimAtPath(inherited_path)
213+
if inherited_prim.IsValid():
214+
_collect_xform_attr_time_samples(inherited_prim, time_samples)
215+
if not time_samples:
216+
start_time = stage.GetStartTimeCode()
217+
end_time = stage.GetEndTimeCode()
218+
if start_time != end_time:
219+
time_samples.update([start_time, end_time])
220+
else:
221+
time_samples.add(start_time)
222+
return sorted(time_samples)
223+
224+
225+
def _normalized_inputs(inputs: dict[str, Any]) -> dict[str, float | tuple[float, float]]:
226+
values = default_camera_ppisp_inputs()
227+
for input_name, value in inputs.items():
228+
if input_name not in values:
229+
raise ValueError(f"Unknown PPISP input: {input_name}")
230+
values[input_name] = _normalize_input_value(input_name, value)
231+
return values
232+
233+
234+
def _merge_shader_inputs_with_cfg(
235+
ppisp_cfg: CameraPPISPCfg,
236+
stage: Any,
237+
input_overrides: dict[str, Any],
238+
) -> CameraPPISPCfg:
239+
parsed_cfg = camera_ppisp_cfg_from_usd_stage(stage, ppisp_cfg.shader_prim_path)
240+
if input_overrides != CAMERA_PPISP_DEFAULT_INPUTS:
241+
parsed_cfg.inputs.update(_normalized_input_overrides(input_overrides))
242+
return parsed_cfg
243+
244+
245+
def _normalized_input_overrides(inputs: dict[str, Any]) -> dict[str, float | tuple[float, float]]:
246+
values = {}
247+
for input_name, value in inputs.items():
248+
if input_name not in CAMERA_PPISP_DEFAULT_INPUTS:
249+
raise ValueError(f"Unknown PPISP input: {input_name}")
250+
values[input_name] = _normalize_input_value(input_name, value)
251+
return values
252+
253+
254+
def _normalize_input_value(input_name: str, value: Any) -> float | tuple[float, float]:
255+
if input_name in CAMERA_PPISP_FLOAT2_INPUTS:
256+
if len(value) != 2:
257+
raise ValueError(f"PPISP input '{input_name}' expects two values.")
258+
return (float(value[0]), float(value[1]))
259+
return float(value)
260+
261+
262+
def _read_first_authored_value(attr: Any) -> Any:
263+
time_samples = attr.GetTimeSamples()
264+
if time_samples:
265+
return attr.Get(time_samples[0])
266+
return attr.Get()
267+
268+
269+
def _collect_xform_attr_time_samples(prim: Any, time_samples: set[float]) -> None:
270+
for attr in prim.GetAttributes():
271+
if attr.GetName().startswith("xformOp:"):
272+
time_samples.update(attr.GetTimeSamples())

0 commit comments

Comments
 (0)