USD概述
下面是关于USD主要功能的简要概述。该文档最初是一份内部备忘录,用来阐述USD的整体功能规划以及为什么需要这些功能,我们之前一直在比较内部使用的两个场景描述系统(Menva 和 TidScene),这也或多或少催生了开发USD的动机。而现在我们决定不在比较并将它们的功能结合在一起,于是就有了USD。当然其中也可能会有些功能令人困惑或者做的不够好,欢迎请在USD技术论坛中告诉我们,以便我们可以解释或者改善。
本文档主要关注USD的合成功能方面,因为它们可能属于最令人陌生的概念。未来也会给出新的文档来讨论USD场景图表的机制,以及几何体和着色架构的细节。
本页内容:
问题描述
为了能够快速、经济地利用行业创新技术,在皮克斯,我们需要能够轻松地将新应用接入生产管线,以补充我们现有的工具集(包括我们专用的绑定与动画系统Presto),而不是使生产管线变得更加复杂。
而实现这一目标遇到多方面的障碍。其中一个主要因素是,我们应用之间的数据流框架本质上是一个异常复杂的格式转换器,结果是同一种数据却生成了多种表现(存储)形式。随着向姿势缓存管线的过渡,我们开始将更多数据转为一种更简单、跨软件的格式(TidScene),但到这并没有带来多少改善。而我们需要解决以下几个关键问题:
- 我们没有一个好的数据模型来表示所有工作流程。许多工作流内部已经拥有良好的数据模型,具有强大的分层和覆盖功能,但不同工作流之间的数据模型差异很大,我们花费了太多的时间和精力来转换数据,从而才能在工作流和数据包之间传递信息。
- 多种格式的资产与镜头文件导致更多的处理时间(Maya文件只能用Maya处理,Menva文件只能用Presto处理)。
- 我们依靠复杂的构建流程来生成我们需要的所有数据表示(由我们计划在生产管线中使用的应用程序决定),并保持同步。
- 即使扩展构建流程,也不是所有资产数据都能在所有应用之间互通(出于各种原因),这意味着在序列或镜头中只有部分数据能够被覆盖。
- 我们一直将构建变体作为资产的重要功能,但随着我们新动画系统的部署,变体是实时的,只能在动画系统中访问。
- 由于最后两点,艺术家需要预先规划资产在构建流程中的用途,因为在生产管线的末端就无法统一修改这些资产了
目标
从较高层次来讲。USD项目的目标在于:
- 提供模型和镜头(几何体、材质着色等)统一描述形式(数据模型),让任何应用程序都可以使用。
- 提供多种引用程序之间的数据通信方式,不仅要支持在各种应用程序中对数据进行分层、无损的编辑操作,同时也能够支持传统而且更可控的塌陷或烘焙操作。
- 为生产管线中的应用软件制定严格的通信协议
- 允许资产组合(集合)和镜头按需更新,而不必重新生成整个数据集
- 提供一种快速简洁的表示形式,同时便于扩展到大场景(可以有效控制应用程序的内存占用,项目所有资产和镜头的磁盘存储大小,以及网络带宽和缓存效率),而且方便调试。
对于项目生产最重要的三点:
- 稳定
- 高效
- 简单
为什么使用USD而不是Alembic
在USD项目开始时,对于Alembic和两个使用中的场景描述系统,我们有考虑过是否要在这三者之间选择一个作为基础来构建我们的场景数据管线。 不过很明显,文件引用和无损编辑的能力对于实现上述的可扩展以及增量更新的目标至关重要。虽然Alembic提供了一个足够好的解决方案来表示塌陷、烘焙或者动态的场景描述,但因为它并没有文件引用或者编码操作的能力,所以无法成为我们的管线数据的基础。
不过这并不排除将来Alembic和USD合并为一个实体。在此之前,原生Alembic文件可以作为USD中引用操作符的输入——也就是说,在一个USD场景中,用关系图来表示其中的文件引用关系的话,任何叶子节点都可以是Alembic文件。
USD格式可以包含哪些数据
根据上述的目标,在我们的生产管线中,USD所包含的数据需要能够在绝大多数应用软件中使用。而该场景描述系统之外的数据则应该以最适合使用它的应用程序的格式存储(如:纹理与着色器定义),但是这些数据的呈现方式应该在USD中记录下来(如:一个指向本地文件的指针),从而一个USD文件就代表了最终的资产描述。
术语:什么是合成?
在本文档中,我们将讨论合成场景描述,合成功能,以及合成选项或者其他的结构化的场景描述单元。像是计算机图形学的许多术语一样,“合成”已经是一个通用且广泛使用的术语。对于USD,合成则表示“文件引用”和“分层”。合成行为遵循USD核心模块中定义的一套严格的规则,在本文档中,我们将概述这些行为所支持的特性。
动机:可移植的管线数据
这一节是对皮克斯管线中的对象进行的简要分类,因为他们与USD的目标息息相关。下面便是对资产结构的基本描述,而这些资产结构非常适合通过整个管线提供给每个应用程序使用。
模型:
- 包含几何体、着色器和纹理数据,以及其他结构化数据
- 可能包含多个变体(如模型变体,材质变体,LOD等)
- 可以由其它模型组合而成(通过引用其他集合或模型组)
- 可能涉及到用于特定软件的绑定文件,但是这些数据不属于组合模型
镜头:
- 由模型组合而成
- 包含时间采样动画
- 包含分层的特效编辑和动画
- 包含相机信息
- 可能也包含灯光以及合成信息
配置文件:
- 由层次化的键值对组成
- 层次化结构横跨整个工作室,直至每个制作单位、电影,每个产品、序列以及每个模型
初看的话,配置似乎是一个与USD管线数据完全无关的问题,但是在皮克斯有很多系统
都会将配置数据以引用的方式进行覆盖,而在整个管线中都需要获取到那些数据。当然
这里配置文件是作为一种可选项,可以根据实际需求决定是否使用。
数据存储功能
数值表示
USD应支持对时间变化、层次化、键值对数据的存储。存储的数据应为强类型,并支持创建特定作用域的数据(如几何体和着色器等)
受性能和可移植性的限制,随时间变化的数据应该基于顶点采样而不是基于样条线。原因是:
- 应用程序之间对于顶点的操作行为是最一致的
- 由于样条线需要根据时间进行求值,会产生性能消耗
- 表示方式更简洁,不需要插值,只需要表示每一帧的数据
- 计算插值似乎超出了场景描述系统的范围
采样数据的插值应该留给应用软件去处理;USD核心能够提供数据的时间采样解析,但是
插值并不应该属于核心功能
数据组合与对象模型
类型化以及基于采样的数据都是保存在属性中的。而一个基元(primitive,简称prims)对象包含了一组属性。除了属性,基元对象还能够包含其他的基元对象,从而允许我们构建基于命名空间且层次化的模型和镜头的表示形式。同时基元对象的设计模式中,属性也可以由多种类型的属性组成。
属性和基元对象都可以存放元数据,这些数据不会随着时间而改变;例如,属性的类型和文档都会编码为元数据,基元对象的设计框架也可以。
最后,对于组成一个场景的所有基元对象,按照它们的层级关系,最顶层的基元对象我们称之为舞台。舞台就是基元对象组成的场景图表,为场景构建提供了生命周期和制作方面的管理功能。
ASCII与二进制
基于数据的简洁描述以及方便调试的需求,ACSII码非常适合用于存储引用、变体以及小型数据。另一方面对于大型数据,我们也需要可扩展、高性能数据流送、随机读取的二进制编码。对于项目生产,更多的是对性能而不是易用性的需求,但是如果可以的话,两者都要。
USD包含了一个灵活的文件格式插件系统,允许对任一文件格式进行解析、动态翻译(如果需要的话),并合成USD文件。USD将始终提供完整且稳定的ASCII编码的表示形式,以及高效的二进制编码表示形式。我们已经发现ASCII编码非常易于调试,我们计划将其用于旧有资产的存档,这样就保证不断更新迭代的应用软件始终可以解析这些资产的数据,而无需对过时弃用的二进制文件继续提供支持。
合成功能
层
分层是最简单也是最基础的合成功能。场景描述中的分层在概念上跟Photoshop中的分层类似:我们可以为合成系统提供有序的输入图层列表,形成一个合并的图层数据视图。但是在Photoshop中,对于每个图层,同一个位置的像素拥有众多的混合方式,而USD对于场景数据则只支持少量几种合并操作(并不是因为有什么限制,而是有助于保持合成场景的可理解性)。除了一两个特殊的场景描述元素(其中一个是列表操作,下面会讲到),图层中的绝大多数数据都能够接受有序的合并操作。
通常来讲,一个最顶层的图层会指定一个有序列表,列表中就包含了合成这个顶层图层的所有子图层。所有对这个顶层图层的引用(通过动态合成)都会自动将它的子图层也包含进来。我们有时候把一个图层及其子图层(递归的)称为图层堆栈。
上面的图片就演示了通过分层并根据部门和工作流程来组织场景描述的方式。Shot_Layout.usd层包含了一个镜头里的所有角色,该层由Layout部门负责创建与制作。而Shot_Sets.usd层则专门由Sets部门负责。每个部门都可以按照自己的进度独立工作,而不会影响到对方的数据。
工作流程:
- 拆分、整理序列和镜头数据并分配给各个部门
- 模型内部进行拆分整理——按照模型的制作流程可以分为几何层、材质着色层、绑定层
- 特效动画分层
- 特效细分编辑分层
- 模拟动画分层,例如树木动态动画
- 灯光分层
激活
一个舞台上的基元对象可以处于两种状态之一,即活动状态或停用状态。当一个基元对象被停用时,那么它的子图表都会从合成图表中剔除,同时该基元对象便不会参与大多数场景图表的行为(比如默认情况下它不会被列在其父层的子列表中)。而激活状态也是一种属性类型同其他属性一样可以参与合成,一个基元组件在引用链的不同部分,可以同时处于停用或者重新启用状态。
工作流程:
- 调试:渲染单个角色或道具比渲染整个场景要快得多。激活选项就可以让用户决定一个子图表是否要从场景中剔除,而不需要在重新生成一个场景。
- 资产重组:在某些情况下,你可以快速剔除引资产的一部分,而不必引用新的资产
- 封装变体:如果对于一个模型的几种变体之间的差别主要在于一小部分几何结构(例如一个有把手的杯子和没把手的杯子),我们可以在一个层里面定义所有相关的几何体(这样也更方便应用材质着色)并添加一个变体集合,该变体集合提供一个激活选项来控制切换变体。
覆盖式引用
除了分层,另一个最基本的合成机制就是引用了。他允许实例化一个外部的图层的场景描述,而不是把内容简单复制到引用它的图层中。这个跟C++代码中的头文件包含类似,而且也是递归性质的。
一个引用由以下部分组成:
- 用于识别所引用的外部文件的资产路径
- 用于放置引用的场景描述的本地场景路径(例如 /World/anim/chars/Buzz)
- 一个远程场景路径(在外部文件中)用于从中提取信息
- 应用在所有随时间变化的数据的时间偏移与比例,用于动画时间重定
一旦合成了一个场景时,本地场景路径放置引用的层的位置会替换掉远程场景路径名称,被引用的层的子图表看起来就好像出现在了引用的位置。下面的图片展示了一个无覆盖的简单引用的例子:
该示例中展示的是一个镜头引用一个模型,当然模型也可以引用模型或者一个模型集合。
“列表操作”和引用列表编辑
所有的基元对象都可以引用任意数量的层,被引用的图层的相对强度是按照资产路径在引用列表中的顺序定义的。由于“引用”实际上就是一些列表,USD提供“列表操作(List Op)”,可作用于所有图层,用来编辑一个基元对象中的引用列表。在图层堆栈中的任何子层中,给定一个基元对象的命名空间位置,我们可以进行添加、删除或者给基元中的任何引用进行重新排序;这些列表编辑操作会以子层的强度顺序的相反顺序应用在引用列表上面。
覆盖引用的值
如果主引用层的子图表中的某个属性与被引用层中基元的某个属性重合,那么这些属性的数值就会被合成,首先会检查主引用层的数值,然后在检查被引用层的数值。下面图中就是顶点数值被覆盖的例子:
镜头动画中的顶点数值覆盖了被引用图层的顶点数值,但是其余的属性不变
工作流程:
- 构造集合
- 构造模型组——在皮克斯我们会在一个“角色组”中引用一个角色模型及其所有衣服布料、道具以及辅助模型并发布出去
- 构造镜头——在镜头中一个姿势缓存引用了模型,仅覆盖与镜头中的值不同的属性。在皮克斯我们选择允许这些引用的层保持活动状态,从而无需重新烘焙姿势缓存就能获取资产更新,当然这个是非必须的;引用可以以各种方式本地化为姿势缓存,更好地把控本地化和文件尺寸之间的权衡
- 增量姿势缓存——允许镜头在播放动画时单独缓存每个模型
变体
通常对于一个模型我们需要为它制作多种不同的几何结构或材质着色。而我们并不是针对每种变化都单独制作一个副本,而是在场景描述中声明一组变体,之后就可以在场景中按需要选择一个变体参与合成。USD中的变体是用变体集(VariantSet)来声明的,它用来定义多个变体,每个变体都会分别呈现不同的外观。变体集可以通过变体选择选项来指定一个(也只能选择一个)变体
- 变体集——一组变体的名称,比如,对于一个咖啡杯可能有带把手的和不带把手的,那么就可以创建一个变体集叫做“把手”,里面包含“有把手”和“无把手”两个变体。
- 变体——一个变体提供一种外观,就像带把手的咖啡杯。变体集里的每一个变体都可以在所属的变体集命名空间中的任何位置覆盖或创建场景描述——变体并不需要具有相类似的特征,不过在皮克斯,一组变体通常都具有相类似的特征
- 变体选择——每个变体集都有一个选项用来指定一个变体参与场景合成
变体组合
变体的真正优势在于,我们可以在一个基元对象(在皮克斯管线中,通常在模型的根基元
对象中)中定义多个变体集,每个变体集指定的变体便以一种非常直观的方式组合在一起。
多个变体集可以处于同一层级,在这种情况下它们在场景描述中的排列顺序就决定了各自
的相对强度(以应对多个变体集对同一个属性都提供了选项的情况)。但是变体集还可以
嵌套,比如每个模型变体拥有多个LOD变体,那么我们就需要将LOD变体集合嵌套在模型变
体集合下。
变体示例在文档结尾处的管线数据示例一节
类与继承
像Katana中的CEL表达式能够使用少量的逻辑对许多属性进行模式化批量编辑,我们发现将此类编辑操作表现为场景描述会非常有用,当数据沿着管线流送下来时,编辑操作可以保持实时并且可修改(但是不可覆盖!)。
在USD中,任何基元都可以继承一个或多个类基元,并且会继承它们的命名空间层级结构以及属性值。类基元是一种特殊的基元,因为它是”抽象的”而且不会被纳入渲染;类基元可以拥有任意数量的和任意类型的子基元,也可以在其中定义任意数量的属性或合成操作符。如果类基元定义了子基元,那么这些子基元都会作为继承该类基元的子基元被实例化。
USD中的继承对比OOP中的继承
熟悉C++这样的面向对象语言的技术美术与工程师可能会好奇USD中的类与编程语言中的类
有什么联系。在C++中,当继承一个基类时,我们会继承基类的行为,同时子类可以覆盖
基类的行为。
虽然USD用户可能会根据解析得到的基元的类型名元数据(用来确定其设计模式)来修改
其中存放的数据,但是USD中包含的场景描述除了本节内容中列举的通用合成行为之外,
并没有定义其它的行为。因此USD中的类继承只是继承结构化数据。USD中的类提供了一种
简洁明了的数据组织方式,适用于场景描述中类的大量实例。“完全烘焙”的导出过程可以
选择将类中的数据塌陷到每个实例,尽管我们可以选择分别将每个类的相关属性“本地化”
到导出文件中,但完全塌陷这些类并没有带来多少好处,而且可能会增加文件尺寸。
类还有一个很有用的功能就是类的定义可以连同深层嵌套的引用结构一起打包。例如下面的图表,我们可以继承“_class_Book”声明叫做“Book”的资产(该_class_Book可能继承自_class_Prop,而_class_Prop则可能继承自“_class_Model”,在皮克斯一般类的层级深度为4)。在Shot_Sets.usd层中,我们已经引用了“Book”资产并创建了三个书的实例,前面两本书的“BookCover”属性的值会从“Book”资产中继承,而对于第三本书我们直接指定为红色。
值得一提的是最后一点,因为识别并保留类中的例外属性值的能力有时候不适用于基于模式匹配的批量编辑。
模型层级
我们已经讨论了管线中的资产分类,包括模型和模型组。USD允许您扩展此分类(使用可自定义的类型层次结构),但这两个”核心类别非常重要,因为它们是定义任何聚合、序列或镜头中的模型层次结构的基础。
支持模型层次结构概念的主要原因在于,有许多常见和重要的任务可以只在由模型的根基元对象表示的模型“接口”上执行,而且很少会出现所有镜头同时加载到内存的情况。模型层次结构为我们提供了一个自然和方便的粒度来管理我们需要操作的场景的”工作集合”。
舞台载入与引用
当一个场景作为舞台上打开时,我们可以选择加载并合成(”加载”)整个场景呈现出来,或只是加载模型层级结构,后者可以非常快地完成并只消耗少量内存。当我们打开“未加载”的舞台之后,可以自由选择加载单个模型或则一组模型。模型层级的概念,以及低延迟内省模型层级的能力,在皮克斯的管线中非常重要。为了使模型层次结构尽可能高效和低延迟,我们引入了一种称为“有效载荷”的合成功能。有效载荷是一种特殊的引用,它所指向的内容在场景加载的初始阶段会被忽略;在舞台的模型层级视图中,必须明确指定加载一个模型来合成根基元对象,从而提取模型基元有效载荷所指向的场景描述。与所有合成功能一样,在管线中你可以自由选择是否启用有效载荷。不使用有效载荷的唯一后果是,模型层级结构可能会变得延迟更高和更庞杂,因为仅仅只是为了合成每个模型实例的根基元,就得打开所有被引用到的资产场景描述。而使用有效载荷时,模型层级结构中的每个模型可以只显示一个非常小的“资产接口”。