15. Three.js 源码级解析:GLSL 的工程化艺术
从“手写原生 WebGL”回到“使用 Three.js”,你可能会觉得 Three.js 是一个黑盒。 实际上,Three.js 的 Shader 系统是模块化和工程化的典范。
本章我们将拆解 Three.js 源码中处理 GLSL 的核心机制:ShaderChunk。
1. 痛点:为什么需要模块化?
如果你写过复杂的 Shader,你会发现:
- 所有光照计算(Phong、PBR)都要重写一遍?
- 所有雾效、阴影、骨骼动画逻辑都要复制粘贴?
- 代码动辄几千行,难以维护。
Three.js 的解决方案是:像拼积木一样拼凑 Shader。
2. 核心机制:ShaderChunk
在 Three.js 源码中,有一个巨大的 ShaderChunk 对象。它把 GLSL 代码拆成了几百个小片段。
2.1 源码一瞥
你可以在 three/src/renderers/shaders/ShaderChunk 下找到它们。
common.glsl.js (最基础的变量):
#define PI 3.141592653589793
#define PI2 6.283185307179586
#define EPSILON 1e-6
// 一些通用的数学函数
float luminance( const in vec3 rgb ) {
return dot( rgb, vec3( 0.2126, 0.7152, 0.0722 ) );
}
lights_phong_pars_fragment.glsl.js (Phong 光照需要的变量):
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
varying vec3 vNormal;
#endif
struct BlinnPhongMaterial {
vec3 diffuseColor;
vec3 specularColor;
float specularShininess;
float specularStrength;
};
2.2 组装工厂:ShaderLib
当你创建一个 MeshPhongMaterial 时,Three.js 实际上是在后台做字符串拼接:
// 伪代码:Three.js 内部如何组装 Shader
const vertexShader = [
"#include <common>", // 引入通用变量
"#include <uv_pars_vertex>", // 引入 UV 变量定义
"#include <color_pars_vertex>", // 引入顶点颜色变量
"#include <fog_pars_vertex>", // 引入雾效变量
"void main() {",
"#include <uv_vertex>", // 执行 UV 赋值 vUv = uv;
"#include <color_vertex>", // 执行颜色赋值
"#include <begin_vertex>", // 你的自定义变换逻辑通常插在这里
"#include <project_vertex>", // 执行 gl_Position = P * V * M * pos;
"}",
].join("\n");
3. 实战技巧:onBeforeCompile
掌握了这个原理,你就不需要重写整个 ShaderMaterial 了。你可以直接修改 Three.js 的内置材质!
需求:给标准的 MeshStandardMaterial 增加一个“顶点随时间波动”的效果,但保留原有的光照、阴影和 PBR 效果。
做法:使用 onBeforeCompile 钩子,用 replace 替换掉积木块。
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
material.onBeforeCompile = (shader) => {
// 1. 注入 uniform 变量
shader.uniforms.uTime = { value: 0 };
// 保存引用以便在 render loop 中更新
material.userData.shader = shader;
// 2. 修改 Vertex Shader
// A. 在 main() 之前注入变量定义
shader.vertexShader = shader.vertexShader.replace(
"#include <common>",
`
#include <common>
uniform float uTime;
`
);
// B. 修改顶点位置逻辑
// <begin_vertex> 是 Three.js 计算 vec3 transformed = vec3( position ); 的地方
shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
`
#include <begin_vertex>
// 在这里修改 transformed 变量
transformed.y += sin(transformed.x * 2.0 + uTime) * 0.5;
`
);
};
优点:
- 省事:不需要重写 PBR 光照逻辑。
- 兼容:仍然支持阴影、雾效等所有 Three.js 特性。
4. 必背:常用 Chunk 字典
在修改 Shader 时,知道“改哪里”最重要。
Vertex Shader 关键点
| Chunk 名 | 原始内容 | 作用 | 修改建议 |
|---|---|---|---|
<common> | 常量定义 | 全局变量 | 注入 uniforms / attributes 定义 |
<begin_vertex> | vec3 transformed = vec3( position ); | 局部坐标 | 修改顶点位置 (波浪、膨胀) |
<project_vertex> | gl_Position = projection * ... | 投影变换 | 很少修改,除非做非线性投影 |
<worldpos_vertex> | 计算世界坐标 | 世界坐标 | 很少修改 |
Fragment Shader 关键点
| Chunk 名 | 原始内容 | 作用 | 修改建议 |
|---|---|---|---|
<color_fragment> | 颜色计算 | 基础颜色 | 修改物体颜色 (渐变、纹理混合) |
<map_fragment> | 纹理采样 | 贴图处理 | 修改 UV 采样逻辑 |
<normal_fragment_begin> | 法线计算 | 法线贴图 | 修改法线 (做类似 bump map 效果) |
<dithering_fragment> | 抖动处理 | 结束前 | 最后的后处理 (如伽马校正前) |
5. 总结
- Three.js 的 Shader 不是一块铁板,而是由无数个
#include <chunk_name>组成的积木塔。 - 初级玩家:用
ShaderMaterial从零写 Shader。 - 高级玩家:用
onBeforeCompile对现有材质进行“微创手术”,保留原生光照系统的同时实现特效。 - 查阅方法:遇到不懂的 Chunk,直接去 Three.js GitHub 仓库搜
ShaderChunk/chunk_name.glsl。