Skip to content

WebGL - 着色器

Shaders(着色器)是用类似 C 语言的 GLSL(OpenGL Shading Language)编写的小程序,它们直接在 Graphics Processing Unit (GPU,图形处理器) 上执行。它们是 WebGL 的基础,定义了顶点的定位方式和像素(在图形管线中称为 fragment,片元)的上色方式。

WebGL 1.0 使用基于 OpenGL ES 2.0 着色语言的 GLSL ES 1.00。WebGL 2.0 使用基于 OpenGL ES 3.0 的、功能更丰富的 GLSL ES 3.00。本教程主要关注与 WebGL 1.0 相关的 GLSL ES 1.00 概念。

GLSL 有自己的数据类型、限定符(qualifiers)、内置变量和专门为图形编程定制的函数。

WebGL 着色器中常用的数据类型:

序号类型描述
1void表示无值,通常用于不返回任何内容的函数返回类型。
2bool表示布尔值(true 或 false)。
3int表示有符号整数。
4float表示单精度浮点数。
5vec2, vec3, vec4表示一个 2、3 或 4 分量的浮点向量(例如,用于位置、颜色、纹理坐标)。
6bvec2, bvec3, bvec4表示一个 2、3 或 4 分量的布尔向量。
7ivec2, ivec3, ivec4表示一个 2、3 或 4 分量的有符号整数向量。
8mat2, mat3, mat4表示一个 2x2、3x3 或 4x4 的浮点矩阵(用于变换)。
9sampler2D在片元着色器中用于访问 2D 纹理(texture)。
10samplerCube在片元着色器中用于访问立方体贴图纹理。

GLSL ES 1.00 存储限定符(Storage Qualifiers)

Section titled “GLSL ES 1.00 存储限定符(Storage Qualifiers)”

限定符定义了变量的存储方式以及它们如何在 WebGL 应用程序和着色器之间或着色器阶段之间传递数据。

序号限定符描述
1attribute(仅用于顶点着色器)将 WebGL 应用程序提供的逐顶点数据(如位置、颜色、纹理坐标,通过缓冲区对象提供)链接到顶点着色器。其值会随着处理的每个顶点而变化。注意:在 GLSL ES 3.00 / WebGL 2 中被 in 替代。
2uniform链接由 WebGL 应用程序提供的数据,这些数据在单个绘制调用(draw call)中对于所有顶点或片元保持不变(例如,变换矩阵、光照参数、纹理)。在着色器中是只读的。可在顶点着色器和片元着色器中使用。示例:uniform mat4 u_modelViewProjectionMatrix;
3varying将数据从顶点着色器传递到片元着色器。GPU 在数据到达片元着色器之前,会在图元(primitive)表面上对这些值进行插值。示例:varying vec3 v_normal;。注意:在 GLSL ES 3.00 / WebGL 2 中被 out(在顶点着色器中)和 in(在片元着色器中)替代。

GLSL ES 要求为 float 和 int 类型指定精度,以管理性能和功耗,尤其是在移动设备上。限定符有 highp(高精度)、mediump(中等精度)、lowp(低精度)。通常在着色器开头设置默认精度。

// Example: Setting default float precision in a fragment shader
precision mediump float;

顶点着色器对于在绘制调用中提交的每个顶点都会执行一次。它的主要职责是计算顶点在 3D 空间中的最终位置(裁剪空间坐标,clip space coordinates)。它通过 attribute 变量接收逐顶点数据,通过 uniform 变量接收全局数据。

顶点着色器中常执行的关键任务:

  • 对顶点位置应用变换(模型、视图、投影矩阵)。
  • 变换法向量(normal vectors)用于光照计算。
  • 生成或变换纹理坐标。
  • 通过 varying 变量将数据(如颜色、法向量、纹理坐标)传递给片元着色器。

内置顶点着色器变量 (GLSL ES 1.00)

Section titled “内置顶点着色器变量 (GLSL ES 1.00)”
序号变量描述
1gl_Position(highp vec4,输出)顶点着色器的强制输出。它指定顶点在裁剪空间坐标中的位置。随后的固定功能阶段会使用此值进行图元组装、裁剪等操作。
2gl_PointSize(mediump float,输出)指定绘制点图元(gl.POINTS)时点的大小(以像素为单位)。仅在绘制点时相关。

这个最小化的顶点着色器接收 2D 坐标,并将其作为裁剪空间位置输出。

// Input: Per-vertex 2D coordinates from a buffer
attribute vec2 a_position;
// Uniform: A transformation matrix (example)
uniform mat4 u_matrix;
void main() {
// Output the final clip space position.
// Convert 2D position to 4D (x, y, z, w)
// z = 0.0 (assuming 2D drawing on z=0 plane)
// w = 1.0 (required for perspective division)
// Apply transformation matrix (optional)
gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
}

在这个示例中,a_position 通过 JavaScript 链接的顶点缓冲区接收数据。u_matrix 会通过 gl.uniformMatrix4fv 设置。最终的 gl_Position 决定了顶点在透视除法和视口(viewport)变换后出现在屏幕上的位置。

片元着色器(Fragment Shader / Pixel Shader)

Section titled “片元着色器(Fragment Shader / Pixel Shader)”

顶点处理完毕并组装成图元(如三角形)后,片元着色器会为屏幕上被图元覆盖的每个像素执行。它的主要职责是确定该像素(片元)的最终颜色。

片元着色器中常执行的关键任务:

  • 根据插值数据(来自 varying 变量)、uniforms 和纹理计算最终颜色。
  • 执行光照计算。
  • 将纹理应用到表面。
  • 实现雾等效果。

内置片元着色器变量 (GLSL ES 1.00)

Section titled “内置片元着色器变量 (GLSL ES 1.00)”
序号变量描述
1gl_FragCoord(mediump vec4,只读输入)包含片元在窗口坐标系下的坐标 (x, y, z, 1/w)。
2gl_FrontFacing(bool,只读输入)如果片元属于正面图元(基于顶点缠绕顺序和剔除设置),则为 true。
3gl_PointCoord(mediump vec2,只读输入)包含点图元(gl.POINTS)内的 2D 坐标,范围从 0.0 到 1.0。对于点纹理化很有用。
4gl_FragColor(mediump vec4,输出)主要的输出变量。指定片元的最终 RGBA 颜色。注意:在 GLSL ES 3.00 / WebGL 2 中已弃用,使用自定义的 out 变量代替。
5gl_FragData[n](mediump vec4,输出数组)用于写入多个渲染目标(Multiple Render Targets, MRT),这是 WebGL 1 中通过扩展或 WebGL 2 中的标准特性。注意:在 GLSL ES 3.00 中也已弃用。

这个最小化的片元着色器将图元的每个像素设置为纯绿色。

// Set default precision for floats
precision mediump float;
void main() {
// Output a solid green color (Red=0, Green=0.8, Blue=0, Alpha=1.0)
gl_FragColor = vec4(0.0, 0.8, 0.0, 1.0);
}

这里确定的 gl_FragColor 值会写入当前像素的颜色缓冲区。

在 JavaScript 中存储、编译和链接着色器

Section titled “在 JavaScript 中存储、编译和链接着色器”

在你的 JavaScript 代码中,你需要加载、编译顶点和片元着色器,并将它们链接成一个 WebGL 可以执行的着色器程序(shader program)。

着色器代码通常存储为 JavaScript 字符串。你可以直接嵌入它们或从单独的文件中获取。

const vsSource = `
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
`;
const fsSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red
}
`;

每个着色器(顶点着色器和片元着色器)都必须单独编译。

步骤:

  1. 创建一个着色器对象:const shader = gl.createShader(type);(其中 type 是 gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER)
  2. 附加源代码:gl.shaderSource(shader, sourceString);
  3. 编译着色器:gl.compileShader(shader);
  4. 检查编译错误(至关重要!):使用 gl.getShaderParameter(shader, gl.COMPILE_STATUS) 和 gl.getShaderInfoLog(shader) 来检测并报告错误。编译失败很常见!

示例辅助函数(概念性):

function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
// Check compilation status
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`An error occurred compiling the ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${gl.getShaderInfoLog(shader)}`);
gl.deleteShader(shader);
return null;
}
return shader;
}
// Usage:
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

当两个着色器都成功编译后,将它们链接(link)成一个单独的着色器程序。

步骤:

  1. 创建一个程序对象:const shaderProgram = gl.createProgram();
  2. 附加编译好的着色器:gl.attachShader(shaderProgram, vertexShader);,gl.attachShader(shaderProgram, fragmentShader);
  3. 链接程序:gl.linkProgram(shaderProgram);
  4. 检查链接错误(至关重要!):使用 gl.getProgramParameter(shaderProgram, gl.LINK_STATUS) 和 gl.getProgramInfoLog(shaderProgram) 来验证是否成功链接。

示例链接代码(概念性):

// Assuming vertexShader and fragmentShader are compiled successfully
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// Check linking status
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error(`Unable to initialize the shader program: ${gl.getProgramInfoLog(shaderProgram)}`);
gl.deleteProgram(shaderProgram); // Clean up
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
return null;
}
// Optional: Detach and delete shaders after successful linking if no longer needed
// gl.detachShader(shaderProgram, vertexShader);
// gl.deleteShader(vertexShader);
// gl.detachShader(shaderProgram, fragmentShader);
// gl.deleteShader(fragmentShader);
// Tell WebGL to use this program for subsequent drawing operations
gl.useProgram(shaderProgram);
// Later, you'll get locations of attributes and uniforms...

在绘制之前,你必须使用 gl.useProgram(shaderProgram); 告诉 WebGL 使用哪个着色器程序。一次只能激活一个程序。

编译和链接过程中的错误检查对于调试 WebGL 应用程序至关重要。着色器错误通常表现为白屏或意想不到的视觉输出。

对于更复杂的着色器开发,可以考虑使用 Shadertoy 或 GLSL Sandbox 等工具进行实时编辑和预览。