Three.js 零基础入门 GLSL 着色器:从概念到实战完整指南
Three.js 零基础入门 GLSL 着色器:从概念到实战完整指南
本文面向只会用 Three.js、不了解 WebGL、没有建模基础的前端开发者,从「是什么 → 为什么 → 怎么写 → 底层原理 → 面试考点」由浅入深讲解着色器,全程结合 Three.js 场景,附带可运行的完整代码示例。
前言
在使用 Three.js 的过程中,我们通过 MeshBasicMaterial、MeshStandardMaterial 等内置材质,就能快速实现常规的 3D 渲染效果。但当我们需要实现波浪水面、溶解消失、扫描线、卡通描边等特殊视觉效果时,内置材质就无能为力了。
这时就需要自定义着色器(Shader)——直接编写运行在 GPU 上的小程序,完全掌控每一个顶点的位置和每一个像素的颜色,实现任意想要的视觉效果。
本文将带你从零入门 GLSL 着色器,所有知识点都锚定在 Three.js 的使用场景中,无需额外的 WebGL 基础也能看懂。
一、着色器基础概念
1.1 什么是着色器(Shader)
着色器是直接运行在显卡(GPU)上的小程序,专门用来控制 3D 渲染过程中「顶点位置计算」和「像素颜色计算」两个核心环节。
如果把内置材质比作「成品衣服」,只能选尺码、选颜色;那么着色器就是「缝纫机 + 布料」,你可以自由设计款式、图案和动态效果,自由度极高。
编写着色器使用的语言叫做 GLSL(OpenGL Shading Language),语法与 C 语言相似,是专门为 GPU 并行计算设计的语言。
1.2 为什么着色器要跑在 GPU 上
理解 CPU 和 GPU 的差异,就能明白着色器为什么快:
- CPU:像一位全能大厨,擅长处理复杂逻辑,但一次只能做一件事,适合串行执行的业务逻辑。
- GPU:像几千名流水线工人,每个人只做简单重复的工作,但几千人同时开工,处理海量并行任务效率极高。
3D 场景中,一个模型有上万个顶点、上百万个像素,每个点都要独立计算位置和颜色——这种「大量重复、逻辑简单」的计算,交给 GPU 并行处理,性能比 CPU 高出几十上百倍。
1.3 什么时候需要自定义着色器
Three.js 内置材质能满足 80% 的常规需求,以下场景通常需要自定义着色器:
- 特殊顶点动画:波浪水面、旗帜飘动、模型变形溶解
- 定制视觉效果:扫描线、故障艺术、卡通描边、全息投影
- 高级纹理处理:多图动态混合、流动光效、自定义滤镜
- 海量粒子系统:几万粒子同时运动,GPU 并行计算远快于 JS
- 全屏后处理:泛光、景深、电影调色、画面特效
二、两大核心着色器
WebGL 渲染流水线中,开发者可以自定义的着色器有两种,一前一后配合工作,缺一不可。
2.1 顶点着色器(Vertex Shader)
职责:处理模型的每一个顶点,计算出顶点最终在屏幕上的位置。 执行频率:每个顶点执行一次。 类比:塑形车间的工人,负责把每个顶点挪到正确的位置,确定物体的形状、大小和角度。
核心输入输出:
- 输入:每个顶点的属性数据(坐标、法线、UV 等)
- 必选输出:
gl_Position,顶点最终的裁剪空间坐标 - 可选输出:若干
varying变量,用于传递给片元着色器
2.2 片元着色器(Fragment Shader)
职责:处理屏幕上的每一个像素,计算出该像素的最终颜色。 执行频率:每个像素执行一次。 类比:上色车间的工人,形状已经确定,负责给每个像素涂颜色、贴纹理、算光影。
核心输入输出:
- 输入:插值后的
varying变量、全局uniform变量、纹理贴图 - 必选输出:
gl_FragColor,像素最终的 RGBA 颜色
一句话总结:顶点着色器管形状位置,片元着色器管颜色效果。
三、GLSL 基础语法
GLSL 是强类型语言,语法简洁,核心概念很少,掌握以下几点就能写基础效果。
3.1 基础数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
float | 浮点数,最常用 | 1.0、0.5 |
int | 整数 | 2、100 |
bool | 布尔值 | true、false |
vec2/vec3/vec4 | 2/3/4 维向量 | vec3(1.0, 0.0, 0.0) 表示红色 |
mat4 | 4×4 矩阵,用于坐标变换 | 投影矩阵、模型矩阵 |
sampler2D | 2D 纹理采样器 | 用于读取贴图颜色 |
⚠️ 注意:GLSL 中浮点数必须写小数点,例如
1.0不能写成1,否则会报错。
3.2 三大核心变量类型(面试高频)
这是着色器最核心的概念,三者的作用域、更新频率和用途完全不同,必须分清。
① attribute:顶点属性
- 特点:每个顶点拥有独立的值,各不相同;只能在顶点着色器中读取。
- 设置方:JavaScript 端通过
BufferAttribute传入。 - 典型例子:顶点坐标
position、顶点法线normal、纹理坐标uv。 - 类比:每个员工的工号、姓名,人人不同,入职时确定。
② uniform:全局统一变量
- 特点:同一帧内,所有顶点、所有像素读取到的值完全相同;顶点和片元着色器均可使用。
- 设置方:JavaScript 端每帧可更新一次。
- 典型例子:时间
time、灯光颜色、纹理贴图、相机矩阵。 - 类比:公司统一发布的通知,所有人都一样,可定期更新。
③ varying:插值变量
- 特点:顶点着色器赋值,片元着色器接收;GPU 会在三角形内部对该变量自动做线性插值。
- 作用:在两个着色器之间传递数据。
- 典型例子:传递 UV 坐标、顶点颜色、世界坐标。
- 类比:三角形三个顶点分别是红、绿、蓝,中间像素自动形成渐变色,这个渐变就是 varying 自动完成的。
3.3 必写内置变量
gl_Position:顶点着色器必须赋值,vec4类型,代表顶点最终的屏幕位置。gl_FragColor:片元着色器必须赋值,vec4类型,代表像素最终颜色(RGBA)。
3.4 常用内置函数
不用死记,写多了自然熟悉:
sin(x) / cos(x):正弦/余弦,做波动、循环动画必备mix(a, b, t):线性混合,t=0 返回 a,t=1 返回 b,过渡效果神器clamp(x, min, max):将数值限制在 [min, max] 范围内texture2D(sampler, uv):从纹理中采样指定坐标的颜色length(v):计算向量长度
四、Three.js 中实战入门
Three.js 提供了 ShaderMaterial 封装类,自动帮我们传入投影矩阵、模型视图矩阵、顶点位置、法线、UV 等常用变量,无需手动声明,开发效率很高。
下面我们从最简示例开始,逐步进阶。
4.1 示例一:纯色立方体
先实现一个效果等同于 MeshBasicMaterial 的纯色着色器,熟悉完整结构。
顶点着色器
void main() {
// 固定写法:投影矩阵 × 模型视图矩阵 × 顶点坐标
// 作用:将 3D 空间顶点转换为屏幕坐标
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
说明:
projectionMatrix、modelViewMatrix、position均为 Three.js 自动注入,直接使用即可。
片元着色器
void main() {
// 输出纯红色:vec4(红, 绿, 蓝, 透明度),范围 0.0 ~ 1.0
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
Three.js 调用代码
import * as THREE from 'three';
// 1. 着色器代码字符串
const vertexShader = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 2. 创建着色器材质
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader
});
// 3. 正常创建网格,用法与普通材质完全一致
const geometry = new THREE.BoxGeometry(1, 1, 1);
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
4.2 示例二:uniform 传时间,实现颜色呼吸动画
通过 uniform 变量从 JS 传入时间,让颜色随时间动态变化。
片元着色器
uniform float uTime;
void main() {
// sin 函数让红色在 0~1 之间循环波动
float r = sin(uTime) * 0.5 + 0.5;
gl_FragColor = vec4(r, 0.2, 0.6, 1.0);
}
Three.js 更新代码
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 }
}
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
// 更新 uniform 值,着色器会实时读取
material.uniforms.uTime.value = time;
renderer.render(scene, camera);
}
animate();
4.3 示例三:顶点动画,实现波浪平面
修改顶点着色器,改变顶点 Y 坐标,实现正弦波浪效果。
顶点着色器
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
// 根据 X 坐标和时间计算波动高度
pos.y += sin(pos.x * 3.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
Three.js 几何体设置
// 分段数越高,波浪越平滑
const geometry = new THREE.PlaneGeometry(4, 4, 64, 64);
五、渲染流水线底层原理
了解大致流程有助于理解着色器的工作位置,也是面试常考点。
5.1 简化版渲染流水线
- 顶点处理阶段:执行顶点着色器,将 3D 顶点坐标经过 MVP 矩阵变换,计算出屏幕上的 2D 坐标。
- 图元装配与光栅化:将顶点组成三角形,并计算三角形覆盖了哪些像素点(即把矢量图形转为像素点阵)。
- 插值阶段:对
varying变量在三角形内部进行线性插值,每个像素得到一个过渡值。 - 片元处理阶段:执行片元着色器,为每个像素计算最终颜色。
- 测试与混合阶段:经过深度测试(前后遮挡)、模板测试、Alpha 混合,最终写入帧缓冲显示到屏幕。
5.2 重点理解:插值
插值是 varying 变量的核心特性。三个顶点设置了不同的值,三角形内部的像素无需手动计算,GPU 会自动根据像素到三个顶点的距离,生成线性过渡的值。
例如三个顶点颜色分别为红、绿、蓝,中间像素会自动形成彩虹渐变效果——这就是插值的作用。
六、典型使用场景
并非所有效果都需要自定义着色器,内置材质能实现的就优先用内置材质,保证开发效率。
自定义着色器的典型应用场景:
- 顶点动画:水面波浪、旗帜飘动、模型变形溶解、布料效果
- 视觉特效:扫描线、故障艺术、溶解消失、卡通描边、全息投影
- 纹理处理:多图动态混合、流动光效、自定义滤镜、视频特效
- 粒子系统:海量粒子运动、颜色变化,GPU 并行计算性能远高于 JS
- 后处理效果:全屏泛光、景深、运动模糊、电影调色、画面风格化
七、高频面试题解析
1. 顶点着色器和片元着色器分别负责什么?执行频率?
- 顶点着色器:处理每个顶点,计算顶点最终屏幕位置;每个顶点执行一次。
- 片元着色器:处理每个像素,计算像素最终颜色;每个像素执行一次。
- 二者分工:顶点定形状,片元定颜色。
2. attribute、uniform、varying 三种变量的区别?
- attribute:顶点属性,每个顶点值不同,仅顶点着色器可读,由 JS 端几何体数据传入。
- uniform:全局统一变量,一帧内所有像素值相同,两个着色器均可读,由 JS 端每帧更新。
- varying:插值变量,顶点着色器赋值,片元着色器接收,GPU 自动做线性插值,用于两阶段间传递数据。
3. ShaderMaterial 和 RawShaderMaterial 有什么区别?
- ShaderMaterial:Three.js 高度封装,自动注入常用矩阵和顶点属性,开发简单,适合绝大多数场景。
- RawShaderMaterial:完全原生,所有变量、矩阵均需手动声明计算,灵活度最高但开发成本高,适合底层定制与极致优化。
4. 什么是光栅化?
光栅化是将顶点组成的矢量三角形,转换为屏幕上离散像素点的过程。顶点着色器只确定三个顶点的位置,光栅化计算出三角形覆盖的所有像素,之后每个像素执行一次片元着色器进行上色。
5. 为什么着色器做动画比 JS 计算快?
因为着色器运行在 GPU 上,GPU 采用大规模并行架构,几千个计算核心可以同时处理成千上万个顶点/像素;而 JS 运行在 CPU 单线程上,只能串行计算。对于大量重复的简单计算,GPU 性能是 CPU 的几十至上百倍。
6. 如何将 JS 数据传递给着色器?
分两类:
- 逐顶点数据:存入几何体的
BufferAttribute,作为attribute变量传入顶点着色器。 - 全局统一数据:在材质的
uniforms对象中定义,JS 修改value属性即可实时更新。
本文发于个人博客,欢迎交流指正。如果对你有帮助,可以关注后续更多 Three.js 与 WebGL 进阶内容。