GLSL 语言入门:着色器世界的 JavaScript
本教程共 40 篇 · 第 34 篇 · 更新于 2026-08-14 · 约 9 分钟阅读
本节目标:掌握 GLSL 的数据类型、精度与存储限定符、常用内置函数,理解 gl_Position、gl_FragColor 和 texture2D,能读懂并修改第 33 章那种着色器代码。
第 33 章的例子能跑,但代码里的 vec3、varying、sin(uTime) 都是什么意思?这一章把 GLSL 这门语言讲透。GLSL 全称 OpenGL Shading Language,是写顶点着色器和片元着色器的语言。它长得像 C 语言:强类型、分号结尾、有 main 函数。如果你写过 C,上手很快;只写过 JavaScript,下面这些差异要留意。
标量:float、int、bool
GLSL 只有三种基础标量:
float a = 1.0; // 浮点数,必须带小数点
int b = 2; // 整数
bool c = true; // 布尔值
最重要的规则:浮点数必须写成 1.0,写成 1 就是整数。GLSL 是强类型,float 和 int 不能直接运算:
float x = 1.0 * float(2); // 用 float() 显式转换
忘记转换是新手报错的第一来源。
向量:vec2、vec3、vec4
图形编程里,坐标、颜色、方向全是向量。GLSL 提供 2 到 4 维浮点向量:
vec2 a = vec2(1.0, 2.0);
vec3 b = vec3(1.0); // 所有分量都是 1.0
vec4 c = vec4(a, 3.0, 4.0); // 从 vec2 扩展成 vec4
向量分量可以用三套名字访问:xyz(坐标)、rgb(颜色)、stpq(纹理坐标)。同一个向量,写 .x、.r、.s 拿到的都是第一个分量,按语义选顺眼的:
vec3 color = vec3(0.2, 0.4, 0.6);
float r = color.r; // 0.2
float x = color.x; // 0.2,同一个分量
还可以重排、复制分量,这叫 swizzle:
vec2 uv2 = color.rg; // 取前两个分量
vec3 flipped = color.zyx; // 反转顺序
向量之间可以整体运算,这是 GLSL 高效的关键:
vec3 a = vec3(1.0);
vec3 b = a * 2.0; // 每个分量都乘 2,等价于 vec3(2.0)
不用写循环,一个运算符处理所有分量。
矩阵:mat2、mat3、mat4
矩阵用来做变换,3D 里最常用 mat4。第 33 章的 projectionMatrix * modelViewMatrix * vec4(position, 1.0) 就是矩阵乘向量,把顶点从局部坐标一路变换到裁剪坐标。新手阶段不需要手写矩阵,three.js 会把现成的矩阵作为 uniform 传进来,照用即可。
矩阵之后还有两个常用类型。定长数组和结构体:
float values[4]; // 定长数组
vec3 points[10];
struct Light { // 结构体
vec3 position;
vec3 color;
};
Light sun = Light(vec3(0.0, 5.0, 0.0), vec3(1.0, 1.0, 0.9));
vec4 的第四个分量 w 也别忽略。3D 坐标写成 vec4(x, y, z, 1.0) 才能和 4×4 矩阵相乘,1.0 是”这是个点”的标记。变换之后 GPU 还会做透视除法:xyz 同时除以 w,远处的点自然变小。这正是 gl_Position 赋值的完整含义。
采样器:sampler2D
sampler2D 是纹理句柄,配合 texture2D 函数从纹理里取颜色:
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(uTexture, vUv);
gl_FragColor = texColor;
}
texture2D 返回 vec4:前三个分量是 RGB,第四个是透明度。vUv 范围 0~1,超出范围后的采样行为由纹理的 wrap 设置决定(第 15 章讲过)。
精度限定符
GPU 的浮点精度可以调节,精度越低算得越快、越省电,但误差越大:
highp:高精度,顶点着色器 float 的默认值;mediump:中精度。注意 GLSL ES 1.00 规范里片元着色器的 float 没有默认精度,three.js 会自动注入 highp;lowp:低精度,颜色这类宽容数据够用。
片元着色器里做复杂计算(比如距离场)时,mediump 可能不够,需要声明:
precision highp float;
Notethree.js 的 ShaderMaterial 会自动注入合适的精度声明,大多数时候不用自己写。但如果你把着色器代码搬到别处用,缺失精度声明会直接编译失败。
存储限定符
三个存储限定符对应第 33 章的数据流:
| 限定符 | 谁写 | 谁读 | 典型例子 |
|---|---|---|---|
| attribute | 几何体 | 顶点着色器 | position、uv、normal |
| uniform | JavaScript | 两个着色器 | uTime、uColor |
| varying | 顶点着色器 | 片元着色器 | vUv(自动插值) |
自定义 attribute 需要几何体配合。比如给每个粒子单独的大小:
geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
attribute float scale; // 顶点着色器里声明同名 attribute
attribute 这个名字在 GLSL3 里改叫 in,three.js 默认按 GLSL1 语法,attribute 照写。 uniform 的传递方向相反:JS 侧往 ShaderMaterial 的 uniforms 里塞值,着色器侧同名声明。名字必须一致,类型要匹配——JS 里 { value: 1.0 } 对应 GLSL 的 uniform float,{ value: texture } 对应 uniform sampler2D。对不上不会报错,但你会拿到默认值。这是排查”uniform 没生效”的第一站。
内置函数:着色器的武器库
GLSL 内置了几十个数学函数,全部是逐分量运算。最高频的八个:
mix(a, b, t) // 线性插值:t=0 取 a,t=1 取 b
smoothstep(e0, e1, x) // 平滑过渡,0~1 之间是缓和的 S 曲线
fract(x) // 取小数部分,做重复图案的利器
clamp(x, 0.0, 1.0) // 限制范围
step(edge, x) // x < edge 返回 0,否则返回 1
length(v) // 向量长度
dot(a, b) // 点积,判断方向、算光照
normalize(v) // 归一化,方向不变、长度变 1
配合使用就能画出各种图案。棋盘格:
vec2 grid = fract(vUv * 8.0); // 每格 0~1
float cell = step(0.5, grid.x) * step(0.5, grid.y); // 0 或 1
圆形:
float d = distance(vUv, vec2(0.5)); // 到中心的距离
float circle = smoothstep(0.4, 0.2, d); // 边缘柔和过渡的圆
sin、cos 做周期动画,0.5 + 0.5 * sin(uTime) 把 -11 的波形映射到 01,这是最常用的”呼吸”写法。
再补几个常用函数:
pow(x, 2.0) // x 的平方
abs(x) // 绝对值
mod(x, y) // 取余
distance(a, b) // 两点距离
min(a, b) // 取较小值
max(a, b) // 取较大值
dot 值得单独说。两个向量点积等于夹角余弦乘长度乘积:方向相同接近 1,垂直接近 0。光照里”法线朝不朝光源”就用它判断,这是所有光照模型的基石。
gl_Position 与 gl_FragColor
两个特殊输出变量,是着色器的”出口”:
- 顶点着色器必须写
gl_Position:顶点的裁剪空间坐标,GPU 靠它决定画在哪; - 片元着色器写
gl_FragColor:像素最终颜色,第四分量是透明度。
不写 gl_Position,顶点无处安放;不写 gl_FragColor,像素默认黑色。这两个名字来自 GLSL1:GLSL ES 3.00 里 gl_FragColor 改为用户声明的 out 变量,gl_Position 仍是内置输出变量。three.js 默认兼容老写法。
一个完整的 uv + texture2D 示例
把本章的知识串起来:用 canvas 生成一张渐变纹理,着色器里采样它,再叠加正弦波纹。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>34 GLSL 语言入门</title>
<style>body { margin: 0; overflow: hidden; }</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
uniform float uTime;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
// 亮度波纹:沿 y 方向的正弦波,范围 0~1
float wave = 0.5 + 0.5 * sin(vUv.y * 12.0 + uTime * 3.0);
// 采样纹理
vec4 texColor = texture2D(uTexture, vUv);
// mix 在蓝色和橙色之间按波纹混合
vec3 color = mix(vec3(0.1, 0.3, 0.8), vec3(0.95, 0.5, 0.1), wave);
gl_FragColor = vec4(color * texColor.rgb, 1.0);
}
</script>
<script type="module">
import * as THREE from 'three';
// canvas 渐变纹理
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 256, 256);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, '#ffcc00');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 256);
const texture = new THREE.CanvasTexture(canvas);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.z = 2;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uTexture: { value: texture }
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 1.6), material);
scene.add(mesh);
renderer.setAnimationLoop(() => {
material.uniforms.uTime.value += 0.02;
renderer.render(scene, camera);
});
</script>
</body>
</html>
读这个片元着色器的顺序:
- wave:用 vUv.y 和 uTime 造一条上下滚动的波纹,范围 0~1;
- texColor:texture2D 从纹理里取色;
- mix:按 wave 在蓝、橙之间插值;
- 最后乘上纹理色,透明度固定 1。
Note关于 GLSL 版本:three.js 的 ShaderMaterial 默认按 GLSL ES 1.00 语法编译(texture2D、gl_FragColor、attribute 都是 1.00 的写法),底层 WebGL2 由 three.js 自动兼容。想用 GLSL3 的新语法(texture()、out 变量),把材质设置成
glslVersion: THREE.GLSL3,同时全部代码要换成新写法。入门阶段用默认的 1.00 即可,网上大多数着色器教程也是这个写法。
到这里,手写 GLSL 的基础就齐了。第 35 章换个思路:用 JavaScript 对象代替字符串,这就是 three.js 的新着色语言 TSL。