[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] 骨骼动画混合效果的主要内容,如果未能解决你的问题,请参考以下文章

[OpenGL] 骨骼动画混合效果

使用vue学习three.js之创建动画-创建骨骼动画,使用SkinnedMesh制作蒙皮

骨骼动画是啥原理?

3dmax教程 人物+骨骼+蒙皮+动画教程

gpu 蒙皮的矩阵计算

如何使用 assimp 在 C++ 中旋转蒙皮模型的骨骼?