1. 程式人生 > >unity 對Animator動畫系統的研究

unity 對Animator動畫系統的研究

mask 可用 spec void 跑動 邏輯 綁定 -1 don

unity的新動畫系統叫Mecanim,使用Animator來取代舊系統Animation,按Unity文檔的慣例:知識點主要分2部分:unity manual和unity script,讀者可以邊看文章邊查閱文檔,最好能動手測試。

文章的開始之前,先講幾個基本的知識的:

1.創建動畫的一個基本步驟是設置一個unity3d可理解的簡化後的骨骼到骨架中實際骨骼的映射;在Mecanim的術語中,這個映射稱為Avatar,即avatar是骨骼到骨架的映射。

  技術分享(圖片來自網)

  Avatar主要用於類人骨骼模型,可以實現角色之間的Retargeting。非類人模型可以認為骨架就是骨骼。

2.構建模型的基本步驟:

  modelling->rigging->skinning(建模->構建骨架->蒙皮)

  1.modelling 建模:

    1.Observe a sensible topology(遵循合理的拓撲結構),一個合理的標準是動畫帶動的網格變形是漂亮的;

    2.註意網格的縮放比例。最好做一下各個建模軟件模型的導入測試,來設置好正確的縮放比例(不同建模軟件導入比例不一樣)

    3.安放角色使得角色的腳站在坐標原點或者模型的“錨點”。角色通常是豎直地走在地面上,如果角色的錨點(也就是他的變換中心)在地面上會更容易控制。

    4.如果是類人模型,則盡量使用T字姿態建模(Unity為類人模型提供了許多功能和優化)

    5.整理你的模型,去掉垃圾。只要可能的話,覆蓋孔洞,焊接頂點並且移除隱藏的面,這會對蒙皮有幫助,特別是自動蒙皮過程。

  2.Rigging 搭骨架:創建骨架上的關節來控制你的模型進行運動。

    非類人模型的話,可以認為沒有骨架,只有骨骼,骨骼直接控制動畫,類人模型是骨架控制動畫。步驟1中的模型已經有腳,手,頭,武器等骨骼,還有受擊骨骼等,這些可以用來控制模型或者懸掛額外物件。

  3.Skinning 蒙皮:給骨架附加網格。

    1.把網格中的頂點綁定到骨骼,包括硬綁定(一個頂點指定一個骨骼,不是一一對應,可能多個頂點指定的是一個骨骼)和軟綁定(一個頂點指定多個骨骼,每個骨骼有一定權重)
    2.蒙皮的實操步驟:先自動蒙皮,接著用一個測試動畫看看蒙皮效果並根據此效果慢慢改啊改。
    3.每個頂點最多綁定4個骨骼,這是U3D的上限

3.動畫文件導入unity後,我們對它的處理和設置,這些在Inspector面板:Animation Importing settings

  技術分享

  分3個頁簽:

    Model:這裏的參數基本由美術來定,其他的使用默認就好了,這裏只提4個參數:

      1.scale factor 模型縮放比例,不同建模軟件導入比例不一樣,unity的物理單位是1m,這個根據不同建模軟件設置

      2.readable/writeable和UITexture一樣,如果開了unity就必須拷一份到內存,盡量不開。
      3.Generate Colliders一般用於生成場景的mesh collider,其他地方就不要打開了。
      4.Normals&Tangents是導入時對頂點和切線的處理,比如是否導入模型的頂點等,可限制模型精度和影響後續渲染等,比如vfshader就需要用到模型的 normal。

      技術分享

    Rig:這裏Animation Type包括(Generic/Humanoid/Legacy/None)

      Generic 用於非類人模型;Humanoid用於類人模型

      Avatar Definition:可以使用現有的avatar,也可以create from this model,一般地,模型網格文件選create,這個時候,Avatar子資產被增加到模型資產的下面,即Avatar是unity根據網格文件生成的,對Humanoid類型,還會自動匹配骨骼到骨架,不符合會報錯;而動畫文件使用copy,使用已有資源。

    技術分享

    Animations:對一個Animation Clip動畫片段進行設置

      前面部分的設置這裏就不講了,下面的clip片段我們看到Start和End參數,這表示從一個fbx動畫中截取一段給clip使用,多個clip共用一個動畫資源,所以,可以一個模型的所有動作都搞到一個動畫文件中,各個動作去裏面取一段即可;也可以一個模型每個動作都有一個動畫文件,分開管理。

    技術分享

    中間的參數比較煩躁,略過,先提一下Animations頁簽最下面的幾個有意思的參數:curves,events,mask,這2個簡單的參數可以帶來許多有趣的功能,在後面會講到。

    技術分享

講完上面幾個基本知識點後,下面我們來分點看幾個有趣的應用:

1.AnimationEvent

  Unity manual部分:

    這個在animations頁簽的下面,可以給clip加幀事件,即播到某幀時觸發某個事件:

    1.給clip的某些幀上加event,這個Function就是事件的名字,其他的是這個函數的參數

    2.定義一個腳本來接受這個事件,比如這個圖需要定義一個腳本,並且腳本裏定義了void ani(xxx){}函數

    3.參數的處理:根據腳本的函數定義格式傳參數,比如void ani(int a),則傳Int那個參數,ani(Object a)則傳Object那個參數,ani(float a, string b)則傳Float和String這2個參數,而,ani(AnimationnEvent a)則傳整個event進去(包括所有參數和當前event對應的clip的信息)

    ps:添加了event的clip的animator物體必須掛上定義了該事件名Function的函數的腳本,否則會報錯

    技術分享

  Unity Script部分:可查看class AnimationEvent,主要是獲得該event所在clip的一些信息:與此event相關的stateinfo,clipinfo,和該event本身的信息:event調用的函數名functionName、函數調用的參數:float/int/string/Object(采用哪個看函數定義式)、time(事件的觸發時間)

  實際應用:這個event機制可以在播到某些幀執行一些事件。那麽我們可以,在某個點播某個特效,在某個點播某個聲音,在某個點進行一些畫面特效,在某個點進行對敵人“擊退”“擊飛”“擊浮空”,非常有利於實現各種節奏效果!還可以加入技能打斷機制:當播到某2個幀之間可以被打斷:比如我遠撲某個玩家,結果我在空中被打斷,我就被彈回來。這種效果一定很爽吧。讀者可以在一個專門的腳本中定義各種接受事件的函數,並進行相應處理來進行使用此event機制。

2.AnimationCurve:

  Unity manual部分:

  添加curve,這個curve和event有點像但又不同,event是幾幀,curves 則是每幀,curves可以配合OnAnimatorMove使用,比如每幀不同速度前進:

      void OnAnimatorMove()
         {
             Animator animator = GetComponent<Animator>();          
             Vector3 newPosition = transform.position;
            newPosition.z += animator.GetFloat("Runspeed") * Time.deltaTime; //RunSpeed是Curves的曲線變量,控制移動,
             transform.position = newPosition;    //由此類似方法可以讓角色一個動畫各種移動。
         }

  官方文檔給出一個例子是:

  比如人在冰冷環境呼吸時,呼氣的水霧由粒子系統控制,那麽就可以在播呼吸動畫或者站立動畫時,通過Animator.Get參數(一個Curve的name)獲得當前數值,控制水霧大小。

  技術分享

  Unity Script部分:

    屬性:keys(關鍵幀key集合),length(the num of keys),postWrapMode(最後一幀循環類型),preWrapMode(第一幀循環類型),this[int]獲取關鍵幀

    接口:Evaluate(time):計算某個time時曲線的value

  總結:curves感覺就是:邊播動畫邊幹點其他事情,event則像播到一些幀就幹點事情。2者配合使用能讓你的動畫系統變得豐富起來,好好使用這兩個小小的利器吧。

3.Animation Layers和遮罩 實現:邊走邊吃蘋果

  1.給吃蘋果動畫加遮罩:在animation tab中的Mask中加遮罩,只勾選吃蘋果的那部分骨骼,3種情況添加:     

      A.加現有的遮罩文件,可以通過Assets->Create->Avatar Mast創建一個遮罩
      B.如果是給類人模型的動畫,使用點擊部位加遮罩
      C.如果是通用模型的動畫,勾選關節加遮罩

  技術分享

  2.創建新Layer:EatApple Layer, 把第1步的遮罩拖到這裏的Mask參數中,設置該layer設置比走路動畫Layer高,並設置該Layer的Blending為override,這樣,播走路動畫和播吃蘋果動畫就可以同時進行並且播蘋果動畫override了走路動畫的上半身動畫。

  技術分享

4.Animator Override Controller:

  顧名思義,它就是Override “Animator Controller”的,先簡單說一下Animations Controller:

    Animation Controller可以認為它就是動畫狀態機, Animator動畫系統是通過Animator Controller來控制動畫播放的,裏面存著指向各個動畫片段Animation Clip的引用,和播放動畫的邏輯,比如狀態轉移等.

  而Animator Override Controller則用於拓展一個已存在的Animtor Controller,它只是在後者基礎上在某些狀態播新的動畫Clip而已,保持後者的狀態機邏輯,結構等等:retaining the original’s structure, parameters and logic.

  所以在應用方面,Animator Override Controller能做到一個狀態機(Animator Controller)實現多套動作,非常利於維護。按官方說法就是:

    如果一類模型可以共用動畫狀態機,則可以弄一個基礎Animtor Controller,裏面是基本的動畫狀態邏輯,然後給某個模型使用就弄一個Animtor Override Controller,然後把其中不同的動畫Clip換成自己的,這樣就只需維護一份動畫狀態機了,省心省力。比如NPC系統的各個NPC。許多怪物也可以共用,主角更不用說。

5.1種換裝系統

    (復習一個知識點:Skinning 蒙皮:給骨架附加網格,把網格中的頂點綁定到骨骼)

    先加載2個模型出來,第1個模型是基礎模型,skinmesh/動畫/Animator等都有,第2個模型則是一個帶skinmesh的幾乎空的模型,第一個是源模型,第二個是目標模型,我們現在要做的就是用第2個模型的skinmesh替換掉第1個模型的skinmesh,做法如下:

    a.獲得第一個模型的skinmesh old_meshrender,獲得第2個模型的skinmesh dst_meshrender,獲得第一個模型的所有骨骼 Transform[] bones.

    b.目的是獲得dst_meshrender中各個頂點映射到bones的各個骨骼列表,即為dst_meshrender網格中各個頂點找到第1個模型中對應的骨骼,即為“新皮”重新指定骨骼,然後把old_meshrender的網格sharemesh換成dst_meshrender的sharemesh,把old_meshrender的骨骼bones換成新的骨骼列表(當然還是第1個模型的),簡而言之,就是:為第1個模型重新指定網格(第二個模型的),因為網格是新的,所以需要重新映射網格頂點到骨骼,看代碼可能更容易理解:

    SkinnedMeshRender dst_meshR = newModel.GetComponent<SkinnedMeshRender>();
    SkinnedMeshRender old_meshR = oldModel.GetComponent<SkinnedMeshRender>();
    Transform[] bones = old_meshR.bones;

    Transform[] newBones = new Transform[dst_meshR.bones.Length];
    for (int i = 0; i < dst_meshR.bones.Length;++i)
    {
        for (int j = 0; j < old_meshR.bones.Length; ++j)
        {
            if (old_meshR.bones[j].name == dst_meshR.bones[i].name)
            {
                newBones[i] = old_meshR.bones[j];
                break;
            }
        }
    }

    old_meshR.sharedMesh = dst_meshR.sharedMesh;
    old_meshR.bones = newBones;
    old_meshR.sharedMaterials = dst_meshR.sharedMaterials;

    這種換皮技術原理清晰、簡單,也方便使用,並且只換皮(網格)不換模型骨架,所以對動畫系統等非常方便,只需把那些東西綁定到原始模型即可。

    但需要註意的是,各個換裝模型的骨骼要規範,盡量骨架同原始模型一致,至少需要做到骨骼“可以多不可以少”。

6.混合樹

  狀態轉移和混合樹,雖然兩者都是用來制作平滑動畫,但區別是大大的(讀者可只看前3點即可):
    1.狀態轉移是從一個動畫狀態平滑轉移到另外一個動畫狀態,2個狀態的動畫可以區別很大也表現得漂亮,不能維持中間狀態,即不能產生新的動畫讓你播;是動畫狀態機的一部分,是一個過程。
    2.動畫混合樹是把多個動畫混合起來:
      blend multiple animations smoothly by incorporating parts of them all to varying degrees
      將角色不同的部分混合,對一個部分而言,是混合所有動畫中該部分的角度,形成該部分的新角度,是混合動畫。
      有參數控制各個動畫的混合權重,以達到目的(比如左轉:一開始是向前的權重大,向左的權重小,然後。。。)
      要使混合效果好,各個動畫需要造型類似,這個和轉移不同;動畫混合樹作為一種特殊的狀態存在在動畫狀態機中,是一個狀態,新的狀態。
    3.鑒於上述描寫,制作混合樹的子動畫需要註意:
      Examples of similar motions could be various walk and run animations. In order for the blend to work well, the movements in the clips must take place at the same points in normalized time. For example, walking and running animations can be aligned so that the moments of contact of foot to the floor take place at the same points in normalized time (e.g. the left foot hits at 0.0 and the right foot at 0.5). Since normalized time is used, it doesn’t matter if the clips are of different length.
      我的理解是,走路動畫和奔跑動畫都在0.7的時候右腳觸地,不然0.7的時刻融合後,右腳在半空,也就是說動作在一些關鍵點比如離地和觸地。在時間線上的百分比應該相近的,在此基礎上,各個動作的Length不用考慮,比如run就比walk短,這無所謂的。
    4.混合樹的創建,選一個State->create blend tree,雙擊blend tree進入混合樹編輯頁面,右邊Inspector中就是控制這個混合樹混合的anim clips和參數。
    5.一些參數:有一個Compute Thresholds的參數,可以幫忙指定各種閾值控制混合。比如X方向的速度等。
    6.給混合樹添加已創建的參數:在Parameter選擇欄裏手動鍵入,回車!這個同給動畫狀態機加參數是一樣的
    7.上面的4是針對一維混合,二維混合一般用不到,不討論,多維混合蠻有趣的,可以用於控制臉部表情,多維混合的做法(那控制臉部表情做例子):給出多個表情動畫,每個動畫對應一個參數控制混合的權重,這樣通過控制各個參數可以“生產”出很多有意思的表情動畫了。

8.之前一直不知道怎麽移動瀏覽Animator 窗口的各自狀態:Alt+鼠標左鍵!

9.animator的TargetMatching技術

  假如你有一個跳躍的動畫,要實現跳躍到某個物體上的效果,可以考慮用animator的TargetMatching技術:

  get到模型的Animator ani,然後

  animator.MatchTarget(jumpTarget.position, jumpTarget.rotation, AvatarTarget.LeftFoot, new MatchTargetWeightMask(Vector3.one, 1f), 0.141f, 0.78f);
  0.14,和0.78是起跳和跳完所在時間百分比
  註:我測試沒成功,沒看到跳到某個物體的效果,=。=,或許哪步出錯了。

10.IK控制骨骼

  步驟:
    1.模型的Rig的Animation Type選Humanoid,動畫狀態機controller的Layer勾上IK Pass;
    2.寫個腳本掛到模型預制上,打開void OnAnimatorIK()函數,在這個函數裏控制IK:
  具體看Manual的InverseKinematics.html:控制模型轉向和各個部位的Rotation和Position
  註:已測試,效果還有趣,可能在一些有意思的地方用到,或許項目想做一系列可控的動畫會用到吧,否則,直接制作動畫就解決了。

11.根運動

  根運動是動畫本身自帶的,比如行走動畫,如果不apply root motion則“原地踏步”(in-place),如果應用則向前行走。

  實際項目中一般,不apply root motion,而通過代碼來設置transform postion,總結而言:

    1.不apply root motion並且該animator組件所在obj上的控制腳本不實現OnAnimatorMove,且所有腳本也不控制該模型position,=>模型原地踏步;
    2.不apply root motion並且該animator組件所在obj上的控制腳本不實現OnAnimatorMove,但其他腳本控制該模型position,=>模型不原地踏步;
    3.animator組件所在obj上的控制腳本實現OnAnimatorMove(這個時候apply root motion變不可選了),且所有腳本也不控制該模型position,=>模型不原地踏步,又OnAnimatorMove控制;
    4.animator組件所在obj上的控制腳本實現OnAnimatorMove(這個時候apply root motion變不可選了),且其他腳本控制該模型position,=>模型不原地踏步,即受其他腳本控制又受OnAnimatorMove控制;
    5.apply root motion(由4可知不能掛帶OnAnimatorMove的腳本了),且所有腳本也不控制該模型position,=>模型不原地踏步,按動畫裏根動作移動;
    6.apply root motion(由4可知不能掛帶OnAnimatorMove的腳本了),且其他腳本控制該模型position,=>模型不原地踏步,按動畫裏根動作移動的同時也受其他腳本控制;

  PS1:上述只提了位置,根運動也包括rotation,且控制邏輯也是同樣的。

    官方說明:Root motion is the effect where an object‘s entire mesh moves away from its starting point but that motion is created by the animation itself rather than by changing the Transform position. Note that applyRootMotion has no effect when the script implements a MonoBehaviour.OnAnimatorMove function.Changing the value of applyRootMotion at runtime will re-initialize the animator.

11.一些優化建議

  1.The Animator doesn’t spend time processing when a Controller is not set to it
  2.少用縮放動畫曲線
  3.When importing Humanoid animation use a BodyMask to remove IK Goals or fingers animation if they are not needed.
   如果是類人動畫,用身體遮罩剔除IK Goals和不需要的細節動畫(如手指動畫)
  4.When you use Generic, using root motion is more expensive than not using it. If your animations don’t use root motion, make sure that you have no root bone selected.
  如果選通用模式,不要使用root motion,不要選擇root bone
  5.Use hashes instead of strings to query the Animator.用Hashid而不用string來獲取動畫狀態或參數,動畫的Play函數重載了hash和string,可以考慮做一個映射,string->hash,使用hash獲取參數(播動畫),來代替string.
  6.使用curves來實現一些額外表現:在播動畫的時候同時搞事情。
  7.Rig頁簽的optimize gameobjects(選create from this model才可用)

12.Animator Component

  Unity Manual:參考這篇:http://www.cnblogs.com/Tearix/p/6941156.html

  Unity Script:屬性和接口太多了,看官方文檔吧,有個翻譯可以參考一下,但那個只是作者自己的理解,不保證準確:

    http://www.cnblogs.com/hont/p/5100472.html?utm_source=tuicool&utm_medium=referral

13.一個坑

  Play(A);Play(B);則最終Play了A;以前好像遇到過有概率不播某個動畫的情況,沒查明是否是因此造成的。解決方法是:

  Play(A);Update(x);Play(B);則最終Play了B,用Update更新一下就可以覆蓋了。如果是CrossFade,情況又復雜一些了,這個問題我專門和一位網友討論過,這裏不提。

unity 對Animator動畫系統的研究