十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenUSD 开发指南:从零构建 usdview Python 插件并掌握 usdviewApi 编程接口

OpenUSD 开发指南:从零构建 usdview Python 插件并掌握 usdviewApi 编程接口 OpenUSD 开发指南从零构建 usdview Python 插件并掌握 usdviewApi 编程接口【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD本文基于 OpenUSD 官方教程 tut_usdview_plugin.rst 编写完整演示如何创建一个 Python 插件容器PluginContainer并将其注入 usdview 的菜单栏同时深入讲解usdviewApi对象提供的数据模型访问、截图捕获等 API 能力。读完本文你将能够独立开发自己的 usdview 命令插件、使用deferredImport优化插件启动性能并理解 OpenUSD 仓库中 plugin.py 的插件加载底层机制最终把仓库自带的 SendMail 示例插件改造为自己的生产力工具。一、创建插件容器PluginContainer1.1 建立插件目录usdview 的插件本质是一个 Python 模块通过 Pixar 的 libplug 插件系统被发现和加载。首先创建一个专门存放插件的目录建议放在 USD 构建会扫描插件的任意位置并预留嵌套结构以便未来安装更多插件mkdir -p some path/usdviewPlugins/tutorialPlugin/1.2 编写插件模块的__init__.py在插件模块的__init__.py中定义一个继承自PluginContainer的容器类# tutorialPlugin/__init__.py from pxr import Tf from pxr.Usdviewq.plugin import PluginContainer def printMessage(usdviewApi): print(Hello, World!) class TutorialPluginContainer(PluginContainer): def registerPlugins(self, plugRegistry, usdviewApi): self._printMessage plugRegistry.registerCommandPlugin( TutorialPluginContainer.printMessage, Print Message, printMessage) def configureView(self, plugRegistry, plugUIBuilder): tutMenu plugUIBuilder.findOrCreateMenu(Tutorial) tutMenu.addItem(self._printMessage) Tf.Type.Define(TutorialPluginContainer)PluginContainer是“知道如何注册新的命令插件、并把它们挂到 usdview UI 上”的基类其核心是两个虚方法registerPlugins(plugRegistry, usdviewApi)容器被 libplug 发现后usdview 插件系统首先调用该方法让容器把命令插件注册进插件注册表PluginRegistry。每个命令插件需要三要素唯一标识字符串identifier、显示名称display name、回调函数callback。由于标识必须全局唯一良好的实践是在前面加上容器名作前缀如TutorialPluginContainer.printMessage。所有命令插件回调的唯一参数都是usdviewApi对象。configureView(plugRegistry, plugUIBuilder)注册完所有命令插件后usdview 调用该方法给插件一个把命令暴露到 UI 的机会。目前插件只能创建简单的菜单栏菜单以及打开新的 Qt 窗口。本例创建了一个名为 Tutorial 的菜单并把 Print Message 命令加入其中。由于插件通过 libplug 加载见 pxr/plug/overview.dox容器类还必须用Tf.Type.Define()定义为一个新的Tf.Type这样 libplug 才能找到它。1.3 编写plugInfo.json在插件目录下创建plugInfo.json描述文件{ Plugins: [ { Type: python, Name: tutorialPlugin, Info: { Types: { tutorialPlugin.TutorialPluginContainer: { bases: [pxr.Usdviewq.plugin.PluginContainer], displayName: Usdview Tutorial Plugin } } } } ] }编写自己的插件容器时只需从上面的示例中修改三处Name字段改为自己的插件 Python 模块名Types下的类型名改为自己的PluginContainer类型全名模块名.类名displayName改为自己的显示名称。1.4 配置环境变量libplug 加载 Python 插件的方式是直接 import 对应模块因此需要设置两个环境变量PYTHONPATH必须包含插件所在的目录即上面创建的usdviewPlugins/否则 Python 无法 import 到tutorialPlugin模块。如果尝试仓库中的 SendMail 示例则应将 extras/usd/examples/usdviewPlugins 加入PYTHONPATH。PXR_PLUGINPATH_NAME必须包含插件目录自身的路径本例中即tutorialPlugin/所在路径libplug 才会扫描其中的plugInfo.json。配置完成后启动usdview菜单栏应出现新的 Tutorial 菜单点击其中的 Print Message控制台会打印 Hello, World!。如果 Tutorial 菜单没有出现请排查使用绝对路径设置上述环境变量并确认文件名严格为__init__.py与plugInfo.json大小写、下划线都不能差。二、插件加载机制源码解析OpenUSD 仓库中插件系统的完整实现在 plugin.pyusdview启动时由 appController.py 调用plugin.loadPlugins(...)完成全部加载。从源码结构看加载链路验证了教程描述的每一步发现容器loadPlugins()通过Plug.Registry.GetAllDerivedTypes(PluginContainerTfType)找出所有已定义的PluginContainer派生类型plugin.py#L292-L322。这也是为什么容器类必须Tf.Type.Define——没有对应的 Tf.Typelibplug 根本无法发现它。确定性加载顺序所有插件按插件名plugin.name字母序加载单个插件内的多个容器按类型名字母序加载。若某容器的pythonClass为None即plugInfo.json中声明的类型与模块内实际 import 路径不匹配usdview 会打印 WARNING 并跳过该容器。两阶段初始化先对每个容器依次调用registerPlugins(registry, usdviewApi)全部注册成功后再统一创建PluginUIBuilder并调用每个容器的configureView(registry, uiBuilder)plugin.py#L328-L342。这与教程先注册、后配 UI的叙述完全一致。命名冲突保护PluginRegistry.registerCommandPlugin()在检测到重复的插件name时抛出DuplicateCommandPlugin异常plugin.py#L221-L246。loadPlugins捕获到该异常后会打印警告并中止全部插件初始化Plugins will not be loaded.——所以标识符前缀约定MyPluginContainer.myPluginName是硬性要求而非风格偏好。菜单构造细节PluginUIBuilder.findOrCreateMenu()在初始化时会把主窗口菜单栏上已有的内置菜单预注册进内部字典plugin.py#L256-L289因此插件可以复用/追加到 usdview 自带菜单而不会创建标题重复的菜单。PluginMenu.addItem(commandPlugin, shortcutNone)还支持可选的键盘快捷键参数会用QKeySequence绑定到生成的QAction上plugin.py#L177-L190并把命令的description设置为 tooltip——registerCommandPlugin的第四个可选参数description即为此 tooltip 文本。命令执行CommandPlugin.run()在菜单项被点击时调用内部就是self._callback(self._usdviewApi)印证了回调只接收usdviewApi一个参数的契约plugin.py#L132-L166。三、使用 usdviewApi 与 usdview 交互能创建命令插件之后就可以通过usdviewApi对象与 usdview 交互。查看完整 API 列表的方式在 usdview 中打开解释器窗口菜单Window -- Interpreter输入help(usdviewApi)。API 核心能力概览如下实现位于 usdviewApi.pyusdviewApi.dataModel—— usdview 状态的完整表示插件可获取的大部分数据和功能都经由数据模型暴露stage当前的Usd.Stage对象currentFrameusdview 当前帧viewSettings一组仅影响视口的设置集合通常由 usdview 的 Display 菜单控制例如complexity场景细分复杂度subdivision complexityfreeCamerausdview 未通过某个 camera prim 观察时所使用的相机对象插件可修改它以改变视图renderMode模型渲染模式平滑着色、平直着色、线框等。selection当前 prim 与属性选择状态常用方法包括 prim 选择的getFocusPrim()、getPrims()、setPrim(prim)、addPrim(prim)、clearPrims()以及属性选择的getFocusProp()、getProps()、setProp(prop)、addProp(prop)、clearProps()。usdviewApi.qMainWindow—— usdview 的 Qt 主窗口对象可作为其他 Qt 窗口与对话框的父窗口parent但不应用于其他任何用途。usdviewApi.PrintStatus(msg)—— 在 usdview 窗口底部打印状态消息对应 usdviewApi.py#L194-L197 中转发到appController.statusMessage的实现。GrabViewportShot()/GrabWindowShot()—— 分别捕获渲染视口或整个主窗口的截图返回QImageusdviewApi.py#L210-L219。从源码结构看当前版本的UsdviewApi还额外暴露了若干教程未逐一列举的属性与方法例如stageIdentifier根层标识符、selectedPrims/selectedPaths当前选中的 prim 列表、currentGfCamera最近一次计算的 Gf 相机副本、viewportSize视口像素尺寸、SetViewportRenderer()/GetViewportRendererNames()切换与枚举渲染器插件、UpdateViewport()调度一次重绘等usdviewApi.py#L29-L245。建议在开发插件时以help(usdviewApi)的实时输出为准。四、延迟导入Deferring Importsusdview 的设计目标是快速启动所以身为好的 usdview 公民插件应尽量快速加载。有些 Python 模块的 import 耗时明显最佳实践是在命令第一次被调用时才惰性lazy导入。最简方式是把插件逻辑拆到独立的 Python 文件并使用PluginContainer提供的deferredImport(moduleName)方法。4.1 拆分模块把printMessage放入新文件printer.py。由于该函数没有重量级依赖我们在文件被导入时打印一行以便验证延迟是否生效# tutorialPlugin/printer.py print(Imported printer!) def printMessage(usdviewApi): print(Hello, World!)4.2 普通导入对照基线先按普通方式导入确认基线行为# tutorialPlugin/__init__.py - Normal Import from pxr import Tf from pxr.Usdviewq.plugin import PluginContainer from . import printer class TutorialPluginContainer(PluginContainer): def registerPlugins(self, plugRegistry, usdviewApi): self._printMessage plugRegistry.registerCommandPlugin( TutorialPluginContainer.printMessage, Print Message, printer.printMessage) def configureView(self, plugRegistry, plugUIBuilder): tutMenu plugUIBuilder.findOrCreateMenu(Tutorial) tutMenu.addItem(self._printMessage) Tf.Type.Define(TutorialPluginContainer)此时运行 usdview控制台会立即打印 Imported printer!。接下来改为延迟导入4.3 延迟导入# tutorialPlugin/__init__.py - Deferred Import from pxr import Tf from pxr.Usdviewq.plugin import PluginContainer class TutorialPluginContainer(PluginContainer): def registerPlugins(self, plugRegistry, usdviewApi): printer self.deferredImport(.printer) self._printMessage plugRegistry.registerCommandPlugin( TutorialPluginContainer.printMessage, Print Message, printer.printMessage) def configureView(self, plugRegistry, plugUIBuilder): tutMenu plugUIBuilder.findOrCreateMenu(Tutorial) tutMenu.addItem(self._printMessage) Tf.Type.Define(TutorialPluginContainer)改动仅两处移除顶部的from . import printer在registerPlugins内用self.deferredImport(.printer)代替。再次运行 usdview只有真正调用printMessage时才会看到 Imported printer!——模块只会被导入一次多次调用printMessage也只会在第一次打印该消息。4.4 DeferredImport 的实现原理deferredImport返回一个假模块对象DeferredImportplugin.py#L31-L97它对任何属性访问都返回一个包裹函数该函数在第一次被调用时才通过importlib.import_module真正导入目标模块以self.__module__为 package 解析相对导入名如.printer取出目标函数并转发调用参数。两个值得注意的边界行为在模块真正导入之前DeferredImport无法知道目标模块里是否存在某个函数因此它假设你引用的任何对象都是函数如果你引用了目标模块中不存在的函数在调用时会抛出ImportErrorFailed deferred import: callable object ... not found若模块本身找不到则抛出 module not found 的ImportError。五、SendMail 示例插件USD 发行版自带一个示例插件sendMail.py。把它加入插件路径后在自己的PluginContainer中注册时指定sendMail.SendMail作为命令回调函数即可。调用 SendMail 后会弹出一个对话框让用户填写收件人、主题与正文并可选择发送整个 usdview 主窗口或仅渲染视口的截图。阅读 sendMail.py 源码可以看到多处 API 实战用法usdviewApi.GrabWindowShot()与usdviewApi.GrabViewportShot()分别捕获两种截图并把图片临时落盘为 JPEG 附件sendMail.py#L33-L55若视口截图不可用例如使用了--norender则对话框中只提供 Window 选项usdviewApi.qMainWindow作为父窗口创建QtWidgets.QDialog演示了插件创建模态对话框的标准写法sendMail.py#L188-L203邮件正文自动填充了多条 API 数据usdviewApi.stageIdentifier当前文件、usdviewApi.selectedPrims选中的 prim 路径、usdviewApi.frame当前帧、usdviewApi.dataModel.viewSettings.complexity细分复杂度、usdviewApi.currentGfCamera相机信息是组织诊断报告类插件正文的参考模板sendMail.py#L205-L235发送环节在本地启动 SMTP 客户端smtplib.SMTP(localhost)要求本机运行邮件服务_GetSenderAddress()可改造为自动填充发件人地址。六、生产环境中组织 usdview 插件PluginContainer系统允许发现并执行任意数量的插件模块其设计初衷是方便非构建专家添加新的 usdview 插件。虽然本教程的registerPlugins()只注册了一个命令但它完全可以注册任意数量的命令configureView()也能创建并配置任意数量的菜单。Pixar 内部的做法是把所有插件放进单一模块这样有两个优势模块一旦被维护者搭好后续需要新增插件的用户无需了解或改动任何plugInfo.json文件当所有命令的注册都集中在一个地方时把命令组织成一套连贯、有序、层次清晰的菜单要容易得多。七、小结一个最小可用的 usdview 插件由三部分构成含PluginContainer子类的 Python 模块__init__.py、plugInfo.json描述文件以及正确的PYTHONPATH/PXR_PLUGINPATH_NAME环境配置插件生命周期为libplug 发现容器类型 →registerPlugins()注册命令 →configureView()挂菜单全部实现在 pxr/usdImaging/usdviewq/plugin.py命令名重复会中止整个插件系统初始化务必遵守容器名前缀命名约定usdviewApi是插件与 usdview 交互的唯一接口核心包括dataModelstage/当前帧/视口设置/选择集、qMainWindow新窗口的父窗口、PrintStatus()、GrabViewportShot()/GrabWindowShot()用deferredImport()把重量级依赖推迟到首次调用是保持 usdview 快速启动的标准做法生产环境建议将所有命令集中到一个插件模块中统一维护plugInfo.json与菜单结构。【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表