1. 程式人生 > >MeshEditor(五) 網格頂點動畫(變形動畫)

MeshEditor(五) 網格頂點動畫(變形動畫)

原始碼已上傳至github,並持續更新,連結請看底部。(本帖跟隨github持續更新)

網格頂點動畫(變形動畫)是針對於物體的形狀可以隨意變換並記錄為關鍵幀的動畫,雖然模型的頂點資料還是應該交給GPU繪製才是正道,CPU重新整理模型頂點始終是個吃力不討好的事(不過我好像至始至終就是在幹吃力不討好的事來著),所以變形動畫還是別用到過於複雜的模型之上,畢竟到頭來吃力的只會是你的CPU,不過一些簡單的模型倒不用擔心,像什麼旗幟飄揚什麼的,不用開啟3DMAX(前提是得會用這東西K動畫),不用侷限於Unity的animator系統(畢竟給你一個做得像旗幟的cube,你能用animator調出一個飄動的動畫?),只需簡單的幾步拖拽便可以K出一個動畫,並且可以將動畫資訊儲存為本地檔案,實現多專案間複用,同時,頂點數相同的模型也可以複用動畫。

變形動畫完全不同於Unity Animator系統的機制,事實上跟它半毛錢關係都沒有,所以這兩種動畫在同一物體上是可以共同存在的,事實上,眾所周知,Animator的關鍵幀只會記錄物體的transform元件的position、rotation以及scale的數值變化(當然其他元件的部分屬性它也是可以記錄的,比如Image的Color),其餘的很多屬性改變都不會被它視為有另一關鍵幀產生,而變形動畫只會記錄模型的頂點資料作為關鍵幀,完全不會改動transform元件的屬性,所以這兩種動畫完全可以共存。

好了,進入正題,我以給一個cube調節一個變形動畫為例子講解一下整個流程及實現的思路。

第一步:

為cube新增我們的變形動畫編輯器元件(MeshAnimation)

新增動畫幀:以Scene場景中當前物體的狀態資訊儲存為一個新的關鍵幀,這裡的程式碼主要是記錄每個頂點的位置

/// <summary>
    /// 新增動畫幀
    /// </summary>
    public void AddFrame()
    {
        Vector3[] vertices = new Vector3[_Vertices.Length];
        for (int i = 0; i < _Vertices.Length; i++)
        {
            vertices[i] = _Vertices[i].transform.position;
        }
        _VerticesAnimationArray.Add(vertices);
    }


我們最好先在cube的初始狀態就新增一個動畫幀,以便於播放動畫時它會從初始狀態開始

第二步:

現在我們多新增幾個關鍵幀,目前每幀的狀態都是保持在初始形態

第三步:

我們的第一幀就讓他保持初始狀態,現在選中第二幀,同時在場景中調節cube的形態,當你覺得滿意的時候,點選apply應用就可以將物體的狀態應用到當前的第二幀資料,當然如果這一關鍵幀不想要了,點選delete刪除即可

/// <summary>
    /// 應用動畫幀
    /// </summary>
    public void ApplyFrame()
    {
        //如果當前動畫幀資料存在,則應用當前物體的各頂點資料至當前動畫幀
        if (_NowSelectFrame >= 0 && _NowSelectFrame < _VerticesAnimationArray.Count)
        {
            for (int i = 0; i < _Vertices.Length; i++)
            {
                _VerticesAnimationArray[_NowSelectFrame][i] = _Vertices[i].transform.position;
            }
        }
    }
/// <summary>
    /// 刪除動畫幀
    /// </summary>
    public void DeleteFrame()
    {
        //如果當前動畫幀資料存在,則刪除當前動畫幀資料
        if (_NowSelectFrame >= 0 && _NowSelectFrame < _VerticesAnimationArray.Count)
        {
            _VerticesAnimationArray.RemoveAt(_NowSelectFrame);
            _NowSelectFrame = -1;
        }
    }

我們將cube調節成這個樣子,然後點選apply應用關鍵幀

第四步:

選中第三個關鍵幀,再調到自己滿意的形態,並再點選apply應用

第五步:

選中第四個關鍵幀,這裡我們要讓他有個緩衝的效果,也就是說跟第三幀的差距小一點

然後我們的第四幀就調成了這個慫樣~

第六步:

第五幀我們就要讓他發射出去(前幾幀是收縮,蓄勢,然後第五幀猛地彈出~~有沒有一種發射炮彈的感覺~~),當然如果你想複製某一幀的話,只需選中這一幀,點選新增關鍵幀,最後面就會多出來與此幀相同的一幀,然後在此基礎上調節下一幀更方便

第七步:

之後就是給他K幾個反彈回來的緩衝關鍵幀,注意這裡選中任意一幀場景中的cube就會變化到那一幀的形態(這種方式是仿Animator的),隨意修改之後點選應用可以儲存,不點選應用預設改動無效,所以修改之後,如果覺得滿意,一定要點選apply應用,否則待你切換到其他幀時,這一幀改動的資料就將丟失

 /// <summary>
    /// 選定指定幀
    /// </summary>
    public void SelectFrame(int frameIndex)
    {
        //如果當前動畫幀資料存在,則選定當前動畫幀,所有頂點應用當前動畫幀資料
        if (frameIndex >= 0 && frameIndex < _VerticesAnimationArray.Count)
        {
            _NowSelectFrame = frameIndex;
            for (int i = 0; i < _Vertices.Length; i++)
            {
                _Vertices[i].transform.position = _VerticesAnimationArray[frameIndex][i];
            }
        }
    }

第八步:

完成之後點選預覽按鈕就可以馬上在Scene介面看到cube的動畫效果,這裡沒截圖,後面用動畫播放器播放的時候再截圖

 因為指令碼就算添加了編輯器執行的標識,它的update函式依然不會逐幀執行,而是在場景物體發生變化的時候才執行,所以這裡的動畫預覽函式不能放在update裡,那麼只有將之加入到Unity編輯器逐幀重新整理週期了

/// <summary>
    /// 預覽動畫
    /// </summary>
    public void PlayAnimation()
    {
        //沒有動畫可以預覽
        if (_VerticesAnimationArray.Count <= 0)
        {
            return;
        }
        //預覽從第一幀開始(頂點動畫陣列下標0)
        _AnimationIndex = 0;
        //重置記錄動畫播放上一序列的變數
        _AnimationLastIndex = -1;
        //重建新的動畫片段
        _AnimationFragment = new Vector3[_Vertices.Length];
        //重置動畫播放控制器
        _AnimationPlayControl = 0;
        //動畫進入到第一幀
        for (int i = 0; i < _Vertices.Length; i++)
        {
            _Vertices[i].transform.position = _VerticesAnimationArray[0][i];
        }
        _IsPlay = true;
        //將重新整理動畫函式註冊到Unity編輯器幀執行模組
        EditorApplication.update += PlayingAnimation;
    }


動畫重新整理函式採用將每個關鍵幀切分為動畫片段的方式,將片段迴圈累加給cube的網格頂點

/// <summary>
    /// 動畫預覽中
    /// </summary>
    void PlayingAnimation()
    {
        if (_IsPlay)
        {
            //動畫播放至最後一幀,動畫播放完畢
            if (_AnimationIndex + 1 >= _VerticesAnimationArray.Count)
            {
                //動畫播放完畢
                _IsPlay = false;
                //清除重新整理動畫函式的註冊
                EditorApplication.update -= PlayingAnimation;
                //動畫迴歸到第一幀
                for (int i = 0; i < _Vertices.Length; i++)
                {
                    _Vertices[i].transform.position = _VerticesAnimationArray[0][i];
                }
                return;
            }
            //當前動畫播放序列不等於上一幀序列,則進入下一幀
            if (_AnimationIndex != _AnimationLastIndex)
            {
                _AnimationLastIndex = _AnimationIndex;
                //分割動畫片段
                for (int i = 0; i < _AnimationFragment.Length; i++)
                {
                    _AnimationFragment[i] = (_VerticesAnimationArray[_AnimationIndex + 1][i] - _VerticesAnimationArray[_AnimationIndex][i])/ _AnimationPlaySpeed;
                }
            }
            //動畫進行中
            for (int i = 0; i < _Vertices.Length; i++)
            {
                _Vertices[i].transform.position += _AnimationFragment[i];
            }
            //動畫控制器計數
            _AnimationPlayControl += 1;
            //動畫控制器記錄的一個動畫幀播放完畢
            if (_AnimationPlayControl >= _AnimationPlaySpeed)
            {
                _AnimationPlayControl = 0;
                _AnimationIndex += 1;
            }
            RefishMesh();
        }
    }

第九步:

這裡是重點了,記得點選匯出動畫,如果你直接點選編輯完成或是突然有了什麼好想法跑去VS裡隨意改了下指令碼導致Unity編輯器重新編譯的話,很遺憾你的動畫資料都會丟失,記得匯出完畢了之後再點選編輯完成

使用scriptableobject序列化動畫資料至asset檔案中,這裡的坑是真坑,路徑必須還得是Asset開頭,字尾必須還得是asset,剛開始坑了我不少無辜的時間

/// <summary>
    /// 匯出動畫
    /// </summary>
    public void ExportAnimation()
    {
        //動畫幀數小於等於1不允許匯出
        if (_VerticesAnimationArray.Count <= 1)
            return;
        //建立動畫資料檔案
        MeshAnimationAsset meshAnimationAsset = ScriptableObject.CreateInstance<MeshAnimationAsset>();
        //記錄動畫頂點數
        meshAnimationAsset._VertexNumber = _RecordAllVerticesList.Count;
        //記錄動畫幀數
        meshAnimationAsset._FrameNumber = _VerticesAnimationArray.Count;
        //記錄動畫幀資料
        meshAnimationAsset._VerticesAnimationArray = new Vector3[_VerticesAnimationArray.Count * _RecordAllVerticesList.Count];
        for (int n = 0; n < _VerticesAnimationArray.Count; n++)
        {
            for (int i = 0; i < _VerticesAnimationArray[n].Length; i++)
            {
                for (int j = 0; j < _AllVerticesGroupList[i].Count; j++)
                {
                    int number = n * _RecordAllVerticesList.Count + _AllVerticesGroupList[i][j];
                    EditorUtility.DisplayProgressBar("匯出動畫", "正在匯出頂點資料(" + number + "/" + meshAnimationAsset._VerticesAnimationArray.Length + ")......", 1.0f / meshAnimationAsset._VerticesAnimationArray.Length * number);
                    meshAnimationAsset._VerticesAnimationArray[number] = transform.worldToLocalMatrix.MultiplyPoint3x4(_VerticesAnimationArray[n][i]);
                }
            }
        }
        //建立本地檔案
        string path = "Assets/" + GetComponent<MeshFilter>().sharedMesh.name + "AnimationData.asset";
        AssetDatabase.CreateAsset(meshAnimationAsset, path);

        EditorUtility.ClearProgressBar();
    }

如下就是我們匯出來的動畫資料,可以看到裡面包含了10個關鍵幀,適用於一切有24個網格頂點的模型(網格頂點是可操控頂點的3倍),當然他的原主是cube

第十步:

然後,為cube新增變形動畫播放器元件(MeshAnimationPlayer)並將我們的CubeAnimationData拖到其MeshAnimationAsset屬性上,每一個MeshAnimationPlayer對應一個AnimationData檔案,暫不支援程式碼中動態變更

MeshAnimationAsset:動畫播放器的目標asset檔案,頂點數量需與當前掛載物體一致

AnimationPlaySpeed:動畫播放速度,注意,這裡是值越小播放越快

另外兩個引數是開啟迴圈播放和啟動時即播放,我們勾選啟動播放,然後執行程式,下面是動態效果圖

其他效果:

一個看起來有點醜又有點僵硬的機甲變形(用最新的骨架調節方式,雖然這樣還是顯得一團糟)

原形:

編輯狀態:

變形動畫:

MeshAnimationPlayer的播放有外部可控開關

/// <summary>
    /// 播放動畫
    /// </summary>
    public void Play()
    {
        //從第一幀開始播放(頂點動畫陣列下標0)
        _AnimationIndex = 0;
        //重置記錄動畫播放上一序列的變數
        _AnimationLastIndex = -1;
        //重置動畫播放控制器
        _AnimationPlayControl = 0;
        //動畫跳轉到第一幀
        SelectFrame(_AnimationIndex);
        _IsPlaying = true;
    }
    /// <summary>
    /// 停止播放
    /// </summary>
    public void Stop()
    {
        _IsPlaying = false;
        //動畫迴歸到第一幀
        SelectFrame(0);
    }


以及要獲取當前動畫是否播放中,可以直接讀取_IsPlaying屬性。

-----by MeshEditor