render_product_output (FET035 Render Products)#
Property |
Value |
|---|---|
Test name |
render_product_output |
Feature(s) |
FET_035_RTX |
Engine |
Kit / Isaac Sim (>=2024.2.0) |
Test version |
0.1.0 |
Running with simready-benchmark#
This test is implemented and available in the simready-benchmark-kit-suite package.
Run it against an asset:
simready-benchmark --assets path/to/asset.usd --features FET035
The test skips automatically if no authored RenderProduct prim is found (excluding OmniverseKit internal prims).
Summary#
Reads the camera path, resolution, and AOV source names from each authored
RenderProduct prim, then creates a matching Replicator render product and
reads back AOV data to confirm each declared annotator produces non-empty
output. Output images are saved per annotator for visual inspection.
What Pass Guarantees#
A reviewer, PM, or OEM can trust that every RenderProduct in the scene has
a valid camera relationship, at least one declared AOV, and that each
declared sourceName corresponds to a registered Replicator annotator that
produces non-empty output at the authored resolution. The asset is ready for
use in an SDG pipeline that expects these properties to be correctly authored.
What It Checks#
For each authored RenderProduct prim, the test reads the camera relationship,
resolution, and sourceName values from orderedVars. If the camera or source
names are absent the prim is silently skipped. For RenderProduct prims whose
camera is a USD Camera prim, a Replicator render product is created with the same
camera and resolution, annotators are attached matching each sourceName, one frame
is triggered, and data is verified:
Color annotators (
LdrColor,HdrColor, semantic annotators): pass on non-empty array alone. Color may be black when the scene has no materials — this is expected for minimal test fixtures.Geometry annotators (
DepthLinearized,normals, etc.): additionally require at least one non-zero value, confirming geometry is visible from the camera.
A reference cube is placed 2 m in front of each camera along its world-space forward direction so geometry annotators have meaningful hits.
OmniLidar prims are skipped — multiple RTX LiDAR sensors in the same
Replicator pass cause a CUDA conflict. Their point cloud output is covered by
the lidar_point_cloud test.
Key thresholds from config_defaults:
warmup_frames: 30 (physics + timeline frames before triggering the Replicator step)default_resolution: (512, 512) (fallback if the authored RP has noresolutionattribute)
How It Works#
Stage traversal: RenderProduct prims are typically authored in a /Render scope that is a sibling of the asset’s defaultPrim. The framework’s load_asset() references only the defaultPrim subtree, making /Render invisible to the composed stage. The test opens the raw asset file via Usd.Stage.Open(ctx.asset_path) to traverse all prims including /Render.
Property reading: for each authored RenderProduct (excluding OmniverseKit internal prims), the test reads the camera relationship, resolution, and sourceName values from orderedVars directly from the raw asset stage. RenderProducts with a missing camera or no source names are silently skipped.
Camera path remapping: camera prim paths from the raw asset (e.g. /Root/RGB_Camera) are remapped to their equivalents in the composed stage (where cameras were loaded under the framework’s path prefix). Remapping uses a name-based lookup: all Camera prims in the composed stage are indexed by name, then matched by the last component of the raw camera path.
Replicator AOV check: for each Camera-typed render product:
A 0.4 m reference cube is placed 2 m along the camera’s world-space forward direction so depth and normals annotators have geometry to sample.
rep.create.render_product(cam_path, resolution)creates a Replicator-managed render product mirroring the authored camera and resolution.rep.annotators.get(source_name)is called for each authoredsourceName; an unrecognised name fails here.rep.orchestrator.step_async()triggers one Replicator frame, followed by 10next_update_async()calls.Annotator data is validated: color annotators pass on non-empty array; geometry annotators additionally require at least one non-zero value.
OmniLidar prims are identified by name lookup in the composed stage and skipped for the Replicator check (CUDA conflict). Their point cloud output is covered by the lidar_point_cloud test.
Output images are saved to the run output directory (one PNG per annotator per Camera RenderProduct). Color annotators may appear black for minimal fixtures — the companion _alpha.png file confirms geometry hits. Depth and normals images are saved as normalized grayscale.
Failure Cases#
Symptom |
Likely cause |
|---|---|
|
The camera prim path does not exist in the composed stage or is not a renderable |
Annotator not registered |
A |
Geometry annotator all-zero |
No geometry is visible from the camera — check camera transform and cube placement. |
Annotator returned empty array |
The render pipeline produced no output. Increase |
Test skipped (not applicable) |
No authored |
Static validation failed |
The asset did not pass RP.001–RP.003. Fix the static issues first. |
|
This only affects the batch script (which opens the stage directly). The benchmark test uses |
How to Fix#
If an annotator is not registered, verify that the sourceName attribute on
the RenderVar prim matches a name in the Replicator annotator registry.
Common valid names include LdrColor, HdrColor, DepthLinearized,
normals, SemanticSegmentation, and GenericModelOutput.
If geometry annotators are all-zero, confirm that the camera prim’s world transform is correctly authored so the reference cube falls within the camera’s field of view.
Manual Testing in Isaac Sim#
Batch script#
Save the script below to a file (e.g. batch_test_render_product_output.py)
under the repo root and run it with:
# Windows
isaac-sim.bat --no-window --exec "C:\Dev\simready_foundations\batch_test_render_product_output.py"
# Linux
./isaac-sim.sh --no-window --exec "/path/to/simready_foundations/batch_test_render_product_output.py"
Expected output summary:
Overall: PASS (2/2 checks passed)
Output images are written to _batch_test_output/render_product/ with one
file per annotator per Camera RenderProduct. The LdrColor image will appear
black (no material on the reference cube) but the _alpha.png file confirms
geometry hits. The DepthLinearized image shows a dark square (near cube)
on a white background (far clip distance). The normals image shows the cube
face as a coloured patch on a grey background.
import asyncio, os
import numpy as np
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
PASS_ASSET = os.path.join(REPO_ROOT,
"nv_core/sr_specs/tests/data/render_products/RenderProductsCheckerPass.usda")
FAIL_ASSET = os.path.join(REPO_ROOT,
"nv_core/sr_specs/tests/data/render_products/RenderProductsCheckerFail.usda")
OUTPUT_DIR = os.path.join(REPO_ROOT, "_batch_test_output", "render_product")
WARMUP_FRAMES = 30
DEFAULT_RESOLUTION = (512, 512)
COLOR_ANNOTATORS = ("LdrColor", "HdrColor", "SemanticSegmentation",
"SemanticBoundingBox2DLoose", "SemanticBoundingBox2DTight")
def is_kit_internal(p): return "OmniverseKit" in str(p)
def place_cube_in_front_of_camera(stage, cam_path, index):
# Place a 0.4 m cube 2 m in front of the camera along its world forward
# direction so depth and normals annotators have geometry to measure.
from pxr import UsdGeom, Gf, Usd
cam = stage.GetPrimAtPath(cam_path)
if not cam.IsValid(): return
xf = UsdGeom.XformCache(Usd.TimeCode.Default())
world_xf = xf.GetLocalToWorldTransform(cam)
fwd = world_xf.TransformDir(Gf.Vec3d(0, 0, -1)).GetNormalized()
pos = Gf.Vec3d(world_xf.ExtractTranslation()) + fwd * 2.0
cube = UsdGeom.Cube.Define(stage, f"/World/DepthCube_{index}")
cube.GetSizeAttr().Set(0.4)
UsdGeom.XformCommonAPI(cube.GetPrim()).SetTranslate((pos[0], pos[1], pos[2]))
def read_rp_properties(stage, rp_prim):
# Read the authored camera path, resolution, and sourceName list from the RP.
cam_rel = rp_prim.GetRelationship("camera")
vars_rel = rp_prim.GetRelationship("orderedVars")
cam_path = (str(cam_rel.GetTargets()[0])
if cam_rel.IsValid() and cam_rel.GetTargets() else None)
res_attr = rp_prim.GetAttribute("resolution")
resolution = (tuple(int(v) for v in res_attr.Get())
if res_attr.IsValid() and res_attr.Get() is not None
else DEFAULT_RESOLUTION)
source_names = []
if vars_rel.IsValid():
for var_path in vars_rel.GetTargets():
var_prim = stage.GetPrimAtPath(var_path)
if not var_prim.IsValid(): continue
src_attr = var_prim.GetAttribute("sourceName")
if src_attr.IsValid() and src_attr.Get():
source_names.append(src_attr.Get())
return cam_path, resolution, source_names
def save_image(data, label, rp_name, src_name):
from PIL import Image
os.makedirs(OUTPUT_DIR, exist_ok=True)
path = os.path.join(OUTPUT_DIR, f"{label}_{rp_name}_{src_name}.png")
arr = np.array(data, dtype=float)
if arr.ndim == 1: return # 1-D buffers (e.g. point cloud) — skip
if arr.ndim == 3 and arr.shape[2] in (3, 4):
rgb = arr[:, :, :3]
mn, mx = rgb.min(), rgb.max()
if mn < 0:
rgb = (rgb + 1.0) / 2.0 * 255 # normals: remap [-1,1] to [0,255]
elif mx <= 1.0:
rgb = rgb * 255 # float [0,1] to uint8 [0,255]
Image.fromarray(rgb.clip(0, 255).astype(np.uint8), "RGB").save(path)
# Save alpha separately for color annotators — useful when RGB is black
# (no material) but alpha=255 confirms geometry was hit.
if arr.shape[2] == 4 and any(c in path for c in COLOR_ANNOTATORS):
alpha_path = path.replace(".png", "_alpha.png")
alpha = arr[:, :, 3].clip(0, 255).astype(np.uint8)
Image.fromarray(alpha, "L").save(alpha_path)
elif arr.ndim == 2:
# Normalize finite values to [0,255] grayscale; inf/nan pixels map to 0.
finite = np.isfinite(arr)
vis = np.zeros_like(arr)
if finite.any():
mn, mx = arr[finite].min(), arr[finite].max()
if mx > mn:
vis[finite] = (arr[finite] - mn) / (mx - mn)
Image.fromarray((vis * 255).astype(np.uint8), "L").save(path)
async def check_structural(stage):
from pxr import UsdRender
rps = [p for p in stage.Traverse()
if p.IsA(UsdRender.Product) and not is_kit_internal(p.GetPath())]
print(f" Authored RenderProduct prims: {len(rps)}")
all_ok = True
for rp in rps:
cam_rel = rp.GetRelationship("camera")
vars_rel = rp.GetRelationship("orderedVars")
cam_ok = cam_rel.IsValid() and len(cam_rel.GetTargets()) > 0
vars_ok = vars_rel.IsValid() and len(vars_rel.GetTargets()) > 0
print(f" {rp.GetPath()}")
print(f" camera : {'PASS' if cam_ok else 'FAIL'}")
print(f" orderedVars: {'PASS' if vars_ok else 'FAIL'}")
if not (cam_ok and vars_ok): all_ok = False
return all_ok, rps
async def check_replicator_aovs(stage, rps, label):
import omni.kit.app, omni.replicator.core as rep
all_ok = True
for rp in rps:
rp_name = rp.GetPath().name
cam_path, resolution, source_names = read_rp_properties(stage, rp)
if not cam_path or not source_names:
print(f" FAIL: {rp_name} — missing camera or sourceNames")
all_ok = False; continue
cam_prim = stage.GetPrimAtPath(cam_path)
if not cam_prim.IsValid():
print(f" FAIL: {rp_name} — camera prim not found")
all_ok = False; continue
if cam_prim.GetTypeName() == "OmniLidar":
print(f" SKIP: {rp_name} — OmniLidar deferred to lidar test")
continue
place_cube_in_front_of_camera(stage, cam_path,
list(rps).index(rp))
# Copy-construct: create a Replicator RP mirroring the authored one.
# rep.create.render_product() uses the authored camera and resolution
# but creates its own internal RP rather than activating the authored
# prim — see Notes and Caveats for the scope limitation.
try:
rep_rp = rep.create.render_product(cam_path, resolution)
rep_rp_path = rep_rp.path if hasattr(rep_rp, "path") else str(rep_rp)
except Exception as e:
print(f" FAIL: {rp_name} — render product creation failed: {e}")
all_ok = False; continue
attached = []
for src_name in source_names:
try:
anno = rep.annotators.get(src_name)
anno.attach(rep_rp_path)
attached.append((src_name, anno))
print(f" PASS: {rp_name} — annotator '{src_name}' attached")
except Exception as e:
print(f" FAIL: {rp_name} — '{src_name}' not registered: {e}")
all_ok = False
if not attached: continue
try:
await rep.orchestrator.step_async()
except Exception:
await rep.orchestrator.run_async(num_frames=1)
for _ in range(10):
await omni.kit.app.get_app().next_update_async()
for src_name, anno in attached:
try:
data = anno.get_data()
arr = (np.array(data.get("data", list(data.values())[0]))
if isinstance(data, dict) else np.array(data))
if arr.size == 0:
print(f" FAIL: {rp_name} '{src_name}' — empty")
all_ok = False
else:
nonzero = bool(np.any(arr != 0))
is_color = src_name in COLOR_ANNOTATORS
if not is_color and not nonzero:
print(f" FAIL: {rp_name} '{src_name}' — all-zero")
all_ok = False
else:
print(f" PASS: {rp_name} '{src_name}'"
f" shape={arr.shape} nonzero={nonzero}")
save_image(arr, label.split()[0].lower(), rp_name, src_name)
except Exception as e:
print(f" SKIP: {rp_name} '{src_name}' error: {e}")
return all_ok
async def run_test(asset_path, label, expect_pass):
import omni.usd, omni.kit.app, omni.kit.commands, omni.physx, omni.timeline
print(f"\n--- {label} ---")
await omni.usd.get_context().open_stage_async(asset_path)
for _ in range(5): await omni.kit.app.get_app().next_update_async()
omni.kit.commands.execute("CreatePrimWithDefaultXform", prim_type="DistantLight")
for _ in range(3): await omni.kit.app.get_app().next_update_async()
stage = omni.usd.get_context().get_stage()
struct_ok, rps = await check_structural(stage)
omni.physx.get_physx_interface().start_simulation()
omni.timeline.get_timeline_interface().play()
for _ in range(WARMUP_FRAMES): await omni.kit.app.get_app().next_update_async()
aov_ok = await check_replicator_aovs(stage, rps, label)
omni.timeline.get_timeline_interface().stop()
passed = struct_ok and aov_ok
print(f" Structural : {'PASS' if struct_ok else 'FAIL'}")
print(f" Replicator : {'PASS' if aov_ok else 'FAIL'}")
return passed if expect_pass else not passed
async def main():
print("=" * 60)
print("Batch test: Render product output (FET035 RP.001/RP.002/RP.003)")
print("=" * 60)
results = [
await run_test(PASS_ASSET, "Pass fixture (expect PASS)", True),
await run_test(FAIL_ASSET, "Fail fixture (expect FAIL)", False),
]
print(f"\nOverall: {'PASS' if all(results) else 'FAIL'}"
f" ({sum(results)}/{len(results)} checks passed)")
import os as _os; _os._exit(0)
asyncio.ensure_future(main())
Expected Result#
The benchmark test writes one PNG per annotator per Camera RenderProduct to the
run output directory. File names follow the pattern {rp_name}_{src_name}.png
(e.g. RGB_CameraRp_normals.png). Color annotators (LdrColor, HdrColor) that
have an alpha channel also produce a companion _alpha.png file.

The normals annotator output for the RGB_CameraRp render product from the
PASS fixture (RenderProductsCheckerPass.usda). The reference cube placed 2 m
in front of the camera appears as a coloured patch — each colour encodes the
surface normal direction. The surrounding grey background corresponds to the
far-clip plane (no geometry hit). A non-grey patch confirms that the camera,
render product wiring, and normals annotator are all functioning correctly.
The LdrColor image will appear black for minimal fixtures (no material on the
reference cube), but the _alpha.png companion image shows white pixels where
the cube geometry was hit. The DepthLinearized image shows a dark square (near
cube) on a white background (far clip plane).
Notes and Caveats#
Scope limitation — Replicator creates its own render product:
rep.create.render_product() creates a Replicator-managed render product
rather than activating the authored RenderProduct prim in the stage. The
test mirrors the authored configuration (camera path, resolution, sourceName
values) but does not route output through the authored
RenderProduct → orderedVars → RenderVar chain. Activating cold-authored
RenderProduct prims with Hydra is a known API gap currently under development
by the Replicator team (#omni-rtx, Avinash Devalla, March 2026).
Full pipeline verification — future work:
End-to-end verification that the authored RenderProduct → RenderVar chain
actually writes the declared AOVs to disk requires native Hydra activation of
the authored prim, which is deferred. When the API becomes available, it would
allow attaching annotators directly to the existing authored path rather than
creating a new one.
Color annotator image appearance:
The LdrColor image will appear black for the minimal test fixture because
the reference cube has no material. The _alpha.png companion image confirms
geometry is present (white pixels where the cube is hit). The depth and
normals images are more informative for visual inspection.
OmniLidar RenderProducts:
OmniLidar prims are skipped for the Replicator check because running two
RTX LiDAR sensors with the GenericModelOutput annotator in the same
Replicator pass causes a CUDA memory conflict and crashes Isaac Sim. Point
cloud output is verified by the lidar_point_cloud test.