Zhicheng

Robotics, Learning, and Control

0%

import omni.ui as ui 使用指南:在 Isaac Sim 中构建调试界面

1. 为什么要用 omni.ui

在 Isaac Sim 中做机器人、传感器或控制算法开发时,除了看 3D 视口,我们通常还需要一些额外的界面来辅助调试,比如:

  • 显示当前状态值
  • 动态观察一段时间内的数值变化
  • 提供按钮、开关、输入框来控制实验
  • 用结构化布局把调试信息整理到一个窗口里

这类界面最常用的接口之一就是:

1
import omni.ui as ui

omni.ui 是 Omniverse / Isaac Sim 提供的原生 UI 工具库。它可以用来快速创建窗口、布局、文本、按钮、图表等控件,适合做轻量级工具面板、调试面板和交互界面。

下图是我在 Isaac Sim 仿真时使用这个接口做出来的效果:

Isaac Sim 中基于 omni.ui 的调试界面示例


2. omni.ui 能做什么

它的功能可以简单理解为两大类。

2.1 布局和显示

它可以创建各种基本界面元素,例如:

  • ui.Window:窗口
  • ui.Label:文本标签
  • ui.Button:按钮
  • ui.StringField:文本输入框
  • ui.IntField / ui.FloatField:数值输入
  • ui.CheckBox:复选框
  • ui.ComboBox:下拉框
  • ui.Plot:曲线图

2.2 容器和排版

它也提供了常用布局容器:

  • ui.VStack:垂直排列
  • ui.HStack:水平排列
  • ui.ZStack:叠加排列
  • ui.ScrollingFrame:带滚动条的区域
  • ui.CollapsableFrame:可折叠分组

所以我们通常会把 omni.ui 用在这些场景里:

  • 实时状态监控窗口
  • 仿真参数调节面板
  • 机器人调试面板
  • 传感器数据显示面板
  • 小型交互工具

3. 最基本的使用方式

3.1 创建一个窗口

最简单的例子如下:

1
2
3
4
5
6
import omni.ui as ui

window = ui.Window("My Window", width=400, height=300)

with window.frame:
ui.Label("Hello Isaac Sim")

这里有两个关键点:

  • ui.Window(...) 用来创建窗口
  • with window.frame: 表示后面的控件都放进这个窗口里

运行后,会出现一个标题为 My Window 的窗口,里面只有一行文本。


4. with 结构为什么这么常见

omni.ui 的代码风格很像声明式布局,常见写法是:

1
2
3
with some_container:
ui.Label(...)
ui.Button(...)

这里的意思不是“执行某个逻辑块”,而是“在这个容器里添加控件”。

比如:

1
2
3
4
5
window = ui.Window("Demo", width=400, height=200)
with window.frame:
with ui.VStack():
ui.Label("Line 1")
ui.Label("Line 2")

表示:

  • 在窗口里放一个垂直布局
  • 在这个垂直布局里放两行标签

5. 常见布局控件

布局是 omni.ui 里最重要的部分之一,因为大部分界面都是靠布局容器拼出来的。

5.1 ui.VStack

垂直堆叠布局:

1
2
3
4
with ui.VStack(spacing=6):
ui.Label("A")
ui.Label("B")
ui.Label("C")

效果就是 A、B、C 从上到下排列。

常见参数:

  • spacing:控件之间的间距
  • height / width:大小控制

5.2 ui.HStack

水平排列布局:

1
2
3
with ui.HStack(spacing=8):
ui.Label("Name")
ui.StringField()

效果是标签和输入框左右排开。

5.3 ui.ZStack

叠层布局,适合把多个元素画在同一个区域里,比如背景和前景叠加。

5.4 ui.ScrollingFrame

如果内容很多,可以放到滚动区域里:

1
2
3
4
with ui.ScrollingFrame():
with ui.VStack():
for i in range(50):
ui.Label(f"Row {i}")

这样内容超出窗口时会出现滚动条。


6. 最常用的基础控件

6.1 ui.Label

显示文本:

1
ui.Label("Current status: running")

也可以指定宽高:

1
ui.Label("speed", width=120, height=20)

6.2 ui.Button

按钮通常带一个回调函数:

1
2
3
4
def on_click():
print("button clicked")

ui.Button("Reset", clicked_fn=on_click)

当按钮被点击时,会执行 on_click()

6.3 ui.CheckBox

适合布尔开关:

1
2
checkbox_model = ui.SimpleBoolModel(False)
ui.CheckBox(model=checkbox_model)

一般会配合 model 一起用,因为 UI 控件的状态通常是通过 model 读写的。

6.4 输入框

omni.ui 中很多输入控件都依赖 model。

例如字符串输入:

1
2
string_model = ui.SimpleStringModel("hello")
ui.StringField(model=string_model)

浮点输入:

1
2
float_model = ui.SimpleFloatModel(0.5)
ui.FloatField(model=float_model)

后续可以通过:

1
value = float_model.get_value_as_float()

读取当前值。


7. model 是什么

这是 omni.ui 初学者最容易卡住的地方。

很多控件并不是直接把值存进控件本身,而是通过一个“数据模型”来管理状态。常见 model 有:

  • ui.SimpleStringModel
  • ui.SimpleIntModel
  • ui.SimpleFloatModel
  • ui.SimpleBoolModel

例如:

1
2
model = ui.SimpleFloatModel(1.23)
field = ui.FloatField(model=model)

这时:

  • field 是显示控件
  • model 才是真正存数值的地方

读取值:

1
current = model.get_value_as_float()

设置值:

1
model.set_value(2.5)

这种设计的好处是:

  • 逻辑和显示分离
  • 多个控件可以共享同一个状态
  • 更适合响应式更新

8. 如何做一个简单的控制面板

下面是一个典型的参数面板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import omni.ui as ui


class ParamPanel:
def __init__(self):
self.speed_model = ui.SimpleFloatModel(0.2)
self.enable_model = ui.SimpleBoolModel(True)

self.window = ui.Window("Control Panel", width=420, height=220)
with self.window.frame:
with ui.VStack(spacing=8):
ui.Label("Simulation Parameters", height=24)

with ui.HStack(height=24):
ui.Label("Speed", width=120)
ui.FloatField(model=self.speed_model)

with ui.HStack(height=24):
ui.Label("Enable", width=120)
ui.CheckBox(model=self.enable_model)

with ui.HStack(height=30):
ui.Button("Apply", clicked_fn=self.on_apply)
ui.Button("Reset", clicked_fn=self.on_reset)

def on_apply(self):
speed = self.speed_model.get_value_as_float()
enabled = self.enable_model.get_value_as_bool()
print("apply:", speed, enabled)

def on_reset(self):
self.speed_model.set_value(0.2)
self.enable_model.set_value(True)

这个例子展示了几个关键模式:

  • 用 model 保存控件状态
  • HStack 做“标签 + 输入框”的一行
  • Button 触发回调
  • 在回调里读取 UI 当前值

9. 如何做一个实时数据显示面板

除了输入控制,omni.ui 也非常适合做状态监视器。

9.1 最简单的数值显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import omni.ui as ui


class StatusPanel:
def __init__(self):
self.window = ui.Window("Status", width=300, height=120)
with self.window.frame:
with ui.VStack(spacing=6):
self.pos_label = ui.Label("position: 0.0")
self.vel_label = ui.Label("velocity: 0.0")

def update(self, pos, vel):
self.pos_label.text = f"position: {pos:.4f}"
self.vel_label.text = f"velocity: {vel:.4f}"

只要在仿真循环里调用:

1
panel.update(pos, vel)

窗口里的文本就会刷新。


10. 如何使用 ui.Plot 画实时曲线

这是调试里非常常用的一种能力。

10.1 最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import omni.ui as ui


class LivePlot:
def __init__(self):
self.data = [0.0] * 100
self.window = ui.Window("Live Plot", width=420, height=180)
with self.window.frame:
with ui.VStack():
self.value_label = ui.Label("0.0000")
self.plot = ui.Plot(
ui.Type.LINE,
-1.0,
1.0,
*self.data,
width=ui.Fraction(1),
height=120,
)

def update(self, value):
self.data.append(float(value))
if len(self.data) > 100:
del self.data[:-100]

vmax = max(abs(min(self.data)), abs(max(self.data)), 1e-3)
self.plot.scale_min = -vmax
self.plot.scale_max = vmax
self.plot.set_data(*self.data)
self.value_label.text = f"{value:.4f}"

10.2 ui.Plot 的核心参数

1
2
3
4
5
6
7
8
ui.Plot(
ui.Type.LINE,
scale_min,
scale_max,
*data,
width=ui.Fraction(1),
height=120,
)

含义如下:

  • ui.Type.LINE:折线图
  • scale_min / scale_max:y 轴上下界
  • *data:数据点序列
  • width / height:控件尺寸

10.3 刷新图像

曲线不会自动更新,必须主动调用:

1
self.plot.set_data(*self.data)

这一步是实时绘图最关键的地方。


11. 多条曲线怎么组织

如果需要显示多路数据,最常见的方法是“每条数据一行”。

例如:

1
2
3
4
5
6
7
8
9
with ui.VStack(spacing=6):
ui.Label("joint_rmse")
plot1 = ui.Plot(...)

ui.Label("base_pitch")
plot2 = ui.Plot(...)

ui.Label("contact_force")
plot3 = ui.Plot(...)

也可以进一步包装成一个类,统一管理:

  • 曲线名称
  • 历史数据
  • 当前值标签
  • plot 控件对象

这样后续新增观测量会更方便。


12. 如何做一个带滚动条的监视器

当观测量很多时,可以把内容放到 ScrollingFrame 中:

1
2
3
4
5
6
self.window = ui.Window("Monitor", width=460, height=500)
with self.window.frame:
with ui.ScrollingFrame():
with ui.VStack(spacing=6):
for i in range(20):
ui.Label(f"signal_{i}")

这样即使信号很多,也不会把窗口撑爆。


13. 控件大小和布局比例怎么控制

常用方法有这些。

13.1 固定宽度

1
ui.Label("Name", width=120)

13.2 使用 ui.Fraction

让控件占据剩余空间的一部分:

1
ui.Plot(..., width=ui.Fraction(1))

这个很适合让图表自动铺满整行。

13.3 设置高度

1
ui.Plot(..., height=90)

对于曲线图尤其常见。


14. omni.ui 常见编程模式

在实际项目里,最常见的是下面这几种组织方式。

14.1 方式一:直接写在一个函数里

适合一次性小工具:

1
2
3
4
def build_ui():
window = ui.Window("Demo", width=300, height=200)
with window.frame:
ui.Label("hello")

优点是简单,缺点是后续不好维护。

14.2 方式二:封装成类

适合中等复杂度的工具面板:

1
2
3
4
5
6
7
8
9
class DemoPanel:
def __init__(self):
self.window = ui.Window("Demo")
with self.window.frame:
self.build_ui()

def build_ui(self):
with ui.VStack():
ui.Label("hello")

优点是结构清晰,状态和回调更容易管理。

14.3 方式三:UI 和逻辑分离

如果项目更复杂,可以把:

  • UI 构建
  • 数据模型
  • 仿真逻辑

拆开管理。

这也是更推荐的工程化做法。


15. 一个完整的小例子:实时调试窗口

下面给出一个稍完整的例子,展示窗口、布局、标签、按钮和实时曲线的配合方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import math
import omni.ui as ui


class DebugWindow:
def __init__(self):
self.history_len = 120
self.time_idx = 0
self.signal = [0.0] * self.history_len

self.window = ui.Window("Debug Window", width=460, height=300)
with self.window.frame:
with ui.VStack(spacing=8):
ui.Label("Realtime Debug Panel", height=24)

with ui.HStack(height=22):
ui.Label("signal", width=120)
self.value_label = ui.Label("0.0000")

self.plot = ui.Plot(
ui.Type.LINE,
-1.0,
1.0,
*self.signal,
width=ui.Fraction(1),
height=140,
)

with ui.HStack(height=30):
ui.Button("Clear", clicked_fn=self.on_clear)
ui.Button("Print", clicked_fn=self.on_print)

def update(self):
value = math.sin(self.time_idx * 0.05)
self.time_idx += 1

self.signal.append(value)
if len(self.signal) > self.history_len:
del self.signal[:-self.history_len]

vmax = max(abs(min(self.signal)), abs(max(self.signal)), 1e-3)
self.plot.scale_min = -vmax
self.plot.scale_max = vmax
self.plot.set_data(*self.signal)
self.value_label.text = f"{value:.4f}"

def on_clear(self):
self.signal = [0.0] * self.history_len
self.plot.scale_min = -1.0
self.plot.scale_max = 1.0
self.plot.set_data(*self.signal)
self.value_label.text = "0.0000"

def on_print(self):
print("current signal:", self.signal[-1])

这个例子里包含了:

  • 一个窗口
  • 一条实时曲线
  • 当前值显示
  • 两个按钮
  • 数据清零和打印逻辑

它已经足够作为很多调试工具的起点。


16. 使用 omni.ui 时的常见注意点

16.1 不要忘记保存控件引用

如果后续还要更新某个控件,比如:

  • Label.text
  • Plot.set_data(...)

那就必须把控件保存成成员变量,例如:

1
2
self.value_label = ui.Label("0.0")
self.plot = ui.Plot(...)

否则后面就拿不到它。

16.2 图表不会自动刷新

更新数据后,需要显式调用:

1
self.plot.set_data(*data)

16.3 数值输入通常要配合 model

输入控件不是直接返回值,通常通过 model 管理。

16.4 界面复杂后建议封装

如果窗口里控件很多,不建议全部堆在一个函数里,最好封装成类。

16.5 实时图表要限制历史长度

如果每一帧都追加数据而不裁剪,列表会越来越长,影响性能。

更常见的写法是只保留固定窗口长度:

1
2
if len(data) > history_len:
del data[:-history_len]

17. omni.ui 适合什么,不适合什么

17.1 很适合

  • 调试面板
  • 参数面板
  • 实时状态窗口
  • 小型交互工具
  • 实验辅助界面

17.2 不太适合

  • 特别复杂的大型前端界面
  • 高级交互动画页面
  • 复杂图表系统

omni.ui 的定位更偏轻量、原生、快速集成,而不是一个完整的 Web 前端框架。


18. 学习顺序建议

如果第一次接触 omni.ui,推荐按这个顺序掌握:

  1. ui.Window
  2. ui.VStack / ui.HStack
  3. ui.Label
  4. ui.Button
  5. model 机制
  6. ui.Plot
  7. 封装成类

这样会比较自然。


19. 总结

import omni.ui as ui 是 Isaac Sim 中构建原生调试界面的基础接口。

它最核心的价值在于:

  • 能快速创建界面
  • 能和仿真代码直接集成
  • 能把“看 3D 画面”升级成“看结构化的状态与趋势”

在实际使用中,你通常只需要先掌握这几件事:

  • ui.Window 创建窗口
  • ui.VStack / ui.HStack 组织布局
  • ui.Labelui.Button 做基础交互
  • ui.SimpleXxxModel 管理输入状态
  • ui.Plot 显示实时曲线

掌握这些之后,就已经足够写出大多数调试面板。

如果后续要继续深入,最值得扩展的方向通常是:

  • 更复杂的布局组织
  • model 驱动的参数系统
  • 多曲线监视器
  • 按钮、输入和仿真控制逻辑联动

一句话概括:

omni.ui 不是为了做漂亮网页,而是为了让你在 Isaac Sim 里快速拥有一个“能看、能改、能调”的原生工具面板。