Skip to content

Add cameras as first-class objects in ModelBuilder/Model - #1

Open
eric-heiden wants to merge 31 commits into
daniela-hase:dev/sensor-batched-camerafrom
eric-heiden:camera-api-batched
Open

Add cameras as first-class objects in ModelBuilder/Model#1
eric-heiden wants to merge 31 commits into
daniela-hase:dev/sensor-batched-camerafrom
eric-heiden:camera-api-batched

Conversation

@eric-heiden

@eric-heiden eric-heiden commented Jul 22, 2026

Copy link
Copy Markdown

Description

Adds cameras as first-class objects in Newton, targeted at dev/sensor-batched-camera so the model-side camera data can feed SensorBatchedCamera (newton-physics#3276) directly.

Camera data model & core module (newton/_src/core/cameras.py)

  • CameraProjection descriptors: CameraPinhole (physical/USD form + from_fov), CameraFisheyeOpenCV / CameraFisheyeFTheta / CameraFisheyeKannalaBrandt, and CameraCustomRays for user-supplied ray bundles (custom camera models).
  • Equal parametric projections dedupe at finalize(); CameraCustomRays shares by object identity — rays are never duplicated and never stored on the Model. Resolution is a per-camera hint that the renderer overrides (custom-ray cameras derive it from the ray array shape).
  • Shared camera math: ray-generation kernels (moved from warp_raytrace/camera_utils.py with back-compat re-exports), compute_camera_rays, eval_camera_world_xforms, pitch/yaw basis + fov/focal helpers now used by ViewerGL/Viser/RTX (removes the duplicated viewer math).

ModelBuilder / Model

  • builder.add_camera(body=-1, *, xform, projection, resolution, enabled, label, custom_attributes); cameras follow world semantics (camera_world, camera_world_start), have labels, optional rigid-body attachment (camera_body), CameraFlags, and AttributeFrequency.CAMERA custom attributes; full add_builder/replicate support (shared projections across replicas).

Importers

  • USD: UsdGeom.Camera prims (load_cameras=True, path_camera_map, body attachment via nearest rigid-body ancestor, orthographic warn+skip).
  • MJCF: <camera> elements incl. fovy/focal/sensorsize/principal/resolution, frames, defaults; non-fixed modes import as fixed with the original mode preserved in the mjcf:camera_mode custom attribute.

Viewers

  • viewer.show_cameras batched frustum visualization (single instanced draw, scales to hundreds of cameras, respects visible worlds) and viewer.set_camera_from_model(index_or_label) + ViewerGL camera dropdown with follow mode.

Integration with this branch

  • Rebuilt directly on dev/sensor-batched-camera — no upstream-main delta is included; the diff is the camera feature only (26 files).
  • The camera work was adapted to this branch's builder/model machinery (dict-based attribute frequencies, add_builder offset table extended with the camera entity) and leaves SensorBatchedCamera untouched (its 22 tests stay green).

Note: fisheye descriptor defaults follow the existing ray-helper defaults (max_fov=2π, FTheta k1=1.0) rather than the draft design doc, so descriptors map 1:1 onto the ray helpers.

Follow-up (separate PR): SensorBatchedCamera.select_cameras() consuming model cameras, lazy per-(projection, resolution) ray-bundle cache vs in-kernel ray computation with a deciding benchmark.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes (docs/concepts/cameras.rst, API registry)
  • CHANGELOG.md has been updated

Test plan

uv run --extra dev -m newton.tests -k test_camera_projections
uv run --extra dev -m newton.tests -k test_camera_rays
uv run --extra dev -m newton.tests -k test_model_cameras
uv run --extra dev -m newton.tests -k test_import_mjcf_cameras
uv run --extra dev -m newton.tests -k test_import_usd_cameras
uv run --extra dev -m newton.tests -k test_viewer_model_cameras
uv run --extra dev -m newton.tests -k test_sensor_batched_camera   # 22/22 on this branch
uv run --extra dev -m newton.tests -k test_sensor_tiled_camera     # 34/34
uv run --extra dev -m newton.tests                                  # full suite: 4045 tests OK

New feature / API change

import math
import newton

builder = newton.ModelBuilder()
builder.add_mjcf("robot.xml")   # <camera> elements imported automatically
builder.add_usd("scene.usda")   # UsdGeom.Camera prims imported automatically

cam = builder.add_camera(
    body=hand_body,             # optional rigid-body attachment (-1 = world-fixed)
    xform=wp.transform((0.0, 0.0, 0.1), wp.quat_identity()),
    projection=newton.CameraPinhole.from_fov(math.radians(60.0)),
    resolution=(640, 480),      # hint; renderer-side resolution overrides
    label="wrist_cam",
)

model = builder.finalize()
model.camera_transform          # wp.array[wp.transform], user-mutable
model.camera_projections        # deduplicated descriptor table
xforms = newton.eval_camera_world_xforms(model, state)  # follows body_q

viewer.show_cameras = True                  # batched frustum visualization
viewer.set_camera_from_model("wrist_cam")   # adopt a model camera's viewpoint

🤖 Generated with Claude Code

@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 18:03 — with GitHub Actions Inactive
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 18:03 — with GitHub Actions Inactive
eric-heiden and others added 28 commits July 22, 2026 18:12
- CameraFisheyeFTheta.k1: 0.0 -> 1.0 (linear coefficient; 0.0 maps all
  angles to zero, matching compute_camera_rays_fisheye_ftheta default)
- All three fisheye classes max_fov: math.pi -> 2.0 * math.pi, matching
  the helpers' defaults in Utils.compute_camera_rays_fisheye_*
- Add inline field docstrings with units to CameraFisheyeOpenCV.fx/fy/cx/cy
- Add max_fov field docstrings to CameraFisheyeFTheta and
  CameraFisheyeKannalaBrandt mirroring CameraFisheyeOpenCV
- Extend test_fisheye_classes_exist to instantiate and hash
  CameraFisheyeFTheta and CameraFisheyeKannalaBrandt
- Fix pre-existing B017 ruff warning: assertRaises(Exception) ->
  assertRaises(AttributeError) in test_projection_immutable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Relocates the warp ray-generation kernels
(compute_camera_rays_pinhole_from_aperture_kernel, the three
fisheye_*_kernel variants, and all their private warp helper funcs)
from newton/_src/sensors/warp_raytrace/camera_utils.py into
newton/_src/core/cameras.py so the core module owns all camera math.

Back-compat re-exports in camera_utils.py keep the existing tiled
sensor call sites working without modification.

Adds compute_camera_rays() dispatcher in cameras.py that accepts any
CameraProjection descriptor and returns a (H, W, 2) vec3f bundle.
CameraCustomRays bundles are returned unchanged; shape mismatches raise.

Adds newton/tests/test_camera_rays.py (5 tests covering pinhole center
ray, FOV accuracy, origin/direction invariants, CustomRays passthrough,
and OpenCV fisheye bundle generation).
Add optional calibration-size fields to the three fisheye dataclasses
(image_width/image_height for CameraFisheyeOpenCV, nominal_width/
nominal_height for CameraFisheyeFTheta and CameraFisheyeKannalaBrandt)
so cameras calibrated at a resolution different from the render size
can express that difference without silently producing wrong rays.

Pass the calibration sizes through to the kernel launches in
compute_camera_rays, falling back to render width/height when None.
Hoist the shared bundle4d allocation before the isinstance branches.

Add a regression test asserting that a 2x calibration size produces
rays distinct from the default (None / render-size) calibration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add explicit type annotation for Model.camera_projections: list[CameraProjection]
- Add '# Camera flags' comment before CameraFlags class to match ShapeFlags pattern
- CameraFlags export in newton/__init__.py already matches ShapeFlags convention

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix two issues in eval_camera_world_xforms:
1. Replace Python `or` truthiness check on warp array with explicit None checks.
   Previously used: body_q = (state.body_q if state is not None else None) or model.body_q
   Now uses proper conditional: if state is not None and state.body_q is not None
2. Move zero-camera early return before out allocation to avoid allocation when not needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add incoming_xform parameter to _parse_cameras_impl so cameras inside
<frame> elements pick up the composed frame transform, mirroring the
existing _parse_sites_impl pattern.  Call _parse_cameras_impl from
process_frames immediately after the sites block so frame-nested
cameras are no longer silently dropped.  Remove unused bare
`import newton` from the camera test module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass `incoming_xform=xform` to `_parse_cameras_impl` at the
worldbody level so world-fixed cameras honor the scene root
transform supplied to `parse_mjcf`/`add_mjcf`, matching the
behavior already in place for shapes and sites.

Add regression test asserting worldbody camera z shifts by 10 m
when `add_mjcf` receives `xform` with a +10 z translation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the guard in log_state to detect when camera_frustum_depth changes
and rebuild frustums accordingly. Also convert _frustum_lines_kernel parameter
annotations from parenthesized form to bracket syntax, matching project
convention. Add test for depth-change rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the follow-mode set_camera_from_model re-apply to before
renderer.render() via a new gui.apply_camera_follow() method so the
updated pose is consumed by the current frame, not the next one.

Cache model.camera_projection_index (static after finalize) as a host
numpy array and preallocate the GPU transform buffer for
eval_camera_world_xforms, eliminating two extra GPU→CPU syncs per frame
in follow mode.

Add FOV adoption test and clarify the identity-Z-up docstring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename _pitch_yaw_to_basis_f64 to pitch_yaw_to_basis_f64, add it to
cameras.py __all__ with a docstring noting it returns plain float64
tuples (contrast with pitch_yaw_to_basis which returns wp.vec3). Update
all three import sites (camera.py x3 and viewer_viser.py).

Extend test_basis_matches_viewport_camera to assert right[k] and up[k]
against cam.get_right() / cam.get_up() in addition to the existing
front[k] check. Augment the gimbal-lock comment in viewer_viser.py with
the migration TODO note.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move function-level imports of pitch_yaw_to_basis and fov_to_focal_length
to module-level imports alongside xform_to_pitch_yaw, fixing PLC0415 ruff
findings and improving consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix camera_world_start shape in docs: [world_count + 2] to match all
  other *_world_start arrays
- Fix pre-existing FoV -> FOV typos in cameras.rst (typos hook)
- Add enable_backward=False to _frustum_lines_kernel (viz-only, matches
  ray kernels in cameras.py)
- Document that viewport Camera.fov is degrees while projection fov is
  radians in set_camera_from_model
- Add test_fisheye_frustum_finite_segments covering the generic 30°
  non-pinhole frustum path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Match the load_cameras parameter description and add the
path_camera_map return-value entry in ModelBuilder.add_usd so
the parity test passes.
Non-fixed camera modes (trackcom, targetbody, etc.) appear in standard
mujoco_menagerie assets used by Newton examples. Emitting a UserWarning
for each such camera caused test_examples tests to fail on "Unexpected
stderr". The camera imports correctly as fixed and the original mode is
already preserved in the mjcf:camera_mode custom attribute, so the
warning conveyed no actionable information.

Remove the warning; document the behaviour in _parse_cameras_impl's
docstring and in the add_mjcf / parse_mjcf Note section. Update the
test to assert no UserWarning is raised and that the mode is preserved,
and fix cameras.rst to drop the UserWarning mention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
add_builder's entity_offsets dict drove get_offset() for
Model.AttributeFrequency.CAMERA custom attributes but lacked a
'camera' entry, causing a ValueError when merging builders that
contain cameras with CAMERA-frequency custom attributes (e.g. the
humanoid MJCF's mjcf:camera_mode). Add start_camera_idx alongside
the other start_*_idx captures and map 'camera' -> start_camera_idx
in entity_offsets, mirroring the pattern for every other frequency.

Regression test: test_add_builder_camera_frequency_custom_attribute
merges two sub-builders each carrying a CAMERA-frequency attribute
and asserts camera_count==2 with correct per-camera values.
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 20:08 — with GitHub Actions Inactive
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 20:08 — with GitHub Actions Inactive
Remove the public eval_camera_world_xforms helper and its stored-array
kernel. Its only consumers were the frustum visualizer (needs every
camera, but per-thread) and set_camera_from_model (needs exactly one),
neither of which benefits from materializing an array of world transforms.

- Frustum kernel composes body_q * camera_transform inline per thread.
- set_camera_from_model reads only the selected camera's rows (sliced,
  so a scene with hundreds of cameras transfers one element, not all).
- Drop the per-layer _camera_xforms_buffer / _camera_proj_index_cache
  that only existed to feed the removed helper.
- Move the body-attached composition coverage into the two consumers'
  tests; drop the now-removed helper's direct tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 20:49 — with GitHub Actions Inactive
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 20:49 — with GitHub Actions Inactive
Follow selected model cameras automatically until manual viewer input
detaches them. Apply per-world offsets during selection and reject
body-attached cameras assigned to another world.
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 22:13 — with GitHub Actions Inactive
@eric-heiden
eric-heiden temporarily deployed to external-pr-approval July 22, 2026 22:13 — with GitHub Actions Inactive
Demonstrate static and body-attached ModelBuilder cameras with batched sensor rendering. Animate the attached camera along a tangent-aligned circular path and add regression coverage and documentation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant