[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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章