RAGE 动画核心:cr* 数据层、Motion Tree 与 MoVE
本页解释 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 都只是在这里描述的机制上做装配与调度。
背景与相关页面#
- 游戏/框架层接入(
fwAnimDirector、组件、clip set、CMovePed、AnimScene、IK):game-animation-and-ik.md。 - 过场动画系统:cutscenes.md。
sysDependency依赖调度器与工作线程模型:../01-runtime/threading-and-render-thread.md。pgBase/datResource资源与重定位:../02-rage-core/paging-and-resources.md;parser/PSO:../02-rage-core/parser-and-metadata.md。- 流式引擎与资产 store:../04-rage-framework/streaming-engine.md。
- 布娃娃/NaturalMotion 与物理驱动姿态:../09-physics/game-physics-and-ragdoll.md。
- 动画任务(
CTaskScriptedAnimation等):../07-ai-tasks/task-catalogue.md。
术语:DOF(degree of freedom,一个 (track, id, type) 三元组,例如“骨骼 id=X 的旋转四元数”);frame(一组 DOF 的值快照);clip(带元数据的动画包装);motion tree(每个角色的一棵动画节点树);compose(把节点树转换为一串依赖作业并实际计算出姿态的过程)。
相关源码文件#
| 目录 | 关键文件 | 作用 |
|---|---|---|
rage/base/src/cranimation | animation.h、animstream.h、animpacker.cpp、animcache.*、animchannel.h、animtolerance.h、framedata.h、frame.h、frameaccelerator.h、framefilters.h、framebuffers.h | 压缩动画、DOF/帧、帧加速表、帧过滤器 |
rage/base/src/crskeleton | skeletondata.h、skeleton.h/.cpp、bonedata.h、jointdata.h | 骨架定义与实例 |
rage/base/src/creature | creature.h、component.h、component*.h | 生物体与组件(骨架、额外 DOF、shader var、物理、mover…) |
rage/base/src/crmetadata | property.h、propertyattributes.h、tag.h、tags.h、tagiterators.h | 属性与时间标签 |
rage/base/src/crbody | iksolverbase.h、iksolver.h、kinematics.h/.cpp、ikbody.h | IK 求解器基类与 RAGE 内置求解器 |
rage/suite/src/crclip | clip.h、clipanimation(s).h、clipanimationexpression.h、clipdictionary.h | clip 类型族与字典 |
rage/suite/src/crextra | expressions.h、expressionops.h、expressionstream.h、mover.h、posematcher.h | 表达式、pose matcher 等 |
rage/suite/src/crparameterizedmotion | parameterizedmotion*.h、blenddatabase.h、registrationcurve.h、parametersampler.h | 参数化运动 |
rage/suite/src/crmotiontree | node.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/move | move.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.h | MoVE 网络运行时 |
game/scene/world | GameWorld.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_Flags | kLooped(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 = 127 | animation.h:492-496、animation.h:259 |
m_Duration、m_Signature、m_Name | 秒数、结构哈希、名称 | animation.h:498-505 |
m_Tracks | atArray<crAnimTrack*>,结构化视图 | animation.h:507-508 |
m_MaxBlockSize、m_RefCount | 打包后最大块尺寸;引用计数 | animation.h:510-514 |
m_Blocks | atArray<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, kNormalizesrc/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/EvaluateChannelssrc/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 才参与组合,这允许局部合成与 LODsrc/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_MASKXYZWsrc/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} KBsrc/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_MirrorIndexsrc/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 与约束权重等) |
ShaderVars | shader 变量动画(UV、帧索引) |
Physical | 物理表达式所需的运动描述(Motion,供表达式 OpMotion) |
Mover、BlendShapes、Telemetry、Camera、Light、ParticleEffect、Event、Move | mover 矩阵、混合形状、遥测、相机/灯光/粒子参数、事件、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,自定义从0x80src/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_HasBlockTagssrc/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) | 类 | 内容 | 证据 |
|---|---|---|---|
kClipTypeAnimation | crClipAnimation | 单个 crAnimation + [start,end] 区间 + rate | clipanimation.h:122-126 |
kClipTypeAnimations | crClipAnimations | 多段动画数组(每段 start/end/rate),总时长,m_Parallel 表示并行或串行 | clipanimations.h:186-206 |
kClipTypeAnimationExpression | crClipAnimationExpression : crClipAnimation | 额外持有 crExpressions | clipanimationexpression.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*/Linearsrc/dev_ng/rage/suite/src/crextra/expressionops.h:41-77。 - 运行态:
Pack()调用crExpressionPacker::Pack把操作树编译成crExpressionStreamsrc/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、propertiessrc/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/posematcher | crPoseMatcher src/dev_ng/rage/suite/src/crextra/posematcher.h:43 | 框架/游戏有 2 个文件 include;框架层 g_PoseMatcherStore 在地图加载后全部加载(游戏页) |
crextra/mover、bonespring、gesturerecognition | crMoverInst : 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:
Compose(root)以显式栈做后序遍历;被禁用节点推入Invalid,Silent节点不产出;被代理(proxied)的节点结果会缓存以供Proxy节点复用;遍历时将节点拷贝到 scratch(SPU 友好)src/dev_ng/rage/suite/src/crmotiontree/composer.cpp:302-419。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。- 依赖函数以 SPU fragment 形式声明:
dependency_add/animation/blend/expression/filter/frame/freeheap/identity/ik/invalid/inversepose/merge/extrapolate/normalize/physical/pose/pose_bounds/proxy/shadervarssrc/dev_ng/rage/suite/src/crmotiontree/composer.cpp:40-58。 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/Pendingsrc/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 = 6src/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、XRefs | move_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 在树中生成 mvNodeStateRoot | move_network.h:291-320、move_network.cpp:103-109 |
mvSubNetwork / mvNodeSubNetwork | 可插入主网络 SubNetwork 槽位的子网络句柄与对应树节点;IsAttached 供游戏查询;带 blend duration 信号 id | move_subnetwork.h:20-111 |
mvCommandBuffer | 包装参数缓冲,提供 AppendSetClip/Float/Boolean/Filter/Frame/Observer/Network、AppendBroadcastRequest、AppendSetFlag/ResetFlag、AppendForceSelectState | move_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:
- 累加
m_Lifetime;EvaluateAndTransition()依序遍历当前状态的转换,对每个转换按序检查全部条件(短路),第一个全部通过的转换生效:可选加载转换过滤器,DoTransition创建目标状态并以 blend 节点交叉淡入;只取第一个命中的转换src/dev_ng/rage/suite/src/move/move_state.cpp:527-646。 - 若本帧未转换,再检查参数缓冲中是否有针对本状态机 id 的
kParameterNodeId(强制选择状态,注释称只用于网络游戏等特殊场合),命中则ApplyState并替换子节点。 - 并发转换数受
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。以下是按主线程调用顺序的实际时序:
| # | 时机(主线程) | 调用 | 动画相关效果 | 证据 |
|---|---|---|---|---|
| 1 | Game Update 前的 Misc 组 | CutSceneManagerWrapper::PreSceneUpdate、CAnimSceneManager::UpdatePreAnim | cutscene 时间推进与事件派发;AnimScene 状态机 | game/Core/game.cpp:2746-2747 |
| 2 | Streaming 组 | CSituationalClipSetStreamer::Update、CPrioritizedClipSetStreamer::Update;fwFacialClipSetGroupManager::Update | clip set 预流式 | game/Core/game.cpp:2760-2774 |
| 3 | CScene::Update → CGameWorld::Process | StartAnimationUpdate() | 锁 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 |
| 5 | AI 之后 | 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 |
| 7 | Render 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::PostSceneUpdate | cutscene post-scene 更新 | game/Core/game.cpp:2997 |
| 10 | 可见性扫描/PostScan | ProcessPvsWhileScanning 与 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::ProcessAfterPreRender | WaitForAllUpdatesToComplete 并解锁;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);已更新但未合成 → 只调度 composesrc/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只做了接口级阅读。