前一篇我們初步修改Shader,並且介紹GLSL的型別、函式、程式進入點、程式最終任務。
本篇將繼續介紹:
經由上一篇,有了型別、函式、程式進入點、程式最終任務這些概念之後,我們其實就有能力修改Shader,創造很多效果。
回顧上一篇,我們準備好了開發環境,並且在shader給定了gl_FragColor
變數,賦予了球體一個顏色。
有了gl_FragColor
,就可以指定球是綠色、紅色、紫色等各種顏色。就像上一篇提到的那樣,我修改R
, G
, B
, A
為0.0
, 1.0
, 0.0
, 0.0
,那個就可以產生綠色。
gl_FragColor=vec4(0.0, 1.0, 0.0, 1.);
有興趣可以看前一篇文章,它介紹如何修改顏色。但操作過你將會發現一件事情:在Fragment Shader當中雖然可以直接指定顏色,可是我們若沒辦法將一些資料傳到的Fragment Shader,那麼Fragment Shader就很孤單的只能換換顏色而已。
為了活化應用,我們必須從javascript傳值到shader,以下將實作開發,使得用戶可以自己調整球體的顏色。
我準備了上手的程式碼,直接複製貼上即可。幾乎跟前一篇的示例一模一樣。主要就是建置three.js的開發環境,然後新增一顆球。
https://codepen.io/umas-sunavan/pen/xxjBXrB?editors=1010
上一篇,我們實例化了ShaderMaterial
,並且指定shader的字串內容為vertex
跟fragment,在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>
目前,球體的顏色長這樣:
接著我們試試看再進階一點的實作:
在HTML加上Range,使得滑鼠可以變更亮度
<main>
+ <input type="range" min="0" max="4" step="0.1" value="0.5" id="intensity">
</main>
首先,在shader加上uIntensity
,我將在下一篇提到uniforms
的功用,我先將重點放在傳值。
const mat = new THREE.ShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment,
+ uniforms: {
+ uIntensity: {
+ value: 0.5 // 實作下一步input事件之前先給它預設值
+ }
+ }
})
在javascript單向綁定值到intensity
+ document.getElementById('intensity').addEventListener( 'input', event => {
+ sphere.material.uniforms.uIntensity.value = +event.target.value
+ })
接著,就可以看到我們的ambient light做好了。基本上,速成的環境光就這樣完成了。事實上,three.js 的環境光AmbientLight 就是在shader中加上一個intensity來控制整個物件的顏色。
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
+ }
+ }
})
但我留了幾個伏筆:
uniforms
是什麼?為什麼我們得加上這個東西?這是three.js專屬的撰寫方式嗎?還是Shader的撰寫方式?uItensity
就能夠在shader宣告變數為0.5?這個名稱可以自己自定義嗎?如果是,那為什麼要加上u前綴?接著就來回答這些問題。
如它的名字,uniform核心概念就是一致。無論vertex shader在那一幀所執行的錨點有多少的,到第幾個了,也無論fragment shader那一幀所執行像素到底幾顆,uniform的數值那幀都不會變。
這也是為什麼,我們過uniform傳入資料。而這也代表,事實上有些變數行為不同,例如重要的變數類型:
Const就很像Js ES6的Const。至於Define,則是用來取代文件中提及的所有關鍵字,主要是替換文字,基本上並沒有因此多儲存資料在記憶體,而比較像是單純置換所有關鍵字而已,所以能提升效率。
可以從JS傳值到Shader中。
由於GPU在渲染畫面時,一個螢幕會有很多平行的thread一起渲染,但無論是哪一個thread在渲染,這個數值固定一致,這也是被稱作uniforms的原因。
透過其他套件,我們能夠從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;
本篇從實作環境光開始,快速建立一個shader案例。在這個案例裡面,我們從javascript傳遞數值到了shader。在傳遞的過程中,發現了陌生的關鍵字uniform,並且經過uniform,帶出其它GLSL類別。
經過本篇跟上一篇的說明,現在我們可以自由的從javascript傳送數值到fragment shader,藉此有了進階的操作。
下一篇,我們將透過數值,建立一個光點。