Skip to main content

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