Three.js 着色器原理解析

622 阅读10分钟

一、着色器基础概念

1.1 什么是着色器

着色器是一种运行在 GPU 上的小程序,用于实现图形渲染中的特定计算任务。在 Three.js 中,着色器主要负责控制 3D 模型的外观表现,包括颜色、光照、纹理等效果。

1.2 着色器的分类

Three.js 中主要使用两种着色器:

  • 顶点着色器 (Vertex Shader):处理每个顶点的位置、法线等属性,决定顶点在屏幕上的位置
  • 片元着色器 (Fragment Shader):处理每个像素的颜色计算,决定最终显示的颜色值

1.3 着色器的工作流程

着色器的基本工作流程如下:

  1. CPU 将 3D 模型数据 (顶点坐标、法线、纹理坐标等) 发送到 GPU
  1. 顶点着色器对每个顶点进行变换计算,确定顶点在裁剪空间中的位置
  1. GPU 进行光栅化处理,将 3D 模型转换为 2D 像素
  1. 片元着色器对每个像素进行颜色计算,确定最终显示的颜色
  1. 最终颜色被写入帧缓冲区,显示在屏幕上

二、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 顶点变换过程

顶点在渲染过程中会经历多个坐标空间的变换:

  1. 模型空间 (Model Space):顶点在模型本地的坐标
  1. 世界空间 (World Space):顶点在整个场景中的坐标
  1. 视图空间 (View Space):顶点相对于相机的坐标
  1. 裁剪空间 (Clip Space):经过投影变换后的坐标
  1. 规范化设备坐标空间 (NDC Space):裁剪空间经过透视除法后的坐标
  1. 屏幕空间 (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 着色器编程。