4、扩展开发基础:掌握Extension的基本结构、manifest.json配置、Python入口和注册方式
Omniverse Kit 是一个高度模块化的应用框架,其核心设计哲学是“一切皆扩展(Extension)”。无论是 UI 工具、仿真逻辑、渲染管线还是硬件接入,最终都以 Extension 的形式被加载、组合和运行。理解 Extension 的基本结构、配置清单和注册机制,是进入 Omniverse Kit 开发世界的第一道门槛。本章将围绕一个最小可运行的 Extension 从零到一的构建过程,深入剖析其内部结构、manifest.json 的字段语义、Python 入口的加载顺序以及 Kit 的扩展注册表机制,帮助开发者建立清晰、正确的扩展开发心智模型。
上图展示了 Extension 从磁盘结构到 Kit 运行时加载的完整链路。左侧是扩展包在文件系统中的典型布局,中间是 Kit 进程内的核心管理组件,右侧则是注册与加载的关键步骤。整个流程的核心在于 manifest.toml 文件,它充当了扩展与 Kit 之间的契约。下面我们将从扩展的基本结构开始,逐步深入到配置细节和代码实现。
4.1 Extension 的基本结构与目录规范
一个标准的 Omniverse Kit Extension 本质上是一个包含特定文件和目录结构的文件夹。虽然 Kit 允许一定程度的灵活性,但遵循社区和官方推荐的规范,可以显著降低维护成本并提高可移植性。一个典型的扩展包结构如下:
my_extension/
├── extension.toml # 扩展清单文件(manifest)
├── python/
│ └── my_extension/
│ ├── __init__.py # Python 包初始化
│ └── extension.py # 扩展主逻辑(可选,但推荐)
├── config/
│ └── extension.toml # 扩展的默认配置(可选)
├── data/
│ ├── icons/
│ └── resources/ # 静态资源(UI、模型等)
├── docs/
│ └── README.md
└── tests/
└── test_extension.py
其中,extension.toml 是唯一强制要求的文件,它必须位于扩展包的根目录。Kit 通过该文件识别一个文件夹是否为合法的扩展。Python 代码通常放置在 python/ 目录下,并以包(Package)的形式组织。这种结构不仅清晰,而且便于 Kit 的依赖解析器正确处理 Python 模块的导入路径。
my_extension 优于 my-extension。因为 Python 模块导入机制不支持连字符,而 Kit 内部会将扩展名与 Python 包名进行关联。
4.2 manifest.json(extension.toml)配置详解
尽管题目中提及 manifest.json,但在 Omniverse Kit 的现代版本中,扩展清单文件的标准格式是 TOML(extension.toml)。TOML 相比 JSON 具有更好的可读性和注释支持,更适合作为配置文件。不过,理解 JSON 格式的开发者可以轻松迁移,因为字段语义是相通的。下面是一个最小但完整的 extension.toml 示例:
[package]
title = "My First Extension"
version = "1.0.0"
description = "A minimal extension for learning Kit API."
keywords = ["example", "tutorial"]
authors = ["Your Name <you@example.com>"]
[package.license]
name = "MIT"
text = "MIT License ..."
[dependencies]
"omni.kit.app" = ">=1.0.0"
"omni.kit.window.toolbar" = ">=1.0.0"
[python]
entry = "my_extension.extension:MyExtension"
[extension]
category = "Examples"
enabled = true
上述配置中,[package] 段定义了扩展的基本元信息,title 和 version 是必填项。[dependencies] 段声明了该扩展所依赖的其他扩展及其版本约束,Kit 的依赖解析器会据此构建加载顺序。[python] 段的 entry 字段是核心,它指定了 Python 入口类的路径,格式为 模块路径:类名。[extension] 段则包含一些运行时属性,如分类和默认启用状态。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
title |
string | 是 | 扩展的显示名称,用于 UI 和日志。 |
version |
string | 是 | 遵循语义化版本规范,如 "1.2.3"。 |
description |
string | 否 | 扩展功能的简要描述。 |
dependencies |
table | 否 | 声明对其他扩展的依赖及版本范围。 |
python.entry |
string | 是* | Python 入口点,格式为 "module:Class"。*如果扩展包含 Python 代码则必填。 |
extension.category |
string | 否 | 用于在 Extension Manager 中分类显示。 |
>= 和 < 符号在 TOML 文件中需要谨慎处理。TOML 要求字符串值必须用引号包裹,因此版本约束应写成 "omni.kit.app" = ">=1.0.0",而不是 >=1.0.0(不带引号会导致解析错误)。
4.3 Python 入口与生命周期方法
当 Kit 决定加载一个扩展时,它会根据 python.entry 字段动态导入指定的模块,并实例化指定的类。这个类不需要继承任何基类,但必须实现两个关键方法:start() 和 shutdown()。start() 在扩展被启用时调用,通常用于创建 UI、注册命令、连接服务等;shutdown() 在扩展被禁用或 Kit 退出时调用,用于释放资源、注销回调。
# python/my_extension/extension.py
import omni.ext
import omni.ui as ui
class MyExtension:
def __init__(self):
self._window = None
def start(self):
"""扩展启动时调用"""
self._window = ui.Window("My Extension", width=300, height=200)
with self._window.frame:
with ui.VStack():
ui.Label("Hello from My Extension!")
ui.Button("Click Me", clicked_fn=self._on_click)
def shutdown(self):
"""扩展关闭时调用"""
if self._window:
self._window.destroy()
self._window = None
def _on_click(self):
print("Button clicked!")
上述代码展示了最基础的扩展实现。start() 方法中创建了一个简单的 UI 窗口,shutdown() 中销毁了该窗口。值得注意的是,start() 和 shutdown() 可能会被多次调用(例如用户在 Extension Manager 中手动禁用和启用扩展),因此实现时必须保证幂等性——即重复调用不会产生错误或资源泄漏。
importlib 模块。当扩展被启用时,Kit 会将扩展的 python/ 目录添加到 sys.path,然后执行 importlib.import_module() 导入入口模块。实例化类后,Kit 会持有该实例的引用,并在适当时机调用其生命周期方法。这种设计使得扩展可以完全控制自己的资源,而不需要依赖 Kit 的垃圾回收机制。
4.4 注册方式:UI、命令与服务
除了生命周期方法,扩展还需要与 Kit 的其他部分交互。这种交互通过“注册”机制实现。常见的注册方式包括:
- UI 注册: 通过
omni.ui创建窗口、面板、菜单项等。窗口可以通过ui.Window创建,菜单项可以通过omni.kit.menu.utils添加。 - 命令注册: 使用
omni.kit.commands模块注册可撤销/重做的命令。命令是 Kit 中执行操作的标准方式,支持事务和宏录制。 - 服务注册: 通过
omni.ext.IExtension接口或carb框架注册服务,供其他扩展查询和调用。 - 事件监听: 订阅 Kit 或 USD 舞台的事件,如
StageEventType、UpdateEventType等。
以命令注册为例,一个典型的命令类需要继承 omni.kit.commands.Command 并实现 do() 和 undo() 方法:
import omni.kit.commands
class CreateSphereCommand(omni.kit.commands.Command):
def do(self, radius: float = 1.0):
# 创建球体的逻辑
self._radius = radius
print(f"Creating sphere with radius {radius}")
return True
def undo(self):
# 撤销操作
print("Undo sphere creation")
# 注册命令
omni.kit.commands.register("MyExtension.CreateSphere", CreateSphereCommand)
注册命令后,其他扩展或脚本可以通过 omni.kit.commands.execute("MyExtension.CreateSphere", radius=2.0) 来调用,并且该操作会自动支持撤销/重做。
4.5 实战场景:构建一个带配置面板的扩展
为了将上述知识串联起来,我们设计一个实战场景:构建一个扩展,它读取配置文件中的参数,并在 UI 中显示一个可调节的滑块。当滑块值改变时,扩展会更新舞台中一个立方体的缩放比例。
背景与约束: 假设我们正在开发一个场景布置工具,需要快速调整基础几何体的尺寸。扩展需要支持配置文件的持久化,以便下次启动时恢复上次的数值。
实现步骤:
- 创建扩展目录结构,编写
extension.toml,声明依赖omni.kit.app和omni.usd。 - 在
config/extension.toml中定义默认参数:scale = 1.0。 - 在
start()中读取配置,创建 UI 窗口和滑块。 - 滑块值改变时,通过
omni.usd.get_context()获取舞台,修改立方体的缩放属性。 - 在
shutdown()中保存配置并销毁窗口。
# 简化版实现
import omni.ext
import omni.ui as ui
import omni.usd
from pxr import UsdGeom
class ScaleControllerExtension:
def __init__(self):
self._window = None
self._scale = 1.0
def start(self):
# 读取配置
self._scale = omni.ext.get_extension_settings().get("scale", 1.0)
self._window = ui.Window("Scale Controller", width=250, height=100)
with self._window.frame:
with ui.VStack():
self._slider = ui.FloatSlider(min=0.1, max=10.0, value=self._scale)
self._slider.model.add_value_changed_callback(self._on_scale_changed)
def _on_scale_changed(self, model):
self._scale = model.get_value_as_float()
# 更新舞台中的立方体
stage = omni.usd.get_context().get_stage()
if stage:
prim = stage.GetPrimAtPath("/World/Cube")
if prim.IsValid():
UsdGeom.Xformable(prim).GetOrderedXformOps()[0].Set(self._scale)
def shutdown(self):
# 保存配置
omni.ext.get_extension_settings()["scale"] = self._scale
if self._window:
self._window.destroy()
结果与复盘: 该扩展成功实现了参数化控制,并且配置在重启后得以保留。通过这个案例,我们实践了 manifest 配置、Python 入口、UI 注册和配置读写。需要注意的是,GetOrderedXformOps()[0] 假设立方体只有一个缩放操作,实际项目中应更稳健地处理变换栈。
4.6 常见误区与总结
在扩展开发初期,开发者常会遇到以下问题:
- 入口类路径错误:
python.entry中的模块路径必须与实际的 Python 包结构完全匹配,且类名大小写敏感。 - 依赖未声明: 如果扩展使用了
omni.usd但未在dependencies中声明,Kit 可能不会自动加载该依赖,导致导入失败。 - shutdown 不彻底: 忘记注销回调或销毁窗口,导致扩展被禁用后 UI 残留或内存泄漏。
- 配置文件路径错误: 扩展的配置应放在
config/extension.toml中,而不是与extension.toml混淆。
start() 方法开头添加 print(f"Starting extension: {__name__}"),并在 shutdown() 中添加对应的日志。通过观察 Kit 的控制台输出,可以快速确认扩展的加载和卸载时机是否正确。
总结而言,掌握 Extension 开发基础的关键在于理解 manifest 的契约作用、Python 入口的生命周期语义以及 Kit 的注册机制。一个结构清晰、配置完整、生命周期管理得当的扩展,是构建复杂应用的基础。建议开发者在动手编写业务逻辑之前,先花时间设计好扩展的目录结构和配置项,这将为后续的维护和扩展带来极大的便利。