一、着色器基础概念
1.1 什么是着色器
着色器是一种运行在 GPU 上的小程序,用于实现图形渲染中的特定计算任务。在 Three.js 中,着色器主要负责控制 3D 模型的外观表现,包括颜色、光照、纹理等效果。
1.2 着色器的分类
Three.js 中主要使用两种着色器:
- 顶点着色器 (Vertex Shader):处理每个顶点的位置、法线等属性,决定顶点在屏幕上的位置
- 片元着色器 (Fragment Shader):处理每个像素的颜色计算,决定最终显示的颜色值
1.3 着色器的工作流程
着色器的基本工作流程如下:
- CPU 将 3D 模型数据 (顶点坐标、法线、纹理坐标等) 发送到 GPU
- 顶点着色器对每个顶点进行变换计算,确定顶点在裁剪空间中的位置
- GPU 进行光栅化处理,将 3D 模型转换为 2D 像素
- 片元着色器对每个像素进行颜色计算,确定最终显示的颜色
- 最终颜色被写入帧缓冲区,显示在屏幕上
二、Three.js 中的着色器实现
2.1 ShaderMaterial
Three.js 提供了 ShaderMaterial 类,用于创建自定义着色器材质。使用 ShaderMaterial 需要提供顶点着色器和片元着色器的源代码。
下面是一个简单的 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: vertexShader,
fragmentShader: fragmentShader
});
// 创建几何体并应用材质
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
2.2 RawShaderMaterial
与 ShaderMaterial 类似,RawShaderMaterial 也用于创建自定义着色器材质。不同之处在于,RawShaderMaterial 不会自动为着色器添加 Three.js 的内置 uniforms 和 attributes。
下面是一个使用 RawShaderMaterial 的示例:
// 创建自定义着色器材质
const vertexShader = `
// 声明顶点位置属性
attribute vec3 position;
// 声明变换矩阵
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
void main() {
// 将顶点位置从模型空间转换到裁剪空间
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
void main() {
// 设置所有像素颜色为蓝色
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
`;
// 创建材质
const material = new THREE.RawShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
modelMatrix: { value: new THREE.Matrix4() },
viewMatrix: { value: new THREE.Matrix4() },
projectionMatrix: { value: new THREE.Matrix4() }
}
});
// 创建几何体并应用材质
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
三、顶点着色器详解
3.1 顶点着色器的基本结构
顶点着色器的基本结构如下:
// 输入属性
attribute vec3 position; // 顶点位置
attribute vec3 normal; // 顶点法线
attribute vec2 uv; // 纹理坐标
// 全局变量
uniform mat4 modelMatrix; // 模型矩阵
uniform mat4 viewMatrix; // 视图矩阵
uniform mat4 projectionMatrix; // 投影矩阵
// 输出变量
varying vec2 vUv; // 传递给片元着色器的纹理坐标
void main() {
// 将顶点位置从模型空间转换到裁剪空间
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
// 将纹理坐标传递给片元着色器
vUv = uv;
}
3.2 顶点着色器内置变量
顶点着色器中有一些重要的内置变量:
- gl_Position:顶点在裁剪空间中的位置,必须赋值
- gl_VertexID:顶点的唯一标识符
- gl_InstanceID:实例化渲染时的实例 ID
3.3 顶点变换过程
顶点在渲染过程中会经历多个坐标空间的变换:
- 模型空间 (Model Space):顶点在模型本地的坐标
- 世界空间 (World Space):顶点在整个场景中的坐标
- 视图空间 (View Space):顶点相对于相机的坐标
- 裁剪空间 (Clip Space):经过投影变换后的坐标
- 规范化设备坐标空间 (NDC Space):裁剪空间经过透视除法后的坐标
- 屏幕空间 (Screen Space):最终显示在屏幕上的坐标
顶点着色器主要完成从模型空间到裁剪空间的变换:
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
四、片元着色器详解
4.1 片元着色器的基本结构
片元着色器的基本结构如下:
// 输入变量
varying vec2 vUv; // 从顶点着色器传递过来的纹理坐标
// 全局变量
uniform sampler2D texture; // 纹理
uniform vec3 color; // 颜色
void main() {
// 从纹理中采样颜色
vec4 texelColor = texture2D(texture, vUv);
// 最终颜色
gl_FragColor = vec4(color * texelColor.rgb, 1.0);
}
4.2 片元着色器内置变量
片元着色器中最重要的内置变量是:
- gl_FragColor:片元的最终颜色,必须赋值
- gl_FragCoord:片元在窗口坐标系中的坐标
- gl_FrontFacing:指示当前片元是否属于正面
- gl_PointCoord:点精灵的坐标
4.3 纹理采样
在片元着色器中,可以通过 texture2D 函数从纹理中采样颜色:
vec4 texelColor = texture2D(texture, vUv);
其中,texture 是纹理采样器,vUv 是纹理坐标。
五、着色器中的数据传递
5.1 Attributes
Attributes 是从 CPU 传递到顶点着色器的 per-vertex 数据,通常用于存储顶点位置、法线、纹理坐标等。
attribute vec3 position; // 顶点位置
attribute vec3 normal; // 顶点法线
attribute vec2 uv; // 纹理坐标
5.2 Uniforms
Uniforms 是从 CPU 传递到着色器的全局变量,在整个渲染过程中保持不变。
uniform mat4 modelMatrix; // 模型矩阵
uniform mat4 viewMatrix; // 视图矩阵
uniform mat4 projectionMatrix; // 投影矩阵
uniform vec3 color; // 颜色
uniform sampler2D texture; // 纹理
在 JavaScript 中设置 uniforms 的示例:
material.uniforms.color.value = new THREE.Color(0xff0000);
material.uniforms.texture.value = texture;
5.3 Varyings
Varyings 是从顶点着色器传递到片元着色器的变量,用于在两者之间传递数据。
顶点着色器中:
varying vec2 vUv;
void main() {
vUv = uv;
// ...
}
片元着色器中:
varying vec2 vUv;
void main() {
vec4 texelColor = texture2D(texture, vUv);
// ...
}
六、高级着色器技术
6.1 光照计算
在着色器中实现光照计算可以创造更真实的视觉效果。下面是一个简单的漫反射光照计算示例:
// 顶点着色器
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
uniform vec3 lightPosition;
varying vec3 vNormal;
varying vec3 vLightDirection;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewMatrix * worldPosition;
vNormal = normalize(normalMatrix * normal);
vLightDirection = normalize(lightPosition - worldPosition.xyz);
}
// 片元着色器
varying vec3 vNormal;
varying vec3 vLightDirection;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main() {
// 计算漫反射光照强度
float diff = max(dot(vNormal, vLightDirection), 0.0);
// 计算最终颜色
vec3 diffuse = diff * lightColor * objectColor;
gl_FragColor = vec4(diffuse, 1.0);
}
6.2 纹理映射
纹理映射是将 2D 纹理应用到 3D 模型表面的过程。下面是一个使用纹理的示例:
// 顶点着色器
attribute vec3 position;
attribute vec2 uv;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
varying vec2 vUv;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
vUv = uv;
}
// 片元着色器
varying vec2 vUv;
uniform sampler2D texture;
void main() {
vec4 texelColor = texture2D(texture, vUv);
gl_FragColor = texelColor;
}
6.3 后处理效果
后处理是在场景渲染完成后应用的效果,可以创造各种视觉效果。下面是一个简单的灰度效果后处理示例:
// 顶点着色器
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
// 片元着色器
varying vec2 vUv;
uniform sampler2D tDiffuse;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
// 计算灰度值
float gray = dot(texel.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(gray, gray, gray, texel.a);
}
七、性能优化
7.1 减少着色器复杂度
复杂的着色器计算会降低性能,尽量简化着色器中的计算。
7.2 批量处理
使用 BufferGeometry 和 InstancedBufferGeometry 可以减少绘制调用,提高性能。
7.3 纹理优化
使用合适的纹理格式和尺寸,避免过大的纹理。
7.4 使用内置材质
Three.js 提供了多种内置材质,如 MeshBasicMaterial、MeshLambertMaterial、MeshPhongMaterial 等,这些材质经过了性能优化,在大多数情况下可以直接使用。
八、调试技巧
8.1 使用 console.log
在 JavaScript 中,可以使用 console.log 输出调试信息。
8.2 使用 debug 面板
Three.js 提供了一个简单的调试面板,可以显示帧率、内存使用等信息。
const stats = new Stats();
document.body.appendChild(stats.dom);
function animate() {
requestAnimationFrame(animate);
stats.update();
renderer.render(scene, camera);
}
8.3 使用浏览器开发者工具
现代浏览器的开发者工具提供了强大的着色器调试功能,可以查看着色器代码、设置断点、检查变量值等。
九、实践案例
9.1 实现一个波浪效果
下面是一个实现波浪效果的完整示例:
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建波浪材质
const vertexShader = `
uniform float time;
uniform float amplitude;
uniform float frequency;
varying vec3 vNormal;
varying vec2 vUv;
void main() {
vUv = uv;
// 计算波浪高度
float height = amplitude * sin(position.x * frequency + time) * sin(position.y * frequency + time);
// 修改顶点位置
vec3 newPosition = position + normal * height;
// 计算变换后的法线
mat3 normalMatrix = mat3(transpose(inverse(modelMatrix)));
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
`;
const fragmentShader = `
varying vec3 vNormal;
varying vec2 vUv;
uniform vec3 lightDirection;
uniform vec3 lightColor;
uniform vec3 baseColor;
void main() {
// 计算漫反射光照
float diffuse = max(dot(vNormal, normalize(lightDirection)), 0.0);
// 最终颜色
vec3 color = baseColor * (0.5 + diffuse * 0.5);
gl_FragColor = vec4(color, 1.0);
}
`;
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
time: { value: 0.0 },
amplitude: { value: 0.5 },
frequency: { value: 2.0 },
lightDirection: { value: new THREE.Vector3(1, 1, 1) },
lightColor: { value: new THREE.Vector3(1, 1, 1) },
baseColor: { value: new THREE.Vector3(0.2, 0.5, 0.8) }
}
});
// 创建平面几何体
const geometry = new THREE.PlaneGeometry(10, 10, 100, 100);
// 创建网格
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新时间
material.uniforms.time.value += 0.05;
renderer.render(scene, camera);
}
animate();
// 响应窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
9.2 实现一个自定义粒子系统
下面是一个实现自定义粒子系统的示例:
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 10;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建粒子材质
const vertexShader = `
attribute vec3 position;
attribute vec3 velocity;
attribute float startTime;
attribute float lifeTime;
uniform float time;
uniform float size;
varying float vOpacity;
void main() {
// 计算粒子存活时间
float age = time - startTime;
// 如果粒子还未激活或已死亡,则将其放置在远处
if (age < 0.0 || age > lifeTime) {
gl_Position = projectionMatrix * modelViewMatrix * vec4(10000.0, 10000.0, 10000.0, 1.0);
vOpacity = 0.0;
return;
}
// 计算粒子当前位置
vec3 pos = position + velocity * age;
// 计算粒子透明度(生命周期的开始和结束时透明度较低)
float lifeRatio = age / lifeTime;
vOpacity = 1.0 - smoothstep(0.0, 0.2, lifeRatio) - smoothstep(0.8, 1.0, lifeRatio);
// 设置粒子大小和位置
gl_PointSize = size * (1.0 - lifeRatio);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShader = `
varying float vOpacity;
void main() {
// 创建圆形粒子
float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
if (dist > 0.5) discard;
// 设置粒子颜色和透明度
gl_FragColor = vec4(1.0, 0.8, 0.4, vOpacity);
}
`;
// 创建粒子系统
const count = 1000;
const geometry = new THREE.BufferGeometry();
// 创建属性数组
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
const startTimes = new Float32Array(count);
const lifeTimes = new Float32Array(count);
// 初始化粒子属性
for (let i = 0; i < count; i++) {
// 位置(从原点发射)
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
// 速度(随机方向)
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 2 + 1;
velocities[i * 3] = Math.cos(angle) * speed;
velocities[i * 3 + 1] = Math.sin(angle) * speed;
velocities[i * 3 + 2] = Math.random() * 2 - 1;
// 开始时间(随机延迟)
startTimes[i] = Math.random() * 5;
// 生命周期
lifeTimes[i] = Math.random() * 3 + 2;
}
// 设置几何体属性
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geometry.setAttribute('startTime', new THREE.BufferAttribute(startTimes, 1));
geometry.setAttribute('lifeTime', new THREE.BufferAttribute(lifeTimes, 1));
// 创建材质
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
time: { value: 0.0 },
size: { value: 20.0 }
},
transparent: true,
depthWrite: false
});
// 创建粒子系统
const particles = new THREE.Points(geometry, material);
scene.add(particles);
// 动画循环
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// 更新时间
material.uniforms.time.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
// 响应窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
十、总结
着色器是 Three.js 中非常强大的功能,通过自定义着色器可以实现各种炫酷的视觉效果。本文介绍了 Three.js 中着色器的基本原理、结构和使用方法,包括顶点着色器、片元着色器、数据传递方式等基础知识,以及光照计算、纹理映射、后处理等高级技术。同时,还提供了性能优化和调试技巧,以及两个实践案例帮助理解和应用着色器。希望本文能帮助你更好地掌握 Three.js 着色器编程。