?本篇文章作者:樂府-貝塔
樂府-貝塔:樂府前端核心開發,從事游戲開發多年,從 Cocos2d-x 做到 Cocos Creator,擅長渲染技術的相關優化。多年的前端開發經驗激發了對技術研究的深厚興趣,希望與更多 Cocos 技術愛好者廣泛交流。
1、簡介
本文主要探討了如何使用?Cocos?Creator?來模擬書本翻頁效果,分別介紹了通過使用貝塞爾曲線和?verlet?積分算法來模擬書頁底邊在翻頁過程中的彎曲形變,最后通過自定義 assembler 傳入更多的頂點,來將原本的書頁紋理渲染成彎曲后的樣子以達到模擬翻頁的效果。
由于篇幅限制,本文沒有包含相關數學公式的具體推導過程,有興趣的同學可以自行上網搜索。
Demo 引擎版本:v2.3.1
Demo 地址:https://github.com/BlubluBlue7/Page3
首先展示一下最終實現的效果:

2、問題分析
書頁在翻動的過程中會產生一定的彎曲形變,這就意味著在每一幀中,我們所看到的書頁都具有不同的形狀,如果使用序列幀動畫來實現,就要對每一幀單獨繪制一張圖片,這樣將產生大量的資源。如果能夠通過代碼來模擬,就可以避免資源問題。
所以,我們要解決的問題就變成了如何將原本的書頁圖片渲染成翻頁過程中的彎曲狀態呢?
我們先觀察一下書頁形變后的樣子:

通過上面這張圖,我們發現,可以將書頁的形變簡化成這樣:
1.把書頁的底邊想象成一根繩子,固定左邊的位置不變,拉著右邊向左上方位移形成一條曲線。
2.將整個書頁沿著這條曲線向上位移。
通過以上兩個步驟,我們就可以模擬出書頁在任何一個時刻的樣子。不同時刻書頁形態的區別實際上就是曲線的不同,只要能夠獲得任意時刻書頁底邊的那條曲線,就能模擬出翻頁的效果。
3.解決方案
那么,該如何獲得那條曲線呢?這里提供兩個方案。
- 貝塞爾曲線
(1)背景介紹
貝塞爾曲線是應用于二維圖形應用程序的數學曲線 ,通過至少 3 個控制點,就可以描述出一條曲線。N 階貝塞爾曲線擁有 N+1 個控制點。
以 3 階貝塞爾曲線為例,它的具體公式如下:


只要確定了起點 P0,控制點 P1,控制點 P2 和終點 P3,就可以畫出這樣一條曲線,它將和 P0P1、P2P3 分別相切。
(2)實現
也就是說,只要找到任意時刻 4 個控制點的位置,我們就可以把書頁底邊的曲線模擬出來。當然,起點的位置其實是固定不變的(除非你把書頁從書上撕下來)。
如果以終點與起點連線和水平面的夾角來描述翻頁過程中的某一個具體的位置,那么我們可以觀察書頁在每一個角度的曲線形狀,然后把控制點的位置推導出來,從而還原整個翻書過程,但這樣顯然是無法實現的。
我們只能先確定部分特殊角度的控制點,然后在這些控制點間做插值。
① 選取幾個特殊角度,獲得控制點的位置,保證在這些特殊角度曲線的形狀是較為真實的。
例如,在 0° 和 180° 時,我們獲得的必然是一條直線,而在 90° 時,曲線大概是長這樣:

② 在特殊角度之間插值。
可以想象,均勻插值是不夠真實的,以 0° 到 90° 為例,終點的運動軌跡應該是類似下圖這樣,x 坐標的變換由慢到快,而 y 坐標的變換則是由快到慢。

選取一個合適的插值函數,就可以模擬出一個還不錯的翻頁效果:

使用貝塞爾曲線來模擬的最大問題在于,這種方法與真實的物理運動是沒有關系的,它在某一個時刻的形狀實際上是根據觀察推測出的一個插值公式得到的。這就導致我們最終得到的曲線長度并不等于書頁的寬度,所以在獲得了曲線之后,我們還需要修正曲線長度與書頁寬度一致,具體的實現會在后文提到。
- verlet積分算法
考慮到貝塞爾曲線的缺點,我們需要一種更加接近真實物理運動規律的方法,也就是第二種實現方案——verlet 積分算法。
(1)背景知識
質點彈簧系統:
模擬物體變形最簡單的方法就是采用彈簧質點系統。在質點彈簧系統中,我們需要定義一系列的質點,也就是有質量的點,并假設質點之間存在一個擁有一定長度、質量為 0 的彈簧。當質點運動時,會受到內力和外力和影響,內力包括彈簧的彈性力和阻尼力,外力包括重力以及空氣阻力等。
模擬物體的運動,實際上就是計算出物體在任意時刻的位置。在質點彈簧系統中,我們可以獲得每個質點受到的力以及它的質量,根據牛頓第二定律,我們就可以算出質點的加速度。有了加速度,再通過一定的算法,就可以計算質點的位置。
顯式歐拉積分算法:
顯式歐拉積分是一種較為簡單的算法。在顯式歐拉積分中,下一時刻的狀態由當前時刻的狀態決定。顯式歐拉積分的問題在于誤差較大且不穩定,所以我們不予采用。

verlet 積分算法:
verlet 積分算法是一種基于位置的積分,通過質點在當前時刻和上一時刻的位置來計算出新的位置。verlet 積分算法在精度和穩定性上都要優于顯式歐拉積分算法,并且計算的復雜度相差不大。

約束:
假設質點間彈簧的彈力系數為無限大,那么當拉伸或壓縮彈簧時,兩個質點之間的距離總會保持在原來的長度。利用這種思想我們就可以通過增加質點間的距離約束來簡化彈力的計算。
(2)實現
現在,我們把書頁底邊看作是由一些水平放置的質點連接而成。當我們移動尾質點的位置時,其他質點將會由于質點間彈簧的約束作用而跟著移動。這樣,我們就可以使用一種較為真實的方法來獲得一條曲線。
使用 verlet 積分來計算曲線的方法如下:
① 首先根據書頁的寬度來定義一系列水平放置的質點。

② 在每一幀先根據 verlet 積分公式來更新每個質點的位置。
當前位置和上一個位置的差可以看作是質點的速度,由于空氣阻力等造成的能量損耗,這里還需要額外乘上一個衰減系數。
加速度的影響這里可以簡化為,一個豎直向下的重力。

③ 通過彈簧約束來修正質點的位置,修正的次數越多,效果也就越好,但同時帶來的性能開銷也就越大。

④ 添加一個移動尾質點的方法。
這里我們就使用和貝塞爾曲線相同的運動軌跡來做對比,可以看到使用 verlet 積分算法得到的結果要更加真實。

(3)參數對效果的影響
使用不同的參數值,會展現出不同的材質效果。影響 verlet 積分算法效果的參數主要有:速度衰減系數、重力和糾正次數。使用的時候需要通過調節參數來獲得一個較為滿意的效果。
速度衰減系數:
速度當前位置與上一個位置的差計算得到,可以看作是慣性的一種近似表現。當運動停止時,各個質點還會由于慣性繼續運動下去。速度衰減系數越大,慣性就越大,模擬出的書頁就會晃動的比較厲害。

重力:
重力會在每一幀將質點向下拉動一定距離,如果書頁在水平方向運動的速度比較慢,就會表現出一種難以將書頁拉起的感覺。

糾正次數:
糾正的次數越多,每兩個質點之間的距離也就越接近初始的固定值,所以糾正次數會影響書頁的柔軟度。需要注意的,糾正次數越多,也就意味著更多的計算量,使用時應該均衡考慮。

補充
1.如何將書頁紋理渲染成沿著曲線向上位移后的樣子?
涉及到渲染問題,我們首先得看一下渲染管線是如何工作的。

首先,頂點著色器會對我們傳入的每個頂點進行處理,處理后的頂點將被組合成三角形,這些三角形經過光柵化會形成片段。最后,經過片段著色器的計算得到每個片段的顏色值。
簡單來說,Sprite 組件根據所在節點的寬、高、坐標、錨點等信息得到了由 4 個頂點圍成的矩形,這個矩形最后顯示到屏幕上時會變成許許多多個小的像素點,而每個像素點的顏色則是 OpenGL 根據片段的紋理坐標和 Sprite 使用的紋理采樣得到的。
如果想要渲染出由曲線圍成的書頁圖片,4個頂點顯然是不夠的。所以,我們需要自定義一個渲染組件來傳入更多的頂點以完成需求:
① 將書頁的上下邊分成若干條線段來擬合曲線,也就是在原本矩形的上下邊的兩個端點之間創建更多的頂點。
② 連接上下邊的頂點形成若干個三角形。

最終得到的圖形可以看作是由若干個小的矩形拼接得到。顯然頂點的數量越多,曲線也就越平滑。而經過前面的分析,每個頂點在任意時刻的坐標我們都可以輕易獲得。
使用貝塞爾曲線方法直接套用貝塞爾公式即可得到,而 verlet 積分方法中的質點則剛好與這里的頂點一一對應。
如果把頂點數調低的話,就可以比較明顯的看出各個小矩形的范圍:

想要傳入更多的頂點,就得自定義渲染組件和 Assembler。
Assembler 是指處理渲染組件頂點數據的一系列方法,每個渲染組件都擁有一個 Assembler 成員。Assembler 中必須要定義 updateRenderData 及 fillBuffers 方法,前者需要更新準備頂點數據,后者則是將準備好的頂點數據填充進 VetexBuffer 和 IndiceBuffer 中 。


2.修正貝塞爾曲線的長度
如果不對貝塞爾曲線做修正的話,在翻動過程中書頁就會被拉寬或縮短:

由于貝塞爾曲線是推測得出的,所以最后獲得的曲線長度與實際書頁寬度并不相等。修正的方法也很簡單,累加當前得到的每兩個頂點之間的距離(也就是小矩形的寬度),如果大于真實寬度,就進行衰減處理,在最后一個頂點處進行多減少補,從而使得所有小矩形寬度之后等于真實寬度。

另一個需要注意的點是紋理坐標的計算,經過貝塞爾曲線公式計算后,頂點將不再是均勻分布,所以紋理坐標必須根據每個小矩形占總長度的比例來獲得。如果使用等分來獲得紋理坐標,效果將如下圖所示:

3.背面紋理實現
在之前演示的效果圖中可以看到,書頁正反面的紋理是不一樣的,這個是如何實現的呢?這里提供兩種思路。
① 背面剔除
通過設置背面剔除,OpenGL 會根據節點的環繞順序來識別正面和背面,并將所有背面的頂點都剔除掉,不參與最終的渲染。
只要使用兩個節點來分別表示書頁正面和背面,并一個剔除掉背面,一個剔除掉正面,即可拼出最終的圖像。但還需要考慮到正面和背面的層級問題,在貝塞爾曲線的實現中,背面始終在正面的層級之上,但 verlet 積分算法實現的效果中就顯然不是這樣了,有時甚至會出現一個面穿插在另一個面中的情況:

所以背面剔除的方法具有一定的局限性。
② 自定義 shader
另一種方法是創建一個自定義 shader,傳入兩張紋理,并增加一個新的頂點屬性來告訴片段著色器應該對哪一張紋理進行采樣。
判斷一個小矩形應該使用正面紋理還是反面紋理其實很簡單,只要通過前后兩個頂點的x坐標來判斷就行,后一個頂點的 x 坐標大則為正。而由于后繪制的頂點本身就會覆蓋前面的頂點,所以使用這種方法就不用考慮層級的問題了。
以上是由 Cocos 開發者 樂府-貝塔 分享的優質技術教程,此文同時參加了 Cocos 中文社區征稿活動,入選優秀稿件。歡迎各位開發者點擊【閱讀原文】查看原文,為作者點贊,與作者進行交流學習!
貝塔所在的樂府互娛,是一家專注于精品移動游戲研發和運營的明星初創企業。核心團隊是《少年三國志》《少年西游記》系列作品的原班人馬,長期深耕卡牌手游等品類,打造過數款月流水過億的產品,包括《少年三國志》《少年西游記》等,游戲累計流水近100億元。目前正在高薪招聘 Cocos Creator 開發工程師,感興趣的小伙伴可以掃下方二維碼了解詳情喲!
