webgl 效能優化初嘗

發表於2017-05-14

上次文章介紹瞭如何用webgl快速建立一個自己的小世界,在我們入門webgl之後,並且可以用原生webgl寫demo越來越複雜之後,大家可能會糾結一點:就是我使用webgl的姿勢對不對。因為webgl可以操控shader加上超底層API,帶來了一個現象就是同樣一個東西,可以有多種的實現方式,而此時我們該如何選擇呢?這篇文章將稍微深入一點webgl,給大家介紹一點webgl的優化知識。

講webgl優化之前我們先簡單回憶一下canvas2D的優化,常用的display list、動態區域重繪等等。用canvas2D多的同學應該對以上的優化或多或少都有了解,但是你對webgl的優化了解麼,如果不瞭解的話往下看就對了~這裡會先從底層影象是如何渲染到螢幕上開始,逐步開始我們的webgl優化。

gpu如何渲染出一個物體

先看一個簡單的球的例子,下面是用webgl畫出來的一個球,加上了一點光的效果,程式碼很簡單,這裡就不展開說了。
一個球
這個球是一個簡單的3D模型,也沒有複雜的一些變化,所以例子中的球效能很好,看FPS值穩定在60。後面我們會嘗試讓它變得複雜起來,然後進行一些優化,不過這一節我們得先了解渲染的原理,知其根本才能知道優化的原理。

我們都知道webgl與著色器是密不可分的關係,webgl當中有頂點著色器和片段著色器,下面用一張圖來簡單說明下一個物體由0到1生成的過程。

0就是起點,對應圖上面的3D mesh,在程式中這個就是3D頂點資訊
1就是終點,對應圖上面的Image Output,此時已經渲染到螢幕上了
我們重點是關注中間那三個階段,第一個是一個標準的三角形,甚至三角形上面用三個圈指明瞭三個點,再加上vertex關鍵字,可以很明白的知道是頂點著色器處理的階段,圖翻譯為大白話就是:
我們將頂點資訊傳給頂點著色器(drawElements/drawArray),然後著色器將頂點資訊處理並開始畫出三角形(gl_Position)

然後再看後兩個圖,很明顯的fragments關鍵字指明瞭這是片元著色器階段。Rasterization是光柵化,從圖上直觀的看就是三角形用三條線表示變成了用畫素表示,其實實際上也是如此,更詳細的可以看下面地址,這裡不進行展開。
如何理解光柵化-知乎
後面階段是上色,可以用textture或者color都可以,反正統一以rgba的形式賦給gl_FragColor
圖中vertexShader會執行3次,而fragmentShader會執行35次(有35個方塊)
發現fragmentShader執行次數遠遠超過vertexShader,此時機智的朋友們肯定就想到儘可能的將fragmentShader中的計算放在vertexShader中,但是能這樣玩麼?

強行去找還是能找到這樣的場景的,比如說反射光。反射光的計算其實不是很複雜,但也稍微有一定的計算量,看核心程式碼

上面反射光程式碼就不細說了,核心就是內建的reflect方法。這段程式碼既可以放在fragmentShader中也可以放在vertexShader中,但是二者的結果有些不同,結果分別如下
放在vertexShader中
放在fragmentShader中

所以說這裡的優化是有缺陷的,可以看到vertexShader中執行光計算和fragmentShader中執行生成的結果區別還是蠻大的。換言之如果想要實現真實反射光的效果,必須在fragmentShader中去計算。開頭就說了這篇文章的主題在同樣的一個效果,用什麼方式是最優的,所以continue~

gpu計算能力很猛

上一節說了gpu渲染的原理,這裡再隨便說幾個gpu相關的新聞
百度人工智慧大規模採用gpuPhysX碰撞檢測使用gpu提速……種種類似的現象都表明了gpu在單純的計算能力上是超過普通的cpu,而我們關注一下前一節shader裡面的程式碼

vertexShader

fragmentShader

可以發現邏輯語句很少,更多的都是計算,特別是矩陣的運算,兩個mat4相乘通過js需要寫成這樣(程式碼來自glMatrix)

可以說相比普通的加減乘除來說矩陣相關的計算量還是有點大的,而gpu對矩陣的計算有過專門的優化,是非常快的

所以我們第一反應肯定就是能在shader中乾的活就不要讓js折騰啦,比如說前面程式碼中將proMatrix/viewMatrix/modelMatrix都放在shader中去計算。甚至將modelMatrix裡面再區分成moveMatrix和rotateMatrix可以更好的去維護不是麼~

但是瞭解threejs或者看其他學習資料的的同學肯定知道threejs會把這些計算放在js中去執行,這是為啥呢??比如下方程式碼(節選自webgl程式設計指南)

vertexShader中

javascript中

這裡居然把proMatrix/viewMatrix/modelMatrix全部在js中計算好,然後傳入到shader中去,為什麼要這樣呢?

結合第一節我們看下vertexShader執行的次數是和頂點有關係的,而每個頂點都需要做物件座標->世界座標->眼睛座標的變換,如果傳入三個頂點,就代表gpu需要將proMatrix * viewMatrix * modelMatrix計算三次,而如果我們在js中就計算好,當作一個矩陣傳給gpu,則是極好的。js中雖然計算起來相較gpu慢,但是勝在次數少啊。
看下面兩個結果

在shader中計算
在js中計算

第一個是將矩陣都傳入給gpu去計算的,我這邊看到FPS維持在50左右
第二個是將部分矩陣計算在js中完成的,我這邊看到FPS維持在60樣的
這裡用的180個球,如果球的數量更大,區別還可以更加明顯。所以說gpu計算雖好,但不要濫用呦~

js與shader互動的成本

動畫就是畫一個靜態場景然後擦掉接著畫一個新的,重複不斷。第一節中我們用的是setInterval去執行的,每一個tick中我們必須的操作就是更新shader中的attribute或者uniform,這些操作是很耗時的,因為是js和glsl程式去溝通,此時我們想一想,有沒有什麼可以優化的地方呢?
比如有一個場景,同樣是一個球,這個球的材質顏色比較特殊

x,y方向上都有著漸變,不再是第一節上面一個色的了,此時我們該怎麼辦?
首先分析一下這個這個球

總而言之就是水平和垂直方向都有漸變,如果按之前的邏輯擴充套件,就意味著我們得有多個uniform去標識
我們先嚐試一下,用如下的程式碼,切換uniform的方式

使用切換uniform的方式

發現FPS在40左右,還是蠻卡的。然後我們考慮一下,卡頓在哪?
vertexShader和fragmentShader執行的次數可以說都是一樣的,但是uniform4fv和drawElements每一次tick中執行了多次,就代表著js與shader耗費了較大的時間。那我們應該如何優化呢?

核心在避免多次改變uniform,比方說我們可以嘗試用attribute去代替uniform
看下結果怎樣

使用attribute的方式

瞬間FPS就上去了對不~所以說靈活變通很重要,不能一味的死板,儘可能的減少js與shader的互動對效能的提高是大大有幫助的~

切換program的成本

上一節我們發現頻繁切換切換uniform的開銷比較大,有沒有更大的呢?
當然有,那就是切換program,我們把之前的例子用切換program的方式試下,直接看下面的例子

點選前慎重,可能會引起瀏覽器崩潰
切換program

已經不需要關心FPS的了,可以直觀的感覺到奇卡無比。切換program的成本應該是在webgl中開銷是非常大的了,所以一定要少切換program

這裡說的是少切換program,而不是說不要切換program,從理論上來說可以單個program寫完整個程式的呀,那什麼時候又需要切換program呢?

program的作用是代替if else語句,相當於把if else抽出來單獨一個program,所以就是如果一個shader裡面的if else多到開銷超過program的開銷,此時我們就能選擇用program啦。

當然這裡的度有點難把握,需要開發者自己多嘗試,結合實際情況進行選擇。這裡有一個關於選擇program還是if else的討論,感興趣的同學可以看看

https://forums.khronos.org/showthread.php/7144-Performance-More-Shaderprograms-VS-IF-Statements-in-Shader

結語

我們這裡從原理觸發,嘗試了webgl的一些優化~如果你有什麼建議和疑惑~歡迎留言討論~

相關文章