NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模

我们已经讨论了着色器和 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 规范。


原文链接:WebGL Shaders and GLSL

BimAnt翻译整理,转载请标明出处