OpenUSD execIr 库详解:用 Invertible Rigs Schemas 构建可求逆的动画绑定系统
【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD
本篇技术指南以 OpenUSD 仓库中的 pxr/exec/execIr/README.md 为骨架,系统讲解 execIr(Invertible Rigs)库的 schema 体系、可求逆绑定(Invertible Rigs)的工作原理,以及在 OpenExec 中通过ComputeWithOverrides()执行正向求值与反向求值的完整流程。读完本文,你将掌握ExecIrController/ExecIrFkController/ExecIrSwitchController/ExecIrJointScope四种 schema 的属性语义、irRole元数据体系,并能理解"切换补偿(switch compensation)"与"IK 转 FK"等动画制作工作流背后的求逆机制,可以直接对照仓库源码与测试用例深入实践。
一、execIr 是什么:定位与现状
execIr(Invertible Rigs,可求逆绑定)库是 OpenExec 体系的一部分,构建在 execUsd 之上(源码位置:pxr/exec/execIr)。它定义了一组 USD schema,用于构建可求逆的绑定(invertible rigs)——即既能正向驱动模型骨架摆出姿态(pose),又能反向从期望姿态反解出驱动输入值的绑定网络。这种反向求解能力是某些动画制作工作流(如切换补偿、IK 转 FK)的必需条件。
⚠️ 重要提醒:execIr 目前仍处于早期阶段。仓库中所有 schema 与 API 均明确标注"功能非常有限、随时可能变更、尚不适合生产使用"(见 pxr/exec/execIr/controller.h 中的
@warning注释,以及 schema.usda 中各 schema 的doc元数据)。此外,OpenExec 目前尚未原生支持求逆,execIr 通过插件计算(plugin computations)临时实现求逆能力——computations.h 中的注释明确指出,这些计算未来将被移入ExecBuiltinComputations,等 OpenExec 原生支持求逆后替换。
在仓库中的代码组织上,execIr 的构建入口为 pxr/exec/execIr/CMakeLists.txt,schema 定义于 schema.usda,生成后的 schema 类见 generatedSchema.usda,针对每种 schema 的计算注册(computation registration)分别位于*Computations.cpp文件。
二、Schema 体系总览
execIr 库提供的核心 schema 有四类(定义均出自 schema.usda):
| Schema | 类型 | 职责 |
|---|---|---|
IrController(ExecIrController) | 抽象基类 | 定义"控制器"的通用契约:显式输入/输出属性 + 正向计算 + 反向计算 |
IrFkController(ExecIrFkController) | 具体类 | 由标量输入属性计算简单正向运动学(FK)变换 |
IrSwitchController(ExecIrSwitchController) | 具体类 | 根据switch属性值从多个输入中选一个透传到输出,可将多个 switch 控制器串联以在多个子绑定之间切换 |
IrJointScope(ExecIrJointScope) | 具体类 | 构成模型动画师接口的 prim 层级,承载标量avars(动画作者写入的属性和矩阵属性) |
其中IrJointScope继承自抽象类IrXformable(ExecIrXformable),后者才是动画接口的核心——它定义了 rest / default / posed 三类空间以及avars:命名空间。
2.1 控制器(Controller):可求逆的核心抽象
ExecIrController是抽象 typed schema,继承自UsdTyped(见 controller.h,schemaKind为AbstractTyped)。它的契约在 schema.usda 的IrController类中定义:
控制器具有显式的输入与输出属性、一个"由输入值计算输出值"的正向计算(forward computation),以及一个"由一组期望输出值反解输入值"的反向计算(inverse computation)。
也就是说,控制器的本质是一个"输入 avars → 输出 space 矩阵"的函数,且该函数可逆。schema.usda 中用irRole元数据区分属性在求逆体系中的角色:
InputAttribute+irIsInvertible = True:可逆输入属性,是反向计算要解出的对象;InputAttribute+irIsInvertible = False:不可逆输入属性(如父空间),只提供正向/反向计算的辅助信息;OutputAttribute+irIsInvertible = True:可逆输出属性,反向计算的求解目标(期望值);SwitchAttribute:切换属性,其值改变正向/反向计算的行为;PassthroughAttribute:透传属性,输出值直接等于输入值。
2.2 FK 控制器:最基础的标量驱动变换
IrFkController(schema.usda)是最简单的具体控制器,由一组标量平移/旋转属性计算出一个 4×4 姿态矩阵。其完整属性清单如下:
| 属性 | 类型 | 默认值 | irRole | 说明 |
|---|---|---|---|---|
parentIn:space | matrix4d | 单位矩阵 | InputAttribute(不可逆) | 父控制器的 posed space |
parentIn:defaultSpace | matrix4d | 单位矩阵 | InputAttribute(不可逆) | 父控制器的 default space |
in:defaultSpace | matrix4d | 单位矩阵 | PassthroughAttribute | 摆姿的"零位"局部到世界变换 |
out:defaultSpace | matrix4d | 单位矩阵 | PassthroughAttribute | 透传in:defaultSpace |
in:tx / in:ty / in:tz | double | 0 | InputAttribute(可逆) | 局部平移量 |
in:rx / in:ry / in:rz | double | 0 | InputAttribute(可逆) | 局部旋转量(角度) |
in:rspin | double | 0 | InputAttribute(可逆) | 求解旋转时被置零的旋转属性 |
in:rotationOrder | token | "XYZ" | SwitchAttribute | 旋转顺序,可选XYZ/XZY/YXZ/YZX/ZXY/ZYX |
out:space | matrix4d | 单位矩阵 | OutputAttribute(可逆) | 计算产生的输出空间矩阵 |
parentIn:space的存在使一个 FK 控制器可以作为另一个的"子节点"串成层级——父子 FK 控制器层级的行为类似传统变换层级,子节点继承父节点的变换,整个控制器网络可集体驱动一个模型(或其一部分,如手臂)摆姿。
源码印证:在 fkControllerComputations.cpp 中,通过EXEC_REGISTER_COMPUTATIONS_FOR_SCHEMA(ExecIrFkController)注册计算:
EXEC_REGISTER_COMPUTATIONS_FOR_SCHEMA(ExecIrFkController) { ExecIrControllerBuilder builder(self, &_Compute, &_Invert); builder.InvertibleInputAttribute<double>(ExecIrTokens->inTx); builder.InvertibleInputAttribute<double>(ExecIrTokens->inTy); builder.InvertibleInputAttribute<double>(ExecIrTokens->inTz); builder.InvertibleInputAttribute<double>(ExecIrTokens->inRx); builder.InvertibleInputAttribute<double>(ExecIrTokens->inRy); builder.InvertibleInputAttribute<double>(ExecIrTokens->inRz); builder.InvertibleInputAttribute<double>(ExecIrTokens->inRspin); builder.NonInvertibleInputAttribute<GfMatrix4d>(ExecIrTokens->parentInSpace); builder.NonInvertibleInputAttribute<GfMatrix4d>(ExecIrTokens->parentInDefaultSpace); builder.InvertibleOutputAttribute<GfMatrix4d>(ExecIrTokens->outSpace); builder.SwitchAttribute<TfToken>(ExecIrTokens->inRotationOrder); builder.PassthroughAttributes<GfMatrix4d>( ExecIrTokens->inDefaultSpace, ExecIrTokens->outDefaultSpace); }正向计算_Compute利用 utils.h 中的工具函数合成矩阵:ExecIr_UtilsComputeLocalTranslation求局部平移向量、ExecIr_UtilsComputeLocalRotation按旋转顺序求局部旋转(GfRotation),最终ExecIr_UtilsCompute把ExecIr_UtilsParams(起始空间、平移朝向、旋转朝向)+ 局部变换合成为输出空间。反向计算_Invert则调用ExecIr_UtilsInvert,以期望的posedSpace为输入,反解出满足该姿态的 tx/ty/tz/rx/ry/rz 等 FK 属性值。
2.3 Switch 控制器:子绑定选择器
IrSwitchController(schema.usda)根据switch属性的值,从两个输入空间中选择一个透传到输出:
| 属性 | 类型 | 默认值 | irRole | 说明 |
|---|---|---|---|---|
switch | token | "rig1" | SwitchAttribute | 当前选中的子绑定,仅允许rig1/rig2 |
rig1:space | matrix4d | 单位矩阵 | InputAttribute(可逆) | rig1 的 posed space |
rig2:space | matrix4d | 单位矩阵 | InputAttribute(可逆) | rig2 的 posed space |
out:space | matrix4d | 单位矩阵 | OutputAttribute(可逆) | 输出所选输入的值 |
多个 switch 控制器可以"串联成组(ganged)",共同在两组子绑定之间切换——一组子绑定在某个时刻提供模型的姿态。从 switchControllerComputations.cpp 的源码可见:正向计算_Compute读取switchtoken,若为rig1则透传rig1Space,若为rig2则透传rig2Space;反向计算_Invert则把期望输出反解回当前被选中的那个 rig 输入("best try"式求解:只求解当前激活子绑定的输入值)。
⚠️ 当前限制:该 schema 硬编码为仅支持两个分别名为
rig1、rig2的绑定。schema 文档说明未来会通过 multi-apply schema 支持任意数量、任意命名的绑定切换。
2.4 JointScope 与 Xformable:动画师接口
IrJointScope(jointScope.h,schemaKind为ConcreteTyped)继承自抽象类IrXformable,表示基于骨架(armature)模型的关节,主要用于特化引导(guide)的绘制。其核心价值来自基类IrXformable定义的属性与语义。
三种空间(schema.usda 中IrXformable的# Spaces一节):
- Rest Space(静止空间):对象"未摆姿时"的局部到世界变换,可理解为变形(deformation)的"绑定姿态(bind pose)"。变形使用 restSpace 与 posedSpace 的局部差异来变形模型。注意:rest space 始终会被正交归一化(orthonormalized)。相关属性为
rest:tx…rest:rz标量与合成的rest:space矩阵。 - Default Space(默认空间):摆姿的"零位"局部到世界变换。它允许与 rest pose 不同,以便为角色提供默认缩放、制作变体(variants),或为动画控制设置更自然的起始位置。相关属性为
default:tx…default:rz标量与合成的default:space矩阵。 - Posed Space(摆姿空间):摆姿后的最终世界空间变换,通常是某组控制器(IK、FK 等)的计算结果。相关属性为
posed:space矩阵与posed:defaultSpace(几乎总是等于default:space,仅在某些特殊场景如 re-rooting 时由控制网络改写)。
avars:命名空间(动画变量):avars:tx/ty/tz/rx/ry/rz(double,默认 0)、avars:rspin(求解旋转时被置零的旋转 avar)、avars:rotationOrder(token,可选六种旋转顺序,仅影响 posed space 的计算,不影响 rest/default 空间)以及avars:defaultSpace、avars:unitScaleFactor(规范单位的大小,供控制器保持尺度无关性,如"平移 1.0 等于在世界空间移动这么远")。
与控制器网络的连接约定:一个或多个控制器连接到avars:命名空间下的一个或多个 avar,计算 posed space,再回写到posed:space属性。若没有外部控制器,avars:下的属性不起任何作用——此时ExecIrXformable会回退为跟随父级的 posed space(并计入局部 rest 与 default 偏移)。
父子与层级:IrXformable还提供parent:space、parent:defaultSpace矩阵属性,用于层级继承。
JointScope 的引导绘制属性:guide:length(double,默认 0)、guide:displayColor(color3f,默认(1, 0.3, 0.3))、guide:displayOpacity(float,默认 0.5)。
三、可求逆绑定(Invertible Rigs)的工作原理
一个由 execIr schema 构建的可求逆绑定由两部分组成(对应 README 的 "Invertible Rigs" 一节):
- joint scope prim 层级:作为动画接口,承载标量 avars(动画作者写入动画的地方)与矩阵属性(其计算值通过属性连接来自控制器的输出变换);
- 控制器网络:控制器的输入属性通过连接(connections)接收 joint scope avars 的值。网络通常包含多个子网络,每个子网络是一个实现特定摆姿方案(posing scheme)的子绑定。switch 控制器负责选择当前激活的子绑定,其
switch属性同样通过属性连接接收(可动画化的)avar 值。
3.1 正向与反向计算
- 正向:以 avar 值为输入,产生一组代表模型骨架摆姿配置的变换值。
- 反向(Inversion):网络接收一组期望变换值,反解出能产生该姿态的输入值。当然,前提是期望姿态可达;当姿态不可达时,控制器逆会产生"最佳尝试(best try)"解——见 controllerBuilder.h 中关于
computeInvertedForwardValue的注释:当控制器逆无法同时满足所有期望输出值、且控制器之间存在依赖时,通过计算"反转的正向值"让网络产生 best try 解。
3.2 求逆的两个关键制作工作流
切换补偿(Switch Compensation):动画师想在连续动画过程中更换控制摆姿的绑定。例如,动画第一段用 FK avars 插值(产生平滑旋转弧线),下一段改用 IK avars 插值(产生线性末端执行器运动)。若直接动画 switch 属性值,模型姿态通常会不连续跳变。要产生连续运动,可在切换时刻调用求逆,反解出维持当前姿态所需的输入 avar 值——这正是 testExecIrSwitchCompensation.cpp 测试的主题。该测试场景定义了一个 "waddler"(摇摆器)绑定:两个 JointScope 配一个 SwitchController,当 switch 为rig1时 Joint1 作为父级控制摆姿、Joint2 继承变换;为rig2时角色互换。旋转 avar 绕当前父 JointScope 的原点旋转,使绑定能交替绕两端"摇摆"。
"IK 转 FK"(IK for FK):动画师在摆姿阶段直接用 IK 操纵末端执行器位置(使用 IK avars),但把动画写入 FK avars 以便用 FK 插值。流程为:先用 IK 绑定基于末端执行器位置计算摆姿空间,然后调用求逆,把计算出的空间值作为期望值,反解 FK 绑定的输入 avar 值。测试 testExecIrFkController.cpp 与 testExecIrJointScope.cpp 覆盖了 FK 正向/反向计算与 JointScope 层级的基本行为。
⚠️ 注意:尽管 IK 是许多可求逆绑定典型用例的核心,execIr 目前并不包含任何 IK 求解器 schema(README 的 note 明确指出)。
四、在 OpenExec 中调用求逆:正向与反向 API
4.1 正向:直接计算posed:space
使用 OpenExec API 计算正向姿态非常直接:只需请求计算每个 joint scope 上的posed:space属性值。由于属性连接的数据流特性,这会传递性地计算posed:space所连接的控制器输出属性的值;而控制器输出属性带有表达式(expressions),能在给定场景中已写入的输入值时,产生控制器正向计算的结果。
4.2 反向:ComputeWithOverrides()与 transient overrides
反向计算需要一个途径来指定期望的骨架姿态。不能通过在场景中写入(authoring)属性值来实现,原因有三:
- 这些值是瞬时数据,本就不应长期留在场景里(会产生"场景垃圾"scene cruft);
- 若清理不干净,残留的已写入值会导致错误结果;
- 写入会引发 exec 网络失效(invalidation),导致下游缓存被不必要地刷新。
因此,OpenExec 提供了专门 API 来指定overrides——在单次求值期间瞬时注入 exec 网络的覆盖值。核心函数为:
ExecUsdSystem::ComputeWithOverrides()它接收两个关键参数:
ExecUsdRequest:包含指示"要计算什么"的 value keys。在求逆场景中,providers 使用需要反解的输入 avars,再加上一个特殊的computeDesiredValue计算(computations.h 中的ExecIrComputationsType::computeDesiredValue),以表示"我要调用求逆";std::vector<ExecUsdValueOverride>:指定要覆盖的 value keys 及其值。求逆场景中,用posed:space属性作为 providers,配以explicitDesiredValue计算(ExecIrComputationsType::explicitDesiredValue),表示"这些属性的期望值被显式覆盖"。
4.3 两个关键 computation 的语义
computations.h 定义了 ExecIr 全部 schema 提供的两种计算(均由ExecIrComputations静态实例导出):
computeDesiredValue:与内置的computeValue相对。computeValue从上游已写入值产生属性的计算值;而computeDesiredValue产生的是"为了达到期望的下游计算值、必须写入的值"——即求逆的目标输出。它设计为与ComputeWithOverrides配合:request 中包含一个或多个computeDesiredValue的 value keys,期望的下游计算值则以explicitDesiredValue的 overrides 形式指定在下游属性上。explicitDesiredValue:为 ExecIr 中所有属性定义。它不依赖其他计算,总是计算为空值,除非在ComputeWithOverrides调用中被显式 override。
controllerBuilder.h 中的模板实现展示了底层的接线逻辑:每个可逆输出属性都注册了explicitDesiredValue(回调直接SetEmptyOutput())与computeDesiredValue(回调_GetExactlyOneDesiredValue,从explicitDesiredValue与computeDesiredValue的入站连接中恰好取一个期望值;若同时出现多个则抛TF_RUNTIME_ERROR);可逆输入属性的computeDesiredValue计算从反向计算的结果映射ExecIrResult中提取自身值,查不到则置空输出。
4.4 自定义可逆控制器的注册范式
若要在场景中自定义可逆控制器,仓库提供的标准范式是EXEC_REGISTER_COMPUTATIONS_FOR_SCHEMA+ExecIrControllerBuilder(完整示例见 controllerBuilder.h 的 doc 注释):
// 一个"输入加一"的最小可逆控制器:正向 y = x + 1,反向 x = y - 1 static ExecIrResult _ForwardCompute(const VdfContext &ctx); static ExecIrResult _InverseCompute(const VdfContext &ctx); EXEC_REGISTER_COMPUTATIONS_FOR_SCHEMA(MyAddOneController) { auto builder = ExecIrControllerBuilder( self, _ForwardCompute, _InverseCompute); // 注册一个可逆输入和一个可逆输出 builder.InvertibleInputAttribute<double>(_tokens->input); builder.InvertibleOutputAttribute<double>(_tokens->output); }builder 提供四类注册方法:
InvertibleInputAttribute<ValueType>():可逆输入(正向计算的输入,反向计算的输出);NonInvertibleInputAttribute<ValueType>():不可逆输入(同时作为正、反向计算的输入,如 FK 的parentIn:space);InvertibleOutputAttribute<ValueType>():可逆输出(正向计算结果,反向计算的期望值输入;不可逆输出尚未实现);SwitchAttribute<ValueType>():切换属性(提供正、反向计算输入,目前实现等价于非可逆输入,未来将承担"确定哪些控制器参与当前姿态"的关键职责);PassthroughAttributes<ValueType>(in, out):透传属性对(输入同时供正、反向计算使用,输出直接引用输入值)。
五、一个完整的可求逆绑定场景(来自测试用例)
仓库测试 testExecIrSwitchCompensation.cpp 提供了一个可直接研读的完整场景——"waddler"摇摆器。其 USD 结构如下(已从测试源码整理):
def Scope "Rig" { def Scope "Anim" { token switch = "rig1" def IrJointScope "Joint1" { matrix4d posed:space.connect = </Rig/Control/Switch1.out:space> def IrJointScope "Joint2" { double rest:tz = 10.0 matrix4d posed:space.connect = </Rig/Control/Switch2.out:space> } } } def Scope "Control" { def IrSwitchController "Switch1" { token switch.connect = </Rig/Anim.switch> matrix4d rig1:space.connect = </Rig/Control/Rig1/FK1.out:space> matrix4d rig2:space.connect = </Rig/Control/Rig2/FK1.out:space> } def IrSwitchController "Switch2" { token switch.connect = </Rig/Anim.switch> matrix4d rig1:space.connect = </Rig/Control/Rig1/FK2.out:space> matrix4d rig2:space.connect = </Rig/Control/Rig2/FK2.out:space> } def Scope "Rig1" { def IrFkController "FK1" { double in:tx.connect = </Rig/Anim/Joint1.avars:tx> # ... 其余 avar 连接 } def IrFkController "FK2" { ... } } def Scope "Rig2" { ... } } }可以看到完整的数据流闭环:
Anim层级中的 joint scope avars(avars:tx等)是动画作者输入的入口,switchtoken 决定当前激活的子绑定;Control层级中的 FK 控制器通过属性连接读取 avars,计算各自子绑定的out:space;- Switch 控制器根据
switch值从 Rig1/Rig2 的输出中选择一个,写入 joint scope 的posed:space; - 当
switch在动画中途改变时,测试调用求逆(computeDesiredValue+explicitDesiredValueoverride),反解出保持当前姿态的输入 avar 值,从而保证切换前后姿态连续。
其他相关测试还包括 testExecIrController.cpp(控制器基类行为)、testExecIrFkController.cpp(FK 正反向计算)与 testExecIrSwitchController.cpp(switch 透传与反向)。
六、结语:execIr 的能力边界与演进方向
execIr 为 OpenUSD 带来了"可求逆绑定"这一面向动画制作工作流的 schema 体系。其核心贡献可归纳为三点:
- 统一的控制器抽象(
ExecIrController):输入/输出/切换/透传属性的irRole元数据体系,加上ExecIrControllerBuilder的注册范式,为在 OpenExec 中实现正、反双向计算提供了清晰框架; - 开箱即用的基础构件:
ExecIrFkController(标量驱动的 FK 变换)与ExecIrSwitchController(子绑定切换)可直接组合成多子绑定可求逆网络; - 瞬态覆盖求值机制:
ComputeWithOverrides()+computeDesiredValue/explicitDesiredValue,让求逆过程不污染场景数据、不引发无谓的缓存失效。
同时需清醒认识其边界:所有 schema 均处于"功能受限、API 可能变更、非生产就绪"状态;switch 控制器硬编码为双绑定;当前不含 IK 求解器;求逆依赖插件计算而非 OpenExec 原生能力(ExecIrComputationsType注释明确计划迁移至ExecBuiltinComputations)。若想深入,建议从 pxr/exec/execIr/schema.usda(schema 定义)、pxr/exec/execIr/controllerBuilder.h(求逆接线核心)与 pxr/exec/execIr/testenv/testExecIrSwitchCompensation.cpp(完整切换补偿场景)三处入手,并结合 pxr/exec/execUsd/README.md 理解其上游 execUsd 执行系统的整体设计。
【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考