基本上,WebGL不只是個圖形程式庫,開發者必須與GPU進行溝通,使用GLSL撰寫程式,因為這是一種著色器語言(Shader language)。

為什麼要用特定語言來撰寫著色器?撰寫時有哪些要注意的地方?又有哪些輔助工具可以使用呢?

著色器能夠執行快速處理的原因

先前我在專欄〈瀏覽器裡的3D建模師〉談過,WebGL開發者需要使用GLSL撰寫著色器程式,透過JavaScript API編譯並送入GPU」。GPU是專門執行繪圖運算的微處理器,在繪圖速度自然比透過HTML5 Canvas API等來得快速,這結論基本上沒錯,然而,若能進一步理解「專門執行繪圖運算」是指什麼,有助於對著色器語言的掌握。

正如我在〈瀏覽器裡的3D建模師〉提及的,著色器程式包含頂點與片段著色器。頂點著色器在編寫時,對象是針對單頂點的演算,片段著色器在編寫時,對象是單個像素的演算。在頂點著色器中,頂點間無法溝通,不知道前一頂點或下個頂點是哪個,更別說是頂點的狀態;頂點也無法得知上一次的狀態為何;在片段著色器中,基本上也是相同,只是頂點換為像素,一樣地,每次都是獨立運算。

若需要處理的畫布有1024 × 768個像素,每秒60個影格,在交給CPU循序處理每個像素的情況下,CPU每秒須處理47,185,920個像素,現代CPU是有多個核心,開發者也可以特意將程式設計為平行處理,讓多個核心來平均分擔這些任務(然而在瀏覽器上做不到,別忘了,JavaScript是單執行緒模型),實際上,GPU就是這麼做的,透過內部許多的微型處理器,平行地處理每個像素。

這也就是為什麼有些開發者會覺得,著色器在學習或編寫上並不容易。因為著色器為了要能配合GPU平行處理的特性,在設計模型上做出了限制,每個頂點或像素都是「獨立地」處理,彼此並不可見,而且不會記憶狀態,無論圖像再怎麼複雜,每個頂點或像素的演算必須是通用的(也就是抽象的),這樣GPU才可以將著色器運行在各個微型處理器上,每個像素的處理,都是個小型任務。

除了平行處理的特性之外,繪圖處理上經常涉及大量的向量、矩陣運算以及數學函數,著色器語言在向量與矩陣的支援上很友善,特定數學函數還能透過硬體來加速,就程式開發的角度來看,專門執行繪圖運算而獲得繪圖優勢,就是這麼一回事。

著色器的程式碼內容

對於著色器的撰寫,若能知道實際上是在進行平行程式設計的任務,其實很重要,因為這讓開發者知道可以做些什麼,又有哪些事做不到。對於做得到的事,也可以用來判斷哪些演算應該放在著色器中,而哪些可以寫在JavaScript(就WebGL而言)。

要將運算實現在哪邊?這完全取決於開發者,我們可以撰寫簡單的著色器,其他都交給JavaScript來處理,或者是除了HTML頁面處理以外的全部運算,都使用著色器來實現。而這會令人直接聯想到效能,理論上,交由GPU運算會有比較好的效率,在〈Introduction to Shaders〉(https://bit.ly/2HOFkZs)也談到這件事,實際上只有CPU才知道怎麼渲染,GPU還是須與CPU合作,亦有常見的效能瓶頸──來自於溝通上的開銷,而不是運算。

對於初次接觸WebGL的前端開發者而言,建議撰寫簡單的著色器,其他由JavaScript來實現;在逐漸熟悉GLSL之後,並發現JavaScript程式中有些通用向量、矩陣或數學函數運算,再來考慮實現在著色器之中。會有此考量的另一個原因在於,GLSL沒什麼通用的除錯工具,這表示著色器中的邏輯不應過於複雜,往往就是一些數學為主的運算。

另一方面,如果著色器摻雜越多瀏覽器的情境,就可能越局限在某些應用場合。為了讓著色器更通用,我們在著色器當中,應儘量只針對頂點、像素進行處理,避免摻雜瀏覽器情境(例如滑鼠座標轉換),而這些也會是考量之一。

當然,詳細地認識GLSL,對著色器可以寫些什麼絕對是有幫助的,最好是個所視即所得的學習方式,〈The Book of Shaders〉是個線上文件,作者以講解片段著色器的設計為主,並提供一個由glslCanvas程式庫(https://bit.ly/2WoIOGc)建立的環境,即便未搭配頂點著色器,也可以搭建出許多立即可見的驚人效果,對於認識GLSL及著色器而言,是不錯的資源。

著色器開發工具?

就WebGL而言,著色器要撰寫在哪?著色器程式碼會作為字串透過WebGL相關的API送進GPU,基本上,寫在JavaScript字串中也可以,當然,在字串中寫程式碼很麻煩;另一方式是寫在<script type="x-shader></script>之間,type只是個自訂型態,因此只要取得DOM的textContent,就可以取得程式碼;開發者也可以寫在獨立的文字檔中,透過XMLHttpRequest、Fetch API之類取得。

如果使用Visual Studio Code,相對來說,有不少著色器延伸模組。例如,Shader languages support for VS Code可以支援語法醒目提示(若是HLSL還可以有函式自動補完、簡易文件說明等),glsl-canvas以方才提到的glslCanvas為基礎做了改進,可以在片段著色器撰寫時即時預覽,另外,還有Shader Toy等模組,相對來說,在著色器工具的支援是比較熱絡而多樣化的。

開發者可能會想,有除錯器嗎?呃!就結論來說,沒有通用的除錯器!因為平行地執行著色器來處理頂點及像素,導致在CPU上運行除錯器的傳統概念行不通;有種說法是,寫出正確的著色器程式就可以減少除錯的機會(https://bit.ly/2HHJtiD),另一種說法是,在著色器領域沒有太多要除錯的(https://bit.ly/2HTufGo)。

這聽來有點可笑,不過,著色器的確會平行地處理頂點、像素,而這讓人聯想到函數式設計,在這類典範中,著重個別函式的功能簡單而正確,從而組合出正確的程式。雖然著色器程式沒有通用的除錯器,也不能列印值,然而,不少文件都建議,可以輸出不同顏色來代表各種預期狀況,例如,在單元測試個別函式時派上用場,而這也表示,著色器的流程應分解為逐個小任務,才能使用這種方式。

如果真的難以除錯,可試著將任務移至JavaScript實現,利用它的除錯器驗證邏輯是否正確;當然,最後的任務是繪製,麻煩的是沒呈現出預期畫面,特別是在處理動畫,瀏覽器上有些外掛,像是Chrome上有WebGL Insight,可以捕捉、控制影格,並顯示函式的呼叫過程。

著色器導向典範

如果對前端與JavaScript有一定的熟悉度,WebGL在JavaScript API方面沒什麼難處,基本上就是按表操課。對我來說,後來反而是著色器語言這方面吸引了我。因為,一開始只覺得,它有個獨特的風味,後來慢慢玩轉了一段時間,發現真正的價值之一,是在於著色器語言的掌握,例如,以向量、矩陣等的運用來思考運算。

另一個價值在於,雖然GLSL語言本身是基於命令式的C語言,而非函數式風格,然而,獨立、無狀態地處理頂點、像素這方面,還是隱含著函數式、平行處理的典範,而如何設計出一個通用的演算,適用於全部像素,然而又能組合出複雜的圖樣,也是滿新奇的概念,因此,這會是個挑戰,然而也會是樂趣之所在!

專欄作者

熱門新聞

Advertisement