Skip to content

[BUG FIX] Fix sensor camera 'lookat' being ignored when 'entity_idx' is set.#2614

Merged
duburcqa merged 13 commits intoGenesis-Embodied-AI:mainfrom
Lidang-Jiang:fix/camera-lookat-entity
Apr 4, 2026
Merged

[BUG FIX] Fix sensor camera 'lookat' being ignored when 'entity_idx' is set.#2614
duburcqa merged 13 commits intoGenesis-Embodied-AI:mainfrom
Lidang-Jiang:fix/camera-lookat-entity

Conversation

@Lidang-Jiang
Copy link
Copy Markdown
Contributor

@Lidang-Jiang Lidang-Jiang commented Mar 28, 2026

Summary

  • Fix BaseCameraSensor.move_to_attach() silently ignoring the lookat and up parameters when a camera is attached to an entity via entity_idx.
  • Replace trans_to_T(pos) (translation-only) with pos_lookat_up_to_T(pos, lookat, up) to preserve the full camera orientation.
  • Add 3 regression tests covering attached camera lookat, equivalence with explicit offset_T, and non-regression for detached cameras.

Fixes #2591

Root cause

In BaseCameraSensor.move_to_attach(), when no explicit offset_T is provided, the offset transform was computed using trans_to_T(pos), which only encodes the position as a translation matrix and discards the rotation derived from lookat/up. This caused all entity-attached cameras to default to identity rotation regardless of their lookat target.

Fix: One-line change from trans_to_T(pos) to pos_lookat_up_to_T(pos, lookat, up), which correctly constructs a 4x4 transform encoding both position and orientation.

-            offset_T = trans_to_T(pos)
+            lookat = torch.tensor(self._options.lookat, dtype=gs.tc_float, device=gs.device)
+            up = torch.tensor(self._options.up, dtype=gs.tc_float, device=gs.device)
+            offset_T = pos_lookat_up_to_T(pos, lookat, up)

Snapshot update required

This fix changes the rendering output for test_rasterizer_attached_batched because the attached camera now correctly applies the lookat orientation instead of using identity rotation. The snapshot for this test needs to be regenerated by a maintainer with the reference hardware/GPU setup.

Before fix (lookat ignored)
[Genesis] [22:27:12] [INFO] ╭───────────────────────────────────────────────╮
[Genesis] [22:27:12] [INFO] │┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈ Genesis ┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈│
[Genesis] [22:27:12] [INFO] ╰───────────────────────────────────────────────╯
[Genesis] [22:27:13] [INFO] Running on [11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz] with backend gs.cpu. Device memory: 11.68 GB.
[Genesis] [22:27:13] [INFO] 🚀 Genesis initialized. 🔖 version: 0.4.3, 🎨 theme: dark, 🌱 seed: None, 🐛 debug: False, 📏 precision: 32, 🔥 performance: False, 💬 verbose: INFO
[Genesis] [22:27:14] [INFO] Scene <553ed15> created.
[Genesis] [22:27:14] [INFO] Adding <gs.engine.entities.RigidEntity>. idx: 0, uid: <d11c76c>, morph: <gs.morphs.Plane>, material: <gs.materials.Rigid>.
[Genesis] [22:27:14] [INFO] Adding <gs.engine.entities.RigidEntity>. idx: 1, uid: <a997cb1>, morph: <gs.morphs.Sphere>, material: <gs.materials.Rigid>.
[Genesis] [22:27:14] [INFO] Building scene <553ed15>...
[Genesis] [22:27:16] [INFO] Compiling simulation kernels...
[Genesis] [22:27:17] [INFO] Building visualizer...
Camera A image shape: (64, 64, 3), mean pixel: 102.3313
Camera B image shape: (64, 64, 3), mean pixel: 102.3313
Images identical: True
BUG CONFIRMED: lookat parameter is ignored when camera is attached to entity.
Both cameras produce the same image despite different lookat targets.
[Genesis] [22:27:26] [INFO] 💤 Exiting Genesis and caching compiled kernels...

Two cameras attached to the same entity with different lookat targets produce identical images (mean pixel: 102.3313 for both). The lookat parameter is silently ignored.

After fix (lookat correctly applied)
[Genesis] [22:27:48] [INFO] ╭───────────────────────────────────────────────╮
[Genesis] [22:27:48] [INFO] │┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈ Genesis ┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈┉┈│
[Genesis] [22:27:48] [INFO] ╰───────────────────────────────────────────────╯
[Genesis] [22:27:48] [INFO] Running on [11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz] with backend gs.cpu. Device memory: 11.68 GB.
[Genesis] [22:27:48] [INFO] 🚀 Genesis initialized. 🔖 version: 0.4.3, 🎨 theme: dark, 🌱 seed: None, 🐛 debug: False, 📏 precision: 32, 🔥 performance: False, 💬 verbose: INFO
[Genesis] [22:27:50] [INFO] Scene <c15c415> created.
[Genesis] [22:27:50] [INFO] Adding <gs.engine.entities.RigidEntity>. idx: 0, uid: <51c662e>, morph: <gs.morphs.Plane>, material: <gs.materials.Rigid>.
[Genesis] [22:27:50] [INFO] Adding <gs.engine.entities.RigidEntity>. idx: 1, uid: <d6d9e15>, morph: <gs.morphs.Sphere>, material: <gs.materials.Rigid>.
[Genesis] [22:27:50] [INFO] Building scene <c15c415>...
[Genesis] [22:27:51] [INFO] Compiling simulation kernels...
[Genesis] [22:27:53] [INFO] Building visualizer...
Camera A image shape: (64, 64, 3), mean pixel: 20.3333
Camera B image shape: (64, 64, 3), mean pixel: 50.6091
Images identical: False
FIXED: lookat parameter is correctly applied when camera is attached to entity.
The two cameras produce different images as expected.
[Genesis] [22:27:55] [INFO] 💤 Exiting Genesis and caching compiled kernels...

Two cameras with different lookat targets now produce different images (mean pixel: 20.3333 vs 50.6091), confirming the lookat parameter is correctly applied.

Unit test results (6 passed, 7 skipped)
============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-9.0.2, pluggy-1.6.0 -- /home/devuser/workspace/public-oss/embodied-robotics/myenv-genesis-2591/bin/python
cachedir: .pytest_cache
rootdir: /home/devuser/workspace/public-oss/embodied-robotics/worktree-genesis-2591
configfile: pyproject.toml
plugins: xdist-3.8.0, anyio-4.13.0, syrupy-5.1.0, forked-1.6.0
created: 1/1 worker
1 worker [13 items]

scheduling tests via WorkStealingScheduling

tests/test_sensor_camera.py::test_batch_renderer[0-cuda]
[gw0] [  7%] SKIPPED tests/test_sensor_camera.py::test_batch_renderer[0-cuda]
tests/test_sensor_camera.py::test_batch_renderer[2-cuda]
[gw0] [ 15%] SKIPPED tests/test_sensor_camera.py::test_batch_renderer[2-cuda]
tests/test_sensor_camera.py::test_destroy_unbuilt_scene_with_camera
[gw0] [ 23%] PASSED tests/test_sensor_camera.py::test_destroy_unbuilt_scene_with_camera
tests/test_sensor_camera.py::test_destroy_idempotent_with_camera
[gw0] [ 30%] PASSED tests/test_sensor_camera.py::test_destroy_idempotent_with_camera
tests/test_sensor_camera.py::test_rasterizer_destroy
[gw0] [ 38%] PASSED tests/test_sensor_camera.py::test_rasterizer_destroy
tests/test_sensor_camera.py::test_batch_renderer_destroy[cuda]
[gw0] [ 46%] SKIPPED tests/test_sensor_camera.py::test_batch_renderer_destroy[cuda]
tests/test_sensor_camera.py::test_raytracer_destroy
[gw0] [ 53%] SKIPPED tests/test_sensor_camera.py::test_raytracer_destroy
tests/test_sensor_camera.py::test_raytracer_attached_without_offset_T
[gw0] [ 61%] SKIPPED tests/test_sensor_camera.py::test_raytracer_attached_without_offset_T
tests/test_sensor_camera.py::test_raytracer[0]
[gw0] [ 69%] SKIPPED tests/test_sensor_camera.py::test_raytracer[0]
tests/test_sensor_camera.py::test_raytracer[1]
[gw0] [ 76%] SKIPPED tests/test_sensor_camera.py::test_raytracer[1]
tests/test_sensor_camera.py::test_attached_camera_lookat_affects_orientation
[gw0] [ 84%] PASSED tests/test_sensor_camera.py::test_attached_camera_lookat_affects_orientation
tests/test_sensor_camera.py::test_attached_camera_lookat_matches_offset_T
[gw0] [ 92%] PASSED tests/test_sensor_camera.py::test_attached_camera_lookat_matches_offset_T
tests/test_sensor_camera.py::test_detached_camera_lookat_still_works
[gw0] [100%] PASSED tests/test_sensor_camera.py::test_detached_camera_lookat_still_works

============================== slowest durations ===============================

(32 durations < 100s hidden.)
=========================== short test summary info ============================
SKIPPED [3] tests/conftest.py:99: Backend 'gs.cuda' not available on this machine
SKIPPED [4] tests/conftest.py:101: RayTracer is not supported because 'LuisaRenderPy' is not available.
======================== 6 passed, 7 skipped in 36.57s =========================
  • 3 new regression tests all PASSED:
    • test_attached_camera_lookat_affects_orientation -- different lookat targets produce different images
    • test_attached_camera_lookat_matches_offset_T -- lookat-derived transform matches equivalent explicit offset_T
    • test_detached_camera_lookat_still_works -- non-regression: detached cameras still work correctly
  • 7 tests skipped (no CUDA/LuisaRender available on test machine)

Test plan

  • Reproduce bug: two attached cameras with different lookat produce identical images (before fix)
  • Verify fix: same cameras now produce different images (after fix)
  • test_attached_camera_lookat_affects_orientation -- PASSED
  • test_attached_camera_lookat_matches_offset_T -- PASSED
  • test_detached_camera_lookat_still_works -- PASSED (non-regression)
  • All existing non-snapshot camera tests pass
  • test_rasterizer_attached_batched snapshot needs regeneration by maintainer (expected due to corrected rendering)

Comment thread tests/test_sensor_camera.py Outdated
Comment thread tests/test_sensor_camera.py Outdated
Comment thread tests/test_sensor_camera.py
Comment thread tests/test_sensor_camera.py
Comment thread tests/test_sensor_camera.py Outdated
Comment thread tests/test_sensor_camera.py Outdated
Comment thread tests/test_sensor_camera.py Outdated
@duburcqa
Copy link
Copy Markdown
Collaborator

Please share the PR on HuggingFace here.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@Lidang-Jiang The snapshot in your PR is blank. This is not ok.

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

@Lidang-Jiang The snapshot in your PR is blank. This is not ok.

In fixing ,pr will be updated soon.

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

Lidang-Jiang commented Apr 3, 2026

Hi @duburcqa, here's the HuggingFace snapshot PR:
https://huggingface.co/datasets/Genesis-Intelligence/snapshots/discussions/10

Update: Verified that both CPU and GPU (gs.cuda, RTX 3050 Ti) backends produce the identical snapshot (157 bytes, 64x64). The rendering is deterministic across backends for this scene.

Will update HUGGINGFACE_SNAPSHOT_REVISION in tests/utils.py once merged.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@Lidang-Jiang Still a blank image. Not ok. Add a tiled ground plane. We never see the sphere. Maybe add several sphere of colours at key locations, one of them being visible for each camera. Moreover, there is a single snapshot instead of one per camera. This is not ok.

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

Updated — addressed all feedback:

Code pushed in a625019.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@Lidang-Jiang great, thank you. Still, yellow and blue sphere are barely visible and the ground plane is not tiled (remove surface=gs.surfaces.Rough(color=(0.4, 0.4, 0.4)),).

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

Fixed in 93c975e:

  • Removed surface=gs.surfaces.Rough() so ground uses default tiled texture
  • Moved blue/yellow spheres closer to attach entity (distance 1.0 instead of 1.5)
  • Lowered attached camera offset for larger sphere appearance
  • Brightened blue (0.3, 0.3, 1.0) and yellow (1.0, 1.0, 0.0)

HuggingFace snapshots updated.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

Moved blue/yellow spheres closer to attach entity (distance 1.0 instead of 1.5)

Last change, please move camera further away instead.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

merged on HF.

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

HF PR has been merged, and I updated HUGGINGFACE_SNAPSHOT_REVISION in tests/utils.py in c9c3efb.

@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

Also reran pytest -xsv -n 0 --snapshot-update tests/test_sensor_camera.py::test_camera_lookat_entity after c9c3efb. It passes locally: 1 passed, 4 snapshots generated.

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

Ok let me finish this PR at this point.

When attaching a camera to an entity via entity_idx, the lookat and up
parameters were silently ignored because move_to_attach() used
trans_to_T(pos) which only sets translation, discarding orientation.

Replace trans_to_T(pos) with pos_lookat_up_to_T(pos, lookat, up) to
preserve the camera orientation defined by lookat/up parameters.

Fixes Genesis-Embodied-AI#2591

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
- Merge 3 tests into single test_camera_lookat_entity
- Remove comments, docstrings, and unused pos_lookat_up_to_T import
- Add png_snapshot check for attached camera rendering

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
- Remove _blurred_kernel_size (not proven necessary)
- Remove surface parameter from sphere entity
- Use common_options dict + for loop for camera creation
- Use for loop for pair-wise image assertions

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
- Add gray ground plane and 4 colored spheres (red/green/blue/yellow)
  at distinct cardinal positions for visual clarity
- Add snapshot assertion for every camera (4 total) instead of just one
- Increase sphere radius and adjust camera distances so spheres are
  clearly visible in 64x64 snapshots

Addresses reviewer feedback on snapshot quality.

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
- Remove surface override to use default tiled ground plane
- Move blue/yellow spheres closer to attach entity for visibility
- Brighten blue (0.3,0.3,1.0) and yellow (1.0,1.0,0.0) colors
- Reposition attached cameras for clearer sphere views

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
Move attached camera offset from (0,0,0.5) to (0,0,1.5) and restore
blue/yellow sphere positions to 1.5m from center as reviewer requested.

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
@duburcqa duburcqa force-pushed the fix/camera-lookat-entity branch 3 times, most recently from 6fc7436 to e47cf34 Compare April 3, 2026 15:44
duburcqa
duburcqa previously approved these changes Apr 3, 2026
@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@claude review

Comment thread genesis/engine/sensors/camera.py
Comment thread tests/test_sensor_camera.py
Comment thread tests/test_sensor_camera.py
@duburcqa duburcqa force-pushed the fix/camera-lookat-entity branch from e47cf34 to 966b622 Compare April 3, 2026 16:08
duburcqa
duburcqa previously approved these changes Apr 3, 2026
@duburcqa duburcqa force-pushed the fix/camera-lookat-entity branch from 390cf90 to 12d0a8e Compare April 3, 2026 16:27
@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@claude review

@duburcqa
Copy link
Copy Markdown
Collaborator

duburcqa commented Apr 3, 2026

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — all three previously flagged issues have been addressed.

Extended reasoning...

Overview

The PR fixes a bug where BaseCameraSensor.move_to_attach() and RaytracerCameraSensor.build() used trans_to_T(pos) (translation-only) instead of pos_lookat_up_to_T(pos, lookat, up) when computing the offset transform for entity-attached cameras. Additional changes in rasterizer.py, viewer.py, and ext/pyrender/viewer.py thread an env_separate_rigid override through the render path so that per-env camera poses trigger env-separate rendering automatically.

Security risks

None. All changes are internal rendering-path logic with no auth, crypto, or permission surfaces involved.

Level of scrutiny

Moderate. The fix is a small, well-motivated one-liner (plus test scaffolding), the logic is easy to audit, and the regression tests are comprehensive. The env_separate_rigid threading change is mechanical and correctly saves/restores the flag around each render call.

Resolution of prior review

My previous review flagged three issues, all of which have been resolved in the current revision: (1) the Raytracer build() path now uses pos_lookat_up_to_T; (2) the shadow-disable loop in test_camera_lookat_entity now uses the correct loop variable; (3) test_rasterizer_attached_batched now uses trans_quat_to_T with the sphere quaternion. The HuggingFace snapshot PR was merged and HUGGINGFACE_SNAPSHOT_REVISION updated accordingly.

duburcqa
duburcqa previously approved these changes Apr 3, 2026
@duburcqa duburcqa changed the title [BUG FIX] Fix camera lookat ignored when entity_idx is set [BUG FIX] Fix sensor camera lookat being ignored when entity_idx is set. Apr 3, 2026
@duburcqa duburcqa changed the title [BUG FIX] Fix sensor camera lookat being ignored when entity_idx is set. [BUG FIX] Fix sensor camera 'lookat' being ignored when 'entity_idx' is set. Apr 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 4, 2026

🔴 Benchmark Regression Detected ➡️ Report

@duburcqa duburcqa merged commit 97fd64d into Genesis-Embodied-AI:main Apr 4, 2026
22 of 23 checks passed
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.

[Bug]: Lookat parameter on gs.sensors.RasterizerCameraOptions not taking effect

2 participants