着色器编程
着色器是运行在 GPU 上的小程序,可以直接控制顶点位置和像素颜色。通过自定义着色器,你可以创建独特的视觉效果,实现标准材质无法达到的表现。本章将介绍 GLSL 着色器语言基础以及如何在 Three.js 中使用自定义着色器。
着色器基础概念
渲染管线
理解着色器之前,需要了解 GPU 渲染管线的基本流程:
- 顶点数据:顶点位置、法线、UV 等属性
- 顶点着色器:处理每个顶点,计算最终位置
- 图元装配:将顶点组装成三角形
- 光栅化:将三角形转换为像素片段
- 片元着色器:计算每个像素的颜色
- 输出合并:深度测试、混合等
顶点着色器和片元着色器是我们可编程的阶段。
GLSL 简介
GLSL(OpenGL Shading Language)是编写着色器的语言,语法类似 C 语言:
// 基本数据类型
float a = 1.0; // 浮点数
int b = 1; // 整数
bool c = true; // 布尔值
// 向量
vec2 uv = vec2(0.5, 0.5); // 二维向量
vec3 position = vec3(1.0, 0.0, 0.0); // 三维向量
vec4 color = vec4(1.0, 0.0, 0.0, 1.0); // 四维向量(RGBA)
// 向量分量访问
color.r, color.g, color.b, color.a // 颜色方式
color.x, color.y, color.z, color.w // 位置方式
color.s, color.t, color.p, color.q // 纹理坐标方式
// 矩阵
mat2 m2; // 2x2 矩阵
mat3 m3; // 3x3 矩阵
mat4 m4; // 4x4 矩阵
// 内置函数
sin(x), cos(x), tan(x) // 三角函数
pow(x, y), sqrt(x) // 幂函数
abs(x), floor(x), ceil(x) // 数学函数
min(x, y), max(x, y) // 比较函数
length(v), normalize(v) // 向量函数
dot(a, b), cross(a, b) // 点积和叉积
mix(a, b, t) // 线性插值
clamp(x, min, max) // 限制范围
step(edge, x) // 阶梯函数
smoothstep(edge0, edge1, x) // 平滑阶梯
ShaderMaterial
Three.js 提供了 ShaderMaterial 来创建自定义着色器材质。
最简单的着色器
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); // 纯红色
}
`;
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader
});
这段代码创建了一个纯红色的材质。让我们逐行分析:
顶点着色器:
position是内置属性,存储顶点的局部坐标modelViewMatrix将顶点从局部空间变换到相机空间projectionMatrix将顶点从相机空间变换到裁剪空间gl_Position是必须设置的输出,表示顶点的最终位置
片元着色器:
gl_FragColor是必须设置的输出,表示像素的最终颜色- 四个分量分别是红、绿、蓝、透明度(范围 0-1)
Uniform 变量
Uniform 是从 JavaScript 传递到着色器的全局变量,所有顶点和片元共享:
const vertexShader = `
uniform float uTime;
uniform vec3 uColor;
varying vec3 vColor;
void main() {
vColor = uColor;
vec3 pos = position;
pos.y += sin(pos.x * 3.0 + uTime) * 0.5; // 波浪效果
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
varying vec3 vColor;
void main() {
float intensity = (sin(uTime * 2.0) + 1.0) * 0.5;
gl_FragColor = vec4(vColor * intensity, 1.0);
}
`;
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x00ff88) }
},
vertexShader,
fragmentShader
});
// 在动画循环中更新
function animate() {
requestAnimationFrame(animate);
material.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
Varying 变量
Varying 用于在顶点着色器和片元着色器之间传递数据:
const vertexShader = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vUv = uv; // 传递 UV 坐标
vNormal = normalMatrix * normal; // 传递变换后的法线
vPosition = position; // 传递顶点位置
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
// 根据 UV 显示渐变色
vec3 color = vec3(vUv.x, vUv.y, 0.0);
// 简单的光照
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diff = max(dot(vNormal, lightDir), 0.0);
gl_FragColor = vec4(color * diff, 1.0);
}
`;
Attribute 变量
Attribute 是每个顶点的数据,只能在顶点着色器中访问:
const vertexShader = `
attribute float aRandom; // 自定义属性
varying float vRandom;
void main() {
vRandom = aRandom;
vec3 pos = position;
pos += normal * aRandom * 0.5; // 根据随机值偏移
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
// 在 JavaScript 中设置 attribute
const geometry = new THREE.BoxGeometry(1, 1, 1, 32, 32, 32);
const count = geometry.attributes.position.count;
const randoms = new Float32Array(count);
for (let i = 0; i < count; i++) {
randoms[i] = Math.random();
}
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
内置变量和矩阵
Three.js 提供了很多内置变量,可以直接在着色器中使用:
顶点着色器内置属性
// 属性(每个顶点的数据)
attribute vec3 position; // 顶点位置
attribute vec3 normal; // 顶点法线
attribute vec2 uv; // UV 坐标
attribute vec2 uv2; // 第二套 UV 坐标
attribute vec3 color; // 顶点颜色
内置 Uniform
// 矩阵
uniform mat4 modelMatrix; // 模型矩阵
uniform mat4 viewMatrix; // 视图矩阵
uniform mat4 projectionMatrix; // 投影矩阵
uniform mat4 modelViewMatrix; // 模型视图矩阵
uniform mat3 normalMatrix; // 法线矩阵
uniform mat4 modelViewMatrixInverseTranspose; // 模型视图矩阵逆转置
// 相机
uniform vec3 cameraPosition; // 相机世界位置
// 时间
uniform float time; // 时间(需要手动传入)
使用示例
const vertexShader = `
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vNormal = normalMatrix * normal;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// 菲涅尔效果
float fresnel = pow(1.0 - abs(dot(normal, viewDir)), 3.0);
gl_FragColor = vec4(vec3(fresnel), 1.0);
}
`;
实用着色器示例
波浪平面
const vertexShader = `
uniform float uTime;
uniform float uWaveHeight;
uniform float uWaveFrequency;
varying vec2 vUv;
varying float vElevation;
void main() {
vUv = uv;
vec3 pos = position;
// 多层波浪叠加
float wave1 = sin(pos.x * uWaveFrequency + uTime) * uWaveHeight;
float wave2 = sin(pos.y * uWaveFrequency * 0.5 + uTime * 0.7) * uWaveHeight * 0.5;
float wave3 = sin((pos.x + pos.y) * uWaveFrequency * 0.3 + uTime * 1.3) * uWaveHeight * 0.3;
pos.z = wave1 + wave2 + wave3;
vElevation = pos.z;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShader = `
uniform vec3 uColorA;
uniform vec3 uColorB;
uniform float uWaveHeight;
varying vec2 vUv;
varying float vElevation;
void main() {
// 根据高度混合颜色
float mixStrength = (vElevation / uWaveHeight + 1.0) * 0.5;
vec3 color = mix(uColorA, uColorB, mixStrength);
gl_FragColor = vec4(color, 1.0);
}
`;
const geometry = new THREE.PlaneGeometry(10, 10, 64, 64);
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uWaveHeight: { value: 0.3 },
uWaveFrequency: { value: 2.0 },
uColorA: { value: new THREE.Color(0x0066ff) },
uColorB: { value: new THREE.Color(0x00ff88) }
},
vertexShader,
fragmentShader,
side: THREE.DoubleSide
});
const plane = new THREE.Mesh(geometry, material);
plane.rotation.x = -Math.PI / 2;
scene.add(plane);
发光效果
const vertexShader = `
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform vec3 uGlowColor;
uniform float uIntensity;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// 菲涅尔边缘发光
float fresnel = pow(1.0 - abs(dot(normal, viewDir)), 2.0);
vec3 color = uGlowColor * fresnel * uIntensity;
gl_FragColor = vec4(color, 1.0);
}
`;
全息效果
const vertexShader = `
uniform float uTime;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// 菲涅尔
float fresnel = pow(1.0 - abs(dot(normal, viewDir)), 3.0);
// 扫描线
float scanLine = sin(vUv.y * 100.0 + uTime * 5.0) * 0.5 + 0.5;
scanLine = smoothstep(0.3, 0.7, scanLine);
// 闪烁
float flicker = sin(uTime * 20.0) * 0.1 + 0.9;
vec3 color = uColor * (fresnel * 0.8 + 0.2);
color *= scanLine * 0.3 + 0.7;
color *= flicker;
float alpha = fresnel * 0.8 + 0.2;
gl_FragColor = vec4(color, alpha);
}
`;
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x00ffff) }
},
vertexShader,
fragmentShader,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
blending: THREE.AdditiveBlending
});
纹理扭曲
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D uTexture;
uniform float uTime;
uniform float uDistortion;
varying vec2 vUv;
void main() {
vec2 uv = vUv;
// 扭曲 UV
uv.x += sin(uv.y * 10.0 + uTime) * uDistortion;
uv.y += cos(uv.x * 10.0 + uTime) * uDistortion;
vec4 color = texture2D(uTexture, uv);
gl_FragColor = color;
}
`;
纹理采样
在着色器中使用纹理:
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D uTexture;
uniform sampler2D uNormalMap;
varying vec2 vUv;
void main() {
// 采样纹理
vec4 texColor = texture2D(uTexture, vUv);
vec4 normalColor = texture2D(uNormalMap, vUv);
// 从法线贴图解包法线(0-1 转 -1 到 1)
vec3 normal = normalColor.rgb * 2.0 - 1.0;
gl_FragColor = texColor;
}
`;
const material = new THREE.ShaderMaterial({
uniforms: {
uTexture: { value: textureLoader.load('diffuse.jpg') },
uNormalMap: { value: textureLoader.load('normal.jpg') }
},
vertexShader,
fragmentShader
});
调试着色器
着色器调试比较困难,因为没有 console.log。常用技巧:
颜色调试
// 在片元着色器中,用颜色显示数值
float value = 0.5; // 要调试的值
// 显示为灰度
gl_FragColor = vec4(vec3(value), 1.0);
// 显示为红色(正数)或绿色(负数)
if (value > 0.0) {
gl_FragColor = vec4(value, 0.0, 0.0, 1.0);
} else {
gl_FragColor = vec4(0.0, -value, 0.0, 1.0);
}
// 显示向量
vec3 v = vec3(1.0, 0.5, 0.3);
gl_FragColor = vec4(normalize(v) * 0.5 + 0.5, 1.0); // 将 -1~1 映射到 0~1
编译错误捕获
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader
});
// 检查编译错误
renderer.compile(scene, camera);
const gl = renderer.getContext();
const program = renderer.properties.get(material).currentProgram;
const programLog = gl.getProgramInfoLog(program);
const vertexLog = gl.getShaderInfoLog(program.vertexShader);
const fragmentLog = gl.getShaderInfoLog(program.fragmentShader);
if (programLog || vertexLog || fragmentLog) {
console.error('着色器错误:', { programLog, vertexLog, fragmentLog });
}
性能优化
减少计算
// 不好的做法:在片元着色器中重复计算
void main() {
float value = sin(uTime * 2.0); // 每个像素都计算
gl_FragColor = vec4(vec3(value), 1.0);
}
// 好的做法:在顶点着色器计算,传递到片元着色器
// vertex shader
varying float vValue;
void main() {
vValue = sin(uTime * 2.0); // 每个顶点计算一次
gl_Position = ...;
}
// fragment shader
varying float vValue;
void main() {
gl_FragColor = vec4(vec3(vValue), 1.0);
}
避免分支
// 分支会影响 GPU 并行性能
if (condition) {
color = colorA;
} else {
color = colorB;
}
// 使用 mix 替代
color = mix(colorB, colorA, condition ? 1.0 : 0.0);
// 或者使用 step
color = mix(colorB, colorA, step(0.5, value));
使用内置函数
// 内置函数通常是优化过的硬件实现
// 比自己实现更快
length(v) // 而不是 sqrt(dot(v, v))
distance(a, b) // 而不是 length(a - b)
mix(a, b, t) // 而不是 a + (b - a) * t
完整示例
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 自定义着色器材质
const vertexShader = `
uniform float uTime;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying float vDisplacement;
// 噪声函数
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
void main() {
vUv = uv;
vNormal = normal;
// 噪声变形
float noise = snoise(position * 2.0 + uTime * 0.5);
vec3 newPosition = position + normal * noise * 0.3;
vDisplacement = noise;
vPosition = newPosition;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
uniform vec3 uColorA;
uniform vec3 uColorB;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying float vDisplacement;
void main() {
// 基于变形的颜色
float mixStrength = vDisplacement * 0.5 + 0.5;
vec3 color = mix(uColorA, uColorB, mixStrength);
// 简单光照
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diff = max(dot(normalize(vNormal), lightDir), 0.0);
// 边缘光
vec3 viewDir = normalize(cameraPosition - vPosition);
float fresnel = pow(1.0 - abs(dot(normalize(vNormal), viewDir)), 2.0);
vec3 finalColor = color * (diff * 0.7 + 0.3) + fresnel * 0.5;
gl_FragColor = vec4(finalColor, 1.0);
}
`;
// 创建物体
const geometry = new THREE.IcosahedronGeometry(1, 64);
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColorA: { value: new THREE.Color(0x0066ff) },
uColorB: { value: new THREE.Color(0xff6600) }
},
vertexShader,
fragmentShader
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 动画
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
material.uniforms.uTime.value = elapsed;
mesh.rotation.y = elapsed * 0.2;
controls.update();
renderer.render(scene, camera);
}
animate();
// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
小结
着色器是 Three.js 中最强大但也最复杂的特性:
- GLSL 是着色器语言,类似 C 语言
- 顶点着色器处理顶点位置,片元着色器计算像素颜色
- Uniform 是全局变量,Varying 用于传递数据,Attribute 是顶点属性
- Three.js 提供了丰富的内置变量和矩阵
- 着色器调试困难,可以使用颜色输出调试
- 性能优化包括减少计算、避免分支、使用内置函数
掌握着色器可以创建独特的视觉效果,但也需要注意性能和兼容性问题。