骨骼動畫的插值與融合
早期的 3d 實時渲染,都是簡單的把頂點連成面,然後附上貼圖把模型顯示出來的。若是想讓模型動起來,就製作若干動作下的模型,在檔案中記錄下動作每幀的模型造型的頂點資訊。簡單說,一個動作,就是一系列的獨立模型。這就是所謂的頂點動畫。Quake 的前幾個版本都是這麼幹的。這樣幹主要是因為早期的硬體條件有限,讓硬體去處理頂點,渲染多邊形就已經夠吃力了,沒多餘的計算力去幹別的事情。
後來,隨著遊戲的視覺內容急劇增加,把豐富的動畫全部輸出成一幀一幀的模型,資料量上已經不太吃的消。慢慢的,還加入一些和環境互動的動作,單純的用預生成好的幀頂點動畫已經不再滿足要求。
人們想出一個辦法,把 3d 模型抽象成一組關鍵點的集合。(這個抽象就好比從 2D 到 3D ,把一堆的畫素構成的圖象,抽象到少一個數量級的三角型上)整個模型的運動,可以想象成這些關鍵點的運動,而構成模型的頂點及面,都是受這些關鍵點的影響而已。
這些關鍵點被稱為骨骼,非常之形象,而且在人物動畫中,這些點通常也正好在人物關節的位置。
和 max 裡製作不同。這種建模軟體展現給美術人員的骨骼真的是有體積的實體。而程式處理的時候,所謂的骨骼卻是一個個的點,更準確點說,是一組組空間變換。點只是表達了空間位置而已,並沒有表達出自身的轉動和縮放變換。
使用這些骨骼資訊非常簡單。只需要把模型上的點,根據它們和骨骼間的關係(被稱為面板的東西),乘上對應骨骼的變換矩陣即可得到在骨骼擺成特定姿勢下的正確空間位置。
這樣,我們只需要把骨架擺成特定姿勢,就可以計算出蒙皮上所有相關點的空間座標,並通過顯示卡硬體渲染出來。在現代顯示卡中,GPU 甚至可以幫你做最後一步矩陣變換的乘法。
剩下的工作,就是怎樣得到骨架的資訊了。
3d 人物做任何動作,都可以在 3d 軟體裡製作好,輸出整個動作每個時間點的骨架空間資訊。這些是些離散的資料。按我們遊戲的製作標準,是一秒 12 到 15 幀。(另一種紙娃娃系統暫且不談)
當遊戲的實際渲染幀率更高,或由於特殊需要,想放慢動作運動速度時。預做好的骨架資訊就不夠用了。
當然,你可以讓動作一格格跳,也不太所謂。只是在不同的動作切換時,會比較奇怪。更好的方法是在兩幀動作間做插值處理。
這就是我這幾天工作的重點。其實這是項很老的技術。誰讓我精力實現有限,直到今天才真正賣力研究它呢。
如上所述,當我們不插值,直接把美術輸出的資料來用的時候,一切都是很簡單的。無非是在渲染前,把那些頂點資料乘上個骨骼變換矩陣而已。如果我們需要知道兩幀圖象的中間狀態呢?馬上能想到的是,對每個骨骼點,兩幀的對應變換矩陣做插值。
可惜的是,變換矩陣中的每一項並不可以正交分解,獨立插值的。
而且,雖然最後我們用到的變換資料是每根骨骼在絕對空間中的絕對變換。但實際上在製作這些資料時,骨骼之間是有相互關係,相互影響,最後把各級骨骼的空間變換疊加起來的。
綜合這兩個問題來看,我們需要做的工作,一是儘量把變換矩陣正交分解成可以獨立插值的元素。二是將骨骼的變換行為還原到影響到它的上一級骨骼的區域性空間內。比如兩組動畫間要做過度,可能手的位置差別很大,但是對指關節的運動的插值,卻只應該考慮指頭相對手掌的運動,而不需要考慮手指在絕對空間中的變化量。
假設手指的絕對變換矩陣為 M1 ,手腕的變換矩陣為 M2 。那麼可求得手指在手腕空間中的變換為 M1 * M2' 。(哎,上大學時光顧著玩程式去了,線形代數沒好好學。這幾天補習這方面的時候真累。不過經過自己的推導搞明白後,想忘都比較難了。)
這一步是相對簡單的,但是不是要把整個變換還原到父節點呢?經過一番思考後,我的結論是否。
我們先看另一個問題:做美術製作過程中,如果資料來源是動作捕捉儀,那麼骨骼做變換無非是自旋和位移。就我目前看到的動作捕捉裝置,無非是若干照相器材佈置在空房間中,演員穿上特製的衣服,衣服上的關節處安裝有若干反射光的小球。照相機拍攝到這些小球在空間中的變化,把資料輸出到計算機。所以,我們用這套裝置得到的骨骼資訊,就是一些點的空間移動和自己朝向的改變了。(小球是否能被感知方向暫時我還沒能確認,有空我去公司的捕捉室實地研究一下)
如果是美術在建模軟體裡手工製作的話,因為骨骼被抽象為有體積的實體了。在美術看來,每次操作的是一段骨頭,而資料上,被影響的是骨頭的兩端。最原始的變換行為,其實是旋轉和伸縮。但對應到端點上,就成了位移和旋轉(對父節點是旋轉,對子節點是位移)以及少量的縮放。據我的美術同事講,縮放其實是很少用的,因為很難控制。btw, 據我所知,暴雪到目前為止,都是手調動畫,沒有使用動作捕捉裝置。
綜上,骨骼的變換矩陣,其實是有若干次的縮放,旋轉,和位移變換疊加的結果。
其中,縮放屬於形變;而旋轉和位移都屬於自身在空間中狀態的變化。我們應該分開看這個問題,形變往往是不因骨骼的連線關係而繼承的,如果有複雜形變,(多次旋轉和拉伸的反覆疊加)我們就無法從最終的變換矩陣中分離出一個縮放矩陣。好在,一般不會有人刻意去製造這種變換方式(用動作捕捉更是不可能得到這種變換)。所以,我第一步就從全域性變換矩陣中分離出縮放矩陣來。這個變換是不受骨骼層級連線而傳遞其影響的。
剩下的,就只有若干旋轉和位移資訊包含其中。
我們知道,一個帶方向的點(一個向量)在空間中,無論以什麼次序,做多少旋轉和位移,最終都可以表達為一次旋轉加一次位移。
即,我們分離縮放變換後,計算得到的子節點在父節點空間中的相對變換(M1 * M2')中一定可以把這個矩陣表示為某個旋轉變換 R * 另個位移變換 T 。
固然,這裡的 M1 和 M2' 中也可分解為R1 * T1
以及 T2' * R2'
。但是我嘗試找到最終結果 R 和 T 用 R1 T1 R2 T2 的簡單表達形式,卻失敗了。這都怪大學裡的線性代數沒好好學啊。不過既然我知道最終的結果矩陣中只包旋轉和位移,分離結果函式很容易的。他們分別是
4*4
的變換矩陣中的 3*3
項(旋轉部分)以及一行位移部分。
對於旋轉變換的插值,是不能直接對 3*3 的矩陣線性插值的。正確的方法是把矩陣表達為四元數。話說,搞明白四元數這個東西又頗費了我一些時間,這裡就不展開講了。簡單的說,對矩陣旋轉插值不可行是因為它的 9 個數值並不正交。而四元數則是的四個部分則是相對獨立的。正如在 2D 空間中,2 個數字(複數)可以表達一種旋轉,旋轉的表達在 3D 空間則擴充套件到了四個數字。四元數的插值和計算方法,網上能搜出許多。讀到這裡,我們只需要知道,這是一種方便做插值的描述旋轉變換的表達形式即可。
那麼,我們分離出一次旋轉和一次位移後,分別做插值就可以了嗎?
我認為是不完全正確的。尤其是在動作幅度較大,或兩個關係疏遠的動作之間。
設想一下,如果一個肢勢下,手指向左邊;而另一個姿勢,同一只胳膊,指向了右邊。從旋轉角度看,胳膊上的變換,方向轉了 180 度。如果人的以人的中軸線為 0 看,位置從負變到正。假若我們分別對旋轉和位移插值,過度出中間狀態。正中間的位置,手就會萎縮到幾乎沒有,而不是我們直覺中的旋轉過來。這也是,我們早先的版本,有時人會變紙片的緣故。
問題出在哪裡?
旋轉變換和位移依然不是正交的。
我們直覺上正確的骨架,其實,關節之間的距離是幾乎不變的,也就是一個剛性的骨架。各個關節其實要麼在做自旋,要麼在圍繞父節點公轉。自旋和公轉則是相互正交。如果偶爾有距離改變,那麼其運動也是沿著公轉軸方向靠近或遠離,和旋轉變換也是正交的。
如果我們可以把每個骨骼變換,分解為相對父親的公轉,相對自己的自轉,以及公轉軸上的伸縮,那麼就可以正確的做插值了。既把一個變換表示為形為 R1 * T * R2 的形式,且這個 T 只有一根軸上的變換。
之前,我們已經可以把變換分解為 R * T ,剩下的工作就是把這個轉換一種形式。
這個轉換不算太複雜,但需要使用一點點空間幾何知識和少許線性代數。推導過程就不詳述了。只寫寫大概思路:取一個座標軸做主方向,比如 X 軸。如果原來 T 裡的位移不僅僅包含 X 分量,就把它和 X 軸做叉乘,得到一個垂直的法向量。以這個向量做旋轉軸,以 T 和 x 軸的夾角為旋轉角(可以很簡單的得到這個角的 cos 值),構造一個旋轉變換 R1 ,那麼 T 就能表示為 R1' * T1 * R1 ,其中 T1 只包含了 X 分量,值為 T 中位移向量的長度。它的幾何意義就是這個骨頭到父節點的距離。
整個變換即為 R * R1' * T1 * R1 ,前兩個旋轉變換可以合併為一個旋轉。其幾何意義就是骨骼的自轉量。後一個 R1 是它繞父親的公轉量。
最後,我們可以放心的做各種混合和插值處理了。看起來貌似很多運算,幾乎都是在開發期預生成好的。最終執行時需要計算的東西並不多。
簡單談一下動畫資料的壓縮。
3D MMORPG 裡,動畫資料很容易吃掉大量的硬碟。如果非要考慮壓縮這些資料了,可以考慮這樣幾個方面。上面的思路,每個骨骼點,在每個關鍵幀上需要儲存兩個旋轉和一個軸向位移。這個位移往往在動畫中是不變的,所以就是兩個旋轉變換。儲存兩個四元數即可。
由於四元數是歸一化過的,其實就是三個絕對值小於等於一的數字加一個符號位。我們對旋轉的精度要求其實不會特別的高。可以考慮使用定點數。我大致想到的方案是使用一個 32bit 數字儲存一個四元數,第四個可計算的量的符號位佔用 1bit ,另外三個量分別佔用 10 bit 。分攤開來等價於在每個旋轉方向上,可以分出 1024 個角度,足夠滿足一般需求。由於骨骼動畫中,大量的旋轉都是相對不變的。我們可以簡單的做一個 hash 表 cache 這些 32bit 壓縮的四元數到四個 float 的轉換。應該不需要多大的 cache 就能得到比較高的命中率。
關於骨骼動畫模組的設計。
我個人認為不需要太關注單次插值計算的時間效率。
插值和混合應該是一個可選項,對動畫模組外做成黑盒子。我們只需要至少能提供關鍵幀資訊即可。關鍵幀的最終變換也可以預運算好。
對於模組對外的介面,只需要簡單的一個:給定指定的動畫名字和幀號,返回骨骼變換資訊。並允許提供混合幀的動畫名及幀號,以及混合比。
真正的效能熱點應該在 cache 管理上,而不在生成這些變換資訊的計算上。
今天太晚了,就隨便寫寫到這裡了。把預想的程式完成真是一大快事。