Skip to content

Commit a0d15f6

Browse files
[Newton] Switched to using a customized Newton visualizer (#3355)
1 parent 256bcd6 commit a0d15f6

File tree

5 files changed

+261
-14
lines changed

5 files changed

+261
-14
lines changed

source/isaaclab/isaaclab/sim/_impl/newton_manager.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
from newton.sensors import ContactSensor as NewtonContactSensor
1414
from newton.sensors import populate_contacts
1515
from newton.solvers import SolverBase, SolverFeatherstone, SolverMuJoCo, SolverXPBD
16-
from newton.viewer import RendererOpenGL
1716

1817
from isaaclab.sim._impl.newton_manager_cfg import NewtonCfg
18+
from isaaclab.sim._impl.newton_viewer import NewtonViewerGL
1919
from isaaclab.utils.timer import Timer
2020

2121

@@ -75,6 +75,8 @@ class NewtonManager:
7575
_gravity_vector: tuple[float, float, float] = (0.0, 0.0, -9.81)
7676
_up_axis: str = "Z"
7777
_num_envs: int = None
78+
_visualizer_update_counter: int = 0
79+
_visualizer_update_frequency: int = 1 # Configurable frequency for all rendering updates
7880

7981
@classmethod
8082
def clear(cls):
@@ -98,6 +100,8 @@ def clear(cls):
98100
NewtonManager._cfg = NewtonCfg()
99101
NewtonManager._up_axis = "Z"
100102
NewtonManager._first_call = True
103+
NewtonManager._visualizer_update_counter = 0
104+
NewtonManager._visualizer_update_frequency = NewtonManager._cfg.newton_viewer_update_frequency
101105

102106
@classmethod
103107
def set_builder(cls, builder):
@@ -300,20 +304,33 @@ def render(cls) -> None:
300304
301305
This function renders the simulation using the OpenGL renderer.
302306
"""
307+
303308
if NewtonManager._renderer is None:
304-
NewtonManager._renderer = RendererOpenGL(
305-
path="example.usd",
306-
model=NewtonManager._model,
307-
scaling=1.0,
308-
up_axis=NewtonManager._up_axis,
309-
screen_width=1280,
310-
screen_height=720,
311-
camera_pos=(0, 3, 10),
312-
)
309+
NewtonManager._renderer = NewtonViewerGL(width=1280, height=720)
310+
NewtonManager._renderer.set_model(NewtonManager._model)
311+
NewtonManager._renderer.camera.pos = wp.vec3(*NewtonManager._cfg.newton_viewer_camera_pos)
312+
NewtonManager._renderer.up_axis = NewtonManager._up_axis
313+
NewtonManager._renderer.scaling = 1.0
314+
NewtonManager._renderer._paused = False
313315
else:
314-
NewtonManager._renderer.begin_frame(NewtonManager._sim_time)
315-
NewtonManager._renderer.render(NewtonManager._state_0)
316-
NewtonManager._renderer.end_frame()
316+
# Keep updating the renderer until the training is resumed
317+
while NewtonManager._renderer.is_training_paused():
318+
NewtonManager._renderer.begin_frame(NewtonManager._sim_time)
319+
NewtonManager._renderer.log_state(NewtonManager._state_0)
320+
NewtonManager._renderer.end_frame()
321+
322+
# Use configurable frequency for both paused and unpaused rendering
323+
NewtonManager._visualizer_update_counter += 1
324+
if NewtonManager._visualizer_update_counter >= NewtonManager._visualizer_update_frequency:
325+
if not NewtonManager._renderer.is_paused():
326+
# Render the frame normally when not paused
327+
NewtonManager._renderer.begin_frame(NewtonManager._sim_time)
328+
NewtonManager._renderer.log_state(NewtonManager._state_0)
329+
NewtonManager._renderer.end_frame()
330+
else:
331+
# Just update the renderer when paused (no actual rendering)
332+
NewtonManager._renderer._update()
333+
NewtonManager._visualizer_update_counter = 0
317334

318335
@classmethod
319336
def sync_fabric_transforms(cls) -> None:

source/isaaclab/isaaclab/sim/_impl/newton_manager_cfg.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,10 @@ class NewtonCfg:
2727
If set to False, the simulation performance will be severely degraded.
2828
"""
2929

30+
newton_viwer_update_frequency: int = 1
31+
"""Frequency of updates to the Newton viewer."""
32+
33+
newton_viewer_camera_pos: tuple[float, float, float] = (10.0, 0.0, 3.0)
34+
"""Position of the camera in the Newton viewer."""
35+
3036
solver_cfg: NewtonSolverCfg = MJWarpSolverCfg()
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""
7+
Training-aware viewer that adds a separate pause for simulation/training while keeping rendering at full rate.
8+
9+
This class subclasses Newton's ViewerGL and introduces a second pause mode:
10+
- Rendering pause: identical to the base viewer's pause (space key / Pause checkbox)
11+
- Training pause: stops simulation/training steps but keeps rendering running
12+
13+
The training pause can be toggled from the UI via a button and optionally via the 'T' key.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import newton as nt
19+
from newton.viewer import ViewerGL
20+
21+
22+
class NewtonViewerGL(ViewerGL):
23+
def __init__(self, *args, **kwargs):
24+
super().__init__(*args, **kwargs)
25+
self._paused_training: bool = False
26+
self._paused_rendering: bool = False
27+
self._fallback_draw_controls: bool = False
28+
29+
try:
30+
self.register_ui_callback(self._render_training_controls, position="side")
31+
except AttributeError:
32+
self._fallback_draw_controls = True
33+
34+
def is_training_paused(self) -> bool:
35+
return self._paused_training
36+
37+
def is_paused(self) -> bool:
38+
return self._paused_rendering
39+
40+
# UI callback rendered inside the "Example Options" panel of the left sidebar
41+
def _render_training_controls(self, imgui):
42+
imgui.separator()
43+
imgui.text("IsaacLab Training Controls")
44+
45+
# Toggle button for training pause
46+
training_label = "Resume Training" if self._paused_training else "Pause Training"
47+
if imgui.button(training_label):
48+
self._paused_training = not self._paused_training
49+
50+
# Toggle button for rendering pause
51+
rendering_label = "Resume Rendering" if self._paused_rendering else "Pause Rendering"
52+
if imgui.button(rendering_label):
53+
self._paused_rendering = not self._paused_rendering
54+
55+
# Import NewtonManager locally to avoid circular imports
56+
from .newton_manager import NewtonManager # noqa: PLC0415
57+
58+
imgui.text("Visualizer Update Frequency")
59+
60+
current_frequency = NewtonManager._visualizer_update_frequency
61+
changed, new_frequency = imgui.slider_int(
62+
"##VisualizerUpdateFreq", current_frequency, 1, 20, f"Every {current_frequency} frames"
63+
)
64+
if changed:
65+
NewtonManager._visualizer_update_frequency = new_frequency
66+
67+
if imgui.is_item_hovered():
68+
imgui.set_tooltip(
69+
"Controls visualizer update frequency\nlower values-> more responsive visualizer but slower"
70+
" training\nhigher values-> less responsive visualizer but faster training"
71+
)
72+
73+
# Override only SPACE key to use rendering pause, preserve all other shortcuts
74+
def on_key_press(self, symbol, modifiers):
75+
if self.ui.is_capturing():
76+
return
77+
78+
try:
79+
import pyglet # noqa: PLC0415
80+
except Exception:
81+
return
82+
83+
if symbol == pyglet.window.key.SPACE:
84+
# Override SPACE to pause rendering instead of base pause
85+
self._paused_rendering = not self._paused_rendering
86+
return
87+
88+
# For all other keys, call base implementation to preserve functionality
89+
super().on_key_press(symbol, modifiers)
90+
91+
def _render_ui(self):
92+
if not self._fallback_draw_controls:
93+
return super()._render_ui()
94+
95+
# Render base UI first
96+
super()._render_ui()
97+
98+
# Then render a small floating window with training controls
99+
imgui = self.ui.imgui
100+
# Place near left panel but offset
101+
from contextlib import suppress
102+
103+
with suppress(Exception):
104+
imgui.set_next_window_pos(imgui.ImVec2(320, 10))
105+
106+
flags = 0
107+
if imgui.begin("Training Controls", flags=flags):
108+
self._render_training_controls(imgui)
109+
imgui.end()
110+
return None
111+
112+
def _render_left_panel(self):
113+
"""Override the left panel to remove the base pause checkbox."""
114+
imgui = self.ui.imgui
115+
116+
# Use theme colors directly
117+
nav_highlight_color = self.ui.get_theme_color(imgui.Col_.nav_cursor, (1.0, 1.0, 1.0, 1.0))
118+
119+
# Position the window on the left side
120+
io = self.ui.io
121+
imgui.set_next_window_pos(imgui.ImVec2(10, 10))
122+
imgui.set_next_window_size(imgui.ImVec2(300, io.display_size[1] - 20))
123+
124+
# Main control panel window - use safe flag values
125+
flags = imgui.WindowFlags_.no_resize.value
126+
127+
if imgui.begin(f"Newton Viewer v{nt.__version__}", flags=flags):
128+
imgui.separator()
129+
130+
header_flags = 0
131+
132+
imgui.set_next_item_open(True, imgui.Cond_.appearing)
133+
if imgui.collapsing_header("IsaacLab Options"):
134+
# Render UI callbacks for side panel
135+
for callback in self._ui_callbacks["side"]:
136+
callback(self.ui.imgui)
137+
138+
# Model Information section
139+
if self.model is not None:
140+
imgui.set_next_item_open(True, imgui.Cond_.appearing)
141+
if imgui.collapsing_header("Model Information", flags=header_flags):
142+
imgui.separator()
143+
imgui.text(f"Environments: {self.model.num_envs}")
144+
axis_names = ["X", "Y", "Z"]
145+
imgui.text(f"Up Axis: {axis_names[self.model.up_axis]}")
146+
gravity = self.model.gravity
147+
gravity_text = f"Gravity: ({gravity[0]:.2f}, {gravity[1]:.2f}, {gravity[2]:.2f})"
148+
imgui.text(gravity_text)
149+
150+
# Visualization Controls section
151+
imgui.set_next_item_open(True, imgui.Cond_.appearing)
152+
if imgui.collapsing_header("Visualization", flags=header_flags):
153+
imgui.separator()
154+
155+
# Joint visualization
156+
show_joints = self.show_joints
157+
changed, self.show_joints = imgui.checkbox("Show Joints", show_joints)
158+
159+
# Contact visualization
160+
show_contacts = self.show_contacts
161+
changed, self.show_contacts = imgui.checkbox("Show Contacts", show_contacts)
162+
163+
# Spring visualization
164+
show_springs = self.show_springs
165+
changed, self.show_springs = imgui.checkbox("Show Springs", show_springs)
166+
167+
# Center of mass visualization
168+
show_com = self.show_com
169+
changed, self.show_com = imgui.checkbox("Show Center of Mass", show_com)
170+
171+
# Rendering Options section
172+
imgui.set_next_item_open(True, imgui.Cond_.appearing)
173+
if imgui.collapsing_header("Rendering Options"):
174+
imgui.separator()
175+
176+
# Sky rendering
177+
changed, self.renderer.draw_sky = imgui.checkbox("Sky", self.renderer.draw_sky)
178+
179+
# Shadow rendering
180+
changed, self.renderer.draw_shadows = imgui.checkbox("Shadows", self.renderer.draw_shadows)
181+
182+
# Wireframe mode
183+
changed, self.renderer.draw_wireframe = imgui.checkbox("Wireframe", self.renderer.draw_wireframe)
184+
185+
# Light color
186+
changed, self.renderer._light_color = imgui.color_edit3("Light Color", self.renderer._light_color)
187+
# Sky color
188+
changed, self.renderer.sky_upper = imgui.color_edit3("Sky Color", self.renderer.sky_upper)
189+
# Ground color
190+
changed, self.renderer.sky_lower = imgui.color_edit3("Ground Color", self.renderer.sky_lower)
191+
192+
# Camera Information section
193+
imgui.set_next_item_open(True, imgui.Cond_.appearing)
194+
if imgui.collapsing_header("Camera"):
195+
imgui.separator()
196+
197+
pos = self.camera.pos
198+
pos_text = f"Position: ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f})"
199+
imgui.text(pos_text)
200+
imgui.text(f"FOV: {self.camera.fov:.1f}°")
201+
imgui.text(f"Yaw: {self.camera.yaw:.1f}°")
202+
imgui.text(f"Pitch: {self.camera.pitch:.1f}°")
203+
204+
# Camera controls hint - update to reflect new controls
205+
imgui.separator()
206+
imgui.push_style_color(imgui.Col_.text, imgui.ImVec4(*nav_highlight_color))
207+
imgui.text("Controls:")
208+
imgui.pop_style_color()
209+
imgui.text("WASD - Move camera")
210+
imgui.text("Left Click - Look around")
211+
imgui.text("Right Click - Pick objects")
212+
imgui.text("Scroll - Zoom")
213+
imgui.text("Space - Pause/Resume Rendering")
214+
imgui.text("H - Toggle UI")
215+
imgui.text("ESC/Q - Exit")
216+
217+
# NOTE: Removed selection API section for now. In the future, we can add single env control through this section.
218+
# Selection API section
219+
# self._render_selection_panel()
220+
221+
imgui.end()
222+
return

source/isaaclab/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"mujoco==3.3.6.dev798190254",
5252
"mujoco-warp @ git+https://github.com/google-deepmind/mujoco_warp.git@main",
5353
"newton-physics @ git+https://github.com/newton-physics/newton.git@main",
54+
"imgui-bundle==1.92.0",
5455
]
5556

5657

source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/reach_env_cfg.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from isaaclab.managers import TerminationTermCfg as DoneTerm
1919
from isaaclab.scene import InteractiveSceneCfg
2020
from isaaclab.utils import configclass
21-
#from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
21+
22+
# from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
2223
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
2324

2425
import isaaclab_tasks.manager_based.manipulation.reach.mdp as mdp

0 commit comments

Comments
 (0)