WebGL着色器入门
我们已经讨论了着色器和 GLSL,但还没有真正给它们任何具体细节。 我想我希望通过示例可以清楚地说明这一点,但为了以防万一,让我们尝试使其更清楚。
正如其工作原理中所述,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 规范。
BimAnt翻译整理,转载请标明出处