我們所稱呼的Shader,其實是Fragment Shader以及Vertex Shader的合稱。這兩個出現在Program上,使得我們最終可以計算「每一顆」像素應該呈現的顏色。
我們在three.js製作的光、陰影、物件顏色、材質,都是基於three.js架構所建立的物件。這些物件在認清了彼此之間的關係,最後用Shader來渲染畫面,所以Shader一直都在three.js底層。這是什麼意思呢?
我們再用「Day8: Three.js 你有被光速踢過嗎?解析3D界的黃猿——光的底層原理與介紹」來舉例說明。假設我們在three.js建立了光,然後建立了球體。那麼該如何在我們的顯示器上呈現亮度呢?
Vertex Shader跟 Fragment Shader 身為底層,在這個場景下,就必須經由three.js傳入的參數(光的位置、亮度,以及球體的錨點等等)資料,進行計算,計算出每一顆像素的顏色。
所以說,three.js始終在運用shader做計算。當然我們也可以跳過three.js,直接撰寫計算邏輯。而這就是我們寫shader的時候了。
Vertex Shader每一幀每一個錨點都會執行一遍。如果是一個純粹的立方體,那就有八個錨點,那就會執行八次。執行要來做什麼呢?要負責將錨點投影到螢幕座標中,並且提供Fragment Shader錨點資料,以利錨點計算。也因此它被稱作Vertex(頂點)Shader。
Fragment Shader每一幀每一個像素會跑一次。如果你的電腦解析度是1920x1280,那每一幀Shader就會執行兩百多萬次。如果一秒螢幕跑60幀,那就是大約近一億五千萬次,這是非常大量的計算。當你看完這段文字之後,你的螢幕就已經跑了十五億次了。
Fragment Shader執行要來做什麼呢。它計算出每一個像素的R,G,B,A數值,顯示在螢幕上。就是這麼底層。這也因次它被稱作Fragment(碎片,指像素) Shader。
上面這兩個Shader搭配可以組成一個Program,而這就是WebGL的運作模式。
前面二十幾天鐵人賽篇幅的Three.js,最終會把我們所撰寫的javascript,餵入到WebGL中,最終透過shader渲染出來。
雖然Fragment Shader要短時間內運算多次,但幸好這些都發生在GPU上。GPU平行運算並且得到像素RGB數值,使得這些計算非常快速。這也意味著:當我們執行Shader時,程式碼就已經在GPU上面運行了。
由於Shader特性,每一顆像素沒辦法存取隔壁像素的數值,它們都是「孤獨」的,而且在每一幀運算之後,是不會存下任何變數的。
three.js是已經包好用來運算WebGL的js函式庫,Shader是相對來說低階的程式語言GLSL寫成(需要編譯),但基本上都是在渲染畫面。
除了three.js以外,還有P5.js, WebGL API, babylon.js可以傳值到shader實作,但基本上就只有傳值不一樣而已。
Shader使用GLSL(GL Shader Language)撰寫,是很像C系列語言的語言。該語言的介紹我先挪到下篇,我們先在本篇從three.js加入客製化Shader。
我準備了CodePen,裡面有最基本的three.js場景,這些過去都有介紹到。由於我們聚焦在Shader開發,所以提供程式碼給大家開始。
https://codepen.io/umas-sunavan/pen/XWqPOPM?editors=1010
先帶一下CodePen裡面的東西。我在場景中製作了一顆球,雖然過去有介紹如何建立Mesh物件,但我這邊簡單介紹一下:
const addSphere = () => {
// 實例化球的形狀
const geo = new THREE.SphereGeometry(5,50,50)
// 實例化材質物件
const mat = new THREE.MeshBasicMaterial({color: 0xffff00})
// Mesh得透過上面兩個組成
const mesh = new THREE.Mesh(geo, mat)
// 將球加入到場景
scene.add(mesh)
}
addSphere()
有關three.js的解釋,可以參考「Day2: ThreeJS、OpenGL、WebGL:誰是誰?我要怎麼開始?」
基本上,我們可以透過材質來建立Shader。這跟P5.js以及WebGL API不太相同,我將在後續補充。總之,可以先將MeshBasicMaterial
改成ShaderMaterial
。
const addSphere = () => {
// 實例化球的形狀
const geo = new THREE.SphereGeometry(5,50,50)
// 實例化材質物件
- const mat = new THREE.MeshBasicMaterial({color: 0xffff00})
+ const vertex = ''
+ const fragment = ''
+ const mat = new THREE.ShaderMaterial({
+ vertexShader: vertex,
+ fragmentShader: fragment,
})
// Mesh得透過上面兩個組成
const mesh = new THREE.Mesh(geo, mat)
// 將球加入到場景
scene.add(mesh)
}
目前vertex跟fragment是空的。這兩個要傳入字串,以利WebGL編譯program。
但GLSL需要合法的進入點main()
,並且有規定的變數值需要賦予。我們接著實作:
我們要撰寫字串並傳入到ShaderMaterial
,這有很多種方式。有些人另開json檔案儲存,有些直接宣告在字串上,而我用的方法是:寫在HTML,再用innerHTML取字串。
+ <-- HTML -->
<main>
+ <div style="display: none">
+ <-- 這裡撰寫fragmentShader -->
+ <p id="fragmentShader">
+ varying vec3 vertexNormal;
+ void main(void){
+ gl_FragColor=vec4(0.3, 0.6, 1., 1.);
+ }
+ </p>
+ <-- 這裡撰寫vertexShader -->
+ <p id="vertexShader">
+ varying vec3 vertexNormal;
+ void main(void){
+ vertexNormal = normal;
+ gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
+ }
+ </p>
+ </div>
</main>
再單向綁定到變數裡:
// index.js
const addSphere = () => {
// 實例化球的形狀
const geo = new THREE.SphereGeometry(5,50,50)
// 實例化材質物件
- const vertex = ''
- const fragment = ''
+ const vertex = document.getElementById('vertexShader').innerHTML
+ const fragment = document.getElementById('fragmentShader').innerHTML
const mat = new THREE.ShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment,
})
// Mesh得透過上面兩個組成
const mesh = new THREE.Mesh(geo, mat)
// 將球加入到場景
scene.add(mesh)
}
接著,我們自製Shader就產生了。
圖中可以看到,原本黃色的球,透過Shader處理後變成藍色。
https://codepen.io/umas-sunavan/pen/GRdXemK?editors=1010
本篇我們先確保我們進入Shader的世界。下一篇將嘗試透過Shader製作出一個球。