reference: Efficient Morph Target Animation Using OpenGL ES 3.0 James L. Jones
介紹
移動平臺上對高質量圖形渲染的需求推進了GPU和圖形API的發展,例如OpenGL ES 3.0;這些硬件/API上的進步使得程序員能夠編寫更加簡潔、高效的圖形學算法實現。其中的一個受益領域就是手機遊戲中角色的面部動畫,該技術通常通過目標變形(morph target)來實現。目標變形動畫通常要求美術離線製作模型的多個姿態,然後,在程序運行時,這些姿態會以不同的權重混合在一起,以創建一個動畫序列,例如眨眼、皺眉等表情。與蒙皮技術配合使用,便能構建起功能豐富的角色動畫系統。
從歷史來看,目標變形動畫已經在PC等平臺遊戲中大量使用,但由於早期的圖形API侷限性較大,導致移動平臺上的實現開銷較大。本章介紹了使用OpenGL ES 3.0中引入的transform feedback API的目標變形動畫的高效實現。
之前的工作
基於OpenGL ES 2.0 API的移動平臺GPU,人們提出了一個有趣的幾何紋理方法。該方法根據頂點結構,在紋理中存儲目標姿態之間的頂點位移。變形過程將完全在綁定到幀緩衝區對象的紋理上執行,然後再按頂點將最終紋理作用於置換網格頂點。該方法是存在問題的。最明顯的一個問題是,平臺提供的紋理單元的最大數量可能爲0。這意味着在某些平臺上需要使用替代方案。OpenGL ES 3.0中標準化的Transform feedback功能可以使得實現更加簡單,避免了頂點紋理編解碼的操作。
變形目標
目標變形動畫用於需要對模型應用許多小的逐頂點修改的情況,這與骨骼系統處理的較大幅度的運動變化不一樣。變形目標的一個實用的例子是爲遊戲角色創建逼真的面部表情動畫中的面部肌肉動畫。(見圖4.1)
一種實現需要存儲多個版本的網格體,稱爲目標姿態或關鍵姿態。這些姿態將和基本姿態一起存儲,基本姿態用於表現角色正常狀態下的面部動畫。如果需要創建不同的動畫序列,需要將每個網格頂點的位置按權重與一個或多個目標姿態混合。該權重與相應的目標姿態相關聯,並表示了該目標姿態以多大程度影響了結果。
爲了能夠在目標姿態之間進行融合,可以使用差分網格。這是爲每個目標姿態創建的網格,該網格給出了目標姿態和基本姿態之間的頂點差值。這些差值向量用作整個向量空間中的基底(也就是說,可以通過權重向量,對這些基底進行線性組合,構造最終網格體的每個頂點)。更準確地說,對於時間爲t的每個輸出頂點vi,我們已知基本姿態頂點爲bi,N個目標姿態的頂點爲pi,權重向量爲w,那麼我們有:
上面的公式總結了目標變形動畫所需的所有條件。但是,大多數情況下,每幀的總權重可能只有一部分會改變。因此,我們希望避免每幀重新計算所有目標姿態的權重貢獻。一個更好的方案是跟蹤權重向量的變化以及內存中的當前姿態。對於幀h中的變化,新位置等於當前姿態加上該位置的差值:
我們可以發現,每幀的位置變化取決於每幀的權重變化:
利用以上信息,我們設計了一種方案,僅對發生了變化的權重(即)計算並更新姿態。
實現
這一實現利用綁定到transform feedback對象的頂點緩衝區來跨幀存儲並更新當前姿態。不幸的是,OpenGL ES 3.0不支持同時讀寫同一緩衝區,所以我們可以創建兩個緩衝區交替使用(即每幀輸入緩衝區和輸出緩衝區交換)。通過遍歷頂點並減去基本姿態,就可以預計算得到差異網格。合理的起始數據將被加載到feedback頂點緩衝區(在這個例子中使用了基本姿態)。在每一幀中,我們使用變化的權重來更新頂點緩衝區中的當前姿態。這一更新可以使用批處理,也可以不使用。最終,我們像平常一樣渲染更新後的頂點緩衝區。我們以與更新頂點位置相同的方式更新頂點法線,以得到正確的動畫法線。(見圖4.2)
後更新
在我們更新姿態之前,我們必須首先計算權重的改變。下面的僞代碼演示了這是如何實現的:
// inputs:
// w[] : current frame weight vector
// p[] : previous frame weight vector
// dw[] : delta weight vector
// per-frame weight update
animate(w)
for i = 0 to length(w):
dw[i] = w[i] - p[i]
p[i] = w[i]
使用差異權重,我們現在可以檢查權重向量中哪些數據發生了變化。針對發生變化的權重,使用關鍵姿態執行transform feedback pass。
// inputs:
// q[] : array of difference mesh VBOs
// dw[] : delta weight vector
// vbo[] : array of vertex buffer objects for storing current pose
// tfo : transform feedback object
// shader : shader program for accumulation
// per frame pose update:
glBindTransformFeedback(tfo);
glEnable(RASTERIZER_DISCARD);
glUseProgram(shader);
for(i = 0; i < length(q); i++) :
// only for weights that have chagned:
if(abs(dw[i]) != 0) :
// set the weight uniform
glUniform1f(..., dw[i]);
// bind the output VBO to TBO
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo[1]);
// bind the inputs to vertex shader
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(ATTRIBUTE_STREAM_0, ...);
glBindBuffer(GL_ARRAY_BUFFER, q[i]);
glVertexAttribPointer(ATTRIBUTE_STREAM_1, ...);
// draw call performs per-vertex accumulation in vertex shader.
glEnableTransformFeedback();
glDrawArrays(...);
glDisableTransformFeedback();
// vertices for rendering are referenced with vbo[0]
swap(vbo[0], vbo[1]);
批處理
爲了效率,我們還可以做更多的優化。在這一版本中,通過批處理將更新合併得到更少的pass。我們可以在每個pass中使用一個頂點着色器處理多個關鍵姿態,而不是每次僅對一個姿態傳遞屬性和權重。然後我們就能以儘可能少的pass來執行更新。
// inputs:
// q[] : difference mesh VBOs, where corresponding dw != 0
// vbo[] : array of vertex buffer objects for storing current pose
// dw[] : delta weight vector
// shader[] : shader programs for each batch size up to b
// b : max batch size
// per-frame batched pose update:
// ... similar setup as before ...
for(i = 0;i < length(q); ) :
k = min(length(q), i + b) - i
glUseProgram(shader[k]);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo[1]);
//bind attributes for pass
for(j = 0; j < b; j++) :
if(j < k) :
glEnableVertexAttribArray(ATTRIBUTE_STREAM_1 + j);
glBindBuffer(GL_ARRAY_BUFFER, q[i + j]);
glVertexAttribPointer(ATTRIBUTE_STREAM_1 + j, ...);
else :
glDisableVertexAttribArray(ATTRIBUTE_STREAM_1 + j);
// set the delta weights
glUniform1fv(...);
// bind current pose as input and draw
glEnableVertexAttribArray(ATTRIBUTE_STRREAM_0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(ATTRIBUTE_STREAM_0, ...);
glEnableTransformFeedback();
glDrawArrays(...);
glDisableTransformFeedback();
swap(vbo[0], vbo[1]);
i = i + k;
此技術需要多個版本的shader,其中每個版本都會對更多的輸入屬性執行加法運算,直到達到最大批處理大小。需要注意的是,可用輸入屬性的最大數量受API限制,該值(必須至少爲16)可以通過glGetIntegerv函數查詢,參數爲GL_MAX_VERTEX_ATTRIBS。
結果
在統計圖4.3中的數據時,使用了七個目標的最大批處理大小(批處理大小表示在頂點着色器中執行的加法數量)。該演示針對不同批處理大小多次執行。對於每次運行結果,使用了平均幀率。
這些結果表明,批處理pass減少了動畫更新時重繪幾何圖形的開銷。根據特定幀目標的數量,我們應該選擇足夠大的批處理大小來降低成本。
結論
本章演示了使用transform feedback的高效目標變形動畫系統。該技術可在渲染之前作爲單獨pass計算,並可在此技術基礎上輕鬆實現其他技術(如蒙皮)