ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenUSD 动态文件格式(Dynamic File Formats)完全指南:用组合字段驱动程序化图层内容

OpenUSD 动态文件格式(Dynamic File Formats)完全指南:用组合字段驱动程序化图层内容 OpenUSD 动态文件格式Dynamic File Formats完全指南用组合字段驱动程序化图层内容【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD本篇技术指南以 OpenUSD 官方文档 dynamicFileFormat.md 为核心骨架结合仓库内 PcpPrim Composition与 Sdf 层的真实源码及官方示例系统讲解如何在 USD 中实现动态文件格式——一种能根据 payload 所在 prim 的已合成字段值在组合composition上下文中自动生成文件格式参数进而动态生成图层内容的 SdfFileFormat 扩展机制。读完本文你将掌握从定义插件、注册元数据字段到实现PcpDynamicFileFormatInterface接口、优化变更管理的完整实战路径并能直接对照仓库中的两个官方示例落地自己的动态文件格式。什么是动态文件格式在 OpenUSD 中一个动态文件格式Dynamic File Format是一种特殊的SdfFileFormat它允许图层的内容被动态生成——当该格式的资产以 payload 形式被引用时其内容会在被引用所在 prim 的组合上下文中按需产生。这一特性依赖 USD 早已存在的机制所有图层文件路径都可以携带可选的文件格式参数file format arguments这些参数被追加到图层资产路径之后在图层被打开和读取时传递给对应的文件格式插件。相关 API 包括SdfLayer::GetFileFormatArguments从图层标识符中取出文件格式参数SdfLayer::CreateIdentifier由资产路径与参数构造完整标识符SdfLayer::SplitIdentifier把标识符合并为资产路径与参数两部分。任意文件格式都能借助这些参数指导自己如何把被引用数据文件翻译成合法的 USD 图层。而动态文件格式的独特之处在于它能够合成composeprim 字段或属性默认值以此生成文件格式参数——合成发生在图层将被引入的那个 prim 上下文里。当被合成的字段或属性默认值发生变化时prim 会自动重新生成文件格式参数并创建全新的图层内容。从源码看这套机制的实现集中位于 pxr/usd/pcp 目录PcpDynamicFileFormatInterface接口混入类定义在 dynamicFileFormatInterface.h提供合成上下文的PcpDynamicFileFormatContext定义在 dynamicFileFormatContext.h而触发合成的核心逻辑在 prim 索引构建prim indexing过程中完成见 primIndex.cpp。创建动态文件格式的基本步骤创建一个动态文件格式与创建任何自定义文件格式一样需要先构建一个插件库plugin library实现SdfFileFormat的派生子类并把新类型登记到插件库的plugInfo.json中。以文档中的MyDynamicFileFormat为例其类骨架如下class MyDynamicFileFormat : public SdfFileFormat { public: // Required SdfFileFormat overrides. bool CanRead(const std::string file) const override; bool Read(SdfLayer *layer, const std::string resolvedPath, bool metadataOnly) const override; protected: SDF_FILE_FORMAT_FACTORY_ACCESS; virtual ~MyDynamicFileFormat(); MyDynamicFileFormat() : SdfFileFormat( TfToken(MyDynamicFileFormat), // formatId TfToken(1.0), // versionString TfToken(usd), // target mydynamicfile) // extension {} }构造函数中的四个参数分别定义了该格式的formatId、版本字符串、目标usd以及文件扩展名mydynamicfile。对应的plugInfo.json需要把该类型注册为SdfFileFormat的派生类型{ Plugins: [ { Info: { Types: { MyDynamicFileFormat: { bases: [ SdfFileFormat ], displayName: Dynamic File Format, extensions: [ mydynamicfile ], formatId: MyDynamicFileFormat, primary: true, target: usd } } }, LibraryPath: PLUG_INFO_LIBRARY_PATH, Name: myDynamicFileFormat, ResourcePath: PLUG_INFO_RESOURCE_PATH, Root: PLUG_INFO_ROOT, Type: library } ] }实现动态文件格式的关键一步本文不深入展开是在Read函数中利用追加到图层路径上的文件格式参数生成或改变图层内容的某一部分。这可以通过在Read实现中直接使用参数来完成对于完全程序化的图层也可以创建自定义的SdfAbstractData子类来利用参数。需要注意文件格式参数同时也是图层的身份标识identity的一部分——以相同资产路径但不同参数打开的图层会被视为彼此独立的图层。据此假设我们的MyDynamicFileFormat::Read通过SdfLayer::GetFileFormatArguments从所读取 layer 的资产路径中取出参数这些参数与SdfLayer::FindOrOpen传入的参数一致并利用dynamicName与isPositive两个参数值来改变图层内容。从普通文件格式升级为动态文件格式要让上面的文件格式变得动态类还必须额外继承PcpDynamicFileFormatInterface实现必需ComposeFieldsForFileFormatArguments并按需可选实现CanFieldChangeAffectFileFormatArguments与CanAttributeDefaultValueChangeAffectFileFormatArgumentsclass MyDynamicFileFormat : public SdfFileFormat, public PcpDynamicFileFormatInterface { ... public: // Required PcpDynamicFileFormatInterface overrides void ComposeFieldsForFileFormatArguments( const std::string assetPath, const PcpDynamicFileFormatContext context, FileFormatArguments* args, VtValue *dependencyContextData) const override; // Optional overrides bool CanFieldChangeAffectFileFormatArguments( const TfToken field, const VtValue oldValue, const VtValue newValue, const VtValue dependencyContextData) const override; bool CanAttributeDefaultValueChangeAffectFileFormatArguments( const TfToken attributeName, const VtValue oldValue, const VtValue newValue, const VtValue dependencyContextData) const override; ... }对照接口头文件 dynamicFileFormatInterface.h 可以看到ComposeFieldsForFileFormatArguments第 57 行是纯虚函数派生类必须实现使用给定的 context 合成 prim 元数据字段和/或属性默认值为assetPath对应的图层生成文件格式参数并写入argsCanFieldChangeAffectFileFormatArguments第 83 行默认实现返回 true表示任何传入字段的值变化都会要求重新计算文件格式参数。派生类应在某些字段值变化并不需要重算参数的场景下覆盖此函数以减少不必要的动态 payload 重组合CanAttributeDefaultValueChangeAffectFileFormatArguments第 109 行与上一函数概念类似但面向属性默认值的变化是否需要重算文件格式参数默认同样返回 true。此外接口文档还指出dependencyContextData是一个任意类型的VtValue在ComposeFieldsForFileFormatArguments中可以填入对判断字段变化是否相关有帮助的特定信息它会被保存并在同一 prim 索引上下文中处理字段变化时回传给CanFieldChangeAffectFileFormatArguments。调用时机prim 组合composition过程一旦文件格式实现了PcpDynamicFileFormatInterface那么每当某个 prim spec 包含一个指向带.mydynamicfile扩展名文件的 payload 时在组合该 prim 的过程中就会调用ComposeFieldsForFileFormatArguments。该调用发生在文件被打开之前目的是生成额外的文件格式参数并追加到文件资产路径上。函数可以利用给定的PcpDynamicFileFormatContext来合成payload 被引入处被索引 prim 上某个字段或属性默认值的最强意见strongest opinion。当前上下文只允许求值已定义的插件元数据字段defined plugin metadata fields因此我们必须在plugInfo.json中预先定义计划使用的字段。这一限制未来可能放宽到内置字段例如variantSelection但目前内置字段还不能被使用。上下文也可以求值属性默认值详见下文使用属性默认值计算参数。在 plugInfo.json 中定义自定义元数据字段为了在ComposeFieldsForFileFormatArguments中使用自定义字段计算参数需要在示例的plugInfo.json中通过SdfMetadata定义新的 Sdf 元数据字段dynamicName与dynamicNumber{ Plugins: [ { Info: { SdfMetadata: { dynamicName: { type: string, default: , displayGroup: Core, appliesTo: [prims], documentation:: Example custom string metadata. }, dynamicNumber: { type: int, default: 1, displayGroup: Core, appliesTo: [prims], documentation:: Example custom number metadata } }, ... }, ... } ] }要点type声明字段值类型此处为string与intdefault给出默认值空字符串与 1appliesTo声明可应用的 spec 类型此处为primsdisplayGroup与documentation:用于组织与文档化该字段。仓库中真实示例usdDancingCubesExample的 plugInfo.json 同样用这种方式注册了一个Usd_DCE_Params字段type: dictionary用于以字典形式覆盖各参数usdRecursivePayloadsExample则在其 plugInfo.json 中注册了UsdExample_depth、UsdExample_num、UsdExample_radius、UsdExample_height、UsdExample_argDict等字段。实现 ComposeFieldsForFileFormatArguments接下来实现ComposeFieldsForFileFormatArguments使用context合成dynamicName与dynamicNumber两个元数据字段的最强值从而生成要加入args的文件格式参数void MyDynamicFileFormat::ComposeFieldsForFileFormatArguments( const std::string assetPath, const PcpDynamicFileFormatContext context, FileFormatArguments* args, VtValue *dependencyContextData) const { static const TfToken dynamicNameToken(dynamicName); VtValue dynamicNameValue; if (context.ComposeValue(dynamicNameToken, dynamicNameValue)) { (*args)[dynamicNameToken] TfStringify(dynamicNameValue); } static const TfToken dynamicNumberToken(dynamicNumber); static const TfToken isPositiveToken(isPositive); VtValue dynamicNumberValue; if (context.ComposeValue(dynamicNumberToken, dynamicNumberValue)) { if (dynamicNumberValue.IsHoldingint() dynamicNumberValue.UncheckedGetint() 0) { (*args)[isPositiveToken] true; } else { (*args)[isPositiveToken] false; } } }这段实现演示了两个要点直接转写对于dynamicName把合成得到的字符串值以dynamicName为键写入args加工转写并非总是需要把字段值直接转成同名的参数——对于dynamicNumber我们合成其值并判断是否为正整数然后把true/false以isPositive为键写入args。context.ComposeValue返回的VtValue需要通过IsHoldingint()与UncheckedGetint()进行类型检查后使用。仓库中usdRecursivePayloadsExample的实现fileFormat.cpp更复杂它把字段值提取逻辑抽象为_Params::ExtractValues模板方法支持从PcpDynamicFileFormatContext或参数字典两种来源提取depth、num、radius、height等参数还演示了如何在上下文值之外用argDict字典按 payloadId 覆盖参数。组合上下文 APIComposeValue 与 ComposeValueStackPcpDynamicFileFormatContext定义于 dynamicFileFormatContext.h其公开 API 包括ComposeValue(field, value)第 39 行合成给定字段的值并返回其当前最强意见对字典值字段将返回每个键最强值的合成字典找到值则返回 trueComposeValueStack(field, values)第 51 行合成给定字段的全部意见按从强到弱排序返回对字典值字段各意见的字典不会被逐步合成而是原样放进列表中。注意此函数比ComposeValue慢尤其对非字典字段只有在确实需要强于最强值的更多信息时才应使用ComposeAttributeDefaultValue(attributeName, value)第 58 行合成指定名称属性的 default 字段值并返回最强意见。从上下文头文件注释可知该上下文封装了正在构建的 prim 索引的当前状态允许遍历所有已经组合过的节点为相关字段寻找最强意见。完整流程演示插件就绪后动态文件格式在实践中的工作方式如下。假设我们有如下 USD 文件#usda 1.0 def Root ( references /Params payload ./dynamic.mydynamicfile ) { } def Params ( dynamicName Foo dynamicNumber 8 ) { }Rootprim 有一个指向Paramsprim 的 reference后者为插件字段dynamicName和dynamicNumber提供了值意见Root还有一个指向.mydynamicfile文件的 payload。当计算Root的 prim 索引、索引器组合到该 payload 时会发现文件格式是MyDynamicFileFormat于是调用该格式的ComposeFieldsForFileFormatArguments来生成文件格式参数。此时在组合上下文中已包含对Params的 reference会取其dynamicName与dynamicNumber的最强意见生成参数dynamicName FooisPositive true这些参数被追加到 payload 图层的资产路径上最终解析出的图层路径为dynamic.mydynamicfile:SDF_FORMAT_ARGS:dynamicNameFoo:isPositivetrue如前所述MyDynamicFileFormat的Read函数使用这些参数生成图层的身份与内容。现在更新root.usd给Root自身加上值为Bar的dynamicName字段#usda 1.0 def Root ( dynamicName Bar references /Params payload ./dynamic.mydynamicfile ) { } def Params ( dynamicName Foo dynamicNumber 8 ) { }当Root再次被 prim 组合时payload 被组合的上下文里dynamicName的最强意见来自Root更强的局部意见覆盖了 reference 引入的意见于是生成dynamicName BarisPositive true注意dynamicNumber的最强意见仍然来自Params。此时解析出的 payload 图层路径变为dynamic.mydynamicfile:SDF_FORMAT_ARGS:dynamicNameBar:isPositivetrue于是在不修改 payload 声明本身的前提下我们得到了一个身份与内容都不同于之前的图层——这正是动态文件格式的核心价值同一个 payload 字段可以由 prim 上下文中合成的字段值驱动出不同的图层内容。使用属性默认值计算参数除了元数据字段动态文件格式插件还可以使用uniform 属性默认值uniform attribute defaults来计算文件格式参数。与元数据字段不同这些属性不需要在plugInfo.json中注册。需要注意的限制只能使用属性的默认值default value属性在 USD 数据中应声明为uniform。若要把前面MyDynamicFileFormat插件示例改为使用属性默认值只需把ComposeFieldsForFileFormatArguments的实现改为用ComposeAttributeDefaultValue合成dynamicName与dynamicNumber的属性默认值void MyDynamicFileFormat::ComposeFieldsForFileFormatArguments( const std::string assetPath, const PcpDynamicFileFormatContext context, FileFormatArguments* args, VtValue *dependencyContextData) const { static const TfToken dynamicNameToken(dynamicName); VtValue dynamicNameValue; if (context.ComposeAttributeDefaultValue(dynamicNameToken, dynamicNameValue)) { (*args)[dynamicNameToken] TfStringify(dynamicNameValue); } static const TfToken dynamicNumberToken(dynamicNumber); static const TfToken isPositiveToken(isPositive); VtValue dynamicNumberValue; if (context.ComposeAttributeDefaultValue(dynamicNumberToken, dynamicNumberValue)) { if (dynamicNumberValue.IsHoldingint() dynamicNumberValue.UncheckedGetint() 0) { (*args)[isPositiveToken] true; } else { (*args)[isPositiveToken] false; } } }总体而言虽然 USD 支持在ComposeFieldsForFileFormatArguments实现中同时合成字段值和属性默认值但应尽可能避免在同一插件中混用两者尤其不要为计算同一个文件格式参数同时使用字段值与属性默认值。如果想过滤掉不需要重算文件格式参数的属性默认值变化可以实现CanAttributeDefaultValueChangeAffectFileFormatArguments加入哪些默认值变化需要重算相应参数的判断逻辑。使用该插件的 uniform 属性默认值的 USD 数据形如#usda 1.0 def Root ( references /Params payload ./dynamic.mydynamicfile ) { } def Params ( ) { uniform string dynamicName Foo uniform int dynamicNumber 8 }仓库中的usdDancingCubesExample正是字段 属性默认值双通道的实例其 fileFormat.cpp 先合成字典字段Usd_DCE_Params提取参数再对每个参数调用ComposeAttributeDefaultValue检查同名的 uniform 属性属性默认值存在时优先覆盖字段字典中的值。其测试脚本 testUsdDancingCubesExample.py 验证了属性默认值优先于元数据字典参数的行为。进阶示例两种程序化内容生成路线仓库在 pxr/extras/usd/examples 中提供了两个动态文件格式插件示例注意该目录在仓库中的真实路径为 extras/usd/examples。两者最值得关注的区别在于场景描述scene description的表示方式SdfAbstractData是图层所表示场景描述的基类编写文件格式时我们可以选择使用默认的SdfData类承载场景描述编写自定义数据表示。usdRecursivePayloadsExample基于默认 SdfData 的递归生成该示例位于 extras/usd/examples/usdRecursivePayloadsExample使用文件格式参数递归地生成 prim生成的 prim 携带指向同一文件、但参数不同的 payload。它使用SdfFileFormat::InitData提供的默认SdfData表示与文本形式的 usda 文件格式一致在Read函数中通过标准SdfPrimSpecAPI 创建 prim spec——生成的场景描述简单且精简不值得引入自定义SdfAbstractData类型的复杂度。从 fileFormat.cpp 可以看到其递归逻辑依据depth/num/radius/height参数生成一层环形分布的Xform子 prim_CreateRecursiveChildSpec第 231 行每个子 prim 上设置depth-1、radius*0.5等新参数并再次添加指向同一文件的 payload形成递归第 335-380 行当depth归零时停止递归直接按 usda 读取原文件内容第 399-413 行。示例场景文件 root.usda 演示了如何通过 reference 引入参数 prim 与 payload 列表让组合字段影响动态 payload 的内容。头文件 fileFormat.h 中对各元数据参数depth、num、radius、height、argDict的语义有清晰说明。usdDancingCubesExample自定义 SdfAbstractData 的完全程序化生成该示例位于 extras/usd/examples/usdDancingCubesExample生成一个由动画小方块组成的大方块其场景描述完全程序化。它实现了自己的SdfAbstractData子类并通过重写SdfFileFormat::InitData返回该子类fileFormat.cpp。文件格式从文件格式参数中生成一小撮参数并提供给图层的 data 实现SdfAbstractData子类利用这些参数缓存场景的若干信息并在被请求时按需生成 spec 数据见 dataImpl.h 与 data.h。该示例受益于自定义SdfAbstractData实现避免了在图层打开时预计算每个 prim 的每个时间采样——只有被请求的 spec 才按需生成。此外该示例的Read直接基于图层标识符中的参数初始化数据第 59-85 行完全不解析文件内容并将图层设为只读SetPermissionToSave(false)、SetPermissionToEdit(false)。两者共同印证了文档给出的结论场景描述越复杂、数据量越大尤其是带时间采样的动画数据自定义SdfAbstractData的价值越高简单场景用默认SdfData即可。动态 Payload为何仅限 payload 而非 reference如前所述prim 字段或属性默认值向文件格式参数的合成只发生在动态资产以 payload 形式被引入时这样的 payload 被称为动态 payloaddynamic payload。这一行为是刻意只面向 payload而非 reference的原因有二Payload 是读取图层文件时最弱的组合弧composition arc。因此当 prim 索引遇到动态 payload 时用于合成字段的上下文能够访问这些字段上所有局部或引用而来的意见从而给出处理动态文件参数时最完整的上下文Payload 可以被加载load与卸载unload为重新计算内容取决于文件格式参数之外因素的动态图层提供了便捷手段。一个 prim 索引可以拥有多个 payload 弧其中任意数量都可以是动态的。来自更强 payload 的意见会被纳入较弱动态 payload 计算文件格式参数时的上下文。依赖与变更管理当ComposeFieldsForFileFormatArguments在 prim 索引期间通过ComposeValue或ComposeValueStack计算字段值时该 payload 弧会自动对该字段值注册一条依赖。这意味着 Pcp 的变更管理change management知道哪些字段被用于生成 payload 图层的文件格式参数如果这些字段中的任何一个发生变化就可能需要使包含该 payload 的 prim 索引失效。因此在实现ComposeFieldsForFileFormatArguments时建议只在需要时才调用ComposeValue求值字段——尤其当某些字段的使用是有条件的conditional时这样能避免对未使用字段产生不必要的变更依赖。同样的建议也适用于用ComposeAttributeDefaultValue计算属性默认值的情形。usdRecursivePayloadsExample的_Params::ExtractValuesfileFormat.cpp对此做了示范深度为 0 时提前返回不去提取radius/height等后续字段从而避免引入多余的依赖。用 CanFieldChangeAffectFileFormatArguments 过滤无关变更由于包含动态 payload 的 prim 索引自动依赖被合成字段的变化接口函数CanFieldChangeAffectFileFormatArguments存在的意义就是过滤掉那些已知不会改变文件格式参数的字段变化。回到MyDynamicFileFormatdynamicNumber字段保存整数用于填充布尔参数isPositive。多个dynamicNumber值会产生相同的参数因此可以利用该函数bool MyDynamicFileFormat::CanFieldChangeAffectFileFormatArguments( const TfToken field, const VtValue oldValue, const VtValue newValue, const VtValue dependencyContextData) const { static const TfToken dynamicNumberToken(dynamicNumber); if (field dynamicNumberToken) { if (oldValue.IsEmpty() ! newValue.IsEmpty()) { return true; } const bool oldIsPositive (oldValue.IsHoldingint() oldValue.UncheckedGetint() 0); const bool newIsPositive (newValue.IsHoldingint() newValue.UncheckedGetint() 0); return oldIsPositive ! newIsPositive; } return true; }这里若字段是dynamicNumber我们检查新旧值是否会产生相同的isPositive参数如果相同则返回false告诉 Pcp 变更管理无需使包含该 payload 的 prim 索引失效从而避免不必要的重组合。仓库示例对这一点做了更精细的演示usdDancingCubesExample的CanFieldChangeAffectFileFormatArgumentsfileFormat.cpp对Usd_DCE_Params字典逐键比对只有相关参数键的值确实变化时才返回 true——因为字典中可能含有与参数生成完全无关的键usdRecursivePayloadsExample的对应实现fileFormat.cpp演示了结合dependencyContextData的用法若依赖数据中没有 payloadIdargDict的变化不影响参数返回 falsedepth从一个非正值变为另一个非正值时被 clamp 过同样不影响参数。dependencyContextData参数同时存在于ComposeFieldsForFileFormatArguments与CanFieldChangeAffectFileFormatArguments中接口头文件 dynamicFileFormatInterface.h 的注释对此有明确说明它可在组合阶段填充任意类型数据供同一 prim 索引上下文中的字段变化处理阶段使用为该变化是否真的影响参数提供格式特定的额外上下文。源码验证与测试动态文件格式机制的可靠性有完整的源码与测试支撑接口与上下文dynamicFileFormatInterface.h 定义了三个虚函数的语义dynamicFileFormatContext.h 定义了组合 API 与依赖记录方式合成触发逻辑位于 primIndex.cppPcp 层单元测试TestPcpDynamicFileFormatPlugin.cpp 与 TestPcpAssetPathDynamicFileFormatPlugin.cpp 覆盖了动态 payload 的参数生成、变更依赖等核心行为示例级测试testUsdRecursivePayloadsExample.py 与 testUsdDancingCubesExample.py 通过打开测试场景、修改元数据/属性、再导出动态图层与 baseline 对比的方式端到端验证字段变化 → 参数变化 → 图层内容变化的完整链路其 baseline 文件位于各示例的testenv/testUsd*Example/baseline/目录。总结动态文件格式是 OpenUSD 组合系统中相当精妙的一环它把文件格式参数这一原本静态的资产标识维度与prim 组合上下文这一动态的运行时状态打通通过继承PcpDynamicFileFormatInterface并实现ComposeFieldsForFileFormatArguments让文件格式在 payload 被组合时基于PcpDynamicFileFormatContext合成出的字段/属性默认值生成参数通过SdfMetadata注册自定义字段或直接使用 uniform 属性默认值提供参数输入的两种来源通过CanFieldChangeAffectFileFormatArguments与dependencyContextData精细控制变更管理减少无谓的重组合结合默认SdfData或自定义SdfAbstractData两种场景描述路线可在读文件生成 spec与完全程序化按需生成 spec之间按需取舍。对于需要以紧凑、参数化的方式表达大量程序化内容如过程化场景、按参数展开的几何布局、动画实例化的开发者动态文件格式提供了一条不落盘、可随 prim 上下文联动、且具备增量变更能力的优雅路径。建议进一步阅读 dynamicFileFormat.md 原文并直接研读 usdRecursivePayloadsExample 与 usdDancingCubesExample 两个示例的完整实现作为落地参考。【免费下载链接】OpenUSDUniversal Scene Description项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表