今天分享的是Pixar在Siggraph2019上关于USD的分享,整个系列包含五篇文章,这是第二篇,其他几篇可以通过下面的链接访问,PPT地址见参考文献[1]。
第一篇 【Siggraph2019】USD Introduction and Overview
第三篇 【Siggraph2019】USD Authoring and Advanced Features
这篇文章主要介绍USD的Composition System,文章将从如下几个方面展开:
- Overview
- Composition Behavior
- Pcp
- Inspecting Composition Structure

USD的Composition System已经有非常古老的历史了,其最开始是Pixar的Presto动画软件的一部分,之后经历了多次迭代与优化,目前已经是Presto跟USD的核心组件了。
Composition的作用是将多个文件(Layer)的Scene Description组合成一个完整的系统(view),这种做法对于内容生产管线具有很大的作用,比如可以支持多人对同一个文件进行修改等,同时还可以通过一个个的小物件拼装成一个大物件(比如前面第一篇我们介绍过使用reference composition arc可以实现多个物件的组装),此外,还支持对原始数据进行非破坏性覆盖(即在需要的时候随时可以恢复原始数据)。
前面我们介绍过Reference Composition Arc,与之相对等的还有很多其他的Composition Arc,这里的Arc可以看成是一种操作符(operator):
• Sublayers
• References
• Payloads
• Variants
• Inherits
• Specializes
Composition就是通过一系列的操作符来完成对Scene Description的修改的,而在运行时,Composition Engine则会对Scene Description中的操作符进行解析并按照规则将最终的整体场景效果呈现给用户。
下面我们将对上面列举的操作符做简单介绍。
1. Sublayers
shot.usd
-----------------------
#usda 1.0
(
subLayers = [
@shot_layout.usd@,
@shot_sets.usd@
]
)
Sublayer是最简单的composition arc,通过sublayer,我们可以实现layer之间的层级组装,如上面案例中所展示的,这里有一个root layer名字叫shot.usd,在其下增加了两个sublayer,分别较shot_layout.usd以及shot_sets.usd。

root layer以及按照顺序排列的sublayer可以组成一棵树,对这棵树按照深度遍历可以生成一个Layer Stack,处于Layer Stack顶端的Layer,其对某个属性的修正的话语权要高于Stack底端的Layer(即如果多个Layer对同一个属性进行修正,以最层的Layer为准)。

上图中左侧是sublayer的scenegraph,而右侧则是经过组装后的scenegraph,通过这种做法,可以支持多人对scenegraph进行编辑(比如每个人只编辑属于自己的sublayer就可以了)。

这里介绍了多个layer对同一个属性进行修改时,应该以哪个为准,可以看到shot.usd的属性具有更高话语权。
2. Reference
shot_layout.usd
--------------------------------
def “World”
{
def “chars”
{
def “DukeCaboom”
(
references =
[
@DukeCaboom.usd@</DukeCaboom>
]
)
{
}
}
}
sublayers是layer级别的组装与重用,而reference则是将重用粒度缩小至prim级别,通过reference,我们可以在layer中使用另一个layer中定义的prim。

这个是对上面一段代码的组合效果的解释。

另外,跟sublayer一样,reference对同一属性也是可以进行覆盖的,其中引用者的话语权高于被引用者,如上图所示,另外这里的示例没有展示的是,我们还可以对同一个prim进行多次引用,每个引用都可以有自己独立的override Attributes设置,相当于在一个layer中放置了多个prim instance。
3. Payloads

Reference允许我们将多个小物件组装成一个大物件,但是有时候我们可能不希望所有的数据在运行时都是一次性加载完成的(性能考虑),而是希望如UE的Streaming机制一样,在需要的时候才加载,这时候我们就需要用到Payloads Composition Arc了。通过payloads操作符指定的数据可以在运行时通过多个API进行选择性的加载或卸载。


Load之后,其表现跟之前的Reference是一致的,而没有Load之前,相当于Reference没有使用,因为整个过程是在运行时发生的,因此在编辑阶段并没有其他额外工作。

虽然Payload在项目中使用频率较高,但是有时候我们也希望一些数据是常驻的,即使Payload没有Load的时候依然可见,这时候可以使用上述的Reference跟Payload结合的方式来实现。
4. Variants
DukeCaboom.usd
--------------------------------------
def “DukeCaboom” (
variantSets = [“Cape”]
variants = {
string Cape = “WhiteCape"
}
)
{
variantSet “Cape” = {
“RedCape” {
def “RedCape” { }
}
“WhiteCape” {
def “WhiteCape” { }
}
“NoCape” {
}
}
Variants支持将一系列的可选项塞到一个set(集合,如上面代码中的Cape VariantSet)中,之后根据需要按照一定的规则选择其中的一个Variant来使用(如上面按照名字使用了WhiteCape)。

最终的SceneGraph效果如上图所示。

当然,既然是可选的集合,那么就应该支持动态对选项进行修正,如上图所示,通过等号完成了Cape的更换。另外,这里需要说明的是,Prim支持同时拥有多个VariantSet,每个Set可以用于实现不同属性的可选修正,从而实现对Attributes的更为精细的操控调整。
5. Inherits
SciFiBook.usd
------------------------
class “class_Book”
{
}
def “SciFiBook” (
inherits = [
</class_Book>
]
)
{
}
Inherit提供了C++一样的属性继承,通过继承可以实现对原始数据的重用,并在此基础上增加新的数据设定,从而降低代码的冗余(这里需要注意,Parent Class的属性值具有更高话语权优先级,即如果我们对Subclass的继承属性进行修正,同级别中,属性以Child为主,但是如果Parent Class在更上层对此值进行了覆盖,那么就以Parent的为主)。



上面三张图描述了Inherit的作用,通过将共有属性放在parent中,来减少subclass的重复工作。

将之整合到shot_set中,并在上层做更高优先级的覆盖。
6. Specializes
SciFiBook.usd
----------------------------
def “SciFiBook”
{
def “Materials”
{
def “Paper”
{ }
def “NotePaper” (
specializes = [
</SciFiBook/Materials/Paper>
]
) { }
def “GlossyPaper” (
specializes = [
</SciFiBook/Materials/Paper>
]
) { }
}
Specializes是最近添加的用于在USD中定义材质库的composition arc,其使用方法跟Inherit类似,不过其属性覆写优先级规则有所不同,通过Specializes引入的prim,其原始话语权要弱于当前使用Specializes的Prim(即被Specialize的Prim的属性是可以被使用Specialize的Prim的属性设定所覆盖的,即Parent在上层所覆写的属性数据不会影响到底层Specialize的Child设定的属性)。

如上图所示,NotePaper跟GlossyPaper都是继承自Paper,而GlossyPaper对shininess进行了修正,所以结果如右图所示。

如果我们在更高级别对Parent的属性做了覆写,那么Child的属性设定就不生效了,那么如果我们希望保留GlossyPaper的属性要怎么做呢,这时候就需要用到Specialize了。

对上面的属性覆写优先级做一下总结:

总的来说,用首字母表示就是LIVRPS,发音为liver-peas。

前面介绍的是上层的使用与表现逻辑,下面我们来深入一下代码细节。Composition Engine是在一个叫做Pcp的库中实现的,Pcp是Prim Cache Population的缩写,这个模块用于对整个Scene Graph中的Composition Arc进行评估,并为每个Prim生成一个索引,这个索引用于指明当前Prim以及其对应的Properties的数据应该以哪个地方的覆写为准。

Prim Index可以用一张有向(有序)图来表示,图中的节点表示的是layerstack,也对应于一个个对Prim的覆写数据的来源,其中连接各个节点的箭头线条就是引入这个覆写的一个Composition arc,这也是为什么叫做Composition Arc的原因。
在代码上,prim index是由PcpPrimIndex class来表示的,有多个API可以实现对这个数据的访问。

大部分时间(99%),我们都不需要直接跟Pcp打交道,只需要知道一个USD Composed SceneGraph View就够了,不过也不排除有时候有些需求需要用到,下面会介绍如何使用Pcp。
获取Prim Index最简单的方式就是通过usdview中的Composition Tab来实现,这个会展示一个层级组织的prim index。
def PrintPrimIndex(primIndex):
def _PrintNode(node):
print(node.arcType)
print(node.site)
for child in node.children:
_PrintNode(child)
_PrintNode(primIndex.rootNode)
PrintPrimIndex(usdviewApi.prim.GetPrimIndex())
此外,如上面代码所示,使用Usd以及PcpPrimIndex API也可以实现对graph structure的数据遍历。

如果你想要得到一个图形化的prim index,可以使用上面说到的API导出一个graphviz.dot文件,之后就可以将之转换成上面图片中展示的图形效果了。

最后,也可以打开TF_DEBUG宏,这个宏打开后会为每个Composition step生成一个.dot文件,之后就可以将这个文件转换为一个image sequence。
以上就是Composition的所有内容了。
网友评论