簡介
請先閱讀 Three.js 簡介,因為這是本文件的基礎。
我想討論的是著色器。WebGL 非常出色,正如我之前所說,Three.js (和其他程式庫) 在為您抽象化困難方面做得非常出色。但有時您會想要達到特定效果,或想深入瞭解螢幕上顯示的精彩內容,而著色器幾乎一定會是其中一部分。您可能想從上一個教學課程的基本內容,進階到更複雜的內容。
我會假設您使用的是 Three.js,因為它在著色器方面為我們做了很多工作。您將先瞭解著色器的背景資訊,然後再學習本文後續的進階步驟。
1. 兩個著色器
WebGL 不提供固定管線,也就是說,WebGL 不提供任何現成的算繪方式。不過,它提供可程式化管道,功能更強大,但也更難理解和使用。簡而言之,可程式化管道意味著程式設計師必須負責將頂點等內容算繪到螢幕上。著色器是這個管道的一部分,分為兩種類型:
- 頂點著色器
- 片段著色器
相信您也同意,這兩者本身完全沒有意義。您應該知道,這兩者都完全在顯示卡的 GPU 上執行。這表示我們希望盡可能將工作卸載到這些裝置,讓 CPU 執行其他工作。現代 GPU 已針對著色器所需的功能進行高度最佳化,因此非常適合使用。
2. 頂點著色器
以標準原始形狀為例,例如球體。它是由頂點組成的,對吧?頂點著色器會依序接收每個頂點,並可對其進行處理。頂點著色器會決定如何處理每個頂點,但它有一項責任:必須在某個時間點設定名為 gl_Position 的 4D 浮點向量,也就是頂點在螢幕上的最終位置。這本身就是一個相當有趣的過程,因為我們實際上是在討論如何將 3D 位置 (具有 x、y、z 的頂點) 投影到 2D 螢幕上。幸好,如果我們使用 Three.js 之類的工具,就能以簡短的方式設定 gl_Position,而不會造成過多負擔。
3. 片段著色器
我們已經有了物件和頂點,並將它們投影到 2D 螢幕上,但要使用什麼顏色呢?紋理和光線呢?這正是片段著色器的用途。與頂點著色器非常相似,片段著色器也只有一項必須執行的工作:必須設定或捨棄 gl_FragColor 變數,這是另一個 4D 浮點向量,也就是片段的最終顏色。
但什麼是片段?想像三個頂點構成一個三角形。三角形內的每個像素都必須繪製出來。片段是由這三個頂點提供的資料,用於繪製該三角形中的每個像素。因此,片段會從其組成頂點接收內插值。如果一個頂點是紅色,相鄰頂點是藍色,則顏色值會從紅色、紫色,插補到藍色。
4. 著色器變數
您可以宣告三種變數:Uniforms、Attributes 和 Varyings。這些符號與其他常見語言不符,可能會造成混淆。您可以這樣看待這兩者:
Uniform 會傳送至頂點著色器和片段著色器兩者,且包含在整個影格算繪過程中保持不變的值。例如燈光的位置。
屬性是套用至個別頂點的值。屬性僅適用於頂點著色器。例如每個頂點都有不同的顏色。屬性與頂點具有一對一關係。
Varying 是在頂點著色器中宣告的變數,我們想與片段著色器共用。為此,我們必須在頂點著色器和片段著色器中,宣告相同類型和名稱的變化變數。這項功能的經典用途是頂點的法線,因為這可用於光線計算。
稍後,我們會使用這三種類型,讓您瞭解實際應用方式。
不,請看看您可以建立的最簡單著色器之一。
5. Bonjourno World
以下是頂點著色器的 Hello World:
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
片段著色器也是如此:
/**
* Set the color to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
不過,這不會太複雜,對吧?
在頂點著色器中,Three.js 會傳送幾個統一變數。這兩個 uniform 是 4D 矩陣,稱為模型檢視矩陣和投影矩陣。您不需要確切瞭解這些函式的運作方式,但如果可以,最好還是瞭解一下。簡單來說,就是將頂點的 3D 位置實際投影到螢幕上的最終 2D 位置。
我實際上已將它們從範例程式碼片段中排除,因為 Three.js 會將它們新增至著色器程式碼本身的頂端,因此您不必擔心這樣做。事實上,它還會加入更多資訊,例如光線資料、頂點顏色和頂點法線。如果不使用 Three.js,就必須自行建立及設定所有 uniform 和屬性。真實故事。
6. 使用 MeshShaderMaterial
著色器設定完成後,該如何與 Three.js 搭配使用?
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
接著,Three.js 會編譯並執行附加至網格的著色器,而網格會套用該材質。這真的非常簡單。可能確實如此,但我們討論的是在瀏覽器中執行 3D,因此我認為您預期會具有一定的複雜度。
我們可以在 MeshShaderMaterial 中加入另外兩個屬性:uniforms 和 attributes。兩者都可以接受向量、整數或浮點數,但如前所述,uniforms 在整個影格或所有頂點中都相同,因此通常是單一值。不過,屬性是每個頂點的變數,因此應為陣列。屬性陣列中的值數量與網格中的頂點數量應為一對一關係。
7. 後續步驟
現在,我們要花點時間加入動畫迴圈、頂點屬性和統一變數。我們也會加入變化變數,讓頂點著色器能將部分資料傳送至片段著色器。最終結果是,粉紅色的球體會從上方和側邊發光,並會脈動。這有點像迷幻之旅,但希望這能幫助您充分瞭解這三種變數類型,以及它們彼此之間和基礎幾何圖形的關係。
8. 假光
現在,請更新顏色,讓物件不再是平面彩色物件。我們可以看看 Three.js 如何處理光線,但相信您也知道,這比我們需要的更複雜,所以我們會用假的方式。建議您完整瀏覽 Three.js 的精彩著色器,以及 Chris Milk 和 Google 合作的精彩 WebGL 專案 Rome 的著色器。回到著色器。我們將更新頂點著色器,為片段著色器提供每個頂點法線。我們透過以下方式達成這個目標:
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
在片段著色器中,我們將設定相同的變數名稱,然後使用頂點法線與向量的點積,代表從球體上方和右側照射的光線。最終結果會產生類似 3D 套件中定向光的效果。
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
點積之所以有效,是因為給定兩個向量後,它會產生一個數字,告訴您這兩個向量有多「相似」。如果正規化向量指向完全相同的方向,則會得到 1。如果指向相反方向,則會得到 -1。我們要做的是將這個數字套用到光線。因此,右上方的頂點值會接近或等於 1 (完全照亮),而側邊的頂點值會接近 0,背面則為 -1。我們將負值限制為 0,但當您輸入數字時,最終會得到我們看到的基本光照。
接下來該怎麼做?如果能嘗試調整一些頂點位置,那就太好了。
9. 屬性
現在,請使用屬性為每個頂點附加隨機數字。我們會使用這個數字,沿著頂點的法線向外推。最終結果會是某種奇怪的尖刺球,每次重新整理頁面時都會改變。雖然還不會有動畫效果 (這是下一個步驟),但重新整理幾次頁面後,就會發現圖片是隨機顯示。
首先,請在頂點著色器中加入屬性:
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
看起來如何?
其實沒什麼差別!這是因為 MeshShaderMaterial 中尚未設定該屬性,因此著色器會改用零值。目前就像是預留位置。稍後我們會在 JavaScript 中將屬性新增至 MeshShaderMaterial,Three.js 會自動將兩者連結在一起。
另外請注意,我必須將更新後的位置指派給「新」vec3 變數,因為原始屬性 (與所有屬性一樣) 是唯讀的。
10. 更新 MeshShaderMaterial
接著,我們直接更新 MeshShaderMaterial,加入位移所需的屬性。提醒:屬性是每個頂點的值,因此球體中的每個頂點都需要一個值。如下所示:
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
現在我們看到的是變形的球體,但很酷的是,所有位移都發生在 GPU 上。
11. 生成動畫
我們應該讓這個動起來。我們如何做到?我們需要準備兩項:
- 一個統一變數,用於以動畫呈現每個影格應套用的位移量。我們可以使用正弦或餘弦,因為它們的範圍是 -1 到 1
- JS 中的動畫迴圈
我們將在 MeshShaderMaterial 和頂點著色器中加入 uniform。首先是頂點著色器:
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
接著更新 MeshShaderMaterial:
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
著色器已完成。但現在我們似乎退了一步。這主要是因為振幅值為 0,而我們將其與位移相乘,因此看不到任何變化。我們也尚未設定動畫迴圈,因此不會看到 0 變成其他值。
在 JavaScript 中,我們現在需要將轉譯呼叫包裝成函式,然後使用 requestAnimationFrame 呼叫。我們也需要在該處更新統一變數的值。
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
結論
大功告成!現在可以看到,動畫會以奇怪 (且有點迷幻) 的脈動方式呈現。
著色器還有許多值得探討的主題,希望這篇簡介對您有所幫助。現在,您應該能理解著色器,並有信心能自行建立一些令人驚豔的著色器。