3D动画概述暨骨骼动画实现
引言
本文論述了3D領域內的常見動畫類型的運作機制。不同于其他文章簡單的羅列和介紹每種類型的3D動畫,本文嘗試以一種優化演進的思路對動畫運作機理進行遞進式推演,在這個過程中自然而然的推導出常見的幾種3D動畫類型,以此證明其出現的必要性和合理性。本文盡量以平實簡明的語言來闡述講解,不過如果閱讀者具備初級3D知識,對頂點,矩陣變換,著色器等有一定認識,閱讀效果會更好。另外,本文聚焦在相對宏觀的機制層面,對細節不做過多描述。
動畫宏觀分類
3D領域內,刨除粒子等特殊的動畫效果,對于一個3D物體來說,動畫效果可以概括性分為兩類:紋理變化效果和頂點變化效果,兩者可以模擬出的動畫效果基本互不相交。
紋理變化
紋理變化可以營造出物體表面圖案不斷變化的效果,更進一步,如果結合透明/光照等高級紋理類型,可以實現更加豐富的效果。紋理變化通過改變紋理采樣坐標來實現動畫效果,自然不會引起模型的形變,想象一個靜止的,不斷變換表面顏色的球體。紋理變化方案對應的動畫類型就是UV動畫,該動畫類型擅長模擬水流,巖漿等流體效果,雖然效果做不到非常逼真,但是性價比很高,因為UV動畫的性能損耗極低,在一些遠景或者對視覺要求不是非常高的情況下可以被大量使用。UV動畫的機理非常簡單, 不再做過多展開。下圖是一個UV動畫模擬瀑布的效果:
頂點變化
頂點變化則是通過物體模型頂點的位移來營造出物體不斷形變(嚴格的說是形變和位移,但是在3D范疇內,可以籠統的統稱為形變)的效果,相對的就不會導致物體表面紋理變化。下圖的頭盔合上動作就是一個簡單的頂點變化效果:
頂點動畫
綜合上面的描述和效果,可以看出兩者的效果互不重疊,互相補充,各自有各自的適用場景。不過顯然頂點變化效果在3D動畫領域中的比重更大,想象如果3D世界中的物體都是靜止沒有動作的,那該是多么無聊的一個世界!因此下面重點探討頂點變化動畫效果的實現,既然是通過調整頂點位置來實現變化,那么如何調整頂點就是一個關鍵的問題,我們想要的某個動作效果不是對頂點隨機調整一下就能出來的,頂點的移動需要遵循預設好的一套軌跡,才能達成我們要的效果。這樣問題就變成了預設的動畫軌跡如何表示和保存,以及如何應用到頂點上。
任何問題的思考和解決都需要一個基準點,一般來說,這個基準點就是我們最直觀(一般也是最野蠻)的想法:第一版方案出爐:假設模型的某個動畫有30幀,那么只要把每一幀模型的所有頂點位置記錄下來,然后在動畫運行時根據之前記錄的每幀模型頂點位置進行頂點位置更新即可實現動畫效果。這個方法確實可以工作,并且直觀便于理解。當然缺點也很明顯:一個模型除了其紋理之外,頂點數據是最耗費存儲空間的(越復雜的模型頂點越多,占的空間也越大),上面提到的保存每一幀模型的所有頂點位置,最終會導致模型文件變的非常龐大,進一步導致加載后占用的內存和顯存也非常龐大(后兩者是非常寶貴的資源)。最極端的情況下,一個動畫多一些的復雜模型要求的存儲甚至可能超過設備提供的內存容量。這個方案在性價比和擴展性上都比較糟糕,需要進一步優化。
一個明顯的優化方向是嘗試減少因為動畫而引入的額外信息(主要是頂點數據),先觀察上面那個頭盔動畫的每幀軌跡:
可以觀察到第1幀和第10幀之間的8幀是遵循一定運動規律的(勻速的向下合)。似乎可以從第1幀和第10幀,即動畫的起點和終點推導出中間某個時間點的模型頂點位置信息,這就引入了第一個優化手段:關鍵幀和插值計算,改進方案出爐了:首先,不再保存動畫每一幀的模型頂點信息,取而代之的則是預先指定一系列關鍵幀,關鍵幀的數量視動畫的復雜程度和插值需要而定,分散在整個動畫的不同進度點上,動畫在每個關鍵幀的頂點信息會被保存,當動畫運行到某個時間節點時,取該時間節點前后兩個關鍵幀的頂點信息進行線性插值計算(或者其他插值方式,不過一般是線性),得到的結果就是在動畫在這個節點時的模型頂點信息。以上面序列例,我們只需要保存兩個關鍵幀數據(第1幀和第10幀),其他幀的數據都可以根據時間進度結合這兩個關鍵幀推導出來。
這個優化方案在大多數情況下比初始方案要節省很多的內存空間,以上面序列為例,可以節省80%的空間,以時間換取空間(插值是需要計算時間的),但是當前設備的計算能力遠遠大于插值計算的計算需求,可以接受。這種實現方案其實就是“頂點動畫”,現在仍然在一些動畫場景中得到應用。
剛性階層動畫
上面推導出的頂點動畫方案似乎已經優化的比較充分了,但是優化之路還沒有結束。考慮下頂點動畫的短板,觀察下面這個動畫以及其相對的序列幀,一個小狗晃動著身體:
顯然這個動畫我們很難找到可以被插值求出來的幀,因為小狗的動作相對之前的頭盔來說,沒有運動規律可循。如果要使用頂點動畫,就不得不將每一幀都做為關鍵幀,完全退化為了第一版方案(就像某些計算機算法,在一定的計算場景下復雜度退化很嚴重),頂點動畫不適用這種應用場景,需要探索新的優化方案。先總結下這個動畫場景的特點:模型由多個部件和關節構成,部件之間通過關節鉸接。關節使得了每個部件可以有獨立的運動軌跡,同時部件之間有可以通過關節進行聯動影響,最終復合起來形成了上面的動畫效果。
從上面的場景分析可以得出一種新思路:模擬關節體系(某種意義上是仿生學)。關節可以生效的前提是,原來的總體模型按照動作需求被拆分為子模型,接著用關節鏈接子模型。為了實現總體關節聯動,子模型和關節一起構成一顆驅動樹,在驅動樹中,父節點通過關節驅動子節點運動,子節點進一步驅動更下級的節點,最終實現了上面場景的動畫效果。這種方案被稱為“剛性階層動畫”,“剛性”指的是子模型本身不會形變,只會被關節帶動做移動旋轉等動作,因此子模型也被稱做”剛體”。”階層”指的則是上面提到的驅動樹模型代表的層級關系,有了層級關系才能進行整體聯動。到了這一步,可以看出來,我們構造的這棵樹其實近似于是一棵“骨骼樹”了,剛體就是骨骼,關節則連接和驅動骨骼。下圖就是一個簡單的剛體階層模型的示意圖,可以看出,模型被拆分為了若干的子模型,通
過關節連接構成驅動樹:
新驅動模型下不需要保存每個頂點的動畫信息了。頂點在這個模型中被按需組織整合為骨骼,只需要移動骨骼就可以實現想要的動畫效果。換而言之,頂點的動作軌跡被取代為了骨骼的運動軌跡。很顯然,骨骼的數量要比頂點數量少幾個數量級(舉個不太恰當的比喻,模型的頂點就如同人體的細胞,模型的骨骼對應人體的骨骼,顯然骨骼要比細胞少很多),這就克服了保存巨量頂點信息的缺點,取而代之的是只需要維護骨骼體系和每個骨骼在關鍵幀的變換矩陣即可,運算量也從對每個頂點做插值計算減少到對骨骼樹遍歷矩陣變換。
骨骼動畫
到了這一步,似乎已經接近終點,剛性階段動畫看上去可以很好的滿足關節聯動的動畫場景。遺憾是,還不夠,剛性階段動畫通過引入關節連接剛體骨骼的概念在宏觀粗粒度上滿足了關節聯動的需求,但是也恰恰是這種實現導致了剛性階段動畫的一個瑕疵:觀察上圖紅圈標記的部分,你會發現兩個剛體骨骼之間有著明顯的“空隙”,究其原因,是剛體本身沒有被形變,而剛體是通過關節連接的,有連接就會有空隙。對于某些模型,比如機器人或者機械/節肢類物體,空隙的存在是符合現實觀察結果的。但是呢,對于有表皮的生物體來說,比如人體,這就顯得非常不真實,人體的骨骼關節在扭動時,會有皮膚包裹,不會有視覺上的空隙,只有從X光機觀察時,才能看到骨骼之間的空隙。
上述應用場景的特色是“表皮”,整個模型都被一層柔性表皮包裹,這樣在內部剛體骨骼扭動時,關節處的表皮被拉伸,從而避免了視覺上空隙的出現。那么問題就變成了如何通過程序和建模模擬這層表皮?先回顧剛體骨骼導致空隙的原因是剛體骨骼本身在扭動時沒有形變,導致關節處的空隙不能被覆蓋,如果有辦法把剛體在關節處的頂點進行形變擬合,就可以模擬這層表皮。其中的關鍵點是,關節處的這些頂點,將不再只屬于某個剛體骨骼,而是該關節處所有剛體骨骼在這個點位置的共同部分。獨立的剛體骨骼模型已經不能滿足這個需求了,因為獨立剛體各自的頂點都是專屬的,不能被共享。
這樣就需要一種新的模型組織形式,新的組織形式既要能保持剛性階層動畫的層級和關節特性,又要能體現出關節處頂點是被多個剛體骨骼共同影響的結果,前者限定了新的表述形式還是骨骼驅動樹加關節,后者要求我們打破剛體的物理限制,否則還是不能實現頂點的“共享”。矛盾的焦點就集中在了剛體骨骼上,在打破其物理限制的同時又要保留其概念,解決這個問題的常用的手法就是“虛擬化”:往深一層看,剛體骨骼在我們方案中扮演的是一個“頂點包裹”的統合性角色, 以前包裹的實現形式是建模時的模型拆分(比如分拆為多個Mesh), 屬于物理分割。這次換一種實現方式: 把每個頂點屬于哪個包裹(骨骼)都記錄下來,那么在內存中我們完全可以以此信息構造出一個虛擬的骨骼,而因為這次模型本身沒有拆分,因此頂點之間是沒有界限的,一個頂點可以歸屬(依附)于復數個骨骼,這就達成了上面提到了頂點共享的目的。涉及到動畫骨骼驅動,頂點只被共享還不夠,還要體現出“共同影響”這一點,同一個頂點被復數根骨骼影響,每根骨骼對其會有不一樣的影響力度,這個信息也需要被記錄下來,體現為權重值。下圖就顯示了新的骨骼樹模型,可以看到,模型本身沒有被拆分,在其內部虛擬化了若干根骨骼(藍色部分):
通過這個新的表現方式,關節處的頂點在扭動過程中將被關節的所有骨骼綜合影響,最終移動到一個合適的新位置來避免間隙的出現,就像之前的提到的柔性表皮的作用一樣。下圖就展示了模擬表皮的效果,可以看到,模型的關節部分沒有出現空隙:
這也就是骨骼動畫為什么全名被稱為“骨骼蒙皮動畫”的原因,“蒙皮”指的就是上述解決方案。骨骼蒙皮動畫如其名,擅長的就是有骨骼關節參與的動畫過程,因此也有一定的局限性。比如下圖的人類面部表情動畫,骨骼蒙皮就不太適用,因為人類臉部的肌肉數量很多,層級也復雜,每塊肌肉都需要一根骨骼對應,最終的計算量會比較龐大,骨骼的變換矩陣調整對建模師要求也較高。相對的頂點動畫則更適合這種場景。
動畫方案總結
至此,我們的方案探討基本完畢,總結來看,在動畫優化的過程中引入了關鍵幀,插值,關節,骨骼驅動樹,虛擬化等手段。除了從模型操作層面理解這幾種優化手段外,還可以從數據壓縮的角度進行理解,整個動畫的優化過程實質是一個數據壓縮的過程:
- 最初方案中每一幀保存所有的頂點數據,數據量最龐大,但形變自由度也是最高的,沒有任何約束,任意一個頂點的位置可以任意調整。但很多時候動畫并不需要這種高自由度,而是多少遵循一些限制或者規則,這種情況下最初方案引入的全額數據就顯得冗余,需要進行壓縮。
- 頂點動畫的關鍵幀技術實質就是頂點數據壓縮,通過插值求中間結果這一規則限制,將原來保存每幀頂點數據壓縮到只需要保存幾個關鍵幀的頂點信息。
- 骨骼動畫同理,為自己施加了骨骼驅動樹和只能調整關節等限制,限制力度在大多時候比頂點動畫還要強,因此取得了更高的動畫數據壓縮率。
骨骼動畫的實現
下面簡述骨骼動畫的落地實現,不涉及太多細節,知曉了機理之后,實現即是水到渠成。
首先從骨骼動畫的機制推導出驅動骨骼動畫需要的數據:
- 模型骨骼樹的結構數據,描述模型都有哪些骨骼,以及骨骼之間的連接關系。
- 每個頂點需要這些額外數據: 該頂點依附于哪些骨骼(即被哪些骨骼影響),以及每根骨骼的影響權重。
- 骨骼動畫的關鍵幀數據,每個關鍵幀數據會保存每個骨骼在這個關鍵幀時間節點時相對父骨骼節點的變換矩陣, 每個關鍵幀還會保存自己對應的時間點。
- 動畫通用數據: 名稱, 持續時間等。
上述數據就足以驅動一個骨骼動畫了,下面是在OpenGL環境下驅動骨骼動畫的大致流程:
下面貼出上述流程使用的示例頂點著色器源碼,該示例限定了模型最多有50根骨骼(MAX_BONES), 一個頂點最多被4根骨骼影響(通過vec4屬性類型體現)。
// attribute attribute vec3 attPosition; attribute vec2 attUV; attribute vec4 attBoneIds; // 該頂點被哪些骨骼所影響 attribute vec4 attBoneWeights; // 每根骨骼的影響權重// uniform const int MAX_BONES = 50; // 模型最多有50根骨骼 uniform mat4 bonesMat[MAX_BONES]; // 骨骼變換矩陣數據 uniform mat4 mvpMat;void main() {// 基于每根骨骼的權重和變換矩陣求出在這些骨骼的綜合影響下的骨骼變換矩陣mat4 boneTransform = bonesMat[int(attBoneIds.x)] * attWeights.x;boneTransform += bonesMat[int(attBoneIds.y)] * attWeights.y;boneTransform += bonesMat[int(attBoneIds.z)] * attWeights.z;boneTransform += bonesMat[int(attBoneIds.w)] * attWeights.w;gl_Position = mvpMat * boneTransform * vec4(attPosition, 1.0);textureCoords = attUV; }結語
至此,3D領域動畫論述基本結束,一個概括性結論:3D動畫的本質還是操作數據,各種動畫類型對應了不同的數據操作理念和手法。本文涉及到的只是一些基本的3D動畫類型,在工業級應用中,會出現更多的專業細分領域和動作驅動機制。不過對于相對輕量簡單的應用場景,比如手機攝像頭特效之類的應用,上述動畫類型基本可以覆蓋大部分需求。
總結
以上是生活随笔為你收集整理的3D动画概述暨骨骼动画实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 血压计模块|臂式血压计方案
- 下一篇: 电子血压计方案提供模块芯片开发服务