WebGL著色器快速入門

新缸中之腦 發佈 2024-03-01T21:01:35.358638+00:00

我們已經討論了著色器和 GLSL,但還沒有真正給它們任何具體細節。 我想我希望通過示例可以清楚地說明這一點,但為了以防萬一,讓我們嘗試使其更清楚。推薦:用 NSDT場景設計器 快速搭建3D場景。正如其工作原理中所述,WebGL 每次繪製內容時都需要 2 個著色器。

我們已經討論了著色器和 GLSL,但還沒有真正給它們任何具體細節。 我想我希望通過示例可以清楚地說明這一點,但為了以防萬一,讓我們嘗試使其更清楚。

推薦:用 NSDT場景設計器 快速搭建3D場景。

正如其工作原理中所述,WebGL 每次繪製內容時都需要 2 個著色器。 頂點著色器和片段著色器。 每個著色器都是一個函數。 頂點著色器和片段著色器連結在一起形成著色器程序(或只是程序)。 一個典型的 WebGL 應用程式會有很多著色器程序。

1、頂點著色器

頂點著色器的工作是生成剪輯空間坐標。 它總是採取形式:

void main() {
   gl_Position = doMathToMakeClipspaceCoordinates
}

每個頂點會調用著色器一次。 每次調用它時,你都需要將特殊的全局變量 gl_Position 設置為一些裁剪空間坐標。

頂點著色器需要數據。 他們可以通過 3 種方式獲取數據。

  • 屬性(從緩衝區中提取的數據)
  • uniforms(對於單個繪製調用的所有頂點保持相同的值)
  • 紋理(來自像素/紋素的數據)

1.1 屬性

最常見的方法是通過緩衝區和屬性。 如下代碼創建緩衝區,

var buf = gl.createBuffer();

將數據放入這些緩衝區:

gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);

然後,給定一個著色器程序,讓你在初始化時查找其屬性的位置

var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");

並在渲染時告訴 WebGL 如何將數據從這些緩衝區中拉出並放入屬性中

// turn on getting data out of a buffer for this attribute
gl.enableVertexAttribArray(positionLoc);
 
var numComponents = 3;  // (x, y, z)
var type = gl.FLOAT;    // 32bit floating point values
var normalize = false;  // leave the values as they are
var offset = 0;         // start at the beginning of the buffer
var stride = 0;         // how many bytes to move to the next vertex
                        // 0 = use the correct stride for type and numComponents
 
gl.vertexAttribPointer(positionLoc, numComponents, type, normalize, stride, offset);

在 WebGL 基礎知識中,我們展示了我們不能在著色器中進行任何數學運算,而只能直接傳遞數據。

attribute vec4 a_position;
 
void main() {
   gl_Position = a_position;
}

如果我們將剪輯空間頂點放入我們的緩衝區,它就會起作用。

屬性可以使用 float、vec2、vec3、vec4、mat2、mat3 和 mat4 作為類型。

1.2 Uniforms

對於著色器,unforms是傳遞給著色器的值,這些值對於繪製調用中的所有頂點都保持不變。 作為一個非常簡單的例子,我們可以向上面的頂點著色器添加一個偏移量:

attribute vec4 a_position;
uniform vec4 u_offset;
 
void main() {
   gl_Position = a_position + u_offset;
}

現在我們可以將每個頂點偏移一定量。 首先,我們會在初始化時查找uniforms的位置:

var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");

然後在繪圖之前我們會設置uniforms:

gl.uniform4fv(offsetLoc, [1, 0, 0, 0]);  // offset it to the right half the screen

請注意,uniforms屬於各個著色器程序。 如果有多個具有相同名稱uniforms的著色器程序,兩個uniforms將有自己的位置並擁有自己的值。 調用 gl.uniform ?時,你只是為當前程序設置uniforms。 當前程序是你傳遞給 gl.useProgram 的最後一個程序。

uniforms可以有很多種。 對於每種類型,必須調用相應的函數來設置它。

gl.uniform1f (floatUniformLoc, v);                 // for float
gl.uniform1fv(floatUniformLoc, [v]);               // for float or float array
gl.uniform2f (vec2UniformLoc,  v0, v1);            // for vec2
gl.uniform2fv(vec2UniformLoc,  [v0, v1]);          // for vec2 or vec2 array
gl.uniform3f (vec3UniformLoc,  v0, v1, v2);        // for vec3
gl.uniform3fv(vec3UniformLoc,  [v0, v1, v2]);      // for vec3 or vec3 array
gl.uniform4f (vec4UniformLoc,  v0, v1, v2, v4);    // for vec4
gl.uniform4fv(vec4UniformLoc,  [v0, v1, v2, v4]);  // for vec4 or vec4 array
 
gl.uniformMatrix2fv(mat2UniformLoc, false, [  4x element array ])  // for mat2 or mat2 array
gl.uniformMatrix3fv(mat3UniformLoc, false, [  9x element array ])  // for mat3 or mat3 array
gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ])  // for mat4 or mat4 array
 
gl.uniform1i (intUniformLoc,   v);                 // for int
gl.uniform1iv(intUniformLoc, [v]);                 // for int or int array
gl.uniform2i (ivec2UniformLoc, v0, v1);            // for ivec2
gl.uniform2iv(ivec2UniformLoc, [v0, v1]);          // for ivec2 or ivec2 array
gl.uniform3i (ivec3UniformLoc, v0, v1, v2);        // for ivec3
gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]);      // for ivec3 or ivec3 array
gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4);    // for ivec4
gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]);  // for ivec4 or ivec4 array
 
gl.uniform1i (sampler2DUniformLoc,   v);           // for sampler2D (textures)
gl.uniform1iv(sampler2DUniformLoc, [v]);           // for sampler2D or sampler2D array
 
gl.uniform1i (samplerCubeUniformLoc,   v);         // for samplerCube (textures)
gl.uniform1iv(samplerCubeUniformLoc, [v]);         // for samplerCube or samplerCube array

還有類型 bool、bvec2、bvec3 和 bvec4。 他們使用 gl.uniform?f? 或 gl.uniform?。

請注意,對於數組,你可以一次設置數組的所有uniform。 例如:

// in shader
uniform vec2 u_someVec2[3];
 
// in JavaScript at init time
var someVec2Loc = gl.getUniformLocation(someProgram, "u_someVec2");
 
// at render time
gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]);  // set the entire array of u_someVec2

但是如果你想設置數組的單個元素,你必須單獨查找每個元素的位置。

// in JavaScript at init time
var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]");
var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]");
var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]");
 
// at render time
gl.uniform2fv(someVec2Element0Loc, [1, 2]);  // set element 0
gl.uniform2fv(someVec2Element1Loc, [3, 4]);  // set element 1
gl.uniform2fv(someVec2Element2Loc, [5, 6]);  // set element 2

同樣,如果你創建一個結構:

Struct SomeStruct {
  bool active;
  vec2 someVec2;
};
uniform SomeStruct u_someThing;

你必須單獨查找每個欄位:

var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");
var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");

2、片段著色器

片段著色器的工作是為當前被光柵化的像素提供顏色。 它總是採取形式

precision mediump float;
 
void main() {
   gl_FragColor = doMathToMakeAColor;
}

片段著色器每個像素調用一次。 每次調用它時,都需要將特殊的全局變量 gl_FragColor 設置為某種顏色。

片段著色器需要數據。 他們可以通過 3 種方式獲取數據

  • uniforms(對於單個繪製調用的每個像素保持相同的值)
  • 紋理(來自像素/紋素的數據)
  • Varyings(從頂點著色器傳遞並插值的數據)

2.1 片段著色器中的紋理

從著色器中的紋理獲取值,我們創建一個 sampler2D uniform 並使用 GLSL 函數 texture2D 從中提取值。

precision mediump float;
 
uniform sampler2D u_texture;
 
void main() {
   vec2 texcoord = vec2(0.5, 0.5);  // get a value from the middle of the texture
   gl_FragColor = texture2D(u_texture, texcoord);
}

紋理中的數據取決於許多設置。 至少我們需要創建數據並將其放入紋理中,例如:

var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
var level = 0;
var width = 2;
var height = 1;
var data = new Uint8Array([
   255, 0, 0, 255,   // a red pixel
   0, 255, 0, 255,   // a green pixel
]);
gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

並設置紋理的過濾:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

在初始化時查找著色器程序中的uniforms位置:

var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");

在渲染時將紋理綁定到紋理單元:

var unit = 5;  // Pick some texture unit
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);

並告訴著色器你將紋理綁定到哪個單元:

gl.uniform1i(someSamplerLoc, unit);

2.2 Varying

varying 是一種將值從頂點著色器傳遞到片段著色器的方法,我們在它的工作原理中介紹了這一點。

要使用 varying,我們需要在頂點和片段著色器中聲明匹配的 varying。 我們在頂點著色器中為每個頂點設置一些值。 當 WebGL 繪製像素時,它會在這些值之間進行插值,並將它們傳遞給片段著色器中相應的變量

頂點著色器

attribute vec4 a_position;
 
uniform vec4 u_offset;
 
varying vec4 v_positionWithOffset;
 
void main() {
  gl_Position = a_position + u_offset;
  v_positionWithOffset = a_position + u_offset;
}       

片段著色器

precision mediump float;
 
varying vec4 v_positionWithOffset;
 
void main() {
  // convert from clip space (-1 <-> +1) to color space (0 -> 1).
  vec4 color = v_positionWithOffset * 0.5 + 0.5;
  gl_FragColor = color;
}

上面的例子基本上沒有實際意義。 直接將剪輯空間值複製到片段著色器並將它們用作顏色。 儘管如此,它還是會起作用並產生顏色。

3、GLSL

GLSL 代表圖形庫著色器語言。 它是用語言著色器編寫的。它具有一些在 JavaScript 中肯定不常見的特殊半獨特功能。 它旨在執行計算光柵化圖形通常需要的數學運算。 因此,例如它內置了 vec2、vec3 和 vec4 等類型,分別表示 2 個值、3 個值和 4 個值。 同樣,它有 mat2、mat3 和 mat4,分別代表 2x2、3x3 和 4x4 矩陣。 你可以執行諸如將 vec 乘以標量之類的操作。

vec4 a = vec4(1, 2, 3, 4);
vec4 b = a * 2.0;
// b is now vec4(2, 4, 6, 8);

同樣它可以進行矩陣乘法和向量到矩陣的乘法:

mat4 a = ???
mat4 b = ???
mat4 c = a * b;
 
vec4 v = ???
vec4 y = c * v;

它還具有用於 vec 部分的各種選擇器。 對於 vec4

vec4 v;
  • v.x 與 v.s 和 v.r 以及 v[0] 相同。
  • v.y 與 v.t 和 v.g 以及 v[1] 相同。
  • v.z 與 v.p、v.b 和 v[2] 相同。
  • v.w 與 v.q 和 v.a 以及 v[3] 相同。

它能夠調配 vec 組件,這意味著您可以交換或重複組件。

v.yyyy

與下面一樣:

vec4(v.y, v.y, v.y, v.y)

類似的,

v.bgra

與下面一樣:

vec4(v.b, v.g, v.r, v.a)

在構建 vec 或 mat 時,可以一次提供多個部件。 例如:

vec4(v.rgb, 1)

與下面一樣:

vec4(v.r, v.g, v.b, 1)

你可能會注意到的一件事是 GLSL 的類型非常嚴格:

float f = 1;  // ERROR 1 is an int. You can't assign an int to a float

正確的方法是其中之一:

float f = 1.0;      // use float
float f = float(1)  // cast the integer to a float

上面的 vec4(v.rgb, 1) 示例並沒有抱怨 1,因為 vec4 就像 float(1) 一樣將內容投射到裡面。

GLSL 有一堆內置函數。 他們中的許多人同時在多個組件上運行。 例如:

T sin(T angle)

表示 T 可以是 float、vec2、vec3 或 vec4。 如果你傳入 vec4,你會得到 vec4,它是每個分量的正弦值。 換句話說,如果 v 是一個 vec4 那麼

vec4 s = sin(v);

等價於:

vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));

有時一個參數是浮點數,其餘參數是 T。這意味著浮點數將應用於所有組件。 例如,如果 v1 和 v2 是 vec4 而 f 是一個浮點數,那麼

vec4 m = mix(v1, v2, f);

等價於:

vec4 m = vec4(
  mix(v1.x, v2.x, f),
  mix(v1.y, v2.y, f),
  mix(v1.z, v2.z, f),
  mix(v1.w, v2.w, f));

可以在 WebGL 參考文檔看到所有 GLSL 函數的列表。 如果你喜歡非常枯燥和冗長的東西,你可以試試 GLSL 規範。


原文連結:http://www.bimant.com/blog/webgl-shaders/

關鍵字: