著色器簡介

Paul Lewis

簡介

我們先前提過 Three.js 簡介,您可能會想這麼做,因為在本文中,我會以它做為基礎。

我想瞭解著色器。WebGL 非常出色,而我正如我在 Three.js (和其他程式庫) 之前所說的,其實可以順利解開這些困難。但有時候,您可能會想達到特定效果,或是想深入瞭解這些令人驚豔的畫面如何顯示在螢幕上,而著色器幾乎肯定是該方程式的其中一環。另外,如果你和我一樣,可能會想從上次教學課程的基本概念開始 到有點麻煩的是我會根據您使用 Three.js 的原則,因為在取得著色器的情況下,執行許多快速鍵。我也會提前說明,我會先說明著色器的背景資訊,本教學課程的後面部分就是深入介紹較進階的地區。這是因為著色器一開始看似不尋常,請稍微解釋一下。

1. 兩款著色器

WebGL 不提供固定管道使用,這只是用來簡單表示不開放直接轉譯資料的方法。但「能夠」提供的是程式化管道,這項功能比較強大,也較難以理解及使用。簡而言之, Programmable Pipeline 是指程式設計師負責讓畫面顯示端點等。這個管道屬於這個管道的一部分,並且有兩種類型:

  1. Vertex 著色器
  2. 片段著色器

無論如何,我相信你都同意,絕對沒有意思。您應該知道,這些元件都是完全在圖形卡 GPU 上執行。這表示我們希望盡可能卸載這些產品,讓 CPU 執行其他工作。新型 GPU 針對著色器所需的功能進行了大幅最佳化調整,因此非常適合使用。

2. 頂點著色器

採用標準的原始形狀,例如球體。它是由頂點組成,對吧? 這些頂點會輪流提供一個頂點著色器,並且可以圍繞它們。 頂點著色器實際會隨其作用而決定,但有一項責任是:它必須在某個點設定 gl_Position,也就是 4D 浮點向量,也就是螢幕上頂點的最終位置。這個過程其實相當有趣,因為我們其實是說將 3D 位置 (帶有 x、y、z 的頂點) 插入或投影至 2D 螢幕。很高興得知我們使用 Three.js 之類的服務,可以快速瞭解如何輕鬆設定 gl_Position,不會太過於繁重。

3. 片段著色器

我們有物件和頂點,也已經投影到 2D 螢幕 但顏色又是什麼呢?拍攝紋理和光線時,該怎麼辦?這就是片段著色器的用途。與頂點著色器非常類似,片段著色器也只有一項必要工作:必須設定或捨棄 gl_FragColor 變數,也就是另一個 4D 浮點向量,也就是片段的最終色彩。但什麼是片段?想出三個構成三角形的頂點,而該三角形中的每個像素都要繪製出來。片段是指這三個端點所提供的資料,以便繪製該三角形中的每個像素。 因此,片段會從其組成頂點接收內插值。如果某個頂點為紅色,且鄰點為藍色,則您會看到顏色值從紅色到紫色之間插入至藍色。

4. 著色器變數

介紹變數時,您可以建立三種宣告:制服屬性變化。第一次聽這三者時,我感到非常困惑,因為它們是從未合作過的但您可以按照下列方式思考:

  1. 統一格式會傳送給「兩個」頂點著色器和片段著色器,其中包含的值在整個轉譯影格中都保持不變。而燈具的位置就是一個很好的例子。

  2. 「屬性」是套用至個別端點的值。屬性「只」可供頂點著色器使用。比如每個頂點都有不同的顏色屬性與頂點具有一對一的關係。

  3. 「改變」是我們在頂點著色器中宣告的變數,而我們要與片段著色器共用。為此,我們確保在頂點著色器和片段著色器中,宣告相同類型和名稱的不同變數。這種現象的經典用法是頂點的正常現象,因為可用於光源計算。

稍後,我們會用這三種類型,讓您大致瞭解如何實際應用這些類型。

現在,您已瞭解頂點著色器和片段著色器,以及它們處理的變數類型,現在不妨看看我們能建立的最簡單著色器。

5. Bonjourno 世界

接著是頂點著色器的 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 colour 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. 使用網格 ShaderMaterial

好,我們已設定著色器,但要如何搭配 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 的情況,因此我們知道您預期會有一定程度的複雜性。

我們可以在網格 ShaderMaterial 中新增兩個屬性:制服和屬性。這兩種物件都可以採用向量、整數或浮點值,但正如我先前所述,在整個影格中都相同 (亦即所有頂點都相同),因此它們最後會是單一值。但屬性是每個頂點的變數,因此應該是陣列。屬性陣列中的值數量與網格中的頂點數量之間應為一對一關係。

7. 後續步驟

接下來要花一點時間加入動畫迴圈、頂點屬性和統一屬性。我們也會加入不同的變數,讓頂點著色器可以傳送一些資料至片段著色器。我們最終的結果是,粉紅色的球體從上方和側邊各發出光照,就會閃爍。這屬於行程規劃,但希望能幫助您清楚瞭解三種變數類型,以及變數彼此間和彼此的關係。

8. 假光

請更新顏色,使其不是平面的彩色物體。我們可以看看 Three.js 如何處理照明,但相信大家都知道 這會比我們目前的複雜情況更複雜,因此我們還是要假裝。建議您全面查看 Three.js 中的優質著色器,以及 Chris Milk 和 Google - Rome 針對近期令人驚豔的 WebGL 專案所列出的好色調器。返回著色器。我們會更新 Vertex 著色器,為 Fragment 著色器提供每個頂點標準。具體做法包括:

// 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 colour
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);
}

實際外觀

不太有差異!這是因為網格 ShaderMaterial 尚未設定這項屬性,因此著色器實際上會改用零值。這就像現在是預留位置接著將屬性新增至 JavaScript 中的 MeshShaderMaterial,而 Three.js 則會自動將這兩項屬性完成連結。

另請注意,由於原始屬性 (例如所有屬性) 都是唯讀屬性,所以我必須將更新的位置指派給「新的」 vec3 變數。

10. 更新網格 ShaderMaterial

現在就讓我們使用網格所需的屬性 更新我們的網格 ShaderMaterial,提醒:屬性是每個頂點的值,因此球體中每個頂點都需要一個值。如下所示:

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 中循環播放動畫

我們要為網格 ShaderMaterial 和 Vertex 著色器新增統一格式首先是 Vertex 著色器:

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

接下來,我們要更新網格 ShaderMaterial:

// 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 的倍數,所以顯然沒有改變。我們也沒有設定動畫迴圈,因此才會看到 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);

12. 結論

大功告成!現在您可以看到圖片以奇怪的 (而且略為三) 閃爍的方式呈現動畫效果。

我們還有其他主題可以介紹著色器,希望本課程對您有所幫助。現在您應該可以瞭解著色器,並能有自信地自行創作令人讚嘆的著色器!