著色器簡介

Paul Lewis

簡介

請先閱讀 Three.js 簡介,因為這是本文的基礎。

我想討論的是著色器。WebGL 非常出色,而且如我先前所說,Three.js (和其他程式庫) 都能妥善抽象化,為您解決困難。但有時您會想達成特定效果,或是想深入瞭解螢幕上顯示的精彩內容,而著色器幾乎肯定會是其中的一部分。您可能想從上一個教學課程的基本內容,進階到稍微複雜的主題。

我會以您使用 Three.js 為前提,因為就啟動著色器而言,Three.js 為我們做了許多工作。本文會先說明著色器的背景資訊,稍後再介紹進階步驟。

1. 我們的兩個著色器

WebGL 不提供使用固定管線,這表示您無法直接算繪內容。但它提供的是可程式化管道,功能更強大,但理解和使用難度也更高。簡而言之,可程式化管道是指程式設計師負責將頂點等項目算繪到畫面上。著色器是這個管道的一部分,分為兩種:

  1. 頂點著色器
  2. 片段著色器

相信您也同意,這兩者本身完全沒有意義。您只需要知道這兩者完全在顯示卡的 GPU 上執行。這表示我們希望盡可能將所有工作卸載至這些裝置,讓 CPU 處理其他工作。現代 GPU 經過大幅最佳化,可支援著色器所需的函式,因此非常適合使用。

2. 頂點著色器

以標準原始形狀 (例如球體) 為例。它是由頂點組成的,對吧? 頂點著色器會依序取得每個頂點,並可隨意處理這些頂點。 實際要對每個頂點執行哪些操作,取決於頂點著色器,但頂點著色器有一項責任:必須在某個時間點設定名為 gl_Position 的 4D 浮點向量,這是頂點在螢幕上的最終位置。這本身就是相當有趣的程序,因為我們實際上是在談論如何將 3D 位置 (具有 x、y、z 的頂點) 放到或投影到 2D 螢幕上。幸運的是,如果我們使用 Three.js 等工具,就能以簡短的方式設定 gl_Position,而不會造成過多負擔。

3. 片段著色器

我們已經取得物件及其頂點,並將這些頂點投影到 2D 螢幕,但使用的顏色呢?紋理和光線呢?這正是片段著色器存在的原因。與頂點著色器非常相似,片段著色器也只有一項必做的工作:必須設定或捨棄 gl_FragColor 變數,這是另一個 4D 浮點向量,也是片段的最終顏色。

但什麼是片段?請想出構成三角形的三個頂點。三角形內的每個像素都必須繪製出來。片段是這三個頂點提供的資料,用於繪製該三角形中的每個像素。因此,片段會從構成頂點接收內插值。如果一個頂點是紅色,相鄰頂點是藍色,我們會看到顏色值從紅色經過紫色插補到藍色。

4. 著色器變數

您可以進行三種變數宣告:UniformsAttributesVaryings。這些語言可能令人困惑,因為與其他常見語言不符。你可以這樣理解:

  1. 制服會傳送至頂點著色器和片段著色器,並包含在整個算繪影格中保持不變的值。例如燈具的位置。

  2. 屬性是套用至個別頂點的值。屬性只能用於頂點著色器。例如每個頂點都有不同的顏色。屬性與頂點具有一對一關係。

  3. 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 中,我們會收到幾個制服。這兩個制服是 4D 矩陣,分別稱為「模型檢視矩陣」和「投影矩陣」。您不必瞭解這些函式的確切運作方式,但如果可以,最好還是瞭解函式如何執行作業。簡單來說,這些矩陣會將頂點的 3D 位置實際投影到畫面上的最終 2D 位置。

我實際上已將這些項目從範例程式碼片段中排除,因為 Three.js 會將這些項目加到著色器程式碼頂端,因此您不必擔心要這麼做。事實上,它新增的內容遠不止於此,例如光線資料、頂點顏色和頂點法線。如果您未使用 Three.js 執行這項操作,就必須自行建立及設定所有這些制服和屬性。真人真事。

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。兩者都可以採用向量、整數或浮點數,但如先前所述,整個影格或所有頂點的制服都相同,因此通常是單一值。不過,屬性是每個頂點的變數,因此預期會是陣列。屬性陣列中的值數量,應與網格中的頂點數量一一對應。

7. 後續步驟

現在我們要花一點時間加入動畫迴圈、頂點屬性和統一值。我們也會加入變數,讓頂點著色器可以將一些資料傳送至片段著色器。最終結果是粉紅色球體會從上方和側面發光,並呈現脈動效果。這有點迷幻,但希望有助於您瞭解三種變數類型,以及這些變數類型與彼此和基礎幾何之間的關係。

8. 假光

現在請更新著色,讓物體不再是平面彩色物體。我們可以看看 Three.js 如何處理照明,但相信您也知道,這比我們需要的複雜,因此我們要模擬照明。您應該徹底瀏覽 Three.js 的精彩著色器,以及 Chris Milk 和 Google 令人驚豔的 WebGL 專案 Rome 中的著色器。回到著色器。我們會更新 Vertex Shader,為 Fragment Shader 提供每個頂點法線。我們使用以下不同方式:

// 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 到 1,因此可用於此用途
  2. JS 中的動畫迴圈

我們將在 MeshShaderMaterial 和 Vertex Shader 中加入統一。首先是頂點著色器:

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);

結論

這樣就完成了!您現在可以看到動畫以奇怪 (且有點迷幻) 的脈動方式呈現。

著色器這個主題還有許多內容可以探討,希望這份簡介對您有幫助。現在您應該能看懂著色器,並有信心自行建立一些精彩的著色器。