three.js学习笔记(十四)——Shaders着色器
什么是著色器?
實(shí)際上著色器是WebGL的主要組件之一。如果我們在沒接觸Three.js情況下開始學(xué)習(xí)WebGL,著色器將是我們首先且必須要學(xué)的知識,這也是為什么原生WebGL很難入門。
著色器是一種使用GLSL(OpenGL Shading Language)編寫并在GPU上運(yùn)行的程序。它們被用于定位幾何體的每個頂點(diǎn),并為該幾何體的每個可見像素著色。使用“像素Pixel”來描述其實(shí)并不準(zhǔn)確,因?yàn)殇秩镜拿總€點(diǎn)不一定與屏幕上的每個像素相匹配,因此我們更傾向于使用術(shù)語“片元fragment”。
之后我們會向著色器發(fā)送大量數(shù)據(jù),如頂點(diǎn)坐標(biāo)、網(wǎng)格變換、攝像機(jī)及其視野范圍的信息、顏色、紋理、燈光、霧等參數(shù)。然后,GPU會按照著色器的指示處理所有的這些數(shù)據(jù),接著幾何體便出現(xiàn)在渲染中。
有倆種類型的著色器,并且我們都會用到它們。
頂點(diǎn)著色器Vertex Shader
頂點(diǎn)著色器(Vertex Shader)的作用是定位幾何體的頂點(diǎn)。其思想是發(fā)送頂點(diǎn)位置、網(wǎng)格變換(如定位position、旋轉(zhuǎn)rotation和縮放scale)、攝影機(jī)信息(如定位position、旋轉(zhuǎn)rotation和視野fov)。然后,GPU將按照頂點(diǎn)著色器中的指示處理所有這些信息,以便將頂點(diǎn)投影到2D空間,該空間將成為我們的渲染render,也就是我們的畫布canvas。
頂點(diǎn)著色器對頂點(diǎn)實(shí)現(xiàn)了一種通用的可編程方法。
使用頂點(diǎn)著色器時(shí),其代碼將應(yīng)用于幾何體的每個頂點(diǎn)。但有些數(shù)據(jù)(如頂點(diǎn)的位置)會在每個頂點(diǎn)之間發(fā)生變化。這種類型的數(shù)據(jù)(在頂點(diǎn)之間變化的數(shù)據(jù))稱為attribute屬性變量。
- attribute:使用頂點(diǎn)數(shù)組封裝每個頂點(diǎn)的數(shù)據(jù),一般用于每個頂點(diǎn)都各不相同的變量,如頂點(diǎn)的位置等。
但有些數(shù)據(jù)不需要像網(wǎng)格的位置那樣在每個頂點(diǎn)之間進(jìn)行變換,用于對同一組頂點(diǎn)組成的單個3D物體中所有頂點(diǎn)都相同的變量,如當(dāng)前光源的位置,相機(jī)的位置,這種類型的數(shù)據(jù)(頂點(diǎn)之間不發(fā)生變化的數(shù)據(jù))稱為uniform統(tǒng)一變量。
- uniform:頂點(diǎn)著色器使用的常量數(shù)據(jù),不能被著色器修改,一般用于對同一組頂點(diǎn)組成的單個3D物體中所有頂點(diǎn)都相同的變量,如當(dāng)前光源的位置。
頂點(diǎn)著色器會首先觸發(fā),當(dāng)放置完頂點(diǎn)后,GPU會知道幾何體的哪些像素是可見的,然后可以接著下去使用片元著色器。
片元著色器Fragment Shader
片元著色器的作用是為幾何體的每個可見片元(像素)進(jìn)行著色。
我們會創(chuàng)建片元著色器,可以通過使用uniform將數(shù)據(jù)(像是顏色)和著色器發(fā)送至GPU,之后GPU就會按照指令對每個片元進(jìn)行著色。
片段著色器中最簡單直接的指令是可以使用相同的顏色為所有片段著色。如果我們只設(shè)置了顏色屬性,我們就得到了與MeshBasicMaterial等效的材質(zhì)(每個可見像素都被著色白色)。
參考 著色器語言三種變量attribute、uniform 和 varying
簡單總結(jié)
- 頂點(diǎn)著色器渲染定位頂點(diǎn)位置
- 片段著色器為該幾何體的每個可見片元(像素)進(jìn)行著色
- 片段著色器在頂點(diǎn)著色器之后執(zhí)行
- 在每個頂點(diǎn)之間會有變化的數(shù)據(jù)(如頂點(diǎn)的位置)稱為attribute,只能在頂點(diǎn)著色器中使用
- 頂點(diǎn)之間不變的數(shù)據(jù)(如網(wǎng)格位置或顏色)稱為uniform,可以在頂點(diǎn)著色器和片段著色器中使用
- 從頂點(diǎn)著色器發(fā)送到片元著色器中的插值計(jì)算數(shù)據(jù)被稱為varying
為什么要寫著色器?
Three.js的內(nèi)置材質(zhì)試圖涵蓋盡可能多的情況,但是依舊有其局限性。如果我們想要突破限制,就必須自個寫自己的著色器。
當(dāng)然也可能是出于性能原因。像MeshStandardMaterial這樣的材質(zhì)非常精細(xì),涉及大量代碼和計(jì)算。如果我們編寫自己的著色器,我們可以將功能和計(jì)算保持在最小限度,這樣就可以更好控制與優(yōu)化演算性能了。
編寫我們自己的著色器也是將后期處理添加到渲染中的一個很好的方法,但我們將在后面專門的課程中再看到這一點(diǎn)。
一旦掌握了著色器,它們將成為所有項(xiàng)目中不可或缺的必備工具。
使用原始著色器材質(zhì)(RawShaderMaterial)創(chuàng)建自己的第一個著色器
要創(chuàng)建第一個著色器,我們需要創(chuàng)建特定的材質(zhì)。該材質(zhì)可以是著色器材質(zhì)ShaderMaterial或原始著色器材質(zhì)RawShaderMaterial。這兩者之間的區(qū)別在于前者會自動將一些代碼添加到著色器代碼中(內(nèi)置attributes和uniforms),而后者正如其名,什么都不會添加。
準(zhǔn)備工作
我們的初始場景有一塊MeshBasicMaterial材質(zhì)的簡單平面,將之材質(zhì)替換為RawShaderMaterial原始著色器材質(zhì)。然后你會發(fā)現(xiàn)報(bào)錯了,這是因?yàn)槲覀冞€沒添加著色器。如前所述,我們需要同時(shí)提供頂點(diǎn)著色器和片元著色器,并在里面書寫程序:
const material = new THREE.RawShaderMaterial({vertexShader: `uniform mat4 projectionMatrix;uniform mat4 viewMatrix;uniform mat4 modelMatrix;attribute vec3 position;void main(){gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);}`,fragmentShader: `precision mediump float;void main(){gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);}` })得到這樣的一個結(jié)果:
分離兩種著色器
新建一個shaders文件夾,在里面新建倆個文件vertex.glsl和fragment.glsl,將上邊代碼拷貝到對應(yīng)文件中,再在vscode的插件商店安裝shader languages support for VS code,這樣我們的.glsl文件也就有了語法高亮提示。
之后導(dǎo)入:
然而我們會報(bào)一個webpack錯誤,因?yàn)樾枰嬖Vwebpack如何處理.glsl文件。
去/bundler/webpack.common.js,修改如下:
這個規(guī)則僅告訴webpack提供文件的原始內(nèi)容。然后重啟服務(wù),會看到與前面一樣的場景。
我們在其他材質(zhì)中介紹的大多數(shù)常見屬性(如wireframe、side、transparent、flatShading)仍然適用于RawShaderMaterial
const material = new THREE.RawShaderMaterial({vertexShader: testVertexShader,fragmentShader: testFragmentShader,wireframe: true })
但是像map、alphaMap、opacity、 color等屬性將不再生效,因?yàn)槲覀冃枰约涸谥髦芯帉戇@些特性。
GLSL
OpenGL著色語言(OpenGL Shading Language)是用來在OpenGL中著色編程的語言,它是一種類C語言,下面先學(xué)習(xí)一下基礎(chǔ)語法。
日志
沒有控制臺,因此無法記錄值。這是因?yàn)榇a針對每個頂點(diǎn)和每個片段執(zhí)行。記錄一個值是沒有意義的。
縮進(jìn)
縮進(jìn)不重要,可以隨意。
分號
任何指令的結(jié)尾都需要分號。哪怕忘記一個分號都可能導(dǎo)致編譯錯誤,使得整個材料都不起作用。
變量
GLSL是一種強(qiáng)類型語言,如C語言一般。
不能在操作中混合使用float和int,會報(bào)錯:
float a = 1.0; int b = 2; float c = a * b;但是可以通過類型轉(zhuǎn)換進(jìn)行操作:
float a = 1.0; int b = 2; float c = a * float(b);Vector 2
如果我們想存儲具有x和y屬性的2個坐標(biāo)這樣的值,我們可以使用vec2
vec2 foo = vec2(1.0, 2.0);空的vec2將導(dǎo)致錯誤:
vec2 foo = vec2();我們可以在創(chuàng)建vec2后更改這些屬性:
vec2 foo = vec2(0.0); foo.x = 1.0; foo.y = 2.0;執(zhí)行vec2與浮點(diǎn)相乘等操作將同時(shí)操作x和y屬性:
vec2 foo = vec2(1.0, 2.0); foo *= 2.0;Vector 3
vec3與vec2類似,但具有第三個名為z的屬性。當(dāng)需要3D坐標(biāo)時(shí),使用它非常方便:
vec3 foo = vec3(0.0); vec3 bar = vec3(1.0, 2.0, 3.0); bar.z = 4.0;雖然我們可以使用x、y和z,但我們也可以使用r、g和b。這只是語法糖,結(jié)果完全一樣。當(dāng)我們使用vec3存儲顏色時(shí),它非常有效:
vec3 purpleColor = vec3(0.0); purpleColor.r = 0.5; purpleColor.b = 1.0;vec3可以通過部分使用vec2來創(chuàng)建:
vec2 foo = vec2(1.0, 2.0); vec3 bar = vec3(foo, 3.0);我們也可以使用vec3的一部分來生成vec2:
vec3 foo = vec3(1.0, 2.0, 3.0); vec2 bar = foo.xy;Vector 4
最后,vec4的原理與前兩個類似,但第四個值命名為w或a(顏色alpha)
vec4 foo = vec4(1.0, 2.0, 3.0, 4.0); vec4 bar = vec4(foo.zw, vec2(5.0, 6.0));還有其他類型的變量,如mat2、mat3、mat4或sampler2D,我們將在后面看到這些變量。
在著色器內(nèi),一般命名以gl_開頭的變量是著色器的內(nèi)置變量,除此之外webgl_和_webgl還是著色器保留字,自定義變量不能以webgl_或_webgl開頭。變量聲明一般包含<存儲限定符><數(shù)據(jù)類型><變量名稱>,以attribute vec4 a_Position為例,attribute表示存儲限定符,vec是數(shù)據(jù)類型,a_Position為變量名稱。
原生函數(shù)
GLSL有許多內(nèi)置的經(jīng)典函數(shù),如sin、cos、max、min、pow、exp、mod、clamp,也有非常實(shí)用的函數(shù),如cross、dot、mix、step、smoothstep、length、distance、reflect、refract、normalize。
不幸的是,沒有對初學(xué)者友好的文檔,而且大多數(shù)時(shí)候,我們在網(wǎng)絡(luò)上進(jìn)行搜索,結(jié)果通常出現(xiàn)在以下網(wǎng)站:
- Kronos Group OpenGL reference pages:本文檔涉及OpenGL,但您將看到的大多數(shù)標(biāo)準(zhǔn)函數(shù)都與WebGL兼容。不要忘了WebGL只是一個訪問OpenGL的JavaScript API。
- Book of shaders documentation:《Book of shaders》主要關(guān)注片元著色器,與Three.js無關(guān)。但是這是一個很好的學(xué)習(xí)資源,它有自己的詞匯表。
理解頂點(diǎn)著色器
下面開始嘗試?yán)斫庵骼锩娴膬?nèi)容。
首先注意的是,頂點(diǎn)著色器的目的是將幾何體的每個頂點(diǎn)放置在2D渲染空間上。換句話說,頂點(diǎn)著色器將3D頂點(diǎn)坐標(biāo)轉(zhuǎn)換為2D畫布坐標(biāo)。
main函數(shù)
它會自動調(diào)用,并且不會返回return任何內(nèi)容
void main() { }gl_Position
變量gl_Position原本就已經(jīng)聲明好,是一個內(nèi)置變量,我們只需重新分配它。
這個變量將會包含屏幕上的頂點(diǎn)的位置,main函數(shù)中的代碼便是要正確設(shè)置該變量。
這條長代碼將返回一個vec4,這意味著我們可以直接在gl_Position變量上使用其x、y、z和w屬性。
會看到平面往右上角移動,但是要注意,我們并沒有在3D空間里邊移動平面,雖然看起來像我們在Three.js里面控制位置一樣。不過,我們確實(shí)是在2D空間上移動了平面。
如何理解上面的話,想象一下你在桌上畫一幅帶透視效果的圖畫,然后你將整張圖畫往右上角移動,但是圖畫中的透視效果并未因此而改變。
可能你想要知道如果gl_Position的目標(biāo)是在2D空間上定位頂點(diǎn),那為什么還需要四個值?實(shí)際上這些坐標(biāo)并不是精確的位于二維空間,而是位于我們說的需要四個維度的剪裁空間Clip Space。
剪裁空間Clip Space是指在-1到+1范圍內(nèi)的所有3個方向(x、y和z)上的空間。這就像把所有東西都放在3D盒子里一樣,任何超出此范圍的內(nèi)容都將被“剪裁”并消失。第四個值(w)負(fù)責(zé)透視。
幸運(yùn)的是,所有這些都是自動完成的,作為初學(xué)者,我們不需要掌握所有東西,只用大概了解下就行。
但是我們到底向gl_Position發(fā)送了什么?
Position attributes
首先,我們使用下面代碼檢索頂點(diǎn)位置:
attribute vec3 position;請記住,相同的代碼適用于幾何體的每個頂點(diǎn)。屬性變量attribute是頂點(diǎn)之間唯一會發(fā)生變化的變量。相同的頂點(diǎn)著色器將應(yīng)用于每個頂點(diǎn),“position”屬性將包含該特定頂點(diǎn)的x、y和z坐標(biāo)。
然后,如上面所示的代碼,我們將這個vec3轉(zhuǎn)換成一個vec4,因?yàn)榫仃嚭蚲l_Position都需要用到這個vec4:
Matrices uniforms
每個矩陣都將變換position,直到我們得到最終的剪裁空間坐標(biāo)。
我們的代碼中有3個矩陣,因?yàn)樗鼈兊闹祵τ趲缀误w的所有頂點(diǎn)都是相同的,所以我們使用uniform檢索它們。
每個矩陣都將做一部分變換:
- modelMatrix將應(yīng)用與網(wǎng)格相關(guān)的所有變換。如果我們縮放、旋轉(zhuǎn)或移動網(wǎng)格,這些變換將包含在modelMatrix中并應(yīng)用于position。
- viewMatrix將應(yīng)用相對于相機(jī)的變換。如果我們向左旋轉(zhuǎn)相機(jī),頂點(diǎn)應(yīng)該在右邊。如果我們沿著網(wǎng)格的方向移動相機(jī),頂點(diǎn)會變大,等等。
- projectionMatrix將我們的坐標(biāo)轉(zhuǎn)換為最終的剪裁空間坐標(biāo)。
如果想了解更多關(guān)于矩陣和坐標(biāo)的信息,可以看這篇文章Coordinate-Systems
要應(yīng)用矩陣,我們需要將其相乘。如果要將mat4應(yīng)用于變量,該變量必須是vec4。
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);實(shí)際上有一個代碼更少的版本,便是將viewMatrix和modelMatrix組合成modelViewMatrix,雖然代碼更短但我們對每一步的控制權(quán)也就越弱:
uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix;attribute vec3 position;void main() {gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }為了更好理解和控制position,我們將如下面這樣寫更長些:
uniform mat4 projectionMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix;attribute vec3 position;void main() {vec4 modelPosition = modelMatrix * vec4(position, 1.0);vec4 viewPosition = viewMatrix * modelPosition;vec4 projectedPosition = projectionMatrix * viewPosition;gl_Position = projectedPosition; }這些寫法都是等效的,但是現(xiàn)在能夠通過調(diào)整modelPosition的值來移動整個模型:
void main() {vec4 modelPosition = modelMatrix * vec4(position, 1.0);// 平面向上移動modelPosition.y += 1.0;// ... } void main() {vec4 modelPosition = modelMatrix * vec4(position, 1.0);// 平面變波浪形modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;// ... }理解片元著色器
片元著色器的代碼將應(yīng)用于幾何體的每個可見片元(像素)。這就是為什么片元著色器運(yùn)行在頂點(diǎn)著色器之后。
Precision
在最頂部有這樣一條指令:
precision mediump float;這條指令可以讓我們決定浮點(diǎn)數(shù)的精度,有如下可能值
- highp:會造成性能影響,并可能在某些設(shè)備上無法工作。
- mediump:我們通常使用這個。
- lowp:會由于缺乏精確性而產(chǎn)生某些錯誤。
我們也可以設(shè)置頂點(diǎn)著色器的精度,但這不是必要的。
這一部分在使用著色器材質(zhì)ShaderMaterial會被自動處理,但現(xiàn)在我們使用的是原始著色器材質(zhì)。
gl_FragColor
gl_FragColor與gl_Position相似只不過是用于顏色。跟前者一樣是內(nèi)置變量了,只需要在main函數(shù)中重新設(shè)置值。
這是一個vec4,前三個值是紅色、綠色和藍(lán)色通道(r、g、b),第四個值是alpha(a),下面這代碼應(yīng)用后會看到紫色幾何體:
如果要降低alpha值使能夠看出差異,還需要回到RawShaderMaterial中將transparent屬性設(shè)置為true:
const material = new THREE.RawShaderMaterial({vertexShader: testVertexShader,fragmentShader: testFragmentShader,transparent: true })Attributes
我們在前面已經(jīng)有一個名為position的attribute屬性變量,現(xiàn)在可以直接將這個屬性添加到BufferGeometry中。我們將為每個頂點(diǎn)添加一個隨機(jī)值,并根據(jù)這個值在z軸上移動該頂點(diǎn)。
回到j(luò)s中,我們要在創(chuàng)建完幾何體后,再new一個32位的浮點(diǎn)數(shù)型數(shù)組Float32Array,然后為了知道幾何體中有多少個頂點(diǎn),可以通過幾何體的attributes屬性獲取:
const count = geometry.attributes.position.count const randoms = new Float32Array(count)然后用隨機(jī)值填充該數(shù)組:
for(let i = 0; i < count; i++) {randoms[i] = Math.random() }最后,我們在BufferAttribute中使用該數(shù)組,并將其添加到幾何體的屬性中:
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))setAttribute的第一個參數(shù)是我們設(shè)置的attribute屬性的名字,這也是下面在著色器中使用的名字。命名最好在前面加一個a前綴代表屬性。
BufferAttribute的第一個參數(shù)是數(shù)據(jù)數(shù)組,第二個參數(shù)是組成一個屬性的值的數(shù)量。如果我們要發(fā)送一個位置,我們會使用3,因?yàn)槲恢糜?個值(x、y和z)組成。但在這里,每個頂點(diǎn)只有1個隨機(jī)值,所以我們使用1。
現(xiàn)在,我們可以在頂點(diǎn)著色器中檢索該屬性,并使用它移動頂點(diǎn):
// ... attribute float aRandom;void main() {// ...modelPosition.z += aRandom * 0.1;// ... }最后得到一個尖銳峰巒狀平面:
Varyings
現(xiàn)在我們還想用aRandom屬性給片元著色。但可惜,我們沒法直接在片元著色器中使用attribute屬性變量。
但是,有一種方法可以將數(shù)據(jù)從頂點(diǎn)著色器發(fā)送到片元著色器,稱之為varying可變量。我們必須在倆種著色器上都這樣做。
在頂點(diǎn)著色器上,我們需要在main函數(shù)之前創(chuàng)建varying,這里將該變量命名為vRandom,用v作前綴好區(qū)分,然后在main函數(shù)中賦值:
// ...varying float vRandom;void main() {// ...vRandom = aRandom; }然后在片元著色器用一樣的方式:
precision mediump float; varying float vRandom; void main() {gl_FragColor = vec4(0.5, vRandom, 1.0, 1.0); }最后觀察到如下圖:
varying的一個有趣之處是,頂點(diǎn)之間的值是線性插值的,以此實(shí)現(xiàn)平滑漸變。如果GPU正在兩個頂點(diǎn)之間繪制一個片元,一個頂點(diǎn)的變量為1.0,另一個頂點(diǎn)的變量為0.0,那么片元值將為0.5。
最后重新回歸最初的普通平面。
Uniforms
統(tǒng)一變量uniform是將數(shù)據(jù)從JavaScript發(fā)送到著色器的一種方式。
如果我們想使用同一個著色器但是參數(shù)不一樣,以得到不同的結(jié)果,就可以使用uniform。
兩個著色器中都可以使用,其對每個頂點(diǎn)和片元的數(shù)據(jù)都是相同的。我們的代碼中已經(jīng)有了projectionMatrix、viewMatrix和modelMatrix,但我們沒有創(chuàng)建它們,是Three.js創(chuàng)建的。
下面創(chuàng)建我們自己的統(tǒng)一變量uniform。
為將統(tǒng)一變量添加到我們的material中,要使用uniforms屬性,我們要做一個波浪平面,并且能夠控制波形頻率:
const material = new THREE.RawShaderMaterial({vertexShader: testVertexShader,fragmentShader: testFragmentShader,uniforms:{frequency: { value: 10 }} })在這里我們將統(tǒng)一變量命名為frequency,雖然不是強(qiáng)制性的,但最好在前面加個u前綴以將統(tǒng)一變量和其他數(shù)據(jù)區(qū)分開來:
const material = new THREE.RawShaderMaterial({vertexShader: testVertexShader,fragmentShader: testFragmentShader,uniforms:{uFrequency: { value: 10 }} })如果你也有在看其他的教程或示例,你可能會看到它是這樣聲明uniform的:
uFrequency: { value: 10, type: 'float' }在之前我們必須為其指定類型,但現(xiàn)在已經(jīng)被棄用了。
現(xiàn)在,我們可以在著色器代碼中檢索該值,并在main函數(shù)中使用它:
uniform mat4 projectionMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix; uniform float uFrequency;attribute vec3 position;void main() {// ...modelPosition.z += sin(modelPosition.x * uFrequency) * 0.1;// ... }然后你會發(fā)現(xiàn)結(jié)果是一樣的,但現(xiàn)在我們可以在js中控制波形頻率了。
讓我們把頻率frequency改為vec2來控制水平和垂直方向的波,這里使用Three.js中一個簡單的vec2:
const material = new THREE.RawShaderMaterial({vertexShader: testVertexShader,fragmentShader: testFragmentShader,uniforms:{uFrequency: { value: new THREE.Vector2(10, 5) }} })然后回到著色器,將float改為vec2,在z軸上應(yīng)用位移:
// ... uniform vec2 uFrequency;// ...void main() {// ...modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1;modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1;// ... }
這些值現(xiàn)在由JavaScript控制,所以我們可以將它們添加到我們的Dat.GUI中:
現(xiàn)在讓我們再新加一個uniform來讓平面像在風(fēng)中飄動的旗幟。
我們將使用統(tǒng)一變量向著色器發(fā)送一個時(shí)間值,并在sin函數(shù)中使用:
記得在tick函數(shù)中更新uTime:
const tick = () => {const elapsedTime = clock.getElapsedTime()// Update materialmaterial.uniforms.uTime.value = elapsedTime// ... }更新頂點(diǎn)著色器代碼:
// ... uniform float uTime;// ...void main() {// ...modelPosition.z += sin(modelPosition.x * uFrequency.x + uTime) * 0.1;modelPosition.z += sin(modelPosition.y * uFrequency.y + uTime) * 0.1;// ... }
在使用uTime時(shí)要注意,如果我們使用原生JavaScript的Date.now(),你會發(fā)現(xiàn)不起作用,因?yàn)樗祷氐臄?shù)值對于著色器而言太過龐大了。注意,我們不能發(fā)送太小或太大的統(tǒng)一變量值。
不要忘記這仍然是一個平面,我們可以像以前一樣變換網(wǎng)格。讓我們給飛機(jī)做個旗子形狀。
我們可以在著色器中通過乘以modelPosition.y來實(shí)現(xiàn),但不要忘記,我們?nèi)匀豢梢灾苯釉诰W(wǎng)格上更改位置position、縮放scale和旋轉(zhuǎn)rotation:
const mesh = new THREE.Mesh(geometry, material) mesh.scale.y = 2 / 3 scene.add(mesh)
在片元著色器中也可以使用統(tǒng)一變量,下面使用Three.js的Color作為新的統(tǒng)一變量:
之后你會看到旗幟變?yōu)殚偕?/p>
Textures
下面將我們的旗幟貼圖紋理應(yīng)用于平面上(網(wǎng)上找一張圖片1024*1024即可):
const textureLoader = new THREE.TextureLoader() const flagTexture = textureLoader.load('/textures/flag-CCCP.jpg')然后將它傳給統(tǒng)一變量uTexture:
const material = new THREE.RawShaderMaterial({// ...uniforms:{// ...uTexture: { value: flagTexture }} })下面要將我們的旗幟紋理的顏色應(yīng)用于所有可見片元上,為此我們必須使用texture2D()方法,該方法第一個參數(shù)就是應(yīng)用的紋理,第二個參數(shù)是由在紋理上拾取的顏色的坐標(biāo)組成,我們還沒有這些坐標(biāo),而這聽起來很熟悉,我們正在尋找可以幫助我們在幾何體上投射紋理的坐標(biāo),也就是UV坐標(biāo)。
PlaneBufferGeometry會自動生成這些坐標(biāo),可以通過打印geometry.attributes.uv看到。
因?yàn)樗且粋€屬性,我們可以在頂點(diǎn)著色器中檢索它:
不過,我們需要在片元著色器中使用這些坐標(biāo)。而要將數(shù)據(jù)從頂點(diǎn)著色器發(fā)送至片元著色器,我們要創(chuàng)建一個名為vUv的變量varying,并在main函數(shù)中對其賦值:
// ... attribute vec2 uv;varying vec2 vUv;void main() {// ...vUv = uv; }現(xiàn)在,我們可以在片元著色器中檢索變量vUv傳給texture2D()方法:
precision mediump float;uniform vec3 uColor; uniform sampler2D uTexture;varying vec2 vUv;void main() {vec4 textureColor = texture2D(uTexture, vUv);gl_FragColor = textureColor; }texture2D(...)的輸出值是一個vec4因?yàn)樗瑀gb和a,即便這里沒用到a。
顏色變化
旗幟的顏色變化不是很明顯,如果有亮度的變化像陰影一般就好了,下面要用到的技術(shù)在物理上并不準(zhǔn)確,但應(yīng)該也能起到一點(diǎn)作用。
首先,在頂點(diǎn)著色器中,我們將把風(fēng)的高度存儲在一個變量elevation中,然后通過varying將之發(fā)送至片元著色器:
用這個變量來改變textureColor的r,g,b屬性:
// ... varying float vElevation;void main() {vec4 textureColor = texture2D(uTexture, vUv);textureColor.rgb *= vElevation * 2.0 + 0.5;gl_FragColor = textureColor; }著色器材質(zhì)
到目前為止,我們一直使用原始著色器材質(zhì)RawShaderMaterial,而ShaderMaterial的工作原理其實(shí)是一樣的,只不過其有內(nèi)置attributes和uniforms,精度也會自動設(shè)置。
你可以將材質(zhì)直接替換:
然后在著色器中移除下面這些uniform,attribute和precision:
- uniform mat4 projectionMatrix;
- uniform mat4 viewMatrix;
- uniform mat4 modelMatrix;
- attribute vec3 position;
- attribute vec2 uv;
- precision mediump float;
之后著色器會跟之前一樣正常運(yùn)行,因?yàn)樗鼤詣犹砑由仙厦孢@些。
調(diào)試
調(diào)試著色器很困難,我們不能像JavaScript那樣記錄數(shù)據(jù),因?yàn)槭荊PU對每個頂點(diǎn)和每個片元都執(zhí)行著色器代碼。
然而,Three.js在編譯中傳遞錯誤這方面做得很好。
查錯
如果我們忘打一個分號,Three.js會打印整個著色器代碼并提示我們在第幾行出錯,像這樣:ERROR: 0:99: 'textureColor' : syntax error
閱讀著色器
當(dāng)使用ShaderMaterial時(shí),在報(bào)錯時(shí)查看控制臺的整個著色器代碼也是一種很好的方式,可以觀察到Three.js為我們自動添加了哪些變量。
測試數(shù)值
調(diào)試數(shù)值的另一個解決方案是在gl_FragColor中使用它們。雖然并不精確,但是能看到顏色變化,有時(shí)候這也足夠了。
如果這些值在頂點(diǎn)著色器中,我們可以使用一個變量將其傳遞給片元著色器。
假如我們想看uv長啥樣,我們可以將其發(fā)送至片元著色器,然后在gl_FragColor中使用:
最后
Glslify
GLSLify是一個node module模塊,它改進(jìn)了我們對glsl文件的處理。通過glslify,我們可以像模塊一樣導(dǎo)入和導(dǎo)出glsl代碼。你可以使用glslify-loader并將之加到webpack配置中去。
網(wǎng)站
下面是學(xué)習(xí)著色器的一些網(wǎng)站和油管頻道:
- The Book of Shaders: https://thebookofshaders.com/
- ShaderToy: https://www.shadertoy.com/
- The Art of Code Youtube Channel: https://www.youtube.com/channel/UCcAlTqd9zID6aNX3TzwxJXg
- Lewis Lepton Youtube Channel: https://www.youtube.com/channel/UC8Wzk_R1GoPkPqLo-obU_kQ
筆記都是邊看視頻邊跟著敲代碼邊做筆記的,如果有哪里出錯,還望指出來,好做改進(jìn),謝謝。
總結(jié)
以上是生活随笔為你收集整理的three.js学习笔记(十四)——Shaders着色器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [转载]通过插件支持,Geronimo
- 下一篇: 03.获取网页源代码