[OpenGL] 骨骼动画混合效果
Posted ZJU_fish1996
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OpenGL] 骨骼动画混合效果相关的知识,希望对你有一定的参考价值。
本文主要讨论两个骨骼动画过渡时的混合效果。
在游戏中,动画往往被切分成多个片段(clip),通过组合拼接来构建最终的表现效果。为了确保切换动作时的平滑过渡,使得整体动作更加流畅,需要对前后动作通过插值进行混合。
概念引入
在讨论具体的混合计算之前,我想依然有必要明确一下整个动画系统体系的一些细节。
其中一个需要探讨的问题在于:动画运行时应该维护哪些数据,并以怎样的形式换算进入最终的渲染流程。在原先的骨骼动画demo中,我实际上是按帧存储了每帧每个骨骼的蒙皮矩阵,作为骨骼动画演示已经绰绰有余了。
但对于实际应用而言,这里起码存在了两个问题:一是逐帧的动画数据一般是烘焙的结果,在dcc工具中动画数据一般为关键帧+曲线类型存储,实时插值本身并不耗时,并且可以压缩带宽,所以动画往往都会进行压缩存储;
二是我们除了播放动画外,有时候还需要对动作做一些后处理——比如本篇文章讨论的动画混合,又比如反向动力学效果,此时仅有蒙皮矩阵是不利于运算的,我们应该根据自己的实际需求,存储为局部变换矩阵或全局(模型空间)变换矩阵。特别地,如果存储为局部变换矩阵,在解算的过程中存在不断查找父节点连乘矩阵的步骤,此处合理地安排骨骼的排序可以避免重复的运算。
另外一个需要考虑的问题是,混合是否应该影响到动作的播放长度?我们假设动作A长度为ta帧,B为tb帧,混合帧数为tc,那么动作A,B混合后,总帧长应该为ta+tb,还是ta+tb+tc,又或是介于两者之间?
针对这一问题,我认为比较友好的设计为:混合时间不应该影响原播放长度,混合这一功能本身仅仅是为了更好的表现效果,它不应当破坏原有的体系。那么此处就必然有一个动作“牺牲”一部分姿态。一个比较常见的思路是让目标动作的前tc帧转换为过渡帧,每一帧的矩阵与源动作最后一帧按照当前时间进行权重插值。
具体实现
为了更好地进行混合操作,我们可以将动画存储的数据修改为模型空间下的transform值,而不是最终的蒙皮矩阵。所谓的transform也就是分别存储平移旋转缩放,之后再根据RST进行矩阵构造:
struct STransform
QVector3D position = QVector3D(0, 0, 0);
QVector3D scale = QVector3D(1, 1, 1);
QQuaternion rotation = QQuaternion(0, 0, 0, 1);
;
在实际的插值运算中,我们也是针对每个骨骼模型空间的平移、旋转、缩放分别进行插值(由于项目中骨骼不存在缩放,实际的工程中并没有计算缩放插值)
在切换到新动作,当我们检测到新动作需要混合时,我们根据当前动作localtime,采样动作的transform值,作为缓存:
void CAnimationEngine::PlayAnimation(Object* obj, const string& path)
if(m_animators.find(path) == m_animators.end())
return;
int frame = -1;
string oldPath;
// check need blend, save cache pose
if(m_events.find(obj) != m_events.end() && m_animators.find(m_events[obj].m_path) != m_animators.end())
if(g_animParam.m_nBlendFrame)
oldPath = m_events[obj].m_path;
frame = min(m_animators[oldPath].GetFrameNum(), static_cast<int>(m_events[obj].m_time * FRAME_PER_MS));
m_events[obj] = SEvent(path, g_animParam.m_bLoop, g_animParam.m_nBlendFrame, g_animParam.m_eBlendCurve, g_animParam.m_fSpeed);
if(!oldPath.empty())
CAnimator& animator = m_animators[oldPath];
m_events[obj].m_cachePose = animator.GetTransform(frame);
接下来,在更新骨骼动画的代码中,我们对当前帧数下的采样动作和缓存动作的平移、旋转分别进行插值,混合权重以时间t单位,包含线性混合(t),以及非线性混合(3 * t * t - 2 * t * t)。
插值结束后,我们重新构造模型空间的全局变换矩阵,并乘以绑定矩阵逆矩阵构造蒙皮矩阵,传递给着色器。
bool CAnimationEngine::UpdateAnimation(Object* obj, QOpenGLShaderProgram* program)
// ...
if (event.m_blendFrame > 0 && frame <= event.m_blendFrame && event.m_cachePose.size() > 0)
vector<QMatrix4x4> final;
float ratio = static_cast<float>(frame + 1) / (event.m_blendFrame + 1);
if (event.m_eBlendCurve == EBlendCurve::Smooth)
ratio = ratio * ratio * (-2 * ratio + 3);
for(int i = 0;i < size; i++)
STransform& transform = animator.GetTransform(frame, i);
QQuaternion quat = QQuaternion::slerp(event.m_cachePose[i].rotation, transform.rotation, ratio);
QVector3D trans = Lerp(event.m_cachePose[i].position, transform.position, ratio);
QMatrix4x4& invBindPose = CAnimationEngine::Inst()->GetBone(i)->m_invBindPose;
QMatrix4x4 mat;
mat.translate(trans);
mat.rotate(quat);
mat = mat * invBindPose;
final.push_back(mat);
program->setUniformValueArray(location,final.data(), size);
else
// ...
// ...
return true;
以上是关于[OpenGL] 骨骼动画混合效果的主要内容,如果未能解决你的问题,请参考以下文章