首页 > Personal > Game > 游戏引擎设计系列11-动画系统
2018
09-28

游戏引擎设计系列11-动画系统

动画系统,包括基础的位置信息,骨骼节点信息,骨骼,Pose.制作导出模型的T Pose模型和骨骼的信息,再通过动画导出的骨骼信息值按帧计算出骨骼的当前Pose信息,通过SkinedMesh(一个顶点一般最多和四个骨骼节点相关)的双四元数算法,计算实际mesh的顶点信息.
/// Transform,标识位置信息
Transform {
Vector3 position;
Quaternion rotation;
};

/// Joint,骨骼节点的基本信息
Joint {
int parent_id; // 父节点的ID
Identifier name; // 名字
bool blend_skip; // 是否忽略混合
};

/// 骨骼的基本信息,包含所有骨骼节点的T-Pose信息.骨骼节点的位置信息是以相对于父节点的相对位置信息来存储的.
Skeleton {
const Joint & GetJoint(uint id); // 通过骨骼节点ID获取骨骼节点信息
const Array & GetJointSet(); // 获取骨骼节点信息数组
void SetJointBlendSkip(const Identifier & name, bool blend_skip); // 设置骨骼节点是否参与混和
int GetJointId(const Identifier & name); // 通过骨骼节点名字获取骨骼节点ID
int GetJointParentId(uint id); // 获取骨骼节点的父节点信息
const Identifier & GetJointName(uint id); // 通过骨骼节点ID获取骨骼节点名字
uint GetJointCount(); // 获取骨骼节点数
bool JointIsChildOf(uint id_child, uint id_parent); // 判断骨骼节点是否是给定骨骼节点的孩子
const Array & GetLocalPose(); // 获取T-Pose的Local位置信息数组(所有节点的位置信息都是相对于父节点的
const Array & GetModelPose(); // 获取T-Pose的模型空间下的位置信息数组
const Transform & GetJointLocalPose(uint id); // 获取骨骼节点的Local位置信息
const Transform & GetJonitModelPose(uint id); // 获取骨骼节点的模型空间位置信息
const dentifier & GetName(); // 骨骼名称

Core::Identifier name; // 骨骼的名称
Array joints; // 骨骼点的数组
HashSet name_to_id; // 骨骼名字到骨骼ID的缓存
shared_ptr(Pose) pose; // T-pose, 存储的是Local位置信息,模型空间位置信息生成
};

/// Pose,动画基本的位置信息结构,SkinedMesh通过它来获取当前骨骼的实际位置信息
Pose {
void UpdateModelPose(bool skip_lock_pose = false); // 如果Pose已经dirty,更新所有骨骼节点的模型空间位置信息
void SetLocalPose(const Array & transform); // 设置Pose当前的位置信息
Skeleton GetSkeleton(); // 获得骨骼
void SetSkeleton(Skeleton s); // 设置骨骼
void SetJointLocalPose(uint id, const Transform & transform); // 设置指定骨骼节点的Local位置信息
void SetJointModelPose(uint id, const Transform & transform, bool dirty = true); // 设置指定节点的模型空间位置信息
void SetJointLockPose(uint id, bool flag); // 设置骨骼节点是否锁定,锁定一般用于物理控制动画,如ragdoll
bool GetJointLockPose(uint id); // 获取骨骼节点是否锁定
const Transform & GetJointModelPose(uint id); // 获取骨骼节点的模型空间位置信息
const Transform & GetJointLocalPose(uint id); // 获取骨骼节点的local位置信息
Array & GetLocalPose(); // 获取所有骨骼节点的local位置信息数组
Array & GetModelPose(); // 获取所有骨骼节点的模型空间位置信息数组
void Blend(by_ptr(Pose) pose1, by_ptr(Pose) pose2, float weight); // 将两个Pose的位置信息混合,并赋值给当前Pose

Skeleton skeleton; // 骨骼信息
Array local_pose; // 对应骨骼信息的所有骨骼节点的local位置信息数组
Array lock_pose; // 对应骨骼信息的所有骨骼节点的锁定状态数组
Array model_pose; // 对应骨骼信息的所有骨骼节点的模型空间位置信息数组
bool pose_dirty; // 当前Pose是否已经被修改
};

使用AnimationTrack记录导入的动画信息,里面是按帧保存的所有骨骼节点的local位置信息,同时可能会在某帧添加相应的Event,在运行到该帧时会发出相应消息,比如用来制作脚步踩到地面的声音等等.
使用包含AnimationTrack的Animation来作为最基本的动画播放单元,里面会有采样函数,可以通过这个时间采样出相应的位置信息.
AnimationSet是一组动画的集合,以名字来标识不同动画,在游戏逻辑中可以通过替换AnimationSet来达到替换一组动画的需求,如射击游戏的换枪,就可以用这个实现.
/// AnimationTrack, 动画的帧信息结构
AnimationTrack {
bool GetTransform(int id, uint frame, Transform & transform); // 通过骨骼节点ID和帧数获取local位置信息
bool GetTransform(int id, float time, Transform & transform); // 通过骨骼节点ID和动画时间获取local位置信息,会根据时间算出前后两个帧数,在通过Lerp插值两帧的位置信息

int GetFrameCount(); // 一共有多少帧
float GetFrameRate(); // 帧率是多少,即一秒多少帧,一般是30帧
float GetAnimationTime(); // 根据帧数和帧率计算出的总动画时间

HashSet joint_index; // 骨骼节点名字到Track Id的映射,并不是所有的Track都包含Skeleton上全部节点的数据,可能有些节点不需要动画,就需要骨骼节点到Track Id的映射,当然大部分都是全部包含的.
Array joint_array; // 存储的动画数据
int frame_count; // 一共有多少帧
}

/// Animation, 实际动画播放的信息结构
Animation {
AnimationTrack GetAnimationTrack() { return track; }; // 使用的AnimationTrack
bool SampleTrack(float time, Skeleton skeleton, Array & out, bool auto_circle = true); // 通过时间采样动画,如果不循环,超过动画时间将停在最后一帧动画
bool SampleTrack(float time, Array & joints, Core::Array & out, bool auto_circle = true); // 通过时间采样动画
float GetAnimationTime(); // 动画的总时间,同AnimationTrack上的时间
void BuildJointMap(Skeleton skeleton, Core::Array & joint_map); // 生成Skeleton骨骼节点ID到Track ID的映射

AnimationTrack track; // 保存AnimationTrack
}

/// AnimationSet, Animation的组合
AnimationSet {
void AddAnimation(const Identifier & key, Animation animation); // 按名称添加动画,不同于实际动画名,用来在逻辑中使用
void RemoevAnimation(const Identifier & key); // 删除动画
Animation GetAnimation(const Identifier & key); // 通过名字获取动画
void SetAnimation(const Identifier & key, Animation animation); // 替换相应动画
void OnAnimationDirty(); // 动画数组发生变化时调用,会发出

HashSet animation_set; // 保存动画和名字对应的哈希表
}

整个动画运行系统设计为树形结构,AnimationNode作为基本的动画节点,一个动画节点可能下面还关联了多个节点,同时一个节点可能并不是全部骨骼节点都起作用,可以通过SkeletonMap来处理,最后一个完整的动画输出结果是所有这些节点全部运行结果的整合,通过这个设计可以反正相当复杂的动画表现,如,上下半身的混合,八方向模拟所有方向的移动,移动时叠加射击效果等.另外需要注意一点,所有的动画操作不过时同步也好还是融合,基本都是以Pose为基本单位的.

/// SkeletonMap,
SkeletonMap {
enum BlendType { // 标识一个骨骼节点以什么方式融合到动画当中
kNone, // 不融合
kAddtive, // 叠加, 需要算出动画当前时间的位置信息和初始帧的位置信息的差值,然后再将这个差值作用到动画中去
kReplace, // 替换, 替换掉当前的动画
};

bool AddJoint(const Identifier & name, bool skip_child = true, byte type = kReplace); // 添加骨骼节点,如果不忽略child则加入这个节点下面所有的骨骼节点
bool AddJointFromTo(const Identifier & from, const Identifier & to, byte type = kReplace); // 添加从一个骨骼节点到另一个骨骼节点之中的所有节点
const Array & GetMap() { return map; }; // 获取骨骼节点数组

Core::Array map; // 保存添加的骨骼节点数组
Skeleton skeleton; // 骨骼信息
}

/// AnimationNode, 所有动画节点的基类
AnimationNode {
void Update(F32 time); // 按时间更新动画的更新函数
Pose GetPose(); // 获得当前动画节点的输出Pose
Pose GetAddtivePose(); // 获得当前动画节点的输出Pose,以叠加形式保存, 需要计算当前时间和初始帧的位置信息的差值
void OnActive(); // 当动画节点生效时调用
void OnValid(bool valid); // 当动画节点有效性变化时调用
}

/// AnimationNodeTime, 和时间相关的动画节点的基类
AnimationNodeTime : AnimationNode {
float GetTotalTime(); // 动画的总时间
void SetTime(float time); // 设置当前运行时间
float GetTime(); // 获取当前运行时间
float GetPlayRate(); // 获取动画节点的运行速率
void SetPlayRate(float rate); // 设置动画节点的运行速率
float GetAnimationRate(); // 获取动画的帧率
void SetAnimationRate(float rate); // 设置动画的帧率
}

/// AnimationNodePose : AnimationNodeTime, 动画Pose节点,输出Pose信息的基本节点,根据时间采样动画计算输出的Pose信息,同时可以认为操作修改实际输出结果
/// AnimationNodeList : AnimationNode, 动画选择节点,可以按名字添加多个动画节点,并根据当前名字选择实际输出的动画节点
/// AnimationNodeRandom : AnimationNode, 类似于AnimationNodeList,不同的是动画节点的选择是随机的
/// AnimationNodeSync : AnimationNode, 动画过度节点,通过一定的过度时间,将一个动画节点过度到另一个动画节点, 两个动画节点同时运行,并根据过度时间的Blend两个动画节点,如一个人由走变跑,为了实现更好的效果,不是一下切换到跑,而是慢慢的经过一个从走到跑的过程再完全变成跑
/// AnimationNodeBlend: AnimationNode, 动画融合节点,按权重值将多个动画节点融合在一起,动画节点可以根据SkeletonMap标识融合时那些骨骼节点以什么方式融合(None,Replace,Additive),最终输出一个全新Pose,比如射击游戏玩家保持身体方向不变,上下左右调整枪的方向进行瞄准, 只需要9个方向的动画(左上,左中,左下,上,中,下,右上,右中,右上),就可以融合出平滑的瞄准动画,又比如人物的移动,只需要8个方向(左前,前,右前,左,右,左后,后,右后),就可以模拟出所有方向上平滑的移动.(实际实现时特殊处理为了不同的动画节点AnimationNodeOffset,AnimationNodeDirection)
/// AnimationNodeSmooth : AnimationNode, 动画平滑节点,AnimationNodeSmooth用来修饰其他动画节点,动画节点上面的动画切换,都会按一定的平滑时间从当前Pose平滑过度(Blend)到新的动画Pose.主要优化用来优化动画表现.
/// AnimationNodeAction : AnimationNodeSmooth, 特殊的工具动画节点,本身是平滑动画节点,并且可以在当前动画节点运行的同时,叠加或者替换运行新的动画.方便在游戏逻辑中应用.

因为动画的多层融合和复杂操作,有些动画的表现可能会不符合实际需求,如需要枪手持枪的武器,不做特殊处理的话,手会不能完全正确的拿在枪上,这是需要使用IK让部分骨骼节点在正确的位置上,IK其实最简单的实现就是控制3个骨骼节点,因为生物的动画几乎所有的骨骼节点都没有位移操作只有旋转操作,只需要像铰链一样控制骨骼节点的旋转就能很好的模拟出IK的效果,当然和铰链一样,对于旋转要做一定的角度限制.

最后编辑:
作者:wy182000
这个作者貌似有点懒,什么都没有留下。