使用USDRT优化NVIDIA Omniverse的动态数据更改功能

描述

在 NVIDIA Omniverse 开发中,此前我们已探讨了常见的性能瓶颈、如何使用 Tracy 等工具进行问题定位,并初步介绍了 FSD(Fabric Scene Delegation)与 USDRT 等核心概念。本期文章将聚焦 USDRT,通过具体的代码与示例,深入解读如何利用它来高效优化场景中的动态数据更改。

1. USDRT 基本概念

USD:由 Pixar 开发提供的原生 API 库,有 C++ 和 Python 两种接口,提供对 USD 场景的操作访问,其特点是写入速度慢。

Fabric:由 Omniverse 编写提供的库组件,只有 C++ 一种接口,为底层 USD 的写入提供支持,可理解为一个大的场景缓存。

- Fabric Stage(常称作 Fabric Flat Cache)是 Omniverse 中的一种高性能场景视图。它将 USD 合成后的场景数据“展平”并缓存至 Fabric 内部,旨在为物理、渲染、OmniGraph 等下游系统提供高性能的数据访问。与每次直接遍历 USD Stage 不同,该系统以列式数据、批处理等 GPU 友好的方式组织数据,从而大幅提升访问效率。

- Fabric 解决了传统 USD 开发中的常见性能瓶颈:以往,系统如需查找特定类型的 Prim(例如所有 Cube),必须每次遍历整个 USD Stage,并需手动维护数据缓存及监听 USD Notice 以实现同步更新,这种做法的实现复杂度高且效率有限。而 Fabric Stage 能够自动为多个扩展或系统集中维护这份场景缓存与对应的“脏标记”状态;其内部会按 Tag、属性、类型等标准将 Prim 自动分桶组织,并直接提供 findPrims 等查询接口,以及可将属性数组批量拉取至 CPU 或 GPU 内存的高性能 API(具体用法将在后文详述)。

USDRT(USD Runtime):Omniverse 提供的一套高性能运行时库。其 Python API 设计与原生 USD(pxr)基本同名,但底层通过 Fabric 组件对 USD 文件进行读写(代码中通过 usdrt. 前缀调用)。为解决 Fabric 仅有 C++ 接口的问题,USDRT 专门提供了兼容 USD 的 Python 封装层,便于开发者学习和使用。

开发者

Persistent Data:会被永久写入 USD 文件的数据。

Transient Data:运行时生成的临时数据(如仿真结果、动画效果),不会影响底层 USD 文件。

Composition:按照 USD 规则(如继承、覆盖)将多个 Layer 合成的机制。

Pre-Composition:合成发生前,多个 Layer 可分别读取状态属性的状态。

After-Composition:合成完成后,所有 Layer 统一融合成一个总 Layer 的最终场景状态。

USDRT 诞生的根本动因,是解决原生 USD 数据写入速度慢的性能瓶颈。为此,Omniverse 团队开发了 USDRT / Fabric 组件,其核心机制是处理场景中 Prim 的属性变化数据,将这些更新暂存于内存或显存中,而非直接、即时地写入底层 USD 文件。当然,系统也提供了相应的 API,可在必要时将最终数据写回 USD。这一过程可通过下图直观理解,并且也提供了快速查询和访问的 API 接口。

开发者

正如在《Omniverse 性能优化系列(一):Tracy Profiler》中所讨论的,渲染器(Renderer)必须等待 USD 写入操作完成,这正是造成卡顿的根本原因。下文将结合具体实例,并再次使用 Tracy 来观察和验证此优化带来的实际效果。

2. USDRT 实践

2.1  “Hello World” in USDRT

上文提到原生的 PXR USD 的 API 和 USDRT 大体相同,不同在于 USD / USDR 的 Stage 获取方式。

USDRT 通过以下方式获取当前已打开的 Stage:

 

stage = usdrt.Usd.Stage.Attach(omni.usd.get_context().get_stage_id())

 

原生 USD 则通过以下方式获取 Stage:

 

stage = omni.usd.get_context().get_stage() 

 

下面为一个 USDRT 的 Hello World,可以用 Omniverse 自带的 Script Editor 打开并运行:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/usdrt_helloworld.py(复制链接至浏览器打开,下同)

 

import usdrt
stage = usdrt.Usd.Stage.Attach(omni.usd.get_context().get_stage_id())
print(f"USD RT Hello world, USDRT Stage {stage}")

 

也可通过其他打开 USD 文件的方式或参考 Stage 相关信息,具体可查看下面的代码和文档:

https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/scenegraph_use.html#stages

 

Import usdrt
stage = usdrt.Usd.Stage.Open(DATA_DIR + "/cornell.usda") 

 

2.2  USDRT 示例讲解

2.2.1  修改 Prim 的 attribute

首先通过一个简单的例子来说明:USDRT 只是影响了场景的 Runtime,并不实际修改 USD 文件本身。

下载并用 Omniverse 打开“cornell.usda”这个场景。附场景文件下载地址:

https://github.com/slayersong/OV_Perf_tutorial/tree/main/usd/tests

通过“/Cornell_Box/Root/Cornell_Box1_LP/White_Wall_Back" 查看这个 Prim Path 下面的 primvars:displayColor,属性值是 [0.5,0.5,0.5]。

开发者

打开 Script Editor 并运行下面的代码,作用在于将颜色修改成(1,0,0):

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/usdrt_color.py

 

from usdrt import Gf, Sdf, Usd, UsdGeom, Vt
import omni
stage = Usd.Stage.Attach(omni.usd.get_context().get_stage_id())
path = "/Cornell_Box/Root/Cornell_Box1_LP/White_Wall_Back"
prim = stage.GetPrimAtPath(path)
attr = prim.GetAttribute(UsdGeom.Tokens.primvarsDisplayColor)
# Get the value of displayColor on White_Wall_Back,
# which is mid-gray on this stage
result = attr.Get()
print(f"before set color is {result}")
attr.Set(Vt.Vec3fArray([Gf.Vec3f(1, 0, 0)]))
result = attr.Get()
print(f"after set color is {result}")

 

此时会发现墙体的颜色变红了,但查看 Omniverse 的 Stage 面板中 primvars:displayColor 这个 attribute 实际数值并未更改,左上角的场景文件也没有变动信息 (usd 文件如有改变会有“*”)。

开发者开发者

通过上图对比可知,USDRT 的修改仅影响运行时的场景表现。若需将这些更改存储至最终的 USD 文件,必须调用以下 API:

 

stage.WriteToStage() 

 

*注:此 API 在某些特定 GPU 下可能导致进程无响应。

2.2.2  USDRT 快速查询场景 Prim

按照传统的 USD API 的做法,查询场景中某一类的 Prim(比如 Mesh)需要对整个 Stage 进行遍历,代码如下:

 

mesh_prims = []
for prim in stage.Traverse():
    if prim.IsA(UsdGeom.Mesh):
        mesh_prims.append(prim)

 

通过分析可知,原生遍历方法的时间复杂度为 O(n)。在大型场景中,若要搜索某一特定 Prim,其耗时将更为显著。针对此性能瓶颈,Fabric / USDRT 为 Prim 的快速查询提供了以下 3 个 API:

UsdStage::GetPrimsWithTypeName(TfToken typeName)

UsdStage::GetPrimsWithAppliedAPIName(TfToken apiName)

GetPrimsWithTypeAndAppliedAPIName(TfToken typeName, TfTokenVector apiNames)

针对上述 API,下面对 Omniverse USD 中的基本概念进行梳理:

Type:在 Omniverse 的 Viewport 中单击鼠标右键,点击“Create”所列出来的选项就是 Type,例如 Shape 中的 Capsule、Cone、Cube 等(Shape 中都是单独的一个 Type 类型,Camera、Curves 等)。创建后在 Stage 的面板中也可看到基本的 Type 类型。

开发者开发者

AppliedAPI:要理解 Applied API,需先了解 USD 中的 API Schema(https://openusd.org/release/glossary.html#api-schema )。它指的是一组可通过特定接口动态应用到 Prim 上、为其增添功能的属性集合。例如,可以为一个 Mesh Prim 增加刚体碰撞的属性,或绑定材质的属性。

 

bindingAPI = UsdShade.MaterialBindingAPI.Apply(prim)
bindingAPI.Bind(materialPrim)

 

USD 中有多少种 API Schema 类型?我们可以在 Omniverse 的 Script Editor 中运行以下代码,查询所有已注册的 API Schema。其中主要包含物理、灯光、UsdGeomModelAPI 等相关类型:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/APISchema_query.py

 

from pxr import Usd, Tf
schema_reg = Usd.SchemaRegistry()
single_apply_list = []
multiple_apply_list = []
for t in Tf.Type.FindByName("UsdAPISchemaBase").GetAllDerivedTypes():
    ifnot (schema_reg.IsAppliedAPISchema(t) or schema_reg.IsMultipleApplyAPISchema(t)):        
        continue
    
    if (schema_reg.IsAppliedAPISchema(t) andnot schema_reg.IsMultipleApplyAPISchema(t)):
        single_apply_list.append(str(t).split("'")[1::2][0])
        
    if (schema_reg.IsMultipleApplyAPISchema(t)):
        multiple_apply_list.append(str(t).split("'")[1::2][0])
# Sort and print
print("Single-apply API Schemas:")
for x insorted(single_apply_list):
    print(x)
print("
Multi-apply API Schemas:")
for x insorted(multiple_apply_list):
    print(x)

 

回到最初的问题:在场景中查找具有特定类型(Type)的 Prim。使用原生 USD API 只能通过遍历整个 Stage 实现,其时间复杂度为 O(n)。而通过 USDRT 提供的 API,时间复杂度可降至 O(1)

可以使用以下三个 API 进行快速查询:

meshPaths = stage.GetPrimsWithTypeName("Mesh")

shapingPaths = stage.GetPrimsWithAppliedAPIName("ShapingAPI")

paths = stage.GetPrimsWithAppliedAPIName("CollectionAPI:lightLink")

完整的代码链接:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/APISchema_query.py

2.3  USDRT vs USD 性能测试

下面通过一个实际案例,来对比动态修改 USD Prim 属性时的性能差异。我们将遍历场景中所有 Mesh 并修改其颜色属性,分别使用原生 USD API 与 USDRT 实现,并记录运行时间。

2.3.1  准备测试场景

首先下载一个复杂度较高的场景资产用于测试:

访问网站下载 Kitchen Set 资产:

https://developer.nvidia.com/usd

解压后,在 Omniverse 中打开 Kitchen_set.usd 文件。

开发者

2.3.2  使用原生 USD API 修改颜色

在 Script Editor 当中运行以下代码:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/usd.py

 

from pxr import Gf, Sdf, Usd, UsdGeom, Vt
import omni
import time
import carb
stage = omni.usd.get_context().get_stage()
color = Vt.Vec3fArray([Gf.Vec3f(0.8, 0.2,0 )])
# 遍历所有 prim,筛选出 Mesh
mesh_prims = []
for prim in stage.Traverse():
    if prim.IsA(UsdGeom.Mesh):
        mesh_prims.append(prim)
t0 = time.perf_counter()
for prim in mesh_prims:
    if prim.HasAttribute(UsdGeom.Tokens.primvarsDisplayColor):
        #carb.log_info(f"The prim is {prim}")
        prim.GetAttribute(UsdGeom.Tokens.primvarsDisplayColor).Set(color)
t1 = time.perf_counter()
elapsed_ms = (t1 - t0) * 1000.0
carb.log_warn(f"[Native USD] Painted {len(mesh_prims)} meshes in {elapsed_ms:.2f} ms")

 

测试结果:

在配置了 NVIDIA RTX 5880 Ada 的测试机上,首次运行耗时约 1337 ms(具体时间因配置而异)。

此时可观察到原生 USD 文件已被修改。

注意:如果不更改颜色值再次运行同一段代码,耗时将降至约 14 ms。如果改变颜色值,时间又会如何?建议动手尝试并思考背后的原因。

开发者

回顾此前在《Omniverse 性能优化系列(一):Tracy Profiler》中的分析:抓取最大渲染运行时间,通过函数的 callstack 可以发现,渲染线程在等待 USD 文件写入完成。这意味着 USD 的写入操作很可能是单线程串行的。USD 数据变更后,会通过 Observer 设计模式逐一通知相关模块,整个过程是同步的。

开发者

2.3.3  使用 USDRT 修改颜色

接下来,使用 USDRT 实现相同的颜色修改操作:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/usdrt.py 

 

from usdrt import Gf, Sdf, Usd, UsdGeom, Vt
import omni
import time
import usdrt
import carb


stage = usdrt.Usd.Stage.Attach(omni.usd.get_context().get_stage_id())


color = Vt.Vec3fArray([Gf.Vec3f(0.2, 0.2,0)])
meshPaths = stage.GetPrimsWithTypeName("Mesh")
t0 = time.perf_counter()
for meshPath in meshPaths:
    prim = stage.GetPrimAtPath(meshPath)
    if prim.HasAttribute(UsdGeom.Tokens.primvarsDisplayColor):
        #carb.log_info(f"The prim is {prim}")
        prim.GetAttribute(UsdGeom.Tokens.primvarsDisplayColor).Set(color)
t1 = time.perf_counter()
elapsed_ms = (t1 - t0) * 1000.0


carb.log_warn(f"[USDRT] Painted {len(meshPaths)} meshes in {elapsed_ms:.2f} ms")

 

测试结果:

相同操作耗时仅约 148 ms,相比原生 USD 提升了近一个数量级。

观察左上角可发现,原生 USD 文件本身并未被修改。

*延伸尝试:根据上文提到的 API 把 traverse stage 变成 USDRT  提供的快速查询 API 并对比时间差异。

开发者开发者

2.4  USDRT 中的 Change Tracking 机制

在 USD 体系中,TfNotice(Tool Foundation Notice)是核心的消息处理机制,它实现了 “场景 / 资源变化 → 自动推送事件” 的监听模式,可应用于监视 Stage 编辑、属性更新、层加载、实时协作以及视口刷新等场景。

USD 原生的 TfNotice 为监听器(Listener)提供了一种回调机制。然而,USDRT 提供了一套完全不同的变化跟踪范式。它提供了一系列主动查询函数,让开发者能够精确跟踪所关心的属性变化。用户需要在合适的频率主动查询并处理这些更改(相当于需要自行封装类似回调的逻辑)。

这一设计变革源于一个关键的性能考量:USD 原生的通知 (Notify) 机制是完全同步的。当发送者 (Sender) 的线程触发通知时,它会阻塞(Block)并等待所有已注册的监听器按顺序处理完毕。这意味着发送者线程的性能严重依赖于最慢的那个监听器。

为了避免这种阻塞,USDRT 采用了非阻塞的主动查询机制。下表概述了其核心 API:

开发者

实践示例:

结合前文提及的注册 Omniverse 的消息函数“Subscribe to Update Event”去跟踪更改:

https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/events.html#subscribe-to-update-events

还是打开之前的“cornell.usda”这个场景,然后跟踪某些 Prim 的颜色属性,利用 AI 辅助,我们编写了一个 RTTrackingTester,它会在异步更新循环中追踪更改并定期清空记录。完整代码如下:

https://github.com/slayersong/OV_Perf_tutorial/blob/main/2.USDRT/RTTrackingTester.py

3. 总结

总的来说,USDRT / Fabric 的本质是对 USD 文件写入的缓存层。其中,Fabric 提供底层 C++ 接口,USDRT 则负责提供与原生 USD API 一致的友好封装,旨在实现代码风格与调用的统一,让开发者以熟悉的方式获得显著的性能提升。

开发者

文案提供 & 技术支持:

宋毅明  NVIDIA Omniverse & OpenUSD 开发者关系经理

*与 NVIDIA 产品相关的图片或视频(完整或部分)的版权均归 NVIDIA Corporation 所有。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分