Skip to content

Commit 6e171d4

Browse files
allow mounted cameras to be run in parallel simulations; added unit test
1 parent fd06778 commit 6e171d4

File tree

2 files changed

+202
-7
lines changed

2 files changed

+202
-7
lines changed

genesis/vis/camera.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def __init__(
9191
self._is_built = False
9292
self._attached_link = None
9393
self._attached_offset_T = None
94+
self._attached_env_idx = None
9495

9596
self._in_recording = False
9697
self._recorded_imgs = []
@@ -124,7 +125,7 @@ def _build(self):
124125
self._is_built = True
125126
self.set_pose(self._transform, self._pos, self._lookat, self._up)
126127

127-
def attach(self, rigid_link, offset_T):
128+
def attach(self, rigid_link, offset_T, env_idx: int | None = None):
128129
"""
129130
Attach the camera to a rigid link in the scene.
130131
@@ -136,9 +137,25 @@ def attach(self, rigid_link, offset_T):
136137
The rigid link to which the camera should be attached.
137138
offset_T : np.ndarray, shape (4, 4)
138139
The transformation matrix specifying the camera's pose relative to the rigid link.
140+
env_idx : int
141+
The environment index this camera should be tied to. Offsets the `offset_T` accordingly. Must be specified if running parallel environments
142+
143+
Raises
144+
------
145+
Exception
146+
If running parallel simulations but env_idx is not specified.
147+
Exception
148+
If invalid env_idx is specified (env_idx >= n_envs)
139149
"""
140150
self._attached_link = rigid_link
141151
self._attached_offset_T = offset_T
152+
if self._visualizer._scene.n_envs > 0 and env_idx is None:
153+
gs.raise_exception("Must specify env_idx when running parallel simulations")
154+
if env_idx is not None:
155+
n_envs = self._visualizer._scene.n_envs
156+
if env_idx >= n_envs:
157+
gs.raise_exception(f"Invalid env_idx {env_idx} for camera, configured for {n_envs} environments")
158+
self._attached_env_idx = env_idx
142159

143160
def detach(self):
144161
"""
@@ -148,6 +165,7 @@ def detach(self):
148165
"""
149166
self._attached_link = None
150167
self._attached_offset_T = None
168+
self._attached_env_idx = None
151169

152170
@gs.assert_built
153171
def move_to_attach(self):
@@ -160,16 +178,15 @@ def move_to_attach(self):
160178
------
161179
Exception
162180
If the camera has not been mounted using `attach()`.
163-
Exception
164-
If the simulation is running in parallel (`n_envs > 0`), which is currently unsupported for mounted cameras.
165181
"""
166182
if self._attached_link is None:
167183
gs.raise_exception(f"The camera hasn't been mounted!")
168-
if self._visualizer._scene.n_envs > 0:
169-
gs.raise_exception(f"Mounted camera not supported in parallel simulation!")
170184

171-
link_pos = self._attached_link.get_pos().cpu().numpy()
172-
link_quat = self._attached_link.get_quat().cpu().numpy()
185+
link_pos = self._attached_link.get_pos(envs_idx=self._attached_env_idx).cpu().numpy()
186+
link_quat = self._attached_link.get_quat(envs_idx=self._attached_env_idx).cpu().numpy()
187+
if self._attached_env_idx is not None:
188+
link_pos = link_pos[0] + self._visualizer._scene.envs_offset[self._attached_env_idx]
189+
link_quat = link_quat[0]
173190
link_T = gu.trans_quat_to_T(link_pos, link_quat)
174191
transform = link_T @ self._attached_offset_T
175192
self.set_pose(transform=transform)

tests/test_rigid_physics.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import math
22
import os
3+
import queue
34
import sys
45
import xml.etree.ElementTree as ET
56

@@ -1034,6 +1035,183 @@ def test_batched_offscreen_rendering(show_viewer, tol):
10341035
assert_allclose(steps_rgb_arrays[0][i], steps_rgb_arrays[1][i], tol=tol)
10351036

10361037

1038+
@pytest.mark.required
1039+
@pytest.mark.skipif(sys.platform == "darwin", reason="Segfault inside 'shadow_mapping_pass' on MacOS VM.")
1040+
@pytest.mark.xfail(reason="This test is not passing on all platforms for now.")
1041+
def test_batched_mounted_camera_rendering(show_viewer, tol):
1042+
scene = gs.Scene(
1043+
vis_options=gs.options.VisOptions(
1044+
env_separate_rigid=False,
1045+
),
1046+
show_viewer=show_viewer,
1047+
show_FPS=False,
1048+
)
1049+
plane = scene.add_entity(
1050+
morph=gs.morphs.Plane(),
1051+
surface=gs.surfaces.Aluminium(
1052+
ior=10.0,
1053+
),
1054+
)
1055+
scene.add_entity(
1056+
morph=gs.morphs.Mesh(
1057+
file="meshes/sphere.obj",
1058+
scale=0.1,
1059+
pos=(-0.2, -0.8, 0.2),
1060+
fixed=True,
1061+
),
1062+
surface=gs.surfaces.Rough(
1063+
diffuse_texture=gs.textures.ColorTexture(
1064+
color=(1.0, 0.5, 0.5),
1065+
),
1066+
),
1067+
)
1068+
scene.add_entity(
1069+
morph=gs.morphs.Mesh(
1070+
file="meshes/sphere.obj",
1071+
scale=0.1,
1072+
pos=(-0.2, -0.5, 0.2),
1073+
fixed=True,
1074+
),
1075+
surface=gs.surfaces.Rough(
1076+
color=(1.0, 1.0, 1.0),
1077+
),
1078+
)
1079+
scene.add_entity(
1080+
morph=gs.morphs.Mesh(
1081+
file="meshes/sphere.obj",
1082+
scale=0.1,
1083+
pos=(-0.2, -0.2, 0.2),
1084+
fixed=True,
1085+
),
1086+
surface=gs.surfaces.Smooth(
1087+
color=(0.6, 0.8, 1.0),
1088+
),
1089+
)
1090+
scene.add_entity(
1091+
morph=gs.morphs.Mesh(
1092+
file="meshes/sphere.obj",
1093+
scale=0.1,
1094+
pos=(-0.2, 0.2, 0.2),
1095+
fixed=True,
1096+
),
1097+
surface=gs.surfaces.Iron(
1098+
color=(1.0, 1.0, 1.0),
1099+
),
1100+
)
1101+
scene.add_entity(
1102+
morph=gs.morphs.Mesh(
1103+
file="meshes/sphere.obj",
1104+
scale=0.1,
1105+
pos=(-0.2, 0.5, 0.2),
1106+
fixed=True,
1107+
),
1108+
surface=gs.surfaces.Gold(
1109+
color=(1.0, 1.0, 1.0),
1110+
),
1111+
)
1112+
scene.add_entity(
1113+
morph=gs.morphs.Mesh(
1114+
file="meshes/sphere.obj",
1115+
scale=0.1,
1116+
pos=(-0.2, 0.8, 0.2),
1117+
fixed=True,
1118+
),
1119+
surface=gs.surfaces.Glass(
1120+
color=(1.0, 1.0, 1.0),
1121+
),
1122+
)
1123+
scene.add_entity(
1124+
morph=gs.morphs.Mesh(
1125+
file="meshes/sphere.obj",
1126+
scale=0.1,
1127+
pos=(0.2, -0.8, 0.2),
1128+
fixed=True,
1129+
),
1130+
surface=gs.surfaces.Smooth(
1131+
color=(1.0, 1.0, 1.0, 0.5),
1132+
),
1133+
)
1134+
scene.add_entity(
1135+
morph=gs.morphs.Mesh(
1136+
file="meshes/wooden_sphere_OBJ/wooden_sphere.obj",
1137+
scale=0.025,
1138+
pos=(0.2, -0.5, 0.2),
1139+
fixed=True,
1140+
),
1141+
)
1142+
scene.add_entity(
1143+
morph=gs.morphs.Mesh(
1144+
file="meshes/wooden_sphere_OBJ/wooden_sphere.obj",
1145+
scale=0.025,
1146+
pos=(0.2, -0.2, 0.2),
1147+
fixed=True,
1148+
),
1149+
surface=gs.surfaces.Rough(
1150+
diffuse_texture=gs.textures.ImageTexture(
1151+
image_path="textures/checker.png",
1152+
)
1153+
),
1154+
)
1155+
robot = scene.add_entity(
1156+
gs.morphs.MJCF(file="xml/franka_emika_panda/panda.xml"),
1157+
)
1158+
n_envs = 3
1159+
env_spacing = (2.0, 2.0)
1160+
cams = [scene.add_camera(GUI=show_viewer, fov=70) for _ in range(n_envs)]
1161+
scene.build(n_envs=n_envs, env_spacing=env_spacing)
1162+
1163+
T = np.eye(4)
1164+
T[:3, :3] = np.array([[1, 0, 0], [0, -1, 0], [0, 0, -1]])
1165+
T[:3, 3] = np.array([0.1, 0.0, 0.1])
1166+
for nenv, cam in enumerate(cams):
1167+
cam.attach(robot.get_link("hand"), T, nenv)
1168+
1169+
target_quat = np.tile(np.array([0, 1, 0, 0]), [n_envs, 1]) # pointing downwards
1170+
center = np.tile(np.array([-0.25, -0.25, 0.5]), [n_envs, 1])
1171+
rng = np.random.default_rng(42)
1172+
angular_speed = rng.uniform(-10, 10, n_envs)
1173+
r = 0.25
1174+
1175+
ee_link = robot.get_link("hand")
1176+
1177+
steps_rgb_queue: queue.Queue[list[np.ndarray]] = queue.Queue(maxsize=2)
1178+
1179+
for i in range(50):
1180+
target_pos = np.zeros([n_envs, 3])
1181+
target_pos[:, 0] = center[:, 0] + np.cos(i / 360 * np.pi * angular_speed) * r
1182+
target_pos[:, 1] = center[:, 1] + np.sin(i / 360 * np.pi * angular_speed) * r
1183+
target_pos[:, 2] = center[:, 2]
1184+
1185+
q = robot.inverse_kinematics(
1186+
link=ee_link,
1187+
pos=target_pos,
1188+
quat=target_quat,
1189+
rot_mask=[False, False, True], # for demo purpose: only restrict direction of z-axis
1190+
)
1191+
1192+
robot.set_qpos(q)
1193+
scene.step()
1194+
if i < 10:
1195+
# skip the first few frames because the robots start off with the same state
1196+
for cam in cams:
1197+
cam.render()
1198+
continue
1199+
robots_rgb_arrays = []
1200+
for cam in cams:
1201+
rgb_array, *_ = cam.render()
1202+
assert np.std(rgb_array) > 10.0
1203+
robots_rgb_arrays.append(rgb_array)
1204+
steps_rgb_queue.put(robots_rgb_arrays)
1205+
1206+
if steps_rgb_queue.full(): # we have a set of 2 consecutive frames
1207+
diff_tol = 0.05 # expect atlest 5% difference between each frame
1208+
frames_t_minus_1 = steps_rgb_queue.get()
1209+
frames_t = steps_rgb_queue.get()
1210+
for i in range(n_envs):
1211+
diff = frames_t[i] - frames_t_minus_1[i]
1212+
assert np.count_nonzero(diff) > diff_tol * np.prod(diff.shape)
1213+
1214+
10371215
@pytest.mark.required
10381216
@pytest.mark.parametrize("backend", [gs.cpu])
10391217
def test_pd_control(show_viewer):

0 commit comments

Comments
 (0)