跳到主要内容

着色器编程

着色器是运行在 GPU 上的小程序,可以直接控制顶点位置和像素颜色。通过自定义着色器,你可以创建独特的视觉效果,实现标准材质无法达到的表现。本章将介绍 GLSL 着色器语言基础以及如何在 Three.js 中使用自定义着色器。

着色器基础概念

渲染管线

理解着色器之前,需要了解 GPU 渲染管线的基本流程:

  1. 顶点数据:顶点位置、法线、UV 等属性
  2. 顶点着色器:处理每个顶点,计算最终位置
  3. 图元装配:将顶点组装成三角形
  4. 光栅化:将三角形转换为像素片段
  5. 片元着色器:计算每个像素的颜色
  6. 输出合并:深度测试、混合等

顶点着色器和片元着色器是我们可编程的阶段。

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 提供了丰富的内置变量和矩阵
  • 着色器调试困难,可以使用颜色输出调试
  • 性能优化包括减少计算、避免分支、使用内置函数

掌握着色器可以创建独特的视觉效果,但也需要注意性能和兼容性问题。