iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
Software Development

30天成為鍵盤麥可貝:前端視覺特效開發實戰系列 第 22

Day22: WebGL Shader—你好啊大哥哥,沒想到你可以到Shader來呢!

  • 分享至 

  • xImage
  •  

Shader是什麼

我們所稱呼的Shader,其實是Fragment Shader以及Vertex Shader的合稱。這兩個出現在Program上,使得我們最終可以計算「每一顆」像素應該呈現的顏色。

https://ithelp.ithome.com.tw/upload/images/20221014/20142505dleYwK97vb.png

我們在three.js製作的光、陰影、物件顏色、材質,都是基於three.js架構所建立的物件。這些物件在認清了彼此之間的關係,最後用Shader來渲染畫面,所以Shader一直都在three.js底層。這是什麼意思呢?

我們再用「Day8: Three.js 你有被光速踢過嗎?解析3D界的黃猿——光的底層原理與介紹」來舉例說明。假設我們在three.js建立了光,然後建立了球體。那麼該如何在我們的顯示器上呈現亮度呢?

https://ithelp.ithome.com.tw/upload/images/20221014/20142505EeSWtAz8ey.png

Vertex Shader跟 Fragment Shader 身為底層,在這個場景下,就必須經由three.js傳入的參數(光的位置、亮度,以及球體的錨點等等)資料,進行計算,計算出每一顆像素的顏色。

所以說,three.js始終在運用shader做計算。當然我們也可以跳過three.js,直接撰寫計算邏輯。而這就是我們寫shader的時候了。

Shader是什麼:Vertex Shader跟 Fragment Shader

Vertex Shader每一幀每一個錨點都會執行一遍。如果是一個純粹的立方體,那就有八個錨點,那就會執行八次。執行要來做什麼呢?要負責將錨點投影到螢幕座標中,並且提供Fragment Shader錨點資料,以利錨點計算。也因此它被稱作Vertex(頂點)Shader。

Fragment Shader每一幀每一個像素會跑一次。如果你的電腦解析度是1920x1280,那每一幀Shader就會執行兩百多萬次。如果一秒螢幕跑60幀,那就是大約近一億五千萬次,這是非常大量的計算。當你看完這段文字之後,你的螢幕就已經跑了十五億次了。

https://ithelp.ithome.com.tw/upload/images/20221014/20142505gxiFdaEDnK.png

Fragment Shader執行要來做什麼呢。它計算出每一個像素的R,G,B,A數值,顯示在螢幕上。就是這麼底層。這也因次它被稱作Fragment(碎片,指像素) Shader。

上面這兩個Shader搭配可以組成一個Program,而這就是WebGL的運作模式。

前面二十幾天鐵人賽篇幅的Three.js,最終會把我們所撰寫的javascript,餵入到WebGL中,最終透過shader渲染出來。

Shader是什麼:Shader特性

雖然Fragment Shader要短時間內運算多次,但幸好這些都發生在GPU上。GPU平行運算並且得到像素RGB數值,使得這些計算非常快速。這也意味著:當我們執行Shader時,程式碼就已經在GPU上面運行了。

由於Shader特性,每一顆像素沒辦法存取隔壁像素的數值,它們都是「孤獨」的,而且在每一幀運算之後,是不會存下任何變數的。

Shader是什麼:Shader跟three.js的關係

three.js是已經包好用來運算WebGL的js函式庫,Shader是相對來說低階的程式語言GLSL寫成(需要編譯),但基本上都是在渲染畫面。

除了three.js以外,還有P5.js, WebGL API, babylon.js可以傳值到shader實作,但基本上就只有傳值不一樣而已。

Shader要如何開發

Shader使用GLSL(GL Shader Language)撰寫,是很像C系列語言的語言。該語言的介紹我先挪到下篇,我們先在本篇從three.js加入客製化Shader。

Shader要如何開發:準備程式碼

我準備了CodePen,裡面有最基本的three.js場景,這些過去都有介紹到。由於我們聚焦在Shader開發,所以提供程式碼給大家開始。

Shader要如何開發:CodePen

https://ithelp.ithome.com.tw/upload/images/20221014/20142505B1LHYeKaNz.png

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要如何開發:修改材質

基本上,我們可以透過材質來建立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() ,並且有規定的變數值需要賦予。我們接著實作:

Shader要如何開發:新增GLSL

我們要撰寫字串並傳入到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://ithelp.ithome.com.tw/upload/images/20221014/20142505e45bTX70Mw.png

CodePen

https://codepen.io/umas-sunavan/pen/GRdXemK?editors=1010

小結

本篇我們先確保我們進入Shader的世界。下一篇將嘗試透過Shader製作出一個球。

參考資料

texture2D的說明

webglfundamentals的說明

BookOfShader說明


上一篇
Day21: three.js 智慧工廠開發實戰:Dejavu! 讓鏡頭跟著拓海一起飄移:鏡頭追蹤、飄移特效
下一篇
Day23: WebGL Shader——從認識GLSL開始釐清Shader
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言