Universal Scene Description
USD 的核心在于其强大的组合能力(Composition Arcs)。
在 USD 中,Composition Arcs(组合弧) 是实现这一能力的关键机制。它们是一组操作符,定义了如何将多个图层(layers)和场景描述组合成单一的”组合场景图(composed scenegraph)“。理解 Composition Arcs 对于有效使用 USD 至关重要,因为它们支持非破坏性覆盖、多用户工作流程,以及从小型资产构建大型复杂场景的能力。
什么是 Composition Arcs
Composition Arcs 是 USD 中的一组操作符,用于驱动场景组合过程。在运行时,组合引擎会评估这些写入场景描述中的操作符,并将最终组合的场景图呈现给用户。
USD 提供了 六种主要的 Composition Arcs:
- Sublayers(子层)
- Inherits(继承)
- Variants(变体)
- References(引用)
- Payloads(有效载荷)
- Specializes(特化)
每种组合弧都有其特定的用途和行为特征,理解它们的差异是掌握 USD 组合系统的关键。
六种 Composition Arcs 详解
1. Sublayers(子层)
Sublayers 是 USD 中最基本的组合弧。它允许你将一个图层中的场景描述合并到其他图层之上。
工作原理:
- 根图层及其子层形成一个有序的图层树
- 组合引擎对这棵树进行深度优先遍历,生成一个有序的”图层栈(layer stack)”
- 较高层(stronger layers)中的意见(opinions)会覆盖较低层(weaker layers)中的意见
语法示例:
#usda 1.0
(
subLayers = [
@shot_layout.usd@,
@shot_sets.usd@
]
)典型用途:
- 支持多用户协作工作流(例如:布局艺术家和场景布置艺术家同时工作)
- 实现非破坏性覆盖
- 构建图层栈结构
- 可以通过
Sdf.LayerOffset支持时间偏移和缩放
实际案例:
在电影制作中,你可能有一个镜头文件 shot.usd,它子层化了 shot_layout.usd(布局)和 shot_sets.usd(场景布置)。这样,布局艺术家和场景布置艺术家可以同时在各自的图层上工作,而不会相互冲突。
2. References(引用)
References 是 USD 中最常用的组合弧之一,用于将另一个图层栈中某个 prim 的组合场景图层次结构引入当前场景。
工作原理:
- 引用的场景图被封装在”引用 prim”下
- 可以引用外部文件或内部层次结构
- 支持路径映射和重命名
- 可以应用时间偏移(通过
Sdf.LayerOffset)
语法示例:
def "World" {
def "chars" {
def "DukeCaboom" (
references = @DukeCaboom.usd@
) {
}
}
}典型用途:
- 从其他文件聚合场景描述数据
- 构建资产层次结构(从小型资产组装大型资产)
- 资产重用(同一资产可以被引用多次)
- 轻量级数据的文件加载
实际案例: 在《玩具总动员 4》的古董商场场景中,单个道具资产可能被引用多次,分布在整个场景中。每次引用都可以有自己的位置和覆盖属性。
3. Payloads(有效载荷)
Payloads 本质上是可选加载的引用,允许客户端在运行时决定是否加载昂贵的场景描述。
工作原理:
- 行为类似于 References,但可以在运行时加载或卸载
- 通过
UsdStage::Load()和UsdStage::Unload()API 控制 - 卸载时不会产生内存和运行时成本
- 加载/卸载是运行时标志,不会修改图层
语法示例:
def "World" {
def "chars" {
def "DukeCaboom" (
payloads = @DukeCaboom.usd@
) {
}
}
}典型用途:
- 加载重型几何数据
- 按需加载场景元素
- 优化内存使用
- 支持时间偏移(通过
Sdf.LayerOffset)
最佳实践 - “Lofting”: Pixar 的管线中常用一种技术叫”lofting”(提升),即在资产结构中引入一个中间层,包含即使 payload 未加载也需要的属性或元数据,然后这个中间层使用 payload 弧来使所有昂贵的数据可选加载。
实际案例: 场景布置艺术家在处理古董商场场景时,可能不需要看到所有角色的详细几何体。他们可以选择不加载 DukeCaboom 的 payload,从而节省内存和加载时间。
4. Variants(变体)
Variants 允许你在单个资产中打包一组替代方案,用户可以通过”变体选择”来选择要使用的变体。
工作原理:
- 创建包含多个”变体”的”变体集(variant sets)”
- 通过”变体选择”指定要组合的变体
- 变体选择可以在任何更强的图层中覆盖
- 不支持时间偏移
语法示例:
def "DukeCaboom" (
variantSets = ["Cape"]
variants = {
string Cape = "WhiteCape"
}
) {
variantSet "Cape" = {
"RedCape" {
def "RedCape" { }
}
"WhiteCape" {
def "WhiteCape" { }
}
"NoCape" {
}
}
}典型用途:
- 资产的多个变体(如不同颜色、配置)
- LOD(细节层次)切换
- 提供用户可选的替代方案
- 嵌套变体支持复杂的变体组合
实际案例: DukeCaboom 角色可能有一个 “Cape” 变体集,包含 “RedCape”、“WhiteCape” 和 “NoCape” 三个选项。资产默认选择 “WhiteCape”,但在特定镜头中,艺术家可以覆盖这个选择为 “RedCape”。
5. Inherits(继承)
Inherits 允许一个源 prim 上创作的意见影响场景图中所有创作了继承弧指向该源的 prims。
工作原理:
- 类似于面向对象编程中的类继承概念
- 通常与
class说明符一起使用 - 可以继承外部文件或内部层次结构中的 prims
- 不支持时间偏移
- 在强度排序中位于 Sublayers 之后
语法示例:
class "_class_Sphere" {
double radius = 10
color3f[] primvars:displayColor = [(1, 0, 0)]
}
def Sphere "Sphere1" (
inherits = </_class_Sphere>
) {
}
def Sphere "Sphere2" (
inherits = </_class_Sphere>
) {
# 可以覆盖继承的属性
color3f[] primvars:displayColor = [(0, 1, 0)]
}典型用途:
- 对多个(可实例化的)prims 应用编辑而不失去实例化能力
- 定义可重用的”类”或”模板”
- 在不增加原型数量的情况下修改实例
实际案例: 如果你有 100 个引用的球体实例,想要改变它们的材质,可以使用 inherit 弧从一个 class prim 继承材质属性,这样所有球体都会更新,同时保持实例化的内存优势。
6. Specializes(特化)
Specializes 类似于 Inherits,但在强度排序中是最弱的,用于提供”模板”或”基线”值。
工作原理:
- 行为类似 Inherits,但强度排序规则不同
- Specializes 的意见总是比其他所有弧的意见弱
- 可以作为默认值或后备值
- 不支持时间偏移
语法示例:
class "_template_Asset" {
double size = 1.0
string material = "default"
}
def Xform "Asset1" (
specializes = </_template_Asset>
) {
# 任何其他弧的意见都会覆盖 specializes
}典型用途:
- 提供模板值或基线值
- 定义可被任何其他弧覆盖的默认属性
- 广播 specs(prim specs 或 property specs)到图层栈
与 Inherits 的区别:
- Inherits:强度高,用于应用必须保留的覆盖
- Specializes:强度最弱,用于提供可被任何东西覆盖的默认值
LIVRPS 强度排序
USD 使用一个称为 LIVRPS(发音为 “liver-peas”)的强度排序规则来确定当多个图层和组合弧提供冲突的意见时,哪个意见应该胜出。
LIVRPS 代表什么
LIVRPS 是一个助记符,表示组合操作从强到弱的顺序:
- L - Local (Sublayers):在活动根图层栈中搜索直接意见
- 包括 Value Clips(值剪辑),它们比图层上的直接意见弱
- I - Inherits:搜索影响路径的继承,递归应用 LIVRP(无 Specializes)评估
- V - Variants:搜索影响路径的变体,递归应用 LIVRP 评估
- R - References:搜索影响路径的引用,递归应用 LIVRP 评估
- P - Payloads:搜索影响路径的有效载荷,递归应用 LIVRP 评估
- S - Specializes:搜索影响路径的特化,递归应用完整的 LIVRPS 评估,导致 Specializes 意见总是最后
注意:最近 OpenUSD 引入了一个新的组合弧叫 “relocates”(重定位),现在强度排序助记符变成了 LIVERPS(E 代表 rElocates),但本文仍使用传统的 LIVRPS 术语。
强度排序可视化
最强 ↑
│
├─ Local Opinions (Sublayers)
│ └─ Value Clips
├─ Inherits
├─ Variants
├─ References
├─ Payloads
└─ Specializes
│
最弱 ↓
└─ Schema Fallback Values
嵌套组合弧的解析顺序
重要的是要理解,当解析嵌套的组合弧时:
- 在最近的祖先父 prim 或 prim 本身上创作的弧/值剪辑元数据获胜
- “祖先弧”比”直接弧”弱
实际示例
假设我们有以下场景结构:
# shot.usd (Root Layer)
#usda 1.0
(
subLayers = [@shot_layout.usd@]
)
def "World" {
def "DukeCaboom" (
references = @DukeCaboom.usd@
) {
# Local opinion - 最强
double3 xformOp:translate = (10, 0, 0)
}
}
# shot_layout.usd (Sublayer)
over "World" {
over "DukeCaboom" {
# Sublayer opinion - 比 reference 强
double3 xformOp:translate = (5, 0, 0)
}
}
# DukeCaboom.usd (Referenced file)
def "DukeCaboom" {
# Reference opinion - 最弱
double3 xformOp:translate = (0, 0, 0)
}在这个例子中,xformOp:translate 的最终值是 (10, 0, 0),因为:
- Root layer 的直接意见最强
- Sublayer 的意见次之
- Reference 的意见最弱
组合弧的选择指南
根据不同的使用场景,选择合适的组合弧:
按用途选择
| 需求 | 推荐的组合弧 |
|---|---|
| 加载整个文件内容(轻量级,链接到其他文件) | Sublayers |
| 加载文件中特定层次结构(轻量级数据) | References |
| 加载文件中特定层次结构(重型几何数据) | Payloads |
| 在活动图层栈中加载现有子层次结构(带时间偏移) | References(内部) |
| 为多个(实例化的)prims 添加覆盖 | Inherits |
| 提供可被覆盖的基线值 | Specializes |
| 作为层次结构的变体 | Variants |
按时间偏移能力选择
支持时间偏移/缩放(通过 Sdf.LayerOffset):
- ✅ Sublayers
- ✅ References
- ✅ Payloads
不支持时间偏移:
- ❌ Inherits
- ❌ Variants
- ❌ Specializes
按目标类型选择
文件引用(可以指向外部文件):
- Sublayers
- References
- Payloads
内部引用(只能指向当前图层栈内的层次结构):
- Inherits
- Variants
- Specializes
- References(也支持内部引用)
实际应用场景和最佳实践
1. 多用户协作工作流
场景: 多个艺术家同时处理同一个镜头
解决方案: 使用 Sublayers
#usda 1.0
(
subLayers = [
@shot_animation.usd@,
@shot_lighting.usd@,
@shot_fx.usd@,
@shot_layout.usd@
]
)每个部门可以在各自的图层上工作,通过强度排序自然地处理覆盖关系。
2. 资产聚合和重用
场景: 构建由多个子资产组成的复杂资产
解决方案: 使用 References 和 Payloads
def "AntiquesMall" {
def "Props" {
def "Chair_01" (
payload = @chair.usd@
) {
double3 xformOp:translate = (0, 0, 0)
}
def "Chair_02" (
payload = @chair.usd@
) {
double3 xformOp:translate = (5, 0, 0)
}
# ... 更多引用
}
}3. LOD 和资产变体
场景: 一个资产需要多个细节层次或配置
解决方案: 使用 Variants
def "Tree" (
variantSets = ["LOD", "Season"]
variants = {
string LOD = "high"
string Season = "summer"
}
) {
variantSet "LOD" = {
"high" {
def "HighPolyGeo" { }
}
"medium" {
def "MediumPolyGeo" { }
}
"low" {
def "LowPolyGeo" { }
}
}
variantSet "Season" = {
"summer" {
color3f[] primvars:displayColor = [(0, 1, 0)]
}
"autumn" {
color3f[] primvars:displayColor = [(1, 0.5, 0)]
}
"winter" {
color3f[] primvars:displayColor = [(1, 1, 1)]
}
}
}4. 实例化覆盖
场景: 需要修改大量实例化资产的共同属性
解决方案: 使用 Inherits
class "_class_TreeMaterial" {
rel material:binding = </Materials/TreeMaterial_Autumn>
}
def "Forest" {
def "Tree_001" (
instanceable = true
references = @tree.usd@
inherits = </_class_TreeMaterial>
) { }
def "Tree_002" (
instanceable = true
references = @tree.usd@
inherits = </_class_TreeMaterial>
) { }
# ... 数百个实例
}通过修改 _class_TreeMaterial,可以同时更新所有树的材质,同时保持实例化的内存优势。
5. 默认值和模板
场景: 为资产提供默认配置,但允许完全覆盖
解决方案: 使用 Specializes
class "_template_StandardAsset" {
string assetInfo:version = "1.0"
string assetInfo:department = "modeling"
double size = 1.0
}
def "MyAsset" (
specializes = </_template_StandardAsset>
) {
# 任何在这里或更强图层中的意见都会覆盖模板
}组合弧的关键概念
1. 封装(Encapsulation)
当使用 References、Payloads、Inherits、Variants 或 Specializes 弧时,结果会被”封装”在使用弧的 prim 下。这意味着:
- 弧目标的内部结构对外部是隐藏的
- 可以在不影响弧目标的情况下重命名引用 prim
- 路径映射自动处理
2. 列表编辑(List Editing)
除了 Sublayers 外,所有弧都使用列表编辑操作:
- Prepend(前置):添加到列表开头
- Append(追加):添加到列表末尾
- Delete(删除):从列表中删除
- Explicit(显式):完全替换列表
3. 层栈 vs 单层
重要:组合弧目标是层栈(layer stacks),而不是单个层。这意味着它们会递归加载层中的所有内容。
4. 祖先数据流动
当弧目标非根 prim 时:
- 不会接收通常”流动”到层次结构下的父数据
- 例如:primvars、材质绑定或来自祖先 prims 的变换不会”继承”
- 会看到组合结果
调试和检查组合结构
USD 提供了多种工具来检查和调试组合结构:
1. usdview
使用 usdview 的 Composition 面板可以可视化 prim 的组合弧结构。
2. Python API
from pxr import Usd
stage = Usd.Stage.Open("myfile.usd")
prim = stage.GetPrimAtPath("/World/DukeCaboom")
# 查询组合弧
query = Usd.PrimCompositionQuery(prim)
for arc in query.GetCompositionArcs():
print(f"Arc Type: {arc.GetArcType()}")
print(f"Target Path: {arc.GetTargetPath()}")
print(f"Introducing Layer: {arc.GetIntroducingLayer()}")3. usd-inspect 命令
usd-inspect composition /World/DukeCaboom myfile.usd4. 检查属性来源
prim = stage.GetPrimAtPath("/World/DukeCaboom")
attr = prim.GetAttribute("xformOp:translate")
# 获取属性值的来源
stack = attr.GetPropertyStack()
for prop in stack:
print(f"Layer: {prop.GetLayer()}")
print(f"Value: {prop.Get()}")常见陷阱和注意事项
1. 循环依赖
避免创建循环引用:
A references B
B references A # ❌ 错误!
2. 过度使用 Sublayers
Sublayers 会增加图层栈的深度,过多的 sublayers 会影响性能。考虑使用 References 或 Payloads。
3. Inherits vs Specializes 混淆
- 需要强覆盖?使用 Inherits
- 需要弱默认值?使用 Specializes
4. Payload 管理
确保重型数据放在 Payloads 后面,轻量级元数据”lofted”到 payload 外部。
5. 变体性能
过多的变体或深度嵌套的变体可能会影响性能。保持变体结构简单和扁平。
总结
USD 的 Composition Arcs 是构建可扩展、协作友好的 3D 场景描述的强大工具。通过理解六种组合弧及其 LIVRPS 强度排序规则,你可以:
- 构建复杂的场景层次结构:从小型资产组装大型环境
- 支持多用户协作:不同部门同时工作而不冲突
- 实现非破坏性工作流:覆盖而不修改原始数据
- 优化性能:通过 Payloads 和实例化管理内存使用
- 提供灵活性:通过 Variants 支持多种资产配置
快速参考
| 组合弧 | 强度 | 时间偏移 | 主要用途 |
|---|---|---|---|
| Sublayers | 最强(L) | ✅ | 图层合并、多用户协作 |
| Inherits | 强(I) | ❌ | 类继承、实例覆盖 |
| Variants | 中(V) | ❌ | 资产变体、LOD |
| References | 中弱(R) | ✅ | 资产聚合、轻量级引用 |
| Payloads | 弱(P) | ✅ | 重型数据、可选加载 |
| Specializes | 最弱(S) | ❌ | 模板值、默认属性 |
掌握这些概念需要时间和实践,但一旦理解,你将能够充分利用 USD 的强大功能来构建复杂的生产管线。建议通过实际项目和 usdview 工具不断实验,加深对组合系统的理解。
参考资料:
- OpenUSD 官方文档:https://openusd.org
- USD Composition (Siggraph 2019):https://openusd.org/files/Siggraph2019_USD%20Composition.pdf
- Learn OpenUSD (NVIDIA):https://docs.nvidia.com/learn-openusd/
- USD Survival Guide:https://lucascheller.github.io/VFX-UsdSurvivalGuide/