Skip to content

Commit 5fd25fe

Browse files
authored
[FEATURE] Introduce interactive viewer plugins. (#2004)
1 parent 3e8dc52 commit 5fd25fe

File tree

25 files changed

+1397
-1816
lines changed

25 files changed

+1397
-1816
lines changed

.github/workflows/examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
run: |
5555
python -m pip install --upgrade pip setuptools wheel
5656
pip install torch --index-url https://download.pytorch.org/whl/cpu
57-
pip install -e '.[dev,usd]' pynput
57+
pip install -e '.[dev,usd]'
5858
5959
- name: Get gstaichi version
6060
id: gstaichi_version

examples/IPC_Solver/ipc_arm_cloth.py

Lines changed: 112 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,19 @@
1414
; - Roll Right (Rotate around X)
1515
u - Reset Scene
1616
space - Press to close gripper, release to open gripper
17-
esc - Quit
1817
"""
1918

20-
import threading
2119
import argparse
22-
import numpy as np
2320
import csv
2421
import os
2522
from datetime import datetime
26-
from pynput import keyboard
27-
from scipy.spatial.transform import Rotation as R
23+
24+
import numpy as np
2825
from huggingface_hub import snapshot_download
2926

3027
import genesis as gs
31-
32-
33-
class KeyboardDevice:
34-
def __init__(self):
35-
self.pressed_keys = set()
36-
self.lock = threading.Lock()
37-
self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
38-
39-
def start(self):
40-
self.listener.start()
41-
42-
def stop(self):
43-
self.listener.stop()
44-
self.listener.join()
45-
46-
def on_press(self, key: keyboard.Key):
47-
with self.lock:
48-
self.pressed_keys.add(key)
49-
50-
def on_release(self, key: keyboard.Key):
51-
with self.lock:
52-
self.pressed_keys.discard(key)
53-
54-
def get_cmd(self):
55-
return self.pressed_keys
28+
import genesis.utils.geom as gu
29+
from genesis.vis.keybindings import Key, KeyAction, Keybind
5630

5731

5832
def build_scene(use_ipc=False, show_viewer=False, enable_ipc_gui=False):
@@ -109,18 +83,12 @@ def build_scene(use_ipc=False, show_viewer=False, enable_ipc_gui=False):
10983
euler=(0, 0, 0),
11084
),
11185
)
112-
scene.sim.coupler.set_ipc_link_filter(
113-
entity=entities["robot"],
114-
link_names=["left_finger", "right_finger"],
115-
)
116-
117-
material = (
118-
gs.materials.FEM.Elastic(E=1.0e4, nu=0.45, rho=1000.0, model="stable_neohookean")
119-
if use_ipc
120-
else gs.materials.Rigid()
121-
)
12286

12387
if use_ipc:
88+
scene.sim.coupler.set_ipc_link_filter(
89+
entity=entities["robot"],
90+
link_names=["left_finger", "right_finger"],
91+
)
12492
cloth = scene.add_entity(
12593
morph=gs.morphs.Mesh(
12694
file="meshes/grid20x20.obj",
@@ -171,14 +139,15 @@ def build_scene(use_ipc=False, show_viewer=False, enable_ipc_gui=False):
171139
return scene, entities
172140

173141

174-
def run_sim(scene, entities, clients, mode="interactive", trajectory_file=None):
142+
def run_sim(scene, entities, add_keybinds, mode="interactive", trajectory_file=None):
175143
robot = entities["robot"]
176144
target_entity = entities["target"]
145+
is_running = True
177146

178147
robot_init_pos = np.array([0.5, 0, 0.55])
179-
robot_init_R = R.from_euler("y", np.pi)
148+
robot_init_quat = gu.xyz_to_quat(np.array([0, np.pi, 0])) # Rotation around Y axis
180149
target_pos = robot_init_pos.copy()
181-
target_R = robot_init_R
150+
target_quat = robot_init_quat.copy()
182151

183152
n_dofs = robot.n_dofs
184153
motors_dof = np.arange(n_dofs - 2)
@@ -189,18 +158,63 @@ def run_sim(scene, entities, clients, mode="interactive", trajectory_file=None):
189158
trajectory = []
190159
recording = mode == "record"
191160

161+
# Gripper state (use list for mutability in closures)
162+
gripper_closed = [False]
163+
164+
# Control parameters
165+
dpos = 0.002
166+
drot = 0.01
167+
192168
def reset_scene():
193-
nonlocal target_pos, target_R
194-
target_pos = robot_init_pos.copy()
195-
target_R = robot_init_R
196-
target_quat = target_R.as_quat(scalar_first=True)
169+
target_pos[:] = robot_init_pos
170+
target_quat[:] = robot_init_quat
197171
target_entity.set_qpos(np.concatenate([target_pos, target_quat]))
198172
q = robot.inverse_kinematics(link=ee_link, pos=target_pos, quat=target_quat)
199173
robot.set_qpos(q[:-2], motors_dof)
200174

201175
# entities["cube"].set_pos((random.uniform(0.2, 0.4), random.uniform(-0.2, 0.2), 0.05))
202176
# entities["cube"].set_quat(R.from_euler("z", random.uniform(0, np.pi * 2)).as_quat(scalar_first=True))
203177

178+
# Register keybindings
179+
if add_keybinds:
180+
181+
def move(dpos_delta: tuple[float, float, float]):
182+
target_pos[:] += np.array(dpos_delta, dtype=gs.np_float)
183+
184+
def rotate(axis: str, angle: float):
185+
# Create rotation quaternion for the specified axis
186+
euler = np.zeros(3)
187+
axis_map = {"x": 0, "y": 1, "z": 2}
188+
euler[axis_map[axis]] = angle
189+
drot_quat = gu.xyz_to_quat(euler)
190+
target_quat[:] = gu.transform_quat_by_quat(target_quat, drot_quat)
191+
192+
def toggle_gripper(close: bool = True):
193+
gripper_closed[0] = close
194+
195+
def stop():
196+
nonlocal is_running
197+
is_running = False
198+
199+
scene.viewer.register_keybinds(
200+
Keybind("move_forward", Key.UP, KeyAction.HOLD, callback=move, args=((-dpos, 0, 0),)),
201+
Keybind("move_backward", Key.DOWN, KeyAction.HOLD, callback=move, args=((dpos, 0, 0),)),
202+
Keybind("move_left", Key.LEFT, KeyAction.HOLD, callback=move, args=((0, -dpos, 0),)),
203+
Keybind("move_right", Key.RIGHT, KeyAction.HOLD, callback=move, args=((0, dpos, 0),)),
204+
Keybind("move_up", Key.N, KeyAction.HOLD, callback=move, args=((0, 0, dpos),)),
205+
Keybind("move_down", Key.M, KeyAction.HOLD, callback=move, args=((0, 0, -dpos),)),
206+
Keybind("yaw_left", Key.J, KeyAction.HOLD, callback=rotate, args=("z", drot)),
207+
Keybind("yaw_right", Key.K, KeyAction.HOLD, callback=rotate, args=("z", -drot)),
208+
Keybind("pitch_up", Key.I, KeyAction.HOLD, callback=rotate, args=("y", drot)),
209+
Keybind("pitch_down", Key.O, KeyAction.HOLD, callback=rotate, args=("y", -drot)),
210+
Keybind("roll_left", Key.L, KeyAction.HOLD, callback=rotate, args=("x", drot)),
211+
Keybind("roll_right", Key.SEMICOLON, KeyAction.HOLD, callback=rotate, args=("x", -drot)),
212+
Keybind("reset_scene", Key.U, KeyAction.HOLD, callback=reset_scene),
213+
Keybind("close_gripper", Key.SPACE, KeyAction.PRESS, callback=toggle_gripper, args=(True,)),
214+
Keybind("open_gripper", Key.SPACE, KeyAction.RELEASE, callback=toggle_gripper, args=(False,)),
215+
Keybind("quit", Key.ESCAPE, KeyAction.PRESS, callback=stop),
216+
)
217+
204218
# Load trajectory if in playback mode
205219
if mode == "playback":
206220
if not trajectory_file or not os.path.exists(trajectory_file):
@@ -232,7 +246,7 @@ def reset_scene():
232246

233247
print(f"\nMode: {mode.upper()}")
234248
if mode == "record":
235-
print("Recording trajectory... Press ESC to stop and save.")
249+
print("Recording trajectory...")
236250
elif mode == "playback":
237251
print("Playing back trajectory...")
238252

@@ -248,99 +262,59 @@ def reset_scene():
248262
print("l/;\t- Roll Left/Right (Rotate around X axis)")
249263
print("u\t- Reset Scene")
250264
print("space\t- Press to close gripper, release to open gripper")
251-
print("esc\t- Quit")
265+
if mode in ["interactive", "record"]:
266+
print("\nPlus all default viewer controls (press 'i' to see them)")
252267

253268
# reset scene before starting teleoperation
254269
reset_scene()
255270

256271
# start teleoperation or playback
257-
stop = False
258272
step_count = 0
259273

260-
while not stop:
261-
if mode == "playback":
262-
# Playback mode: replay recorded trajectory
263-
if step_count < len(trajectory):
264-
step_data = trajectory[step_count]
265-
target_pos = step_data["target_pos"]
266-
target_R = R.from_quat(step_data["target_quat"])
267-
is_close_gripper = step_data["gripper_closed"]
268-
step_count += 1
269-
print(f"\rPlayback step: {step_count}/{len(trajectory)}", end="")
270-
# Check if user wants to stop playback
271-
pressed_keys = clients["keyboard"].pressed_keys.copy()
272-
stop = keyboard.Key.esc in pressed_keys
274+
try:
275+
while is_running:
276+
if mode == "playback":
277+
# Playback mode: replay recorded trajectory
278+
if step_count < len(trajectory):
279+
step_data = trajectory[step_count]
280+
target_pos[:] = step_data["target_pos"]
281+
target_quat[:] = step_data["target_quat"]
282+
gripper_closed[0] = step_data["gripper_closed"]
283+
step_count += 1
284+
print(f"\rPlayback step: {step_count}/{len(trajectory)}", end="")
285+
else:
286+
print("\nPlayback finished!")
287+
break
273288
else:
274-
print("\nPlayback finished!")
275-
break
276-
else:
277-
# Interactive or recording mode
278-
pressed_keys = clients["keyboard"].pressed_keys.copy()
279-
280-
# reset scene:
281-
reset_flag = False
282-
reset_flag |= keyboard.KeyCode.from_char("u") in pressed_keys
283-
if reset_flag:
284-
reset_scene()
285-
286-
# stop teleoperation
287-
stop = keyboard.Key.esc in pressed_keys
288-
289-
# get ee target pose
290-
is_close_gripper = False
291-
dpos = 0.002
292-
drot = 0.01
293-
for key in pressed_keys:
294-
if key == keyboard.Key.up:
295-
target_pos[0] -= dpos
296-
elif key == keyboard.Key.down:
297-
target_pos[0] += dpos
298-
elif key == keyboard.Key.right:
299-
target_pos[1] += dpos
300-
elif key == keyboard.Key.left:
301-
target_pos[1] -= dpos
302-
elif key == keyboard.KeyCode.from_char("n"):
303-
target_pos[2] += dpos
304-
elif key == keyboard.KeyCode.from_char("m"):
305-
target_pos[2] -= dpos
306-
elif key == keyboard.KeyCode.from_char("j"):
307-
target_R = R.from_euler("z", drot) * target_R
308-
elif key == keyboard.KeyCode.from_char("k"):
309-
target_R = R.from_euler("z", -drot) * target_R
310-
elif key == keyboard.KeyCode.from_char("i"):
311-
target_R = R.from_euler("y", drot) * target_R
312-
elif key == keyboard.KeyCode.from_char("o"):
313-
target_R = R.from_euler("y", -drot) * target_R
314-
elif key == keyboard.KeyCode.from_char("l"):
315-
target_R = R.from_euler("x", drot) * target_R
316-
elif key == keyboard.KeyCode.from_char(";"):
317-
target_R = R.from_euler("x", -drot) * target_R
318-
elif key == keyboard.Key.space:
319-
is_close_gripper = True
320-
321-
# Record current state if recording
322-
if recording:
323-
step_data = {
324-
"target_pos": target_pos.copy(),
325-
"target_quat": target_R.as_quat(), # x,y,z,w format
326-
"gripper_closed": is_close_gripper,
327-
"step": step_count,
328-
}
329-
trajectory.append(step_data)
289+
# Interactive or recording mode
290+
# Movement is handled by keybinding callbacks
291+
# Record current state if recording
292+
if recording:
293+
step_data = {
294+
"target_pos": target_pos.copy(),
295+
"target_quat": target_quat.copy(),
296+
"gripper_closed": gripper_closed[0],
297+
"step": step_count,
298+
}
299+
trajectory.append(step_data)
300+
301+
# control arm
302+
target_entity.set_qpos(np.concatenate([target_pos, target_quat]))
303+
q, err = robot.inverse_kinematics(link=ee_link, pos=target_pos, quat=target_quat, return_error=True)
304+
robot.control_dofs_position(q[:-2], motors_dof)
305+
# control gripper
306+
if gripper_closed[0]:
307+
robot.control_dofs_force(np.array([-1.0, -1.0]), fingers_dof)
308+
else:
309+
robot.control_dofs_force(np.array([1.0, 1.0]), fingers_dof)
330310

331-
# control arm
332-
target_quat = target_R.as_quat(scalar_first=True)
333-
target_entity.set_qpos(np.concatenate([target_pos, target_quat]))
334-
q, err = robot.inverse_kinematics(link=ee_link, pos=target_pos, quat=target_quat, return_error=True)
335-
robot.control_dofs_position(q[:-2], motors_dof)
336-
# control gripper
337-
if is_close_gripper:
338-
robot.control_dofs_force(np.array([-1.0, -1.0]), fingers_dof)
339-
else:
340-
robot.control_dofs_force(np.array([1.0, 1.0]), fingers_dof)
311+
scene.step()
312+
step_count += 1
341313

342-
scene.step()
343-
step_count += 1
314+
if "PYTEST_VERSION" in os.environ:
315+
break
316+
except KeyboardInterrupt:
317+
gs.logger.info("Simulation interrupted, exiting.")
344318

345319
# Save trajectory if recording
346320
if recording and len(trajectory) > 0:
@@ -436,12 +410,14 @@ def main():
436410
elif not os.path.isabs(trajectory_file):
437411
trajectory_file = os.path.join(traj_dir, os.path.basename(trajectory_file))
438412

439-
clients = dict()
440-
clients["keyboard"] = KeyboardDevice()
441-
clients["keyboard"].start()
442-
443413
scene, entities = build_scene(use_ipc=args.ipc, show_viewer=args.vis, enable_ipc_gui=False)
444-
run_sim(scene, entities, clients, mode=args.mode, trajectory_file=trajectory_file)
414+
run_sim(
415+
scene,
416+
entities,
417+
add_keybinds=args.vis or args.mode in ["interactive", "record"],
418+
mode=args.mode,
419+
trajectory_file=trajectory_file,
420+
)
445421

446422

447423
if __name__ == "__main__":

0 commit comments

Comments
 (0)