一、Shader 简介
Shader(着色器)是一段运行在 GPU 上的程序,开发者通过编写程序,可以在图形渲染的过程中实现高度自定义的图形效果(如光照、材质、后处理等),因此 Shader 是游戏开发中必备的一项重要技能。
示例图(一个使用 Shader 实现的溶解效果):
二、为何需要 Shader?
因为 Shader 是利用 GPU 来执行渲染图像的程序,比起走 CPU 绘制图像的形式,GPU 渲染图形的效率会高得多。
对于一帧图像来说,它会由非常多的像素点构成,每个像素点的位置、颜色都需要经过计算处理并绘制到屏幕上。
CPU 的计算能力自然是非常强大的,如果仅有少数的像素需要进行计算,CPU 可以非常快速地得到结果。但问题在于一个图像需要计算的像素数量实在太庞大了,而 CPU 的核心数量较少,导致绘制完一帧图像的总时长会非常高。
而 GPU 专为大规模并行计算设计,其包含数千个小型计算核心(如 NVIDIA RTX4090 具备 16,384 个 CUDA 核心),尤其适合处理高度重复、可并行的任务(如顶点变换、片元着色)。
英伟达的专家曾通过机器串行/并行喷射绘画的展示,来科普 GPU 高效绘图的能力:
首先以串行喷射的绘画,来模拟 CPU 绘图的形式:
接着是以万箭齐发喷射的绘画,来模拟 GPU 绘图的形式:
因此在图像处理领域,擅长与 GPU 打交道的 Shader 是一门必备的技能。
三、如何编写 Shader?
3.1 GLSL
GLSL(OpenGL Shading Language) 是当下最主流的 Shader 编程语言,它基于 OpenGL ES / WebGL 标准。
下方是一段简单的 GLSL 代码示例:
attribute vec2 a_position;
attribute vec4 a_color;
varying vec4 v_color;
void main() {
v_color = a_color;
gl_Position = vec4(a_position, 0.0, 1.0);
}
我们会在后续的文章里仔细介绍 GLSL 的语法。
前文提到 “GLSL 基于 OpenGL ES/WebGL 标准”,这是因为:
- GLSL 是基于 OpenGL ES 的着色器编程语言;
- WebGL 是基于 OpenGL ES 的 Web 图形标准(留意是标准,不是语言);
- WebGL 是基于 javascript + GLSL 的组合来开发和应用 Shader 效果的,其中 GLSL 是实现着色器编程的核心语言(javascript 仅起到一个桥接作用)。
下方是一个以 WebGL 标准来开发 Shader 的简单示例:
// 将 GLSL 代码段赋值给 js 变量 vertexSource
const vertexSource = `
attribute vec2 a_position;
attribute vec4 a_color;
varying vec4 v_color;
void main() {
v_color = a_color;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// 创建着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
在后续文章中我们将经常看到这种 javascript + GLSL 的混搭式代码,故本系列也可以作为 WebGL 的知识点来学习。
💡 本系列后续将在游戏引擎 Cocos Creator 中实现各种 Shader 效果(Cocos Creator 的 Shader 底层支持 GLSL ES 标准)。
Cocos Creator 可将项目构建为跨平台的游戏(而不是纯 Web 平台),故本系列主题为 Shader 而非 WebGL。
3.2 其它 Shader 开发语言(仅做了解)
除了 GLSL,还可以使用如下语言来开发 Shader:
- HLSL (High-Level Shading Language) :
微软 DirectX 的 Shader 语言,需通过工具转换为 GLSL 来跨平台。 - MSL (Metal Shading Language) :
苹果 Metal 的 Shader 语言。 - WGSL (WebGPU Shading Language) :
下一代的跨平台 WebGPU Shader 语言,未来可能成为 Web 标准,截止本文发布时还处于草案阶段。
💡 其中使用 WGSL 开发的 WebGPU Shader,在 Web 端(例如浏览器)具备直接调用 GPU 的能力,相较基于 GLSL 书写的 WebGL Shader 会有更高的性能。
但是因为 WebGPU 的兼容性较差(特别是移动端),故暂时按下不表。
四、渲染管线
渲染管线(Rendering Pipeline)是图形渲染的核心流程,它描述了从 数据 到最终 屏幕像素图像 的完整处理过程,而 Shader 程序用于控制渲染管线中的特定阶段,因此理解渲染管线阶段和协作机制,是掌握 Shader 编程、性能优化和高级渲染效果的基础。
4.1 渲染管线的核心阶段
一个经典的渲染管线会按顺序依次执行如下几个阶段:
1. 应用阶段(Application Stage)
-
输入:2D Sprite 数据,或者 3D 模型、纹理、材质、灯光等数据。
-
处理:
- CPU 准备数据(如模型矩阵、摄像机参数)。
- 调用图形 API(如
gl.drawElements
)将数据提交给 GPU。
2. 顶点处理阶段(Vertex Processing)
-
输入:顶点数据(Vertex Data)。
-
处理:通过顶点着色器(Vertex Shader) 计算和设置每个顶点的位置、尺寸等属性。
3. 图元装配(Primitive Assembly)
-
输入:顶点数据。
-
处理:
- 将顶点组装成基本图元(点、线、三角形)。
- 执行裁剪(Clipping):剔除屏幕外的图元。
4. [可选] 几何着色器处理(Geometry Shading)
-
输入:图元数据。
-
处理:接收完整的图元(如一个三角形、一条线段),可以修改、丢弃或生成新的图元。
5. 光栅化(Rasterization)
-
输入:图元(如三角形)。
-
处理:
- 将连续的几何图形离散化为片元(Fragment) ,即像素候选。
- 计算每个片元的位置、深度、插值后的属性(如 UV、颜色)。
💡 进一步了解片元
- 片元是光栅化阶段的产物:
当几何图形(如三角形)被光栅化时,它会被分割成许多小单元,每个单元对应一个携带了信息的候选像素,即片元。- 片元携带的信息:
包括颜色、深度(Z值)、纹理坐标、模板值等。- 片元 ≠ 最终像素:
片元只是潜在的像素 (所以才叫候选像素),后续可能被丢弃(如被遮挡)或被修改(如混合半透明颜色)。
6. 片元处理阶段(Fragment Processing)
-
输入:光栅化后的片元数据。
-
处理:通过片元着色器(Fragment Shader) 计算每个片元的颜色(纹理采样、光照计算等)。
7. 测试与混合(Testing & Blending)
-
输入:片元颜色和深度信息。
-
处理:
- 执行深度测试(Depth Test)、模板测试(Stencil Test)。
- 将通过测试的片元进行混合(Blending):与帧缓冲区中已有的颜色按混合方程(如 alpha 混合)合成。
- 写入帧缓冲区(Frame Buffer)。
8. 输出到屏幕
- 处理:帧缓冲区的像素数据最终显示在屏幕上。
4.2 Shader 在渲染管线中的作用
上述的渲染管线流程中,顶点处理阶段和片元处理阶段都需要 Shader 的参与:
- 在顶点处理阶段,使用 Vertex Shader(顶点着色器) 对顶点进行处理;
- 在几何着色器处理阶段,使用 Geometry Shader(几何着色器) 对已组装的图元进行进一步增/删处理(进而生成新图元),留意此阶段并非渲染管线必经阶段,且移动端对几何着色器的支持有限;
- 在片元处理阶段,使用 Fragment Shader(片元着色器) 对片元数据进行处理。
我们可以将渲染管线流程简化为:
数据 → Vertex Shader(顶点着色器)→ 图元装配 → [可选] Geometry Shader(几何着色器)→ 光栅化 → Fragment Shader(片元着色器)→ 测试与混合
该流程示意图如下(蓝色模块表示可通过 GLSL 编程的 Shader 处理模块):
在下篇文章,我们会编写顶点着色器和片元着色器,来绘制最简单的一个点。