iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
Software Development

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

Day24: WebGL Shader——透過自製環境光實作shader傳值

  • 分享至 

  • xImage
  •  

前一篇我們初步修改Shader,並且介紹GLSL的型別、函式、程式進入點、程式最終任務。

本篇將繼續介紹:

  1. 實作即時變化的環境光
    1. 實作即時變化的環境光:回顧上一篇開發的顏色
    2. 準備程式碼
    3. 簡單說明上面程式碼的要點
  2. 從實作過程中釐清變數類型
    1. 從實作過程中釐清:Uniform是什麼?
    2. 從實作過程中釐清:從實作過程中釐清:變數類型Const or #define @
    3. 從實作過程中釐清:變數類型Attributes
    4. 從實作過程中釐清:變數類型Uniforms
    5. 從實作過程中釐清:變數類型Varying

實作即時變化的環境光

經由上一篇,有了型別、函式、程式進入點、程式最終任務這些概念之後,我們其實就有能力修改Shader,創造很多效果。

實作即時變化的環境光:回顧上一篇開發的顏色

回顧上一篇,我們準備好了開發環境,並且在shader給定了gl_FragColor變數,賦予了球體一個顏色。

有了gl_FragColor,就可以指定球是綠色、紅色、紫色等各種顏色。就像上一篇提到的那樣,我修改R, G, B, A0.0, 1.0, 0.0, 0.0,那個就可以產生綠色。

gl_FragColor=vec4(0.0, 1.0, 0.0, 1.);

https://ithelp.ithome.com.tw/upload/images/20221015/201425053M9QcuzcCj.png

有興趣可以看前一篇文章,它介紹如何修改顏色。但操作過你將會發現一件事情:在Fragment Shader當中雖然可以直接指定顏色,可是我們若沒辦法將一些資料傳到的Fragment Shader,那麼Fragment Shader就很孤單的只能換換顏色而已。

為了活化應用,我們必須從javascript傳值到shader,以下將實作開發,使得用戶可以自己調整球體的顏色。

實作即時變化的環境光:準備程式碼

我準備了上手的程式碼,直接複製貼上即可。幾乎跟前一篇的示例一模一樣。主要就是建置three.js的開發環境,然後新增一顆球。

https://ithelp.ithome.com.tw/upload/images/20221015/20142505KIA8V7maeX.png

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

實作即時變化的環境光:簡單說明上面程式碼的要點

上一篇,我們實例化了ShaderMaterial,並且指定shader的字串內容為vertexfragment,在ShaderMaterial用GLSL編譯,最後在GPU渲染。

const addSphere = () => {
	const vertex = document.getElementById('vertexShader').innerHTML
	const fragment = document.getElementById('fragmentShader').innerHTML
    const geo = new THREE.SphereGeometry(5,15,15)
    const mat = new THREE.ShaderMaterial({
		//  指定vertexShader程式碼來源
		vertexShader: vertex,
		//  指定fragmentShader程式碼來源
		fragmentShader: fragment,
	})
    const mesh = new THREE.Mesh(geo, mat)
    scene.add(mesh)
    return mesh
}

addSphere()

在HTML,我給了字串,分別代表Vertex Shader跟Fragment Shader,但我還沒有介紹其原理。

<p id="fragmentShader">
    void main(void){
    gl_FragColor=vec4(0.6, 0.3, 1.0, 1.0);
    }
</p>
<p id="vertexShader">
    void main(void){
    gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
    }
</p>

目前,球體的顏色長這樣:

https://ithelp.ithome.com.tw/upload/images/20221015/20142505kynis8hJYP.png

接著我們試試看再進階一點的實作:

實作即時變化的環境光:單向綁定HTML數值

  1. 在HTML加上Range,使得滑鼠可以變更亮度

    <main>
    + <input type="range" min="0" max="4" step="0.1" value="0.5" id="intensity">
    </main>
    
  2. 首先,在shader加上uIntensity,我將在下一篇提到uniforms 的功用,我先將重點放在傳值。

    const mat = new THREE.ShaderMaterial({
    		vertexShader: vertex,
    		fragmentShader: fragment,
    +		uniforms: {
    +			uIntensity: {
    +				value: 0.5 // 實作下一步input事件之前先給它預設值
    +			}
    +		}
    	})
    
  3. 在javascript單向綁定值到intensity

    + document.getElementById('intensity').addEventListener( 'input', event => {
    + 	sphere.material.uniforms.uIntensity.value = +event.target.value
    + })
    

完成品

接著,就可以看到我們的ambient light做好了。基本上,速成的環境光就這樣完成了。事實上,three.js 的環境光AmbientLight 就是在shader中加上一個intensity來控制整個物件的顏色。

Untitled

CodePen

https://ithelp.ithome.com.tw/upload/images/20221015/20142505XfyHR2fxV7.png

https://codepen.io/umas-sunavan/pen/JjvzrMq

從實作過程中釐清變數類型

如果你重看前面時做的第二點,你會發現,我們使用傳送了intensity ,使得我們可以調整物體亮度,如下面的程式碼,three.js可以傳送數值0.5給shader的變數uIntensity

const mat = new THREE.ShaderMaterial({
		vertexShader: vertex,
		fragmentShader: fragment,
+		uniforms: {
+			uIntensity: {
+				value: 0.5
+			}
+		}
	})

但我留了幾個伏筆:

  1. uniforms是什麼?為什麼我們得加上這個東西?這是three.js專屬的撰寫方式嗎?還是Shader的撰寫方式?
  2. 為什麼我加上uItensity 就能夠在shader宣告變數為0.5?這個名稱可以自己自定義嗎?如果是,那為什麼要加上u前綴?
  3. 我們目前都把一致的數值傳送給FragmentShader,它一致的將數值渲染在物體的所有像素中,可以傳送各不相同的數值給像素嗎?例如只要判斷是左邊數來第100個像素以上,我就換一個顏色,可以這樣做嗎?

接著就來回答這些問題。

從實作過程中釐清:Uniform是什麼?

如它的名字,uniform核心概念就是一致。無論vertex shader在那一幀所執行的錨點有多少的,到第幾個了,也無論fragment shader那一幀所執行像素到底幾顆,uniform的數值那幀都不會變。

這也是為什麼,我們過uniform傳入資料。而這也代表,事實上有些變數行為不同,例如重要的變數類型:

從實作過程中釐清:變數類型Const or #define @

Const就很像Js ES6的Const。至於Define,則是用來取代文件中提及的所有關鍵字,主要是替換文字,基本上並沒有因此多儲存資料在記憶體,而比較像是單純置換所有關鍵字而已,所以能提升效率。

從實作過程中釐清:變數類型Attributes

  1. 依據不同的vertex變化的變數。
  2. 可用來從外部獲取資料,主要是從buffers抓取資料。例如從js取得資料到Shader中。
  3. 可轉成其他變數如vec3。

從實作過程中釐清:變數類型Uniforms

  1. 可以從JS傳值到Shader中。

  2. 由於GPU在渲染畫面時,一個螢幕會有很多平行的thread一起渲染,但無論是哪一個thread在渲染,這個數值固定一致,這也是被稱作uniforms的原因。

  3. 透過其他套件,我們能夠從js傳值到Shader。

    例如three.js將uniform傳值的方法:

    const mat = new THREE.ShaderMaterial({
    		vertexShader: vertex,
    		fragmentShader: fragment,
    		uniforms: {
    			uIntensity: {
    				value: 0.5
    			}
    		}
    	})
    

    如此一來,就能在Shader中存取該值

    // 在fragment shader,重新宣告uIntensity為uniform即可
    uniform float uIntensity;
    void main(void){
    gl_FragColor=vec4(uIntensity * 1.0, uIntensity * 1.0, uIntensity * 0.0, 1.0);
    }
    

    又例如p5.js的作法:

    theShader.setUniform("u_resolution", [width, height]); // 傳螢幕大小給shader
    

    接著在Fragment宣告同名的名稱,即可從Js接收資料並且加以應用。

    uniform vec2 u_resolution;
    

從實作過程中釐清:變數類型Varying

  1. 跟uniform不同,uniform在每個像素渲染時保持不變,但在varying則各不相同。
  2. 通常用來傳遞texture coordinates

小結

本篇從實作環境光開始,快速建立一個shader案例。在這個案例裡面,我們從javascript傳遞數值到了shader。在傳遞的過程中,發現了陌生的關鍵字uniform,並且經過uniform,帶出其它GLSL類別。

經過本篇跟上一篇的說明,現在我們可以自由的從javascript傳送數值到fragment shader,藉此有了進階的操作。

下一篇,我們將透過數值,建立一個光點。


上一篇
Day23: WebGL Shader——從認識GLSL開始釐清Shader
下一篇
Day: 25 使用Shader創造漸層
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言