Skip to content

Commit 63721ad

Browse files
authored
[FEATURE] Support attaching camera sensors with Rasterizer batched rendering. (#2212)
1 parent 8c2ff73 commit 63721ad

6 files changed

Lines changed: 67 additions & 42 deletions

File tree

‎genesis/engine/sensors/camera.py‎

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,6 @@ def move_to_attach(self):
267267
link_pos = self._link.get_pos()
268268
link_quat = self._link.get_quat()
269269

270-
# Handle batched case - use first environment
271-
if link_pos.ndim > 1:
272-
link_pos = link_pos[0]
273-
link_quat = link_quat[0]
274-
275270
link_T = trans_quat_to_T(link_pos, link_quat)
276271
camera_T = torch.matmul(link_T, offset_T)
277272

@@ -448,9 +443,6 @@ def _create_standalone_context(self, scene):
448443
)
449444
env_separate_rigid = False
450445
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-
454446
if scene.n_envs > 1:
455447
gs.logger.warning(
456448
"Rasterizer with n_envs > 1 is slow as it doesn't do batched rendering consider using BatchRenderer instead."
@@ -498,11 +490,6 @@ def _update_camera_pose(self):
498490
link_pos = self._link.get_pos()
499491
link_quat = self._link.get_quat()
500492

501-
# Handle batched case - use first environment
502-
if link_pos.ndim > 1:
503-
link_pos = link_pos[0]
504-
link_quat = link_quat[0]
505-
506493
# Apply pos directly as offset from link
507494
from genesis.utils.geom import transform_by_quat
508495

@@ -609,11 +596,6 @@ def build(self):
609596
link_pos = self._link.get_pos()
610597
link_quat = self._link.get_quat()
611598

612-
# Handle batched case - use first environment
613-
if link_pos.ndim > 1:
614-
link_pos = link_pos[0]
615-
link_quat = link_quat[0]
616-
617599
# Apply pos directly as offset from link
618600
from genesis.utils.geom import transform_by_quat
619601

@@ -758,8 +740,7 @@ def build(self):
758740
resolutions = [s._options.res for s in all_sensors]
759741
if len(set(resolutions)) > 1:
760742
gs.raise_exception(
761-
f"All BatchRendererCameraSensor instances must have the same resolution. "
762-
f"Found: {set(resolutions)}"
743+
f"All BatchRendererCameraSensor instances must have the same resolution. Found: {set(resolutions)}"
763744
)
764745

765746
br_options = BatchRendererOptions(

‎genesis/ext/pyrender/renderer.py‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,9 @@ def _floor_pass(self, scene, flags, seg_node_map=None, env_idx=-1):
354354
glClearColor(0.0, 0.0, 0.0, 1.0)
355355
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
356356

357-
V, P = self._get_camera_matrices(scene)
357+
V, P = self._get_camera_matrices(scene, env_idx)
358+
cam_pose = self._get_camera_pose(scene, env_idx)[:3, 3]
358359

359-
cam_pos = scene.get_pose(scene.main_camera_node)[:3, 3]
360360
screen_size = np.array([self.viewport_width, self.viewport_height], np.float32)
361361

362362
self.jit.forward_pass(
@@ -391,8 +391,8 @@ def _forward_pass(self, scene, flags, seg_node_map=None, env_idx=-1):
391391
glEnable(GL_MULTISAMPLE)
392392

393393
# Set up camera matrices
394-
V, P = self._get_camera_matrices(scene)
395-
cam_pos = scene.get_pose(scene.main_camera_node)[:3, 3]
394+
V, P = self._get_camera_matrices(scene, env_idx)
395+
cam_pos = self._get_camera_pose(scene, env_idx)[:3, 3]
396396

397397
floor_tex = self._floor_texture_color._texid if flags & RenderFlags.REFLECTIVE_FLOOR else 0
398398
screen_size = np.array([self.viewport_width, self.viewport_height], np.float32)
@@ -455,7 +455,7 @@ def _normal_pass(self, scene, flags, env_idx=-1):
455455
program = None
456456

457457
# Set up camera matrices
458-
V, P = self._get_camera_matrices(scene)
458+
V, P = self._get_camera_matrices(scene, env_idx)
459459

460460
# Now, render each object in sorted order
461461
for node in scene.sorted_mesh_nodes():
@@ -690,15 +690,25 @@ def _reset_active_textures(self):
690690
# Camera Matrix Management
691691
###########################################################################
692692

693-
def _get_camera_matrices(self, scene):
693+
def _get_camera_matrices(self, scene, env_idx):
694694
main_camera_node = scene.main_camera_node
695695
if main_camera_node is None:
696696
raise ValueError("Cannot render scene without a camera")
697697
P = main_camera_node.camera.get_projection_matrix(width=self.viewport_width, height=self.viewport_height)
698-
pose = scene.get_pose(main_camera_node)
698+
pose = self._get_camera_pose(scene, env_idx)
699699
V = np.linalg.inv(pose) # V maps from world to camera
700700
return V, P
701701

702+
def _get_camera_pose(self, scene, env_idx):
703+
cam_pos = scene.get_pose(scene.main_camera_node)
704+
if len(cam_pos.shape) == 3:
705+
if cam_pos.shape[0] != 1:
706+
assert env_idx != -1, "We have a multiple camera pose scene, we should be rendering per env"
707+
cam_pos = cam_pos[env_idx]
708+
else:
709+
cam_pos = cam_pos[0]
710+
return cam_pos
711+
702712
def _get_light_cam_matrices(self, scene, light_node, flags):
703713
light = light_node.light
704714
pose = scene.get_pose(light_node)

‎genesis/ext/pyrender/scene.py‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def __init__(self, nodes=None, bg_color=None, ambient_light=None, n_envs=None, n
8989
for node in nodes:
9090
for child in node.children:
9191
if node_parent_map[child] is not None:
92-
raise ValueError("Nodes may not have more than " "one parent")
92+
raise ValueError("Nodes may not have more than one parent")
9393
node_parent_map[child] = node
9494
for node in node_parent_map:
9595
if node_parent_map[node] is None:
@@ -294,7 +294,7 @@ def add(self, obj, name=None, pose=None, parent_node=None, parent_name=None):
294294

295295
if parent_node is None and parent_name is not None:
296296
try:
297-
parent_node, = self.get_nodes(name=parent_name)
297+
(parent_node,) = self.get_nodes(name=parent_name)
298298
except ValueError:
299299
if len(parent_nodes) == 0:
300300
raise ValueError(f"No parent node with name '{parent_name}' found")
@@ -601,7 +601,9 @@ def from_trimesh_scene(trimesh_scene, bg_color=None, ambient_light=None):
601601
return scene_pr
602602

603603
def sorted_mesh_nodes(self):
604-
cam_loc = self.get_pose(self.main_camera_node)[:3, 3]
604+
cam_pos = self.get_pose(self.main_camera_node)
605+
cam_loc = cam_pos[..., :3, 3]
606+
batched_pos = len(cam_pos.shape) == 3
605607
solid_nodes = []
606608
trans_nodes = []
607609
for node in self.mesh_nodes:
@@ -612,7 +614,12 @@ def sorted_mesh_nodes(self):
612614
solid_nodes.append(node)
613615

614616
# TODO BETTER SORTING METHOD
615-
trans_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc))
616-
solid_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc))
617+
if batched_pos:
618+
# FIXME normally sorting should be done PER scene when having a batched rasterizer render
619+
trans_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc[0]))
620+
solid_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc[0]))
621+
else:
622+
trans_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc))
623+
solid_nodes.sort(key=lambda n: -np.linalg.norm(self.get_pose(n)[:3, 3] - cam_loc))
617624

618625
return solid_nodes + trans_nodes

‎pyproject.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ force-exclude = '''
139139
)/
140140
'''
141141

142+
143+
[tool.ruff]
144+
line-length = 120
145+
142146
[tool.pytest.ini_options]
143147
addopts = [
144148
"--color=yes",

‎tests/test_sensor_camera.py‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ def test_rasterizer_camera_sensor_n_envs(show_viewer, png_snapshot):
160160
# Add a plane
161161
scene.add_entity(
162162
morph=gs.morphs.Plane(),
163-
surface=gs.surfaces.Rough(color=(0.4, 0.4, 0.4)),
164163
)
165164

166165
# Add a sphere
@@ -193,9 +192,15 @@ def test_rasterizer_camera_sensor_n_envs(show_viewer, png_snapshot):
193192

194193
@pytest.mark.required
195194
@pytest.mark.skipif(sys.platform == "darwin", reason="Not supported on this machine because it requires OpenGL 4.2.")
196-
def test_rasterizer_camera_sensor_n_envs_attached_camera():
197-
scene = gs.Scene()
195+
def test_rasterizer_camera_sensor_n_attached_camera(show_viewer, png_snapshot):
196+
scene = gs.Scene(show_viewer=show_viewer)
198197

198+
# Add a plane
199+
scene.add_entity(
200+
morph=gs.morphs.Plane(),
201+
)
202+
203+
# Add a sphere
199204
sphere = scene.add_entity(
200205
morph=gs.morphs.Sphere(
201206
radius=0.3,
@@ -206,11 +211,29 @@ def test_rasterizer_camera_sensor_n_envs_attached_camera():
206211
),
207212
)
208213

209-
scene.add_sensor(
210-
gs.sensors.RasterizerCameraOptions(
211-
entity_idx=sphere.idx,
212-
)
214+
options = gs.sensors.RasterizerCameraOptions(
215+
res=(64, 64),
216+
pos=(-0.4, 0.1, 2.0),
217+
lookat=(-0.6, 0.4, 1.0),
218+
fov=60.0,
219+
entity_idx=sphere.idx,
220+
draw_debug=show_viewer,
213221
)
222+
camera = scene.add_sensor(options)
223+
224+
scene.build(n_envs=2)
214225

215-
with pytest.raises(gs.GenesisException, match="does not work with attached cameras yet."):
216-
scene.build(n_envs=2)
226+
# Disable shadows systematically for Rasterizer because they are forcibly disabled on CPU backend anyway
227+
camera._shared_metadata.context.shadow = False
228+
229+
sphere.set_pos([[0.0, 0.0, 1.0], [0.2, 0.0, 0.5]])
230+
scene.step()
231+
232+
data = camera.read()
233+
234+
assert data.rgb.shape == (2, 64, 64, 3)
235+
assert data.rgb.dtype == torch.uint8
236+
assert (data.rgb[0] != data.rgb[1]).any(), "We should have different frames"
237+
238+
for i in range(scene.n_envs):
239+
assert rgb_array_to_png_bytes(data.rgb[i]) == png_snapshot

‎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 = "1df3aa3732abcf9b1701e180e2d266ac9d8d411e"
39+
HUGGINGFACE_SNAPSHOT_REVISION = "f13f28423a8961072832ead74df5d5703e01923e"
4040

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

0 commit comments

Comments
 (0)