Skip to content

Commit 7789708

Browse files
committed
Enabling rasterizer camera sensor to work with n_envs > 1.
1 parent 1523455 commit 7789708

6 files changed

Lines changed: 156 additions & 24 deletions

File tree

genesis/engine/sensors/camera.py

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
from genesis.options.renderers import BatchRenderer as BatchRendererOptions
2525
from genesis.options.vis import VisOptions
2626
from genesis.vis.rasterizer_context import RasterizerContext
27-
from .base_sensor import Sensor, SharedSensorMetadata, RigidSensorMixin, RigidSensorMetadataMixin
27+
from .base_sensor import (
28+
Sensor,
29+
SharedSensorMetadata,
30+
RigidSensorMixin,
31+
RigidSensorMetadataMixin,
32+
)
2833
from .sensor_manager import register_sensor
2934

3035

@@ -195,7 +200,13 @@ class BaseCameraSensor(RigidSensorMixin, Sensor[SharedSensorMetadata]):
195200
- Shared read() method returning torch tensors
196201
"""
197202

198-
def __init__(self, options: "SensorOptions", idx: int, data_cls: Type[CameraData], manager: "gs.SensorManager"):
203+
def __init__(
204+
self,
205+
options: "SensorOptions",
206+
idx: int,
207+
data_cls: Type[CameraData],
208+
manager: "gs.SensorManager",
209+
):
199210
super().__init__(options, idx, data_cls, manager)
200211
self._stale: bool = True
201212

@@ -211,7 +222,9 @@ def _get_cache_dtype(cls) -> torch.dtype:
211222

212223
@classmethod
213224
def _update_shared_ground_truth_cache(
214-
cls, shared_metadata: SharedSensorMetadata, shared_ground_truth_cache: torch.Tensor
225+
cls,
226+
shared_metadata: SharedSensorMetadata,
227+
shared_ground_truth_cache: torch.Tensor,
215228
):
216229
pass
217230

@@ -246,7 +259,10 @@ def move_to_attach(self):
246259
offset_T = torch.tensor(self._options.offset_T, dtype=gs.tc_float, device=gs.device)
247260
else:
248261
pos = torch.tensor(self._options.pos, dtype=gs.tc_float, device=gs.device)
249-
offset_T = trans_quat_to_T(pos, torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=gs.tc_float, device=gs.device))
262+
offset_T = trans_quat_to_T(
263+
pos,
264+
torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=gs.tc_float, device=gs.device),
265+
)
250266

251267
link_pos = self._link.get_pos()
252268
link_quat = self._link.get_quat()
@@ -423,11 +439,29 @@ def build(self):
423439

424440
def _create_standalone_context(self, scene):
425441
"""Create a simplified RasterizerContext for camera sensors."""
442+
if not scene.sim._rigid_only and scene.n_envs > 1:
443+
gs.raise_exception("Rasterizer with n_envs > 1, does not work when using non rigid simulation")
444+
if sys.platform == "darwin":
445+
if scene.n_envs > 1:
446+
gs.raise_exception(
447+
"Rasterizer with n_envs > 1, does not work on Metal because it doesn't support OpenGL 4.2"
448+
)
449+
env_separate_rigid = False
450+
else:
451+
if self._link is not None:
452+
gs.raise_exception("Rasterizer with n_envs > 1, does not work with attached cameras yet.")
453+
454+
if scene.n_envs > 1:
455+
gs.logger.warning(
456+
"Rasterizer with n_envs > 1 is slow as it doesn't do batched rendering consider using BatchRenderer instead."
457+
)
458+
env_separate_rigid = True
426459
vis_options = VisOptions(
427460
show_world_frame=False,
428461
show_link_frame=False,
429462
show_cameras=False,
430463
rendered_envs_idx=range(max(self._manager._sim._B, 1)),
464+
env_separate_rigid=env_separate_rigid,
431465
)
432466

433467
context = RasterizerContext(vis_options)
@@ -501,22 +535,19 @@ def _render_current_state(self):
501535
self._shared_metadata.context.update(force_render=True)
502536

503537
rgb_arr, _, _, _ = self._shared_metadata.renderer.render_camera(
504-
self._get_camera_wrapper(), rgb=True, depth=False, segmentation=False, normal=False
538+
self._get_camera_wrapper(),
539+
rgb=True,
540+
depth=False,
541+
segmentation=False,
542+
normal=False,
505543
)
506544

507545
rgb_tensor = torch.from_numpy(rgb_arr.copy()).to(dtype=torch.uint8, device=gs.device)
508546

509-
# Store in cache
510-
n_envs = self._manager._sim._B
511-
if n_envs <= 1:
512-
# Single environment case - add batch dimension
513-
self._shared_metadata.image_cache[self._idx][0] = rgb_tensor
514-
else:
515-
# Multi-environment rendering is not yet supported for Rasterizer cameras
516-
gs.raise_exception(
517-
f"Rasterizer camera sensors do not support multi-environment rendering (n_envs={n_envs}). "
518-
"Use BatchRenderer camera sensors for batched rendering."
519-
)
547+
if len(rgb_tensor.shape) == 3:
548+
# Single environment rendered - add batch dimension.
549+
rgb_tensor = rgb_tensor.unsqueeze(0)
550+
self._shared_metadata.image_cache[self._idx][:] = rgb_tensor
520551

521552

522553
# ========================== Raytracer Camera Sensor ==========================
@@ -625,7 +656,10 @@ def build(self):
625656
from genesis.utils.geom import trans_quat_to_T
626657

627658
pos = torch.tensor(opts.pos, dtype=gs.tc_float, device=gs.device)
628-
offset_T = trans_quat_to_T(pos, torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=gs.tc_float, device=gs.device))
659+
offset_T = trans_quat_to_T(
660+
pos,
661+
torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=gs.tc_float, device=gs.device),
662+
)
629663
self._camera_obj.attach(self._link, offset_T)
630664

631665
h, w = self._options.res[1], self._options.res[0]
@@ -774,7 +808,12 @@ def _render_current_state(self):
774808
self._shared_metadata.renderer.update_scene(force_render=True)
775809

776810
rgb_arr, *_ = self._shared_metadata.renderer.render(
777-
rgb=True, depth=False, segmentation=False, normal=False, antialiasing=False, force_render=True
811+
rgb=True,
812+
depth=False,
813+
segmentation=False,
814+
normal=False,
815+
antialiasing=False,
816+
force_render=True,
778817
)
779818

780819
# rgb_arr might be a tuple of arrays (one per camera) or a single array

genesis/ext/pyrender/jit_render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ def gen_func_ptr(self):
374374
self.gl = GLWrapper()
375375

376376
IS_OPENGL_42_AVAILABLE = hasattr(self.gl.wrapper_instance, "glDrawElementsInstancedBaseInstance")
377-
OPENGL_42_ERROR_MSG = "Seperated env rendering not supported because OpenGL 4.2 not available on this machine."
377+
OPENGL_42_ERROR_MSG = "Separated env rendering not supported because OpenGL 4.2 not available on this machine."
378378

379379
@nb.jit(
380380
nb.none(

genesis/ext/pyrender/renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def render(self, scene, flags, seg_node_map=None, *, is_first_pass=True, force_s
152152

153153
if flags & RenderFlags.ENV_SEPARATE and flags & RenderFlags.OFFSCREEN:
154154
n_envs = scene.n_envs
155-
use_env_idx = True
155+
use_env_idx = True and scene.n_envs > 1
156156
else:
157157
n_envs = 1
158158
use_env_idx = False

genesis/vis/camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def build(self):
156156
self._raytracer.add_camera(self)
157157
else:
158158
self._is_batched = False
159-
if self._visualizer.scene.n_envs > 0 and self._visualizer._context.env_separate_rigid:
159+
if self._visualizer.scene.n_envs > 1 and self._visualizer._context.env_separate_rigid:
160160
gs.logger.warning(
161161
"Batched rendering via 'VisOptions.env_separate_rigid=True' is only partially supported by "
162162
"Rasterizer for now. The same camera transform will be used for all the environments."
@@ -165,7 +165,7 @@ def build(self):
165165
if self._env_idx is None:
166166
if not self._is_batched:
167167
self._env_idx = int(self._visualizer._context.rendered_envs_idx[0])
168-
if self._visualizer.scene.n_envs > 0:
168+
if self._visualizer.scene.n_envs > 1:
169169
gs.logger.info(
170170
"Raytracer and Rasterizer requires binding to the camera with a specific environment "
171171
"index. Defaulting to 'rendered_envs_idx[0]'. Please specify 'env_idx' if necessary."

tests/test_sensor_camera.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import sys
2+
13
import numpy as np
4+
from numpy.__config__ import show
25
import pytest
6+
import torch
37

48
import genesis as gs
59
from genesis.utils.misc import tensor_to_array
6-
from .utils import assert_allclose
10+
from .utils import assert_allclose, rgb_array_to_png_bytes
711

812

913
@pytest.mark.required
@@ -130,3 +134,92 @@ def _get_camera_world_pos(sensor):
130134
cam_move_dist_offset_T = np.linalg.norm(cam_pos_final_offset_T - cam_pos_initial_offset_T)
131135
assert cam_move_dist_offset_T > 1e-2
132136
assert_allclose(cam_move_dist_offset_T, cam_move_dist, atol=1e-2)
137+
138+
139+
# ========================== Multi-environment tests ==========================
140+
141+
142+
@pytest.mark.required
143+
def test_rasterizer_camera_sensor_n_envs(show_viewer, png_snapshot):
144+
if sys.platform == "darwin":
145+
pytest.skip(
146+
"Batched rendering with Rasterizer backend is not supported on this machine because it requires OpenGL 4.2."
147+
)
148+
149+
scene = gs.Scene(
150+
rigid_options=gs.options.RigidOptions(),
151+
renderer=gs.renderers.Rasterizer(),
152+
show_viewer=show_viewer,
153+
)
154+
155+
# Add a sphere
156+
sphere = scene.add_entity(
157+
morph=gs.morphs.Sphere(pos=(0.0, 0.0, 1.0), radius=0.3),
158+
surface=gs.surfaces.Smooth(color=(1.0, 0.5, 0.5)),
159+
)
160+
161+
options = gs.sensors.RasterizerCameraOptions(
162+
res=(64, 64),
163+
pos=(3.0, 0.0, 1.5),
164+
lookat=(0.0, 0.0, 0.5),
165+
fov=60.0,
166+
draw_debug=show_viewer,
167+
)
168+
camera = scene.add_sensor(options)
169+
n_envs = 2
170+
171+
scene.build(n_envs=n_envs)
172+
sphere.set_pos([[0.0, 0.0, 1.0], [0.2, 0.0, 0.5]])
173+
174+
scene.step()
175+
176+
data = camera.read()
177+
178+
assert data.rgb.shape == (2, 64, 64, 3)
179+
assert data.rgb.dtype == torch.uint8
180+
assert (data.rgb[0] != data.rgb[1]).any(), "We should have different frames"
181+
182+
for i in range(n_envs):
183+
assert rgb_array_to_png_bytes(data.rgb[i]) == png_snapshot
184+
185+
186+
@pytest.mark.required
187+
def test_rasterizer_camera_sensor_n_envs_attached_camera():
188+
if sys.platform == "darwin":
189+
pytest.skip(
190+
"Batched rendering with Rasterizer backend is not supported on this machine because it requires OpenGL 4.2."
191+
)
192+
193+
scene = gs.Scene(
194+
rigid_options=gs.options.RigidOptions(
195+
enable_collision=True,
196+
gravity=(0, 0, -9.8),
197+
),
198+
renderer=gs.renderers.Rasterizer(),
199+
show_viewer=False,
200+
)
201+
202+
# Add a plane
203+
scene.add_entity(
204+
morph=gs.morphs.Plane(),
205+
surface=gs.surfaces.Rough(color=(0.4, 0.4, 0.4)),
206+
)
207+
208+
# Add a sphere
209+
sphere = scene.add_entity(
210+
morph=gs.morphs.Sphere(pos=(0.0, 0.0, 1.0), radius=0.3),
211+
surface=gs.surfaces.Smooth(color=(1.0, 0.5, 0.5)),
212+
)
213+
214+
options = gs.sensors.RasterizerCameraOptions(
215+
res=(64, 64),
216+
pos=(3.0, 0.0, 1.5),
217+
lookat=(0.0, 0.0, 0.5),
218+
fov=60.0,
219+
entity_idx=sphere.idx,
220+
)
221+
scene.add_sensor(options)
222+
n_envs = 2
223+
224+
with pytest.raises(gs.GenesisException, match="does not work with attached cameras yet."):
225+
scene.build(n_envs=n_envs)

tests/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
DEFAULT_BRANCH_NAME = "main"
3737

3838
HUGGINGFACE_ASSETS_REVISION = "701f78c1465f0a98f6540bae6c9daacaa551b7bf"
39-
HUGGINGFACE_SNAPSHOT_REVISION = "ea6ae70386c2b2fbae1387f93ba0e4de1ed7abf7"
39+
HUGGINGFACE_SNAPSHOT_REVISION = "d190d8f651266c56fc54ec9a16b468731211b315"
4040

4141
MESH_EXTENSIONS = (".mtl", *MESH_FORMATS, *GLTF_FORMATS, *USD_FORMATS)
4242
IMAGE_EXTENSIONS = (".png", ".jpg")

0 commit comments

Comments
 (0)