Skip to content

Commit d42940d

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/contact-force-filter-link-idx
2 parents fac69a7 + fe76e6b commit d42940d

33 files changed

Lines changed: 2280 additions & 240 deletions

.github/workflows/examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
run: |
5151
python -m pip install --upgrade pip setuptools wheel
5252
pip install torch --index-url https://download.pytorch.org/whl/cpu
53-
pip install '.[dev,usd]'
53+
pip install '.[dev,render,usd]'
5454
5555
- name: Get Quadrants version
5656
id: quadrants_version

.github/workflows/generic.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135
- name: Install Genesis
136136
shell: bash
137137
run: |
138-
PYTHON_DEPS="dev"
138+
PYTHON_DEPS="dev,render"
139139
# Install USD for all platforms except ARM (usd-core doesn't support ARM)
140140
# This is required for test_mesh.py which tests USD parsing functionality
141141
if [[ "${{ matrix.OS }}" != 'ubuntu-24.04-arm' ]] ; then

.github/workflows/scripts/production_build.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ uv pip install torch --index-url https://download.pytorch.org/whl/cu129
1414
uv pip install --upgrade pip setuptools wheel
1515
uv pip install omniverse-kit --index-url https://pypi.nvidia.com/
1616
uv pip install ".[dev,render,usd]" "pyuipc==0.0.7"
17+
# imgui-bundle has no pre-built wheel for Python 3.10 (which this runner uses) so it is excluded from the
18+
# ``[render]`` extras marker. Install it manually. Disable MicroTeX since we don't use it and its submodule
19+
# is missing from the 1.92.x source distribution.
20+
SKBUILD_CMAKE_DEFINE="IMGUI_BUNDLE_WITH_MICROTEX=OFF" uv pip install imgui-bundle
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Interactive joint control example using ImGui overlay.
2+
3+
Demonstrates:
4+
- Simulation controls (play/pause/step/reset)
5+
- Entity browser with joint sliders
6+
- Visualization toggles
7+
- Camera controls
8+
- Custom user panels via register_panel()
9+
- Scene rebuild (add entities, change scale)
10+
"""
11+
12+
import os
13+
import time
14+
15+
import genesis as gs
16+
from genesis.engine.interactive_scene import InteractiveScene
17+
from genesis.ext.pyrender.overlay import ImGuiOverlayPlugin
18+
19+
gs.init()
20+
21+
interactive = InteractiveScene()
22+
interactive.rebuild(
23+
scene_kwargs=dict(
24+
viewer_options=gs.options.ViewerOptions(
25+
camera_pos=(2.0, 2.0, 1.5),
26+
camera_lookat=(0.0, 0.0, 0.5),
27+
),
28+
show_viewer=True,
29+
),
30+
entities_kwargs={
31+
"Plane": dict(
32+
morph=gs.morphs.Plane(),
33+
),
34+
"Panda": dict(
35+
morph=gs.morphs.MJCF(
36+
file="xml/franka_emika_panda/panda.xml",
37+
),
38+
),
39+
"Box": dict(
40+
morph=gs.morphs.Box(
41+
pos=(0, 0, 1.0),
42+
size=(0.2, 0.2, 0.2),
43+
),
44+
),
45+
},
46+
)
47+
48+
plugin = ImGuiOverlayPlugin()
49+
interactive.viewer.add_plugin(plugin)
50+
51+
52+
def custom_panel(imgui):
53+
imgui.text("Custom Demo Panel")
54+
imgui.text("This panel was registered via register_panel()")
55+
56+
57+
plugin.register_panel(custom_panel)
58+
59+
is_test = "PYTEST_VERSION" in os.environ
60+
horizon = 5 if is_test else None
61+
62+
frame = 0
63+
while interactive.viewer.is_alive():
64+
if plugin.rebuild_requested:
65+
interactive.rebuild(entities_kwargs=plugin.pending_entities_kwargs)
66+
if plugin.should_step():
67+
interactive.scene.step()
68+
frame += 1
69+
if horizon is not None and frame >= horizon:
70+
break
71+
time.sleep(0.01)

examples/viewer_plugin/mouse_interaction.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
parser.add_argument(
1111
"--use_force", "-f", action="store_true", help="Apply spring forces instead of setting position"
1212
)
13+
parser.add_argument("--num_envs", "-b", type=int, default=1, help="Number of environments to create")
1314
args = parser.parse_args()
1415

15-
gs.init(backend=gs.gpu)
16+
gs.init(backend=gs.cpu)
1617

1718
scene = gs.Scene(
1819
viewer_options=gs.options.ViewerOptions(
@@ -51,7 +52,7 @@
5152
)
5253
)
5354

54-
scene.build()
55+
scene.build(n_envs=args.num_envs)
5556

5657
is_running = True
5758

genesis/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,21 @@ def init(
255255
random_seed=seed,
256256
)
257257

258+
# On Metal, share PyTorch MPS's command queue with Quadrants so that GPU-side ordering is automatic and the
259+
# per-interop-point sync overhead (qd.sync / torch.mps.synchronize) is eliminated.
260+
if backend == _gs_backend.metal and device.type == "mps":
261+
mps_queue = qd.interop.get_mps_command_queue()
262+
if not mps_queue:
263+
raise_exception(
264+
"Failed to extract PyTorch MPS's Metal command queue. This is required on Apple Metal for correct "
265+
"GPU synchronisation between Genesis and PyTorch. Please ensure you are using a supported PyTorch "
266+
"version (>= 2.0)."
267+
)
268+
qd_init_kwargs.update(
269+
external_metal_command_queue=mps_queue,
270+
external_metal_command_queue_is_torch_queue=True,
271+
)
272+
258273
# init quadrants
259274
qd_debug = debug and (os.environ.get("QD_DEBUG") != "0")
260275
with redirect_stdout(_qd_outputs):

genesis/_main.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,57 @@ def view(filename, collision, rotate, scale=1.0, show_link_frame=False):
338338
gui_process.join()
339339

340340

341+
def play(filename=None, collision=False, scale=1.0):
342+
import time
343+
344+
gs.init()
345+
346+
scene = gs.Scene(
347+
viewer_options=gs.options.ViewerOptions(
348+
camera_pos=(2.0, 2.0, 1.5),
349+
camera_lookat=(0.0, 0.0, 0.5),
350+
),
351+
show_viewer=True,
352+
)
353+
354+
if filename is None:
355+
scene.add_entity(gs.morphs.Plane())
356+
scene.add_entity(gs.morphs.MJCF(file="xml/franka_emika_panda/panda.xml"))
357+
else:
358+
filename_lower = filename.lower()
359+
morphs = gs.options.morphs
360+
surface = gs.surfaces.Default(vis_mode="visual" if not collision else "collision")
361+
362+
if filename_lower.endswith(morphs.USD_FORMATS):
363+
scene.add_stage(
364+
morph=gs.morphs.USD(file=filename, collision=collision, scale=scale),
365+
vis_mode=surface.vis_mode,
366+
)
367+
elif filename_lower.endswith(morphs.URDF_FORMAT):
368+
scene.add_entity(gs.morphs.URDF(file=filename, collision=collision, scale=scale), surface=surface)
369+
elif filename_lower.endswith(morphs.MJCF_FORMAT):
370+
scene.add_entity(gs.morphs.MJCF(file=filename, collision=collision, scale=scale), surface=surface)
371+
elif filename_lower.endswith(morphs.MESH_FORMATS):
372+
scene.add_entity(gs.morphs.Mesh(file=filename, collision=collision, scale=scale), surface=surface)
373+
else:
374+
gs.raise_exception(
375+
f"Unsupported file format for 'gs play'. Expected {morphs.URDF_FORMAT}, "
376+
f"{morphs.MJCF_FORMAT}, {morphs.MESH_FORMATS}, or {morphs.USD_FORMATS}."
377+
)
378+
379+
scene.build()
380+
381+
from genesis.ext.pyrender.overlay import ImGuiOverlayPlugin
382+
383+
plugin = ImGuiOverlayPlugin()
384+
scene.viewer.add_plugin(plugin)
385+
386+
while scene.viewer.is_alive():
387+
if plugin.should_step():
388+
scene.step()
389+
time.sleep(0.01)
390+
391+
341392
def animate(filename_pattern, fps):
342393
import glob
343394

@@ -365,6 +416,19 @@ def main():
365416
parser_view.add_argument("-s", "--scale", type=float, default=1.0, help="Scale of the entity")
366417
parser_view.add_argument("-l", "--link_frame", action="store_true", default=False, help="Show link frame")
367418

419+
parser_play = subparsers.add_parser("play", help="Interactive viewer with ImGui joint controls and simulation")
420+
parser_play.add_argument(
421+
"filename",
422+
type=str,
423+
nargs="?",
424+
default=None,
425+
help="Optional asset file (URDF/MJCF/Mesh/USD). Defaults to a demo scene.",
426+
)
427+
parser_play.add_argument(
428+
"-c", "--collision", action="store_true", default=False, help="Visualize collision geometry"
429+
)
430+
parser_play.add_argument("-s", "--scale", type=float, default=1.0, help="Scale of the entity")
431+
368432
parser_animate = subparsers.add_parser("animate", help="Compile a list of image files into a video")
369433
parser_animate.add_argument("filename_pattern", type=str, help="Image files, via glob pattern")
370434
parser_animate.add_argument("--fps", type=int, default=30, help="FPS of the output video")
@@ -373,6 +437,8 @@ def main():
373437

374438
if args.command == "view":
375439
view(args.filename, args.collision, args.rotate, args.scale, args.link_frame)
440+
elif args.command == "play":
441+
play(args.filename, args.collision, args.scale)
376442
elif args.command == "animate":
377443
animate(args.filename_pattern, args.fps)
378444
elif args.command is None:

0 commit comments

Comments
 (0)