CD Code Digest Hub /GTA V · v2

RAGE 动画核心:cr* 数据层、Motion Tree 与 MoVE

94 分钟阅读 16 个章节 3 张图

本页解释 RAGE 引擎层的动画系统:从压缩动画数据(crAnimation)、帧(crFrame)、骨架(crSkeleton)、生物体(crCreature)、元数据(crTag/crProperty),到运行时的动作树(motion tree,crmtMotionTree 及其 crmtNode* 节点、crmtComposer 合成作业),再到构建在动作树之上的数据驱动状态机网络 MoVE(mvNetwork/mvSubNetwork)。最后给出游戏一帧内动画各阶段(pre-physics / mid-physics / post-camera / pre-render)的实际时序。

这是 GTAV 角色、车辆、物件、过场动画全部动画表现的底座:游戏层的 fwAnimDirector、CMovePed、IK、cutscene 都只是在这里描述的机制上做装配与调度。

背景与相关页面#

术语:DOF(degree of freedom,一个 (track, id, type) 三元组,例如“骨骼 id=X 的旋转四元数”);frame(一组 DOF 的值快照);clip(带元数据的动画包装);motion tree(每个角色的一棵动画节点树);compose(把节点树转换为一串依赖作业并实际计算出姿态的过程)。

相关源码文件#

目录关键文件作用
rage/base/src/cranimationanimation.h、animstream.h、animpacker.cpp、animcache.*、animchannel.h、animtolerance.h、framedata.h、frame.h、frameaccelerator.h、framefilters.h、framebuffers.h压缩动画、DOF/帧、帧加速表、帧过滤器
rage/base/src/crskeletonskeletondata.h、skeleton.h/.cpp、bonedata.h、jointdata.h骨架定义与实例
rage/base/src/creaturecreature.h、component.h、component*.h生物体与组件(骨架、额外 DOF、shader var、物理、mover…)
rage/base/src/crmetadataproperty.h、propertyattributes.h、tag.h、tags.h、tagiterators.h属性与时间标签
rage/base/src/crbodyiksolverbase.h、iksolver.h、kinematics.h/.cpp、ikbody.hIK 求解器基类与 RAGE 内置求解器
rage/suite/src/crclipclip.h、clipanimation(s).h、clipanimationexpression.h、clipdictionary.hclip 类型族与字典
rage/suite/src/crextraexpressions.h、expressionops.h、expressionstream.h、mover.h、posematcher.h表达式、pose matcher 等
rage/suite/src/crparameterizedmotionparameterizedmotion*.h、blenddatabase.h、registrationcurve.h、parametersampler.h参数化运动
rage/suite/src/crmotiontreenode.h/.cpp、motiontree.h/.cpp、updater.cpp、composer.h/.cpp、dependency_*.frag、motiontreescheduler.h/.cpp、synchronizer.h、observer.h、request.h、nodefactory.h动作树、合成器、调度器
rage/suite/src/movemove.h/.cpp、move_file.cpp、move_network.h/.cpp、move_subnetwork.h、move_node.h/.cpp、move_state*.{h,cpp}、move_types_internal.h、move_parameter*.h、move_command.h、move_config.h、move_version.hMoVE 网络运行时
game/scene/worldGameWorld.cpp帧内动画阶段的调用点

1. 分层总览#

图证据:clip 引用 animation/expressions 见 src/dev_ng/rage/suite/src/crclip/clipanimation.h:122-126、src/dev_ng/rage/suite/src/crclip/clipanimationexpression.h:72;MoVE 通过 crmtRequest 在动作树中生成根节点 src/dev_ng/rage/suite/src/move/move_network.cpp:86-109;更新与合成分离见 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.cpp:261-299;合成器访问节点并推送 animation/IK/pose 依赖见 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:423-470 与 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:656-687;收尾时 PopSkeleton 把帧写回骨架 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:140-160;动作树绑定 crCreature src/dev_ng/rage/suite/src/crmotiontree/motiontree.h:44-50。

2. 压缩动画:crAnimation#

2.1 结构#

crAnimation : pgBase 表示“若干 track 的值随时间变化”,内部同时维护按 track 组织的结构视图与按时间组织的 block/chunk 视图,以便内存布局按时间局部化 src/dev_ng/rage/base/src/cranimation/animation.h:27-40。主查询接口是 CompositeFrame(time, frame) 与带 mover 增量的 CompositeFrameWithMover src/dev_ng/rage/base/src/cranimation/animation.h:102-114。

字段含义证据
m_FlagskLooped(BIT0)、kRaw(BIT3,仅无损压缩)、kMoverTracks(BIT4)、kPacked(BIT8)、kCompact(BIT10);后两者不可序列化animation.h:476-487
m_ProjectFlags项目自定义标志animation.h:489-490
m_NumFrames / m_FramesPerChunk内部帧数(含末尾零时长帧)/每 chunk 帧数,默认 sm_DefaultFramesPerChunk = 127animation.h:492-496、animation.h:259
m_Duration、m_Signature、m_Name秒数、结构哈希、名称animation.h:498-505
m_TracksatArray<crAnimTrack*>,结构化视图animation.h:507-508
m_MaxBlockSize、m_RefCount打包后最大块尺寸;引用计数animation.h:510-514
m_BlocksatArray<datRef<crBlockStream>>,运行时回放用的时间块animation.h:519-520
m_Dofs{u16 m_Id; u8 m_Type; u8 m_Track} 数组animation.h:522-533

签名(signature)是覆盖所有 track/id/type 的哈希,用于快速判断“动画与帧的结构是否一致”,算法必须与 crFrame::CalcSignature 一致 src/dev_ng/rage/base/src/cranimation/animation.h:465-471。开发版(CR_DEV)支持从 .anim 文件(ANI8 格式,SerializeANI8)加载 src/dev_ng/rage/base/src/cranimation/animation.h:309-320、src/dev_ng/rage/base/src/cranimation/animation.h:452-453;运行时版本通过资源(RORC_VERSION)加载 src/dev_ng/rage/base/src/cranimation/animation.h:75-76。

2.2 块流 crBlockStream#

块流是运行时回放用的连续内存块,包含一段帧范围;分“常量段”(静态值)和“变量段”(按帧排序的动画值),以便缓存/SPU 连续访问 src/dev_ng/rage/base/src/cranimation/animstream.h:20-28。

  • 格式类型:kConstantQuat, kConstantVec, kConstant, kRaw, kQuantize, kIndirect, kLinear, kReconstruct, kNormalize src/dev_ng/rage/base/src/cranimation/animstream.h:56-57。
  • 头部:m_Hash(完整性哈希)、m_DataSize(未压缩数据大小)、m_CompactSize(二次压缩后大小,非零即“compact”)、m_ConstantSize、m_ChannelOffset、m_SlopSize、m_NumFrames、m_FrameSize、m_IndirectSize、m_QuantizeSize、m_SegmentSize、m_NumMoverChannels,随后是柔性数组 m_Data[] src/dev_ng/rage/base/src/cranimation/animstream.h:59-85。
  • 两种求值路径:整帧 EvaluateFrame 与按通道列表 FindChannels/EvaluateChannels src/dev_ng/rage/base/src/cranimation/animstream.h:38-54。

打包流程(离线/工具侧,crAnimPacker::Pack):从 track 生成 m_Dofs;块数取第 0 条 track 的 chunk 数;逐块在临时内存中 Create,若需要字节序交换则 Swap,且在 compact 时进一步 Compact;最终以 16 字节对齐分配并拷贝 src/dev_ng/rage/base/src/cranimation/animpacker.cpp:32-80。Compact 使用通用 datCompress 并把结果写入 m_CompactSize src/dev_ng/rage/base/src/cranimation/animpacker.cpp:461-498。

二次压缩块的解压缓存:crAnimCache 用 crThreadSafeAccelerator 缓存解压后的块 src/dev_ng/rage/base/src/cranimation/animcache.h:18-81。缓存键为“块地址 ^ 动画签名 ^ 名称哈希”,未命中则回调 DecompressAnimBlock(拷贝头部 + datDecompress 数据段),条目以锁对象保护以防其他线程回收 src/dev_ng/rage/base/src/cranimation/animcache.cpp:87-121。框架层仅在 ENABLE_ANIM_CACHE 下初始化该缓存(注释写明 PS3 only),而该宏在 GTAV 中为 0 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.h:21、src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:175-181;默认最大块尺寸 CR_DEFAULT_MAX_BLOCK_SIZE = 96 KB src/dev_ng/rage/base/src/cranimation/animtolerance.h:12。这表明 GTAV 发行平台上 compact 块要么不存在、要么由其他路径解压——缺少运行时 compact 动画数据的直接证据,标为未决。

2.3 通道与压缩容差(开发/工具侧)#

crAnimChannel 是 track 内部的压缩通道抽象,内置类型包括 Raw/Static/Quantize/VariableQuantize/IndirectQuantize/Linear/RLE/SmallestThree 四元数/二次与三次 B 样条等,自定义类型从 0x80 起 src/dev_ng/rage/base/src/cranimation/animchannel.h:63-91。压缩容差由 crAnimTolerance → Simple → Complex → Movement 层次描述,并带解压/压缩成本枚举 src/dev_ng/rage/base/src/cranimation/animtolerance.h:28-75、src/dev_ng/rage/base/src/cranimation/animtolerance.h:163、src/dev_ng/rage/base/src/cranimation/animtolerance.h:395。这些通道在打包阶段被转换为 2.2 的块格式(crAnimPacker 为 crAnimation 的 friend)src/dev_ng/rage/base/src/cranimation/animation.h:42。

3. DOF、帧与帧数据:crFrameData / crFrame#

3.1 Track 与格式类型#

Track 枚举(不可重排)覆盖骨骼平移/旋转/缩放、可见性、mover 平移/旋转/缩放、相机(焦距、光圈、DOF 平面、CoC…)、shader UV 动画、blend shape、viseme、面部控制、灯光强度/衰减/锥角/方向、粒子数据等;0x7f 以下为 RAGE 保留,0x80 起为项目自定义 src/dev_ng/rage/base/src/cranimation/framedata.h:20-83。每个 DOF 的值类型只有三种:kFormatTypeVector3、kFormatTypeQuaternion、kFormatTypeFloat src/dev_ng/rage/base/src/cranimation/framedata.h:89-98。游戏层用项目 track(例如 kTrackFirstPersonCameraWeight、kTrackConstraintHelperLeftHandWeight)扩展额外 DOF,见 game-animation-and-ik.md。

3.2 结构/值分离#

  • crFrameData(16 字节对齐)描述“有哪些 DOF”:Dof{u16 m_Offset; u16 m_Id; u8 m_Type; u8 m_Track} 数组、引用计数、签名、DOF 数、每类型计数、帧缓冲大小等 src/dev_ng/rage/base/src/cranimation/framedata.h:176-196、src/dev_ng/rage/base/src/cranimation/framedata.h:272-282。签名使用 Fletcher 算法,与 crAnimation::CalcSignature 必须一致 src/dev_ng/rage/base/src/cranimation/framedata.h:261-267。
  • crFrame 持有值缓冲:m_Buffer、指向 crFrameData 的指针、可选 crFrameAccelerator*、引用计数与签名 src/dev_ng/rage/base/src/cranimation/frame.h:986-994。只有两个帧共有的 DOF 才参与组合,这允许局部合成与 LOD src/dev_ng/rage/base/src/cranimation/frame.h:31-42。外部缓冲必须 16 字节对齐且为 16 的倍数 src/dev_ng/rage/base/src/cranimation/frame.h:56-62。
  • 无效值编码:DOF“无效”用全 1 位模式表示——FastInvalidate 把缓冲 memset 为 0xff,FastIsDofInvalid 判断首个 u32 是否为 UINT_MAX,向量/四元数写入 V_MASKXYZW src/dev_ng/rage/base/src/cranimation/frame.h:1600-1634。这意味着合法数据中不能出现该 NaN 位模式(推断)。

3.3 加速表、过滤器与帧缓冲#

  • crAccelerator 是以签名为键的“最近使用缓存”,取出的条目以 Lock 包装防止被其他线程回收 src/dev_ng/rage/base/src/cranimation/frameaccelerator.h:27-56。crFrameAccelerator 内含 6 个线程安全加速表:过滤器权重、帧↔帧索引、帧↔动画索引、帧↔骨架索引、表达式帧索引、表达式骨架索引 src/dev_ng/rage/base/src/cranimation/frameaccelerator.h:165-198、src/dev_ng/rage/base/src/cranimation/frameaccelerator.h:259-260。框架层全局实例的槽数/堆大小为 {32,128,64,128,64,64} / {80,96,48,64,32,32} KB src/dev_ng/rage/framework/src/fwanimation/directorcomponentcreature.cpp:219-222。
  • 帧过滤器 crFrameFilter 提供逐 DOF 权重,内置 Bone/BoneBasic/BoneMultiWeight/MultiWeight/TrackMultiWeight/Mover/Filters(组合)等,类型 id 分 RAGE、框架(0x40)、项目(0x80)三段 src/dev_ng/rage/base/src/cranimation/framefilters.h:32-58、src/dev_ng/rage/base/src/cranimation/framefilters.h:316、src/dev_ng/rage/base/src/cranimation/framefilters.h:1033。
  • crFrameBuffer 族(Fixed/Dynamic/FrameData)负责为动作树分配临时帧 src/dev_ng/rage/base/src/cranimation/framebuffers.h:30-244;底层 crCommonPool 在框架层配置为峰值 3840 帧 / 1920 KB(PS3/360 为 1280 / 640 KB)src/dev_ng/rage/framework/src/fwanimation/directorcomponentcreature.cpp:213-217,并在每帧 UpdateDerivedClass 中垃圾回收/压缩 src/dev_ng/rage/framework/src/fwanimation/directorcomponentcreature.cpp:239-243。

4. 骨架:crSkeletonData / crSkeleton#

  • crBoneData:默认旋转/平移/缩放、m_NextIndex/m_ParentIndex、名称、DOF 标志、索引、m_BoneId(稳定的骨骼标签)、m_MirrorIndex src/dev_ng/rage/base/src/crskeleton/bonedata.h:164-174。DOF 标志区分可旋转/平移/缩放轴、是否有限制、是否有子节点、是否蒙皮 src/dev_ng/rage/base/src/crskeleton/bonedata.h:137-157。
  • crSkeletonData : pgBase:m_BoneIdTable(boneId→index)、骨骼数组、累积逆矩阵、默认矩阵、父索引、子-父索引对、属性、三种签名(普通/非手性/完整)src/dev_ng/rage/base/src/crskeleton/skeletondata.h:236-252。
  • crJointData 保存旋转/平移/缩放限制(crJointRotationLimit 等)src/dev_ng/rage/base/src/crskeleton/jointdata.h:428、src/dev_ng/rage/base/src/crskeleton/jointdata.h:547-556,供 crmtNodeJointLimit 与 IK 使用。
  • crSkeleton 是实例:pgRef<const crSkeletonData>、父矩阵引用、m_Locals(局部矩阵,拥有)、m_Objects(对象空间矩阵)src/dev_ng/rage/base/src/crskeleton/skeleton.h:154-158。crFrame::Pose 写局部矩阵,crSkeleton::Update 计算对象矩阵 src/dev_ng/rage/base/src/crskeleton/skeleton.h:24-28。

Update 先预取,再令 Objects[0]=Locals[0],对 i≥1 执行 Objects[i] = Objects[parent[i]] * Locals[i]——这隐含“骨骼按父先于子排序”的不变量 src/dev_ng/rage/base/src/crskeleton/skeleton.cpp:139-164。Attach 把对象矩阵(蒙皮时乘以累积逆关节矩阵)输出为 Matrix43 供绘制 src/dev_ng/rage/base/src/crskeleton/skeleton.cpp:266-297。

5. 生物体:crCreature 与组件#

crCreature 是一个角色实例的最高层表示,内含骨架实例与可选模块 src/dev_ng/rage/base/src/creature/creature.h:26-31。成员包括快速查找表 m_FastFindComponents、组件数组、crSkeleton*、mover 矩阵、合成后的 crFrameData*、帧数据工厂、帧加速器、组件工厂与签名 src/dev_ng/rage/base/src/creature/creature.h:238-248。Finalize(dt) 是每步只能调用一次的“提交”src/dev_ng/rage/base/src/creature/creature.h:55-59。

组件类型(自定义从 0x80)src/dev_ng/rage/base/src/creature/component.h:36-62:

类型作用(据文件名与接口,未逐个展开实现)
Skeleton骨骼 DOF ↔ crSkeleton 局部矩阵
ExtraDofs非骨骼的额外 DOF(游戏用于 mover 与约束权重等)
ShaderVarsshader 变量动画(UV、帧索引)
Physical物理表达式所需的运动描述(Motion,供表达式 OpMotion)
Mover、BlendShapes、Telemetry、Camera、Light、ParticleEffect、Event、Movemover 矩阵、混合形状、遥测、相机/灯光/粒子参数、事件、MoVE

组件的虚接口覆盖 PreInit/Finalize/Refresh/Pose/InversePose/Identity/Reset/PostUpdate/PreDraw/DrawUpdate/SwapBuffers/Draw,以及 DOF 迭代 IterateDofs/HasDof/InitDofs src/dev_ng/rage/base/src/creature/component.h:89-197。组件通过工厂分配 src/dev_ng/rage/base/src/creature/component.h:338-344;框架层使用 FactoryPooled,为 Skeleton/ExtraDofs 建 ANIMDIRECTOR_POOL_MAX 大小的池,ShaderVars/Physical 分别为 ANIMDIRECTOR_POOL_PED_MAX/2 与 *3/4 src/dev_ng/rage/framework/src/fwanimation/directorcomponentcreature.cpp:205-211。

6. 元数据:crProperty、crTag、crTags#

  • crPropertyAttribute 类型:Float/Int/Bool/String/BitSet/Vector3/Vector4/Quaternion/Matrix34/Situation/Data/HashString,自定义从 0x80 src/dev_ng/rage/base/src/crmetadata/propertyattributes.h:28-55。
  • crProperty : pgBase 由名称键、属性数组与签名组成 src/dev_ng/rage/base/src/crmetadata/property.h:265-271。
  • crTag : crProperty 是“基于相位的属性包装”,带 m_Start/m_End(相位区间)src/dev_ng/rage/base/src/crmetadata/tag.h:26-29、src/dev_ng/rage/base/src/crmetadata/tag.h:168-171;crTags 持有标签数组与 m_HasBlockTags src/dev_ng/rage/base/src/crmetadata/tags.h:223-225。
  • 迭代器按相位区间/键/属性筛选标签(crTagIteratorBasePhase、crTagIteratorKey、crTagIteratorPropertyAttribute)src/dev_ng/rage/base/src/crmetadata/tagiterators.h:93-170。

游戏中的“事件标签”(脚注、IK、音频、blend-out、mover fixup…)都派生自 crProperty,见 game-animation-and-ik.md。

7. Clip 类型族:crClip#

crClip : pgBase 是“动画数据的高层包装”,附带 tags/properties 并隐藏存储方式 src/dev_ng/rage/suite/src/crclip/clip.h:38-42。

类型(eClipType)类内容证据
kClipTypeAnimationcrClipAnimation单个 crAnimation + [start,end] 区间 + rateclipanimation.h:122-126
kClipTypeAnimationscrClipAnimations多段动画数组(每段 start/end/rate),总时长,m_Parallel 表示并行或串行clipanimations.h:186-206
kClipTypeAnimationExpressioncrClipAnimationExpression : crClipAnimation额外持有 crExpressionsclipanimationexpression.h:18、clipanimationexpression.h:72

项目自定义 clip 类型尚不支持(kClipTypeCustom 被注释)src/dev_ng/rage/suite/src/crclip/clip.h:46-61。基类成员:类型、名称、所属字典 datRef、flags(kIsLooped/kIsResource)、tags、properties、引用计数 src/dev_ng/rage/suite/src/crclip/clip.h:450-468。引用计数语义:AddRef 同时给所属字典加引用;Release(kRefOptionAndDictionary) 释放 clip 时也释放字典 src/dev_ng/rage/suite/src/crclip/clip.h:63-68、src/dev_ng/rage/suite/src/crclip/clip.h:483-500——因此持有 clip 的节点会使流式字典保持驻留(推断,基于 AddRef 实现)。

crClipDictionary : pgBaseRefCounted 内含可选的 crAnimDictionary(及是否拥有)、m_BaseNameKeys、atMap<ClipKey, datOwner<crClip>> src/dev_ng/rage/suite/src/crclip/clipdictionary.h:197-204;开发版通过 crClipLoader* 族加载 src/dev_ng/rage/suite/src/crclip/clipdictionary.h:250-284。游戏中的 clip 字典由 g_ClipDictionaryStore 流式管理,见 game-animation-and-ik.md。

8. 表达式(expressions)#

表达式是数据驱动的“程序化 DOF 计算”(例如辅助骨骼、肩部扭转修正、面部控制器驱动骨骼)。

  • 编辑态:crExpressions : pgBase 内含 crExpression 树(datOwner<crExpressionOp> m_ExpressionOp + 栈深)src/dev_ng/rage/suite/src/crextra/expression.h:159-160、运行态字节码流、读写的 track 列表、运动描述、变量、签名、m_MaxPackedSize 等 src/dev_ng/rage/suite/src/crextra/expressions.h:315-325;字典类型为 pgDictionary<crExpressions> src/dev_ng/rage/suite/src/crextra/expressions.h:32-33。操作节点类型有 Constant/Get/Set/Valid/ComponentGet/Set/ObjectSpace*/Nullary…Nary/SpecialBlend/Curve/LookAt/Motion/Variable*/Linear src/dev_ng/rage/suite/src/crextra/expressionops.h:41-77。
  • 运行态:Pack() 调用 crExpressionPacker::Pack 把操作树编译成 crExpressionStream src/dev_ng/rage/suite/src/crextra/expressions.cpp:920-926。字节码是一个栈机:操作码包括 OpPush/OpPop/OpGet/OpSet/OpObjectGet/OpBranch*/OpAdd/OpQuatMultiply/OpLerp/OpLookAt/OpCurve/OpTime/OpDeltaTime/OpGetVariable 等 src/dev_ng/rage/suite/src/crextra/expressionstream.h:20-29;执行上下文 ExprStreamHelper 携带帧缓冲、帧/骨架索引、DOF 表、骨骼、物理运动、变量与时间 src/dev_ng/rage/suite/src/crextra/expressionstream.h:33-50;流布局为头(hash、对齐参数区大小、参数区大小、操作数、最大栈深)+ 数据 src/dev_ng/rage/suite/src/crextra/expressionstream.h:54-72。
  • 合成器以 PushExpressions(...) 依赖执行表达式,并为其锁定“表达式帧索引/骨架索引”两类加速表(见 3.3)src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:463-470。

animExprData(AeFile/AeDriver/AeController/ExprTree 等)是表达式的创作期数据模型 src/dev_ng/rage/suite/src/animExprData/animExprData.h:62-246、src/dev_ng/rage/suite/src/animExprData/exprTree.h:20-347;对 game/ 与 rage/framework/ 的 #include 搜索未发现引用,这表明它只服务于工具链。

9. 参数化运动(Parameterized Motion,PM)#

PM 把一组示例 clip 通过“注册曲线(registration curve,时间对齐)+ 混合数据库(参数→权重,运行时 k 近邻查询)”组织起来,使调用方用连续参数(方向、速度、坡度…)驱动混合。

  • 数据类型:crpmParameterizedMotionData(kPmDataTypeSimple、kPmDataTypeVariable;composite 被注释)src/dev_ng/rage/suite/src/crparameterizedmotion/parameterizedmotion.h:39-55。
  • crpmParameterizedMotionSimpleData 拥有 crClips、crpmRegistrationCurveData、crpmBlendDatabase、tags、properties src/dev_ng/rage/suite/src/crparameterizedmotion/parameterizedmotionsimple.h:31-50、src/dev_ng/rage/suite/src/crparameterizedmotion/parameterizedmotionsimple.h:125-130;实例 crpmParameterizedMotionSimple 内含注册曲线播放器、权重、参数上下界与插值器 src/dev_ng/rage/suite/src/crparameterizedmotion/parameterizedmotionsimple.h:280-287。
  • crpmBlendDatabase 注释写明“运行时使用 k 近邻搜索求权重”src/dev_ng/rage/suite/src/crparameterizedmotion/blenddatabase.h:27-29;crpmParameterSampler*、crpmMotionController*、*Calculator 用于离线构建 src/dev_ng/rage/suite/src/crparameterizedmotion/parametersampler.h:35、src/dev_ng/rage/suite/src/crparameterizedmotion/motioncontroller.h:27。
  • 运行时由 crmtNodePm 承载,合成器通过 PushNodePm 处理 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:710。游戏侧的 PM 字典 store 为 g_PmDictionaryStore(见游戏页)。

9.1 其他 suite 模块的使用情况#

模块内容游戏中是否使用
crposedb姿态样本库(crPoseSample/crClipPoseSample/crAnimWeight 的 psc)src/dev_ng/rage/suite/src/crposedb/posedb.psc:1-30对 game/ 与 rage/framework/ 的 include 搜索为 0
crstyletransfer风格迁移(style.h、stylegenerator.h、correspondencegenerator.h)同上为 0;crmtNode::kNodeStyle 节点类型存在但未见注册(见 10.1)
crextra/posematchercrPoseMatcher src/dev_ng/rage/suite/src/crextra/posematcher.h:43框架/游戏有 2 个文件 include;框架层 g_PoseMatcherStore 在地图加载后全部加载(游戏页)
crextra/mover、bonespring、gesturerecognitioncrMoverInst : phInst src/dev_ng/rage/suite/src/crextra/mover.h:28、骨骼弹簧、手势识别include 搜索为 0

(上表“include 搜索”指本次在 src/dev_ng/game 与 src/dev_ng/rage/framework 中对 #include "<模块>/ 的 ripgrep 统计,不排除通过其他头间接引用。)

10. 动作树(motion tree)#

10.1 节点类型与注册#

crmtNode 是抽象基类,内置节点类型(自定义从 0x80)src/dev_ng/rage/suite/src/crmotiontree/node.h:57-107:

类别节点
叶子/源Animation、Clip、Pm、Frame、Pose(从骨架反求帧)、Identity、Invalid、Expression(作用于子)
组合Blend、BlendN、AddSubtract、AddN、Merge、MergeN(N 型最多 sm_MaxChildren = 8 子 src/dev_ng/rage/suite/src/crmotiontree/nodeblendn.h:145)
修饰Filter、Mirror、Extrapolate、JointLimit、Capture(把子结果拷到外部帧)、Proxy(引用另一个节点的结果)、Ik
MoVE 专用State、StateRoot、StateMachine、SubNetwork、Reference
预留未实现/未注册Ragdoll、Mover、PonyTail、Style

crmtNode::InitClass 注册核心节点(Animation、Blend、AddSubtract、Filter、Mirror、Frame、BlendN、Extrapolate、Capture、Proxy、AddN、Identity、Merge、Pose、MergeN、Invalid、JointLimit),非核心的 Ik/Clip/Pm/Expression 仅在 INIT_NON_CORE_NODES 为真时注册 src/dev_ng/rage/suite/src/crmotiontree/node.cpp:381-417;该宏为 0 src/dev_ng/rage/suite/src/crmotiontree/node.h:26,因此框架层 fwAnimDirector::InitClass 显式调用 crmtNodeExpression/Pm/Clip/Ik::InitClass src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:340-356,MoVE 节点则由 mvNode::InitClass 注册(见 11.4)。

节点类型信息 NodeTypeInfo 记录类型、名称、创建/放置/资源放置函数指针与大小 src/dev_ng/rage/suite/src/crmotiontree/node.h:416-421。节点实例成员:引用计数(u16)、flags、类型、m_Disabled/m_Silent、长子/兄弟/父指针(左孩子右兄弟树)、首个 observer、所属树、签名、工厂 src/dev_ng/rage/suite/src/crmotiontree/node.h:514-536。

节点内存:crmtNodeFactory 以分页池分配节点(kMaxAllocatorBytes = 512、kChunkMemSize = 8192)src/dev_ng/rage/suite/src/crmotiontree/nodefactory.h:102-103;框架层创建全局工厂,页尺寸 {64, 96, 136, 208, 264} 字节,页数 276(PS3/360 为 92)src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:169-173。

10.2 结构修改:request / observer / synchronizer#

  • 树结构只能通过 crmtMotionTree::Request(request, target) 修改 src/dev_ng/rage/suite/src/crmotiontree/motiontree.h:29-34、src/dev_ng/rage/suite/src/crmotiontree/motiontree.h:66-69。crmtRequest 及其带源的 crmtRequestSource<N>、crmtRequestInsert 等每种节点各有对应的 request 类 src/dev_ng/rage/suite/src/crmotiontree/request.h:28-122,其 GenerateNode 创建节点并可 Replace 现有节点(例如 MoVE 根)src/dev_ng/rage/suite/src/move/move_network.cpp:86-99。
  • crmtObserver 是挂在节点上的智能指针,可接收消息;派生有类型化、仿函数、转发器版本 src/dev_ng/rage/suite/src/crmotiontree/observer.h:26-28、src/dev_ng/rage/suite/src/crmotiontree/observer.h:107-239。节点消息有 kMsgAttach/Detach/PreUpdate/Update/PostUpdate/ShuttingDown/Disconnect/Changed,消息 id 打包了节点类型 src/dev_ng/rage/suite/src/crmotiontree/node.h:310-329。
  • crmtSynchronizer : crmtObserver 实现相位同步与标签同步(crmtSynchronizerTag 最多 32 个标签、4 个属性)src/dev_ng/rage/suite/src/crmotiontree/synchronizer.h:25-27、src/dev_ng/rage/suite/src/crmotiontree/synchronizer.h:287-319、src/dev_ng/rage/suite/src/crmotiontree/synchronizer.h:404-456。

10.3 两阶段求值:Update 与 Compose#

Update(逻辑推进):crmtMotionTree::Update(dt) 用 crmtUpdater 迭代根节点 src/dev_ng/rage/suite/src/crmotiontree/motiontree.cpp:116-128。迭代是非递归的显式栈(深度上限 CRMT_ITERATE_BATCH = 64):先序阶段对带 kFlagReceivePreUpdate 的节点调用 PreUpdate(dt)(MoVE 状态机在此做转换判定),后序阶段清除 Disabled/Silent、对带 kFlagReceiveUpdate 的节点调用 Update(dt),并按 flag 向 observer 发送 Update/PostUpdate 消息;节点在访问期间被 AddRef/Release 以防自毁 src/dev_ng/rage/suite/src/crmotiontree/updater.cpp:20-103、src/dev_ng/rage/suite/src/crmotiontree/motiontree_config.h:14。

Compose(姿态计算):crmtComposer 把节点树翻译为一组 sysDependency 作业 src/dev_ng/rage/suite/src/crmotiontree/composer.h:35-47:

  1. Compose(root) 以显式栈做后序遍历;被禁用节点推入 Invalid,Silent 节点不产出;被代理(proxied)的节点结果会缓存以供 Proxy 节点复用;遍历时将节点拷贝到 scratch(SPU 友好)src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:302-419。
  2. Visit 按节点类型推送依赖:Animation→PushAnimation(time, delta, indices);Clip 视 clip 类型推送动画/表达式;Capture→PopFrame;Ik→PopSkeleton + PushIK(solvers) + PushSkeleton;Pose→推入 extra DOF 帧后 PushSkeleton(从骨架反求)src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:423-470、src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:644-700。
  3. 依赖函数以 SPU fragment 形式声明:dependency_add/animation/blend/expression/filter/frame/freeheap/identity/ik/invalid/inversepose/merge/extrapolate/normalize/physical/pose/pose_bounds/proxy/shadervars src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:40-58。
  4. Commit 在需要 finalize 时追加:PopSkeleton(帧→骨架)、物理表达式 PushPhysical、碰撞体姿态 PoseBounds(有 frag 时)、PushShaderVars、extra DOF 帧回写;若不需要 creature finalize,追加 dependency_freeheap 释放工作内存 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:123-178。

容量上限(超限时 crmtWarning 并放弃本次合成):依赖 256、栈 65、帧 128、同时使用帧 12、代理 56 src/dev_ng/rage/suite/src/crmotiontree/composer.h:83-87、src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:363-374;合成工作堆 CRMT_COMPOSER_HEAPSIZE = 512 KB src/dev_ng/rage/suite/src/crmotiontree/motiontree_config.h:17、src/dev_ng/rage/suite/src/crmotiontree/composerdata.cpp:32。

合成入口本身也是一个 fragment dependency_compose:构造 crmtComposer,Compose 成功后 Commit 并把生成的依赖挂到父依赖(Recycle)之下 src/dev_ng/rage/suite/src/crmotiontree/dependency_compose.frag:22-38。

IK 依赖的边界:dependency_ik 只对 RAGE 内置求解器类型(Simple Spine/Head、Gleicher/Iterative Limb、Arms、Legs、Quadruped)调用 Solve,其他类型(IK_SOLVER_TYPE_CUSTOM、PROXY)落入 default: break src/dev_ng/rage/suite/src/crmotiontree/dependency_ik.frag:65-76。对应地,crmtNodeIk::Update 在更新阶段调用 crKinematics::Iterate(dt) src/dev_ng/rage/suite/src/crmotiontree/nodeik.cpp:64-69。即:求解器的“目标计算”在 update 作业中、“解算写骨架”只有内置类型在 compose 作业中完成。游戏求解器如何利用这一分工见 game-animation-and-ik.md。

10.4 调度:crmtMotionTreeScheduler#

头文件注释描述的是“优先级队列 + 内部工作线程”模型 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.h:20-24、src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.h:45-62,但实现是基于 sysDependency 的三段链(注释与实现不一致,以实现为准):

图证据:Schedule 只能由主线程调用,按 priority 生成 base/bias 优先级(上限 kPriorityHigh),初始化 Recycle(带 postDelegate)、可选 Compose(FRAG_SPU_CODE(dependency_compose),输入为 composer 数据、帧数据、堆、根节点)、Update(带 dt、pre/mid delegate),以 kParentStrong 串联并 sysDependencyScheduler::Insert;若给出父树且其仍在活动,则挂接到父树的 Recycle 之下以保证顺序 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.cpp:79-148;Update/Recycle 处理器 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.cpp:261-331。

  • 每棵树两个状态(update、compose):kStateIdle/Active/Pending src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.h:90-91;WaitOnComplete(elevatePriority) 可把三个依赖提升为 kPriorityCritical 后在线程私有信号量上等待 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.cpp:210-243;可等待线程数上限 sm_MaxNumThreads = 6 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.h:112-116。
  • 父子树依赖用于“司机先于载具”“载具 pre-render 树先于乘员”等顺序约束(调用点见游戏页)。

11. MoVE:Motion tree Visual Editor 网络#

MoVE 是在动作树之上的、由编辑器导出的数据驱动网络:状态机、混合树、参数连线、事件,运行时实例化为 crmtNode 子树并通过“参数缓冲 + 请求/标志”与游戏代码通信。

11.1 初始化与内存#

mvMove::InitClass(params, loaders...) 注册 MoVE 节点类型(mvNode::InitClass)、过渡过滤器,用调用方提供的页内存初始化内部分配器,并安装 clip/expression/PM/filter/transition filter/network def 的加载回调(默认实现多为 Assertf(false) 占位)src/dev_ng/rage/suite/src/move/move.cpp:40-129;InitParams 默认 NetworkRegistrySize = 8 src/dev_ng/rage/suite/src/move/move.h:38-65。游戏在 CMovePed::InitClass 中以 MOVEPED_HEAP_SIZE 页内存、NetworkRegistrySize = 75、以及 fwAnimDirector::Retrieve* 系列回调初始化 MoVE src/dev_ng/game/animation/MovePed.cpp:859-889;该堆在 PC/PS4/XB1 为 4 MB,其他平台 1.25 MB(PS3 从 flex 分配器预取)src/dev_ng/rage/base/src/system/memory.h:94-98、src/dev_ng/game/animation/MovePed.cpp:863-868。代码为 g_mvRefOutputs = !PARAM_refoutput.Get(),即传入 -refoutput 反而关闭输出缓冲的引用计数(与参数说明文字 “Enable AddRef/Remove…” 相反,以代码为准)src/dev_ng/rage/suite/src/move/move.cpp:78-86。

11.2 网络定义文件格式(.mrf)#

mvNetworkDef::Load(filename) 通过 ASSET.Open(filename, "mrf") 读取 src/dev_ng/rage/suite/src/move/move_file.cpp:281-288:

段内容证据
文件头Magic(4 字节 "MoVE")、Version{Major,Minor,Patch,Revision}、DefinitionLength、StringTableLength、XRefsmove_file.cpp:43-51、move_file.cpp:135-157
版本检查Major/Minor/Revision 必须与运行时 2.0.0.0 相等(Patch 不比较)move_file.cpp:161-172、move_version.h:21-38
外部引用XRefs 个“长度+路径”,仅存其 atStringHash(不再递归加载,交给 mvNetworkDefLoader)move_file.cpp:313-346
请求表 / 标志表count + (key,index) 对move_file.cpp:348-360
节点定义流连续的 mvNodeDef{u16 type; u16 index; ...},按类型 Init(stream) 并以 QueryMemorySize() 前进,直到缓冲结束move_file.cpp:110-127、move_node.h:78-103
字符串表末尾原样读入move_file.cpp:362-369

节点定义在内存中的布局与磁盘一致(除编译器填充)src/dev_ng/rage/suite/src/move/move_network.h:71-76;ReadNetwork 的注释警告:导出尺寸或 QueryMemorySize 实现错误会导致越界读取 src/dev_ng/rage/suite/src/move/move_file.cpp:111-115。MV_VALIDATE 只在非 __FINAL 版本检查节点类型合法性 src/dev_ng/rage/suite/src/move/move_config.h:13-17。仅 __DEV && CR_DEV 下额外从 common:/non_final/anim/move/nodemaps/ 等目录加载 *_nodemap.xml 调试名 src/dev_ng/rage/suite/src/move/move_file.cpp:181-240、src/dev_ng/rage/suite/src/move/move_file.cpp:309-311。游戏通过 g_NetworkDefStore 流式加载 network def,见游戏页。

11.3 网络实例、信号与参数缓冲#

类型职责证据
mvSignalData每网络 32 位 request 位 + 32 位 flag 位;flag 是用户开关,request 每次更新后清零、需要每帧重新推送move_network.h:25-67
mvNetworkDef : pgBaseRefCounted请求/标志的 (hash,index) 表、xref、原始节点数据;Create(outputBuffer) 实例化网络move_network.h:76-175
mvMotionWeb网络与子网络基类:引用计数(生命周期交给动作树)、信号、m_ParameterBuffers(当前/内部双缓冲)、输出缓冲、定义、m_Entity(指回 fwMoveNetworkInterface 的“hack”指针)move_network.h:179-287
mvNetwork每个可动画实体唯一的主网络;Init(motionTree, cmds) 用 mvRequestStateRoot 在树中生成 mvNodeStateRootmove_network.h:291-320、move_network.cpp:103-109
mvSubNetwork / mvNodeSubNetwork可插入主网络 SubNetwork 槽位的子网络句柄与对应树节点;IsAttached 供游戏查询;带 blend duration 信号 idmove_subnetwork.h:20-111
mvCommandBuffer包装参数缓冲,提供 AppendSetClip/Float/Boolean/Filter/Frame/Observer/Network、AppendBroadcastRequest、AppendSetFlag/ResetFlag、AppendForceSelectStatemove_command.h:26-93
mvParameter参数类型:Clip、Expressions、Filter(N)、Frame、PM、Boolean、Real、Int、Data、Node、Observer、Network、Reference、Property、NodeId、NodeName、Animation、Request、Flag(GTA 下 RESTRICTED_PARAMTER_TYPES 调整了 Animation 的位置)move_parameter.h:22-82

参数缓冲语义(注释 + 实现):当前缓冲保存本帧从命令缓冲推入、供 MoVE 内部读取的参数;内部缓冲接收节点输出,PrepareBuffers 时两者交换,使内部输出在下一次更新中可作为输入;输出缓冲面向游戏读取 src/dev_ng/rage/suite/src/move/move_network.h:214-255。mvNetwork::PrepareBuffers 先 SwapBuffers 再把命令缓冲内容 PrepareBuffer 进当前参数缓冲,注释要求在“动作树更新之后、AI 推完参数之后”的无竞争区调用 src/dev_ng/rage/suite/src/move/move_network.h:310-316、src/dev_ng/rage/suite/src/move/move_network.h:432-438。框架层实际在动作树 pre-delegate(工作线程、update 之前)中调用它,见游戏页的 MoVE 数据流时序。mvNodeStateRoot::Update 每次更新结束时清空 request 位,并把内部参数拷入输出缓冲的“下一”缓冲 src/dev_ng/rage/suite/src/move/move_state.cpp:918-930。

11.4 MoVE 节点类型系统#

MoVE 节点类型值不必与 crmtNode::eNodes 对齐(例如 MoVE 的 kNodeStateMachine = 1)src/dev_ng/rage/suite/src/move/move_node.h:107-151。每种类型以 NodeTypeInfo 注册:create/init/query-size 函数、参数 set/get 解析函数、参数类型解析、事件解析;参数/事件按 makenetwork 导出的“索引”解析为函数指针,代码与工具的索引必须一致 src/dev_ng/rage/suite/src/move/move_node.h:158-259、src/dev_ng/rage/suite/src/move/move_subnetwork.h:35-51。mvNode::InitClass 注册 SubNetwork/Reference/StateMachine 类、StateMachine、StateRoot、Tail、InlinedStateMachine、以及 Animation/Blend/AddSubtract/Filter/Mirror/Frame/Ik/BlendN/Clip/Pm/Extrapolate/Expression/Capture/Proxy/AddN/Identity/Merge/Pose/MergeN/State/Invalid/JointLimit/SubNetwork/Reference 与 mvSynchronizerTag src/dev_ng/rage/suite/src/move/move_node.cpp:93-135。实例化时通过 mvCreateContext 传递动作树、所属网络、子节点数组、tail 节点与参数缓冲 src/dev_ng/rage/suite/src/move/move_node.h:30-74。

11.5 状态与状态机#

状态定义 mvNodeStateDef 除子节点外还携带四类连线 src/dev_ng/rage/suite/src/move/move_statedef.h:209-337:

  • 参数输入(Parameter{ParameterCrc, NodeIndex, TypeIndex, ParameterIndex}):observer 从参数缓冲读值写到节点;
  • 参数输出(同结构,kOutputIndex = 0xffff):observer 从节点读值写入参数缓冲;
  • 事件(Event{NodeIndex, TypeIndex, EventCrc}):节点事件推入参数缓冲;
  • 运算(Operation + Operator{kPushValue, kFindAndPushValue, kAdd, kMultiply, kFCurve}):烘焙成栈式运算,结果写入目标节点参数。

根状态在 mvNodeStateRoot::Init 中:先把命令缓冲预备进参数缓冲,创建初始子节点,再 RegisterHandlers 安装上述连线,最后清空 request 位 src/dev_ng/rage/suite/src/move/move_state.cpp:888-914。

转换 mvTransition 位域打包 flags:权重信号、转换后阻断更新、时长来自信号、尺寸(14 位)、transitional/immutable、条件数(4 位,即最多 15 个条件)、modifier(3 位)、re-evaluate、同步器类型(2 位)、filter、merge blend,随后是 tag、默认时长、时长信号、权重信号、目标状态偏移 src/dev_ng/rage/suite/src/move/move_types_internal.h:220-317。观察:SetFromFlags 中 Transitional/Immutable 的掩码(0x00300000、0x00680000)与 ConditionCount 的掩码 0x00f00000 位重叠 src/dev_ng/rage/suite/src/move/move_types_internal.h:226-247;配置宏 IGNORE_TRANSITION_TRANSITIONAL_THINGY (1) 的注释表明在数据转换之前这两个标志被忽略 src/dev_ng/rage/suite/src/move/move_config.h:22-23,因此重叠在实际行为中可能无害(推断,未追踪其所有使用点)。

条件类型:InRange、OutOfRange、OnRequest、OnFlag、AtEvent、>、>=、<、<=、LifetimeGreaterThan/LessThan、OnMoveEventTag、BoolEquals;modifier:EaseInOut/EaseOut/EaseIn/Linear/Step src/dev_ng/rage/suite/src/move/move_types_internal.h:63-104;每种条件的内存尺寸表由 CompileTimeAssert 保证与枚举同步 src/dev_ng/rage/suite/src/move/move_state.cpp:159-177。

状态机更新(在 PreUpdate,即动作树 update 的先序阶段)src/dev_ng/rage/suite/src/move/move_state.cpp:488-523:

  1. 累加 m_Lifetime;EvaluateAndTransition() 依序遍历当前状态的转换,对每个转换按序检查全部条件(短路),第一个全部通过的转换生效:可选加载转换过滤器,DoTransition 创建目标状态并以 blend 节点交叉淡入;只取第一个命中的转换 src/dev_ng/rage/suite/src/move/move_state.cpp:527-646。
  2. 若本帧未转换,再检查参数缓冲中是否有针对本状态机 id 的 kParameterNodeId(强制选择状态,注释称只用于网络游戏等特殊场合),命中则 ApplyState 并替换子节点。
  3. 并发转换数受 MOVE_CONCURRENT_TRANSITION_LIMIT = 3 限制,超出的最老转换被强制权重 1.0 结束 src/dev_ng/rage/suite/src/move/move_config.h:31-32、src/dev_ng/rage/suite/src/move/move_state.cpp:850-865。转换事件 MOVE_TRANSITION_EVENTS 被关闭(0)src/dev_ng/rage/suite/src/move/move_config.h:28-29。

图证据:move_state.cpp:888-914、move_state.cpp:488-523、move_state.cpp:527-646。

12. 帧内时序(以源码为准)#

游戏中每个有动画的实体通过 fwAnimDirector 拥有最多三棵动作树:pre-physics(主体动画 + MoVE + mover)、mid-physics(面部,仅在创建 facial rig 时存在)、pre-render(pose 回读 + IK + 表达式)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:80-117。阶段注释见 src/dev_ng/rage/framework/src/fwanimation/animdirector.h:79-103。以下是按主线程调用顺序的实际时序:

#时机(主线程)调用动画相关效果证据
1Game Update 前的 Misc 组CutSceneManagerWrapper::PreSceneUpdate、CAnimSceneManager::UpdatePreAnimcutscene 时间推进与事件派发;AnimScene 状态机game/Core/game.cpp:2746-2747
2Streaming 组CSituationalClipSetStreamer::Update、CPrioritizedClipSetStreamer::Update;fwFacialClipSetGroupManager::Updateclip set 预流式game/Core/game.cpp:2760-2774
3CScene::Update → CGameWorld::ProcessStartAnimationUpdate()锁 kPhasePrePhysics;先更新 synced scene 相位;SU_START_ANIM_UPDATE_PRE_PHYSICS 与 ..._PASS2 两遍为实体调度 pre-physics 树(PASS2 用于需要父树先行的实体:载具以司机为父、依附降落伞的 ped 以降落伞为父,见 Vehicle.cpp:2480-2486、Vehicle.cpp:15959-15967、Ped.cpp:22759-22777)game/Core/game.cpp:2826、game/scene/scene.cpp:808-809、game/scene/world/GameWorld.cpp:1850-1861、GameWorld.cpp:1155-1175
4同上,紧接AI 更新(SU_UPDATE 等)与 UpdateEntitiesThroughAnimQueue动画在工作线程运行的同时主线程做 AI;动作树 post-delegate 把实体推入无锁队列,主线程按完成顺序弹出并执行该实体的 AI 更新(队列暂空时信号量等待)GameWorld.cpp:1914-1970、GameWorld.cpp:1192-1299
5AI 之后EndAnimationUpdate()WaitForAllUpdatesToComplete;SU_END_ANIM_UPDATE_PRE_PHYSICS → fwAnimDirector::EndUpdatePrePhysics(施加动画速度、删除无引用网络玩家、交换 MoVE 输出缓冲、调度 mid-physics 树);解锁GameWorld.cpp:2000-2003、GameWorld.cpp:1177-1190、animdirector.cpp:212-227
6随后CPhysics::Update()物理模拟;mid-physics(面部)树与之并行运行GameWorld.cpp:2042-2061
7Render Update 组camManager::Update → CGameWorld::ProcessPedsEarlyAfterCameraUpdate相机更新后,ped 先做 ProcessPostCamera,可能启动“即时动画更新”(再次锁 pre-physics)game/Core/game.cpp:2900、game/Core/game.cpp:2919-2923、GameWorld.cpp:2423-2451
8同组CGameWorld::ProcessAfterCameraUpdate完成即时动画;post-camera 动画更新:此前未更新或只更新了 mover 而现在可见的实体补做更新/合成,EndPostCameraAnimationUpdate 等待并解锁;最后 CBaseIkManager::ProcessSolvers 分配 IK 求解器game/Core/game.cpp:2962、GameWorld.cpp:2485-2533、GameWorld.cpp:1301-1320
9同组稍后CutSceneManagerWrapper::PostSceneUpdatecutscene post-scene 更新game/Core/game.cpp:2997
10可见性扫描/PostScanProcessPvsWhileScanning 与 FinishPreRender锁 kPhasePreRender;对 PreRender 列表逐实体 PreRender(ped 内部 CIkManager::PreIkUpdate 后 StartUpdatePreRender 调度 pre-render 树,乘员以载具 pre-render 树为父)game/renderer/PostScan.cpp:485-493、PostScan.cpp:773-799、game/Peds/Ped.cpp:23386、Ped.cpp:23419-23431
11渲染准备CRenderer → CGameWorld::ProcessAfterPreRenderWaitForAllUpdatesToComplete 并解锁;AnimScene PreRender 阶段;随后 ped PostPreRender(含 PostIkUpdate)game/renderer/Renderer.cpp:1008、GameWorld.cpp:2540-2553、Ped.cpp:23576

补充:

  • 步骤 3 中每个实体的 CDynamicEntity::StartAnimUpdate 只在“在某视口可见或强制”时调度 pre-physics 树(物件还考虑与相机的距离,便于听到离屏物件的音频事件);否则退出动画队列模式 src/dev_ng/game/scene/DynamicEntity.cpp:763-849。步骤 8 的 post-camera 补更新有两种:从未更新 → 完整 UpdateMove(若已更新则 dt=0);已更新但未合成 → 只调度 compose src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:242-282。
  • “锁定”是全局标志 SetLockedGlobal(bool, phase),代码注释规定锁定期间不得读写带 director 的骨架局部矩阵、不得移除这些实体 src/dev_ng/game/scene/world/GameWorld.cpp:1162-1166;Schedule 断言调用时全局锁已持有 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:149-161。
  • 调试开关 bBlockAsyncAnimUpdate 可让步骤 3 后立即阻塞,消除动画与 AI 的并行 src/dev_ng/game/scene/world/GameWorld.cpp:1865-1869。
  • mid-physics 树在步骤 5 中被调度,而下一次全局等待在步骤 8/11。因此面部动画与物理并行(推断:基于调度与等待点位置;未找到专门等待 mid-physics 的调用)。WaitForAnyActiveUpdateToComplete(excludeMidPhysics) 允许单个 director 的等待排除 mid-physics 树 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:143-171。

13. 约束与边界行为#

  • 结构一致性:动画/帧/骨架之间的快速路径依赖签名一致;签名算法在 crAnimation 与 crFrameData 两处实现,任何改动必须同步(见 2.1、3.2)。
  • 无效 DOF:以全 1 位模式表示,合成时未匹配的 DOF 默认被清为无效(CompositeFrame(..., clear=true))src/dev_ng/rage/base/src/cranimation/animation.h:98-102。
  • 骨骼顺序:crSkeleton::Update 依赖父骨骼索引小于子骨骼。
  • 合成上限:256 依赖/128 帧/64 深度/56 代理;超限时该树本帧合成被放弃(Compose 返回 false,dependency_compose 直接返回 true,不 Commit)src/dev_ng/rage/suite/src/crmotiontree/dependency_compose.frag:32-38。这意味着骨架保留上一帧姿态(推断)。
  • 调度线程约束:Schedule 只能在主线程;pre/mid/post delegate 在工作线程执行,代码必须线程安全 src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.h:59-61、src/dev_ng/rage/suite/src/crmotiontree/motiontreescheduler.cpp:83。
  • MoVE 信号:每网络最多 32 个 request 与 32 个 flag;request 每次更新后自动清除,需要每帧重发 src/dev_ng/rage/suite/src/move/move_network.h:25-48。
  • MoVE 数据兼容:.mrf 的 Major/Minor/Revision 必须匹配,节点参数/事件索引必须与 makenetwork 导出一致;FINAL 版不做节点类型校验。
  • 平台差异:crKinematics 每实例最多 30 个求解器(PC/PS4/XB1)或 10 个(其他)src/dev_ng/rage/base/src/crbody/kinematics.h:64-67;节点池页数、帧公共池、MoVE 堆均按平台缩放(见上文)。__SPU 下合成器会走 scratch 拷贝与代理路径 src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:340-342。

14. 未决问题 / 证据不足#

  • crAnimCache 在非 PS3 平台被禁用,compact 块在现世代平台是否存在、如何解压,未追踪到运行时证据(见 2.2)。
  • 未逐个阅读全部 dependency_*.frag 与 crFrame 组合运算(Blend/Merge/Add 的四元数处理细节、normalize 时机)。
  • crCreatureComponent* 各派生组件的 Pose/InversePose 实现未逐一展开;表中职责部分依据接口与命名。
  • crmtNode::kNodeStyle/Ragdoll/Mover/PonyTail 在枚举中存在,但未在 crmtNode::InitClass、mvNode::InitClass 中注册(见 10.1、11.4);是否有其他注册点未穷尽搜索。
  • MoVE 参数缓冲内部(mvParameterBuffer 的多 Buffer/Map、去重/压缩策略)与 mvSynchronizer、mvObserver 只做了接口级阅读。