簡介
我先前曾介紹 Three.js。如果您尚未閱讀該文章,建議您先行閱讀,因為我會在本文中以此為基礎進行說明。
我想討論的是著色器。WebGL 非常出色,而且如同我先前所述,Three.js (以及其他程式庫) 可為您抽象化難題,但您有時候會希望達到特定效果,或者想要更深入瞭解這些令人驚奇的畫面出現在螢幕上,而著色器幾乎可以成為這個方程式的一部分。此外,如果您和我一樣,可能會想從上一個教學課程中的基本內容,轉而學習一些較複雜的內容。我會以您使用 Three.js 為前提,因為在啟動著色器方面,Three.js 會為我們執行許多繁重的工作。我會一併說明,在開始時,我會說明著色器的內容,而本教學課程的後半部會進入較進階的領域。這是因為著色器乍看之下很不尋常,需要稍微解釋一下。
1. 我們的兩個著色器
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」。當我第一次聽到這三個詞時,我感到非常困惑,因為它們與我曾經使用過的任何東西都不相符。但您可以這麼想:
Uniforms 會傳送至頂點著色器和片段著色器,並包含在整個轉譯影格中保持不變的值。燈具位置就是一個很好的例子。
「屬性」是指套用至個別端點的值。屬性「僅」適用於頂點著色器。例如,每個頂點都有不同的顏色。屬性與頂點之間是一對一關係。
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 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 會將這些屬性新增至著色器程式碼本身的頂端,因此您不必擔心要執行這項操作。事實證明,Truth 實際上新增了更多項目,例如光資料、頂點色彩和頂點法線。如果您在沒有 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 新增兩個屬性:制服和屬性。兩者都可以採用向量、整數或浮點數,但如先前所說,整個影格的製服都相同 (即所有頂點),因此它們通常都是單一值。不過,屬性是每個頂點的變數,因此應為陣列。屬性陣列中的值數量與網格中的頂點數量應一對一。
7. 後續步驟
接下來,我們將花一點時間新增動畫迴圈、頂點屬性和統一函式。我們也會新增變化變數,讓頂點著色器可將部分資料傳送至片段著色器。最終結果是,原本粉紅色的球體會從上方和側面發光,並且會閃爍。這有點奇怪,但希望能讓您充分瞭解這三種變數類型,以及它們彼此之間和基礎幾何圖形的關係。
8. 假燈
讓我們更新顏色,讓它不是平面彩色物件。我們可以看看 Three.js 如何處理照明,但我相信您會發現,這比我們目前需要的更複雜,因此我們會假裝處理。建議您一併查看 Three.js 中出色的著色器,以及 Chris Milk 和 Google Rome 近期製作出色 WebGL 專案的這些著色器。回到著色器。我們會更新 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 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);
}
外觀如何?
其實沒有太大差異!這是因為屬性並未在 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 和 Vertex Shader 中加入均勻變數。首先是 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,且我們將其乘上位移值後,並未看到任何變化。我們也沒有設定動畫迴圈,所以永遠不會發生任何其他變更。
在 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. 結論
大功告成!您現在可以看到動畫以奇怪 (且略帶迷幻) 的脈動方式播放。
我們可以討論許多著色器主題,但希望這篇文章的介紹對您有所幫助。現在您應該能夠在看到著色器時瞭解其內容,並自信地建立一些精彩的著色器!