Three.js 着色器编译与程序管理:深度技术解析
Why
three.js 的 Shader 编译与程序管理系统的核心设计哲学是:用参数化模板 + 哈希缓存,将 GPU 上昂贵的 shader 编译成本摊薄到整个应用生命周期。
具体来说,WebGLPrograms 在每次渲染前调用 getParameters() 将材质属性、灯光数量、场景雾效、渲染器状态等约 100 个字段收集成一个扁平参数对象,再由 getProgramCacheKey() 将其序列化为一个字符串 key,然后在 programsMap(一个 Map<string, WebGLProgram>)中查找------命中则直接复用,未命中才真正调用 gl.compileShader() + gl.linkProgram()。 1
GLSL 源码本身则通过 ShaderChunk(100+ 个 .glsl.js 片段)+ #include 递归展开来组合,配合 replaceLightNums() 在编译时将 NUM_DIR_LIGHTS 等占位符替换为实际数字,以及 #pragma unroll_loop 展开定长循环,使得一套模板 shader 能覆盖几乎所有材质变体,而不需要为每种组合手写独立的 GLSL 文件。 2
程序的生命周期由引用计数(usedTimes)管理:acquireProgram 时 +1,releaseProgram 时 -1,归零才真正销毁 GPU 资源;uniform 位置查询则通过 onFirstUse() 延迟到第一次实际渲染时才执行,避免编译阶段的无效开销。 3
给前端开发者的一句话总结:
WebGL shader 编译是同步阻塞 GPU 的高成本操作,three.js 用"参数哈希 → Map 缓存 → 引用计数"这套组合拳,把编译次数压到最低,同时提供
renderer.compileAsync()让你在场景加载阶段预热缓存,彻底消除首帧的"shader stutter"卡顿------这套"按需编译 + 结构化缓存"的思路,在任何需要管理昂贵异步资源(如 WebAssembly 模块、Worker、GPU Buffer)的前端场景中都值得借鉴。 4 5
一、为什么需要了解这套机制?
在 WebGL 中,每次 draw call 都需要一个已链接的 WebGL Program (着色器程序)。如果每帧都重新编译着色器,性能将灾难性地下降。Three.js 设计了一套精密的着色器编译与缓存系统,核心目标是:
- 将 GLSL 代码拆分为可复用的片段(Chunk)
- 根据材质/场景状态动态组装最终 GLSL
- 通过哈希缓存避免重复编译
- 延迟错误检查到首次使用时
整个系统由以下几个模块协作完成:

二、ShaderChunk:GLSL 片段注册表
ShaderChunk 是一个平铺的对象 ,键为字符串名称,值为 GLSL 源码字符串。每个条目对应一个 .glsl.js 文件。 1
片段分两类:
| 类型 | 命名规范 | 示例 | 用途 |
|---|---|---|---|
| 可复用片段 | <主题>_<阶段> |
shadowmap_pars_vertex |
被多个着色器 #include |
| 完整着色器 | <名称>_vert/frag |
meshphysical_frag |
完整的 main 程序 |
常见片段分类:
| 前缀 | 示例 | 作用 |
|---|---|---|
common |
common |
数学工具、PI 常量 |
lights_* |
lights_physical_pars_fragment |
光照循环模板 |
shadowmap_* |
shadowmap_pars_vertex |
阴影坐标计算 |
fog_* |
fog_pars_fragment |
雾效混合 |
colorspace_* |
colorspace_pars_fragment |
色彩空间转换 |
#include 递归展开
着色器源码中使用 #include <chunk_name> 指令,由 resolveIncludes 函数在编译时做递归文本替换 : 2
javascript
// 匹配 #include <...> 指令
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ];
// 递归展开(chunk 内部也可能有 #include)
return resolveIncludes( string );
}
三、ShaderLib:内置着色器模板库
ShaderLib 将 ShaderChunk 中的完整着色器与 UniformsLib 中的 uniform 组合在一起,形成每种材质的着色器定义 : 3
javascript
const ShaderLib = {
basic: {
uniforms: mergeUniforms([ UniformsLib.common, UniformsLib.envmap, ... ]),
vertexShader: ShaderChunk.meshbasic_vert,
fragmentShader: ShaderChunk.meshbasic_frag
},
lambert: {
uniforms: mergeUniforms([ UniformsLib.common, UniformsLib.lights, ... ]),
vertexShader: ShaderChunk.meshlambert_vert,
fragmentShader: ShaderChunk.meshlambert_frag
},
// ...
};
材质类型 → ShaderLib ID 映射 (定义在 WebGLPrograms 中): 4
| 材质类 | ShaderLib ID | 着色器源 |
|---|---|---|
MeshBasicMaterial |
basic |
meshbasic_vert/frag |
MeshLambertMaterial |
lambert |
meshlambert_vert/frag |
MeshPhongMaterial |
phong |
meshphong_vert/frag |
MeshStandardMaterial |
physical |
meshphysical_vert/frag |
MeshPhysicalMaterial |
physical |
meshphysical_vert/frag |
ShaderMaterial / RawShaderMaterial |
(无,使用用户代码) | 用户自定义 |
四、WebGLPrograms:程序缓存管理器
WebGLPrograms 是整个系统的调度中心 ,每个渲染器实例化一次,管理所有 WebGLProgram 的生命周期。 5
4.1 getParameters:收集着色器参数
这是最关键的函数,它检查材质、几何体、灯光、场景、渲染器状态,生成一个包含约 100 个布尔/数值字段 的 parameters 对象: 6
javascript
const HAS_MAP = !! material.map;
const HAS_NORMALMAP = !! material.normalMap;
const HAS_CLEARCOAT = material.clearcoat > 0;
const HAS_TRANSMISSION = material.transmission > 0;
// ...
const parameters = {
map: HAS_MAP,
normalMap: HAS_NORMALMAP,
numDirLights: lights.directional.length,
numPointLights: lights.point.length,
shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
toneMapping: toneMapping,
// ... ~100 个字段
};
```[7](#0-6)
### 4.2 `getProgramCacheKey`:生成缓存键
将 `parameters` 序列化为一个字符串,作为程序缓存的 key。布尔值通过位掩码(`Layers` 对象)压缩,数值直接追加: [8](#0-7) [9](#0-8)
cacheKey = "physical,USE_MAP,USE_NORMALMAP,3,2,0,1,..."
### 4.3 `acquireProgram`:获取或创建程序 [10](#0-9)
```javascript
function acquireProgram( parameters, cacheKey ) {
let program = programsMap.get( cacheKey );
if ( program !== undefined ) {
++ program.usedTimes; // 命中缓存,引用计数 +1
} else {
program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
programsMap.set( cacheKey, program );
}
return program;
}
function releaseProgram( program ) {
if ( -- program.usedTimes === 0 ) {
program.destroy(); // 引用归零,释放 WebGL 资源
programsMap.delete( program.cacheKey );
}
}
五、WebGLProgram:单个程序的编译生命周期
WebGLProgram 构造函数完成从参数对象到 GPU 可执行程序的全部工作:
"onFirstUse() 延迟" "WebGLRenderingContext" "WebGLProgram 构造函数" "acquireProgram()" "onFirstUse() 延迟" "WebGLRenderingContext" "WebGLProgram 构造函数" "acquireProgram()" #mermaid-svg-34yzeG4TshrSVEGu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-34yzeG4TshrSVEGu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-34yzeG4TshrSVEGu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-34yzeG4TshrSVEGu .error-icon{fill:#552222;}#mermaid-svg-34yzeG4TshrSVEGu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-34yzeG4TshrSVEGu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-34yzeG4TshrSVEGu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-34yzeG4TshrSVEGu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-34yzeG4TshrSVEGu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-34yzeG4TshrSVEGu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-34yzeG4TshrSVEGu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-34yzeG4TshrSVEGu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-34yzeG4TshrSVEGu .marker.cross{stroke:#333333;}#mermaid-svg-34yzeG4TshrSVEGu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-34yzeG4TshrSVEGu p{margin:0;}#mermaid-svg-34yzeG4TshrSVEGu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-34yzeG4TshrSVEGu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-34yzeG4TshrSVEGu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-34yzeG4TshrSVEGu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-34yzeG4TshrSVEGu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-34yzeG4TshrSVEGu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-34yzeG4TshrSVEGu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-34yzeG4TshrSVEGu .sequenceNumber{fill:white;}#mermaid-svg-34yzeG4TshrSVEGu #sequencenumber{fill:#333;}#mermaid-svg-34yzeG4TshrSVEGu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-34yzeG4TshrSVEGu .messageText{fill:#333;stroke:none;}#mermaid-svg-34yzeG4TshrSVEGu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-34yzeG4TshrSVEGu .labelText,#mermaid-svg-34yzeG4TshrSVEGu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-34yzeG4TshrSVEGu .loopText,#mermaid-svg-34yzeG4TshrSVEGu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-34yzeG4TshrSVEGu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-34yzeG4TshrSVEGu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-34yzeG4TshrSVEGu .noteText,#mermaid-svg-34yzeG4TshrSVEGu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-34yzeG4TshrSVEGu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-34yzeG4TshrSVEGu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-34yzeG4TshrSVEGu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-34yzeG4TshrSVEGu .actorPopupMenu{position:absolute;}#mermaid-svg-34yzeG4TshrSVEGu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-34yzeG4TshrSVEGu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-34yzeG4TshrSVEGu .actor-man circle,#mermaid-svg-34yzeG4TshrSVEGu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-34yzeG4TshrSVEGu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 首次渲染时触发 new WebGLProgram(renderer, cacheKey, parameters, bindingStates) 1. 生成 prefixVertex / prefixFragment( 2. resolveIncludes() 递归展开 3. replaceLightNums() 替换 NUM_DIR_LIGHTS 等占位符 4. unrollLoops() 展开 gl.createProgram() WebGLShader → gl.compileShader(vertexShader) WebGLShader → gl.compileShader(fragmentShader) gl.linkProgram(program) program.getUniforms() cachedUniforms = new WebGLUniforms(gl, program) cachedAttributes = fetchAttributeLocations(gl, program)
5.1 #define 宏注入(prefixVertex / prefixFragment)
这是 Three.js 实现条件编译 的核心手段。根据 parameters 中的布尔值,动态拼接 #define 列表,前置到 GLSL 源码之前: 11
glsl
// 自动生成的 prefixVertex 示例(有法线贴图 + 阴影):
precision highp float;
#define SHADER_TYPE MeshStandardMaterial
#define USE_MAP
#define USE_NORMALMAP
#define USE_NORMALMAP_TANGENTSPACE
#define USE_SHADOWMAP
#define SHADOWMAP_TYPE_PCF_SOFT
uniform mat4 modelMatrix;
// ...
GLSL 源码中通过 #ifdef 条件编译对应功能:
glsl
// 在 normalmap_pars_fragment.glsl.js 中
#ifdef USE_NORMALMAP
uniform sampler2D normalMap;
// ...
#endif
5.2 数值占位符替换
replaceLightNums 将 GLSL 中的文本占位符替换为运行时实际数量,使 GLSL 能使用固定大小数组: 12
javascript
// GLSL 源码中写:
// uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
// 编译时替换为:
// uniform DirectionalLight directionalLights[ 3 ];
5.3 循环展开
#pragma unroll_loop_start / #pragma unroll_loop_end 标记的循环在编译时被展开,避免 GPU 上的动态循环开销: 13
glsl
// 源码:
#pragma unroll_loop_start
for ( int i = 0; i < 3; i++ ) {
directionalLight = directionalLights[ i ];
// ...
}
#pragma unroll_loop_end
// 展开后:
directionalLight = directionalLights[ 0 ]; // ...
directionalLight = directionalLights[ 1 ]; // ...
directionalLight = directionalLights[ 2 ]; // ...
5.4 WebGLShader:最小编译单元
WebGLShader 极其简洁,只做一件事: 14
javascript
function WebGLShader( gl, type, string ) {
const shader = gl.createShader( type );
gl.shaderSource( shader, string );
gl.compileShader( shader );
return shader;
}
5.5 并行编译支持(KHR_parallel_shader_compile)
Three.js 支持 KHR_parallel_shader_compile 扩展,允许着色器在后台异步编译,避免主线程卡顿: 15
javascript
// COMPLETION_STATUS_KHR = 0x91B1
this.isReady = function () {
if ( programReady === false ) {
programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );
}
return programReady;
};
六、自定义着色器的缓存:WebGLShaderCache
对于 ShaderMaterial(用户自定义着色器),WebGLShaderCache 负责为相同 GLSL 源码分配稳定的数字 ID,用于缓存键生成: 16
相同 GLSL 源码 → 相同 ID → 相同 cacheKey → 命中缓存
七、完整编译流程总览
#mermaid-svg-52Qdp0FZhB0vnfBJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-52Qdp0FZhB0vnfBJ .error-icon{fill:#552222;}#mermaid-svg-52Qdp0FZhB0vnfBJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-52Qdp0FZhB0vnfBJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .marker.cross{stroke:#333333;}#mermaid-svg-52Qdp0FZhB0vnfBJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-52Qdp0FZhB0vnfBJ p{margin:0;}#mermaid-svg-52Qdp0FZhB0vnfBJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster-label text{fill:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster-label span{color:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster-label span p{background-color:transparent;}#mermaid-svg-52Qdp0FZhB0vnfBJ .label text,#mermaid-svg-52Qdp0FZhB0vnfBJ span{fill:#333;color:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .node rect,#mermaid-svg-52Qdp0FZhB0vnfBJ .node circle,#mermaid-svg-52Qdp0FZhB0vnfBJ .node ellipse,#mermaid-svg-52Qdp0FZhB0vnfBJ .node polygon,#mermaid-svg-52Qdp0FZhB0vnfBJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .rough-node .label text,#mermaid-svg-52Qdp0FZhB0vnfBJ .node .label text,#mermaid-svg-52Qdp0FZhB0vnfBJ .image-shape .label,#mermaid-svg-52Qdp0FZhB0vnfBJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-52Qdp0FZhB0vnfBJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .rough-node .label,#mermaid-svg-52Qdp0FZhB0vnfBJ .node .label,#mermaid-svg-52Qdp0FZhB0vnfBJ .image-shape .label,#mermaid-svg-52Qdp0FZhB0vnfBJ .icon-shape .label{text-align:center;}#mermaid-svg-52Qdp0FZhB0vnfBJ .node.clickable{cursor:pointer;}#mermaid-svg-52Qdp0FZhB0vnfBJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .arrowheadPath{fill:#333333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-52Qdp0FZhB0vnfBJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-52Qdp0FZhB0vnfBJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-52Qdp0FZhB0vnfBJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster text{fill:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ .cluster span{color:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-52Qdp0FZhB0vnfBJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-52Qdp0FZhB0vnfBJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-52Qdp0FZhB0vnfBJ .icon-shape,#mermaid-svg-52Qdp0FZhB0vnfBJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-52Qdp0FZhB0vnfBJ .icon-shape p,#mermaid-svg-52Qdp0FZhB0vnfBJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-52Qdp0FZhB0vnfBJ .icon-shape .label rect,#mermaid-svg-52Qdp0FZhB0vnfBJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-52Qdp0FZhB0vnfBJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-52Qdp0FZhB0vnfBJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-52Qdp0FZhB0vnfBJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
new WebGLProgram 构造
从 ShaderLib 或用户代码获取 GLSL 源码
生成 prefixVertex/prefixFragment
注入 #define 宏
resolveIncludes()
递归展开 #include
replaceLightNums()
NUM_DIR_LIGHTS → 3
unrollLoops()
展开 #pragma unroll_loop
gl.compileShader() + gl.linkProgram()
WebGLRenderer.render()
material + geometry + lights + scene
WebGLPrograms.getParameters()
收集 ~100 个着色器相关状态字段
parameters 对象
map, normalMap, numDirLights, toneMapping...
WebGLPrograms.getProgramCacheKey()
序列化为缓存键字符串
programsMap.get(cacheKey)
program.usedTimes++
直接返回已有 WebGLProgram
programsMap.set(cacheKey, program)
首次使用时 onFirstUse()
缓存 uniform 位置 + attribute 位置
返回 WebGLProgram 实例
八、关键设计总结
| 设计点 | 实现方式 | 文件 |
|---|---|---|
| GLSL 代码复用 | ShaderChunk + #include 递归展开 |
WebGLProgram.js |
| 条件功能开关 | parameters → #define 宏注入 |
WebGLPrograms.js + WebGLProgram.js |
| 避免重复编译 | programsMap 按 cacheKey 缓存 |
WebGLPrograms.js |
| 灯光数量适配 | 文本占位符替换(NUM_DIR_LIGHTS) |
WebGLProgram.js |
| GPU 循环优化 | #pragma unroll_loop 编译期展开 |
WebGLProgram.js |
| 异步编译 | KHR_parallel_shader_compile + isReady() |
WebGLProgram.js |
| 自定义着色器缓存 | WebGLShaderCache 按源码内容去重 |
WebGLShaderCache.js |
| 资源生命周期 | usedTimes 引用计数,归零时 destroy() |
WebGLPrograms.js |
这套机制使得 Three.js 能够用一套通用 GLSL 模板 支撑从最简单的 MeshBasicMaterial 到最复杂的 MeshPhysicalMaterial(含 clearcoat、iridescence、transmission 等),同时保证每种材质配置只编译一次。
Citations
File: src/renderers/webgl/WebGLProgram.js (L1-8)
javascript
import { WebGLUniforms } from './WebGLUniforms.js';
import { WebGLShader } from './WebGLShader.js';
import { ShaderChunk } from '../shaders/ShaderChunk.js';
import { NoToneMapping, AddOperation, MixOperation, MultiplyOperation, CubeRefractionMapping, CubeUVReflectionMapping, CubeReflectionMapping, PCFShadowMap, VSMShadowMap, AgXToneMapping, ACESFilmicToneMapping, NeutralToneMapping, CineonToneMapping, CustomToneMapping, ReinhardToneMapping, LinearToneMapping, GLSL3, LinearTransfer, SRGBTransfer } from '../../constants.js';
import { ColorManagement } from '../../math/ColorManagement.js';
import { Vector3 } from '../../math/Vector3.js';
import { Matrix3 } from '../../math/Matrix3.js';
import { warn, error } from '../../utils.js';
File: src/renderers/webgl/WebGLProgram.js (L214-231)
javascript
function replaceLightNums( string, parameters ) {
const numSpotLightCoords = parameters.numSpotLightShadows + parameters.numSpotLightMaps - parameters.numSpotLightShadowsWithMaps;
return string
.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
.replace( /NUM_SPOT_LIGHT_MAPS/g, parameters.numSpotLightMaps )
.replace( /NUM_SPOT_LIGHT_COORDS/g, numSpotLightCoords )
.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )
.replace( /NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows )
.replace( /NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g, parameters.numSpotLightShadowsWithMaps )
.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows );
}
File: src/renderers/webgl/WebGLProgram.js (L243-276)
javascript
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
return string.replace( includePattern, includeReplacer );
}
const shaderChunkMap = new Map();
function includeReplacer( match, include ) {
let string = ShaderChunk[ include ];
if ( string === undefined ) {
const newInclude = shaderChunkMap.get( include );
if ( newInclude !== undefined ) {
string = ShaderChunk[ newInclude ];
warn( 'WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.', include, newInclude );
} else {
throw new Error( 'THREE.WebGLProgram: Can not resolve #include <' + include + '>' );
}
}
return resolveIncludes( string );
}
File: src/renderers/webgl/WebGLProgram.js (L280-302)
javascript
const unrollLoopPattern = /#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;
function unrollLoops( string ) {
return string.replace( unrollLoopPattern, loopReplacer );
}
function loopReplacer( match, start, end, snippet ) {
let string = '';
for ( let i = parseInt( start ); i < parseInt( end ); i ++ ) {
string += snippet
.replace( /\[\s*i\s*\]/g, '[ ' + i + ' ]' )
.replace( /UNROLLED_LOOP_INDEX/g, i );
}
return string;
}
File: src/renderers/webgl/WebGLProgram.js (L492-530)
javascript
parameters.map ? '#define USE_MAP' : '',
parameters.envMap ? '#define USE_ENVMAP' : '',
parameters.envMap ? '#define ' + envMapModeDefine : '',
parameters.lightMap ? '#define USE_LIGHTMAP' : '',
parameters.aoMap ? '#define USE_AOMAP' : '',
parameters.bumpMap ? '#define USE_BUMPMAP' : '',
parameters.normalMap ? '#define USE_NORMALMAP' : '',
parameters.normalMapObjectSpace ? '#define USE_NORMALMAP_OBJECTSPACE' : '',
parameters.normalMapTangentSpace ? '#define USE_NORMALMAP_TANGENTSPACE' : '',
parameters.displacementMap ? '#define USE_DISPLACEMENTMAP' : '',
parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
parameters.anisotropy ? '#define USE_ANISOTROPY' : '',
parameters.anisotropyMap ? '#define USE_ANISOTROPYMAP' : '',
parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',
parameters.specularMap ? '#define USE_SPECULARMAP' : '',
parameters.specularColorMap ? '#define USE_SPECULAR_COLORMAP' : '',
parameters.specularIntensityMap ? '#define USE_SPECULAR_INTENSITYMAP' : '',
parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
parameters.alphaHash ? '#define USE_ALPHAHASH' : '',
parameters.transmission ? '#define USE_TRANSMISSION' : '',
parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',
parameters.thicknessMap ? '#define USE_THICKNESSMAP' : '',
parameters.sheenColorMap ? '#define USE_SHEEN_COLORMAP' : '',
parameters.sheenRoughnessMap ? '#define USE_SHEEN_ROUGHNESSMAP' : '',
//
File: src/renderers/webgl/WebGLProgram.js (L988-1003)
javascript
// indicate when the program is ready to be used. if the KHR_parallel_shader_compile extension isn't supported,
// flag the program as ready immediately. It may cause a stall when it's first used.
let programReady = ( parameters.rendererExtensionParallelShaderCompile === false );
this.isReady = function () {
if ( programReady === false ) {
programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );
}
return programReady;
};
File: src/renderers/shaders/ShaderLib.js (L9-50)
javascript
const ShaderLib = {
basic: {
uniforms: /*@__PURE__*/ mergeUniforms( [
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.fog
] ),
vertexShader: ShaderChunk.meshbasic_vert,
fragmentShader: ShaderChunk.meshbasic_frag
},
lambert: {
uniforms: /*@__PURE__*/ mergeUniforms( [
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.emissivemap,
UniformsLib.bumpmap,
UniformsLib.normalmap,
UniformsLib.displacementmap,
UniformsLib.fog,
UniformsLib.lights,
{
emissive: { value: /*@__PURE__*/ new Color( 0x000000 ) },
envMapIntensity: { value: 1 }
}
] ),
vertexShader: ShaderChunk.meshlambert_vert,
fragmentShader: ShaderChunk.meshlambert_frag
},
File: src/renderers/webgl/WebGLPrograms.js (L16-23)
javascript
function WebGLPrograms( renderer, environments, extensions, capabilities, bindingStates, clipping ) {
const _programLayers = new Layers();
const _customShaders = new WebGLShaderCache();
const _activeChannels = new Set();
const programs = [];
const programsMap = new Map();
File: src/renderers/webgl/WebGLPrograms.js (L28-44)
javascript
const shaderIDs = {
MeshDepthMaterial: 'depth',
MeshDistanceMaterial: 'distance',
MeshNormalMaterial: 'normal',
MeshBasicMaterial: 'basic',
MeshLambertMaterial: 'lambert',
MeshPhongMaterial: 'phong',
MeshToonMaterial: 'toon',
MeshStandardMaterial: 'physical',
MeshPhysicalMaterial: 'physical',
MeshMatcapMaterial: 'matcap',
LineBasicMaterial: 'basic',
LineDashedMaterial: 'dashed',
PointsMaterial: 'points',
ShadowMaterial: 'shadow',
SpriteMaterial: 'sprite'
};
File: src/renderers/webgl/WebGLPrograms.js (L124-145)
javascript
const HAS_MAP = !! material.map;
const HAS_MATCAP = !! material.matcap;
const HAS_ENVMAP = !! envMap;
const HAS_AOMAP = !! material.aoMap;
const HAS_LIGHTMAP = !! material.lightMap;
const HAS_BUMPMAP = !! material.bumpMap;
const HAS_NORMALMAP = !! material.normalMap;
const HAS_DISPLACEMENTMAP = !! material.displacementMap;
const HAS_EMISSIVEMAP = !! material.emissiveMap;
const HAS_METALNESSMAP = !! material.metalnessMap;
const HAS_ROUGHNESSMAP = !! material.roughnessMap;
const HAS_ANISOTROPY = material.anisotropy > 0;
const HAS_CLEARCOAT = material.clearcoat > 0;
const HAS_DISPERSION = material.dispersion > 0;
const HAS_IRIDESCENCE = material.iridescence > 0;
const HAS_SHEEN = material.sheen > 0;
const HAS_TRANSMISSION = material.transmission > 0;
const HAS_ANISOTROPYMAP = HAS_ANISOTROPY && !! material.anisotropyMap;
File: src/renderers/webgl/WebGLPrograms.js (L185-210)
javascript
const parameters = {
shaderID: shaderID,
shaderType: material.type,
shaderName: material.name,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
defines: material.defines,
customVertexShaderID: customVertexShaderID,
customFragmentShaderID: customFragmentShaderID,
isRawShaderMaterial: material.isRawShaderMaterial === true,
glslVersion: material.glslVersion,
precision: precision,
batching: IS_BATCHEDMESH,
batchingColor: IS_BATCHEDMESH && object._colorsTexture !== null,
instancing: IS_INSTANCEDMESH,
instancingColor: IS_INSTANCEDMESH && object.instanceColor !== null,
instancingMorph: IS_INSTANCEDMESH && object.morphTexture !== null,
outputColorSpace: ( currentRenderTarget === null ) ? renderer.outputColorSpace : ( currentRenderTarget.isXRRenderTarget === true ? currentRenderTarget.texture.colorSpace : ColorManagement.workingColorSpace ),
alphaToCoverage: !! material.alphaToCoverage,
File: src/renderers/webgl/WebGLPrograms.js (L393-430)
javascript
function getProgramCacheKey( parameters ) {
const array = [];
if ( parameters.shaderID ) {
array.push( parameters.shaderID );
} else {
array.push( parameters.customVertexShaderID );
array.push( parameters.customFragmentShaderID );
}
if ( parameters.defines !== undefined ) {
for ( const name in parameters.defines ) {
array.push( name );
array.push( parameters.defines[ name ] );
}
}
if ( parameters.isRawShaderMaterial === false ) {
getProgramCacheKeyParameters( array, parameters );
getProgramCacheKeyBooleans( array, parameters );
array.push( renderer.outputColorSpace );
}
array.push( parameters.customProgramCacheKey );
return array.join();
File: src/renderers/webgl/WebGLPrograms.js (L486-540)
javascript
function getProgramCacheKeyBooleans( array, parameters ) {
_programLayers.disableAll();
if ( parameters.instancing )
_programLayers.enable( 0 );
if ( parameters.instancingColor )
_programLayers.enable( 1 );
if ( parameters.instancingMorph )
_programLayers.enable( 2 );
if ( parameters.matcap )
_programLayers.enable( 3 );
if ( parameters.envMap )
_programLayers.enable( 4 );
if ( parameters.normalMapObjectSpace )
_programLayers.enable( 5 );
if ( parameters.normalMapTangentSpace )
_programLayers.enable( 6 );
if ( parameters.clearcoat )
_programLayers.enable( 7 );
if ( parameters.iridescence )
_programLayers.enable( 8 );
if ( parameters.alphaTest )
_programLayers.enable( 9 );
if ( parameters.vertexColors )
_programLayers.enable( 10 );
if ( parameters.vertexAlphas )
_programLayers.enable( 11 );
if ( parameters.vertexUv1s )
_programLayers.enable( 12 );
if ( parameters.vertexUv2s )
_programLayers.enable( 13 );
if ( parameters.vertexUv3s )
_programLayers.enable( 14 );
if ( parameters.vertexTangents )
_programLayers.enable( 15 );
if ( parameters.anisotropy )
_programLayers.enable( 16 );
if ( parameters.alphaHash )
_programLayers.enable( 17 );
if ( parameters.batching )
_programLayers.enable( 18 );
if ( parameters.dispersion )
_programLayers.enable( 19 );
if ( parameters.batchingColor )
_programLayers.enable( 20 );
if ( parameters.gradientMap )
_programLayers.enable( 21 );
if ( parameters.packedNormalMap )
_programLayers.enable( 22 );
if ( parameters.vertexNormals )
_programLayers.enable( 23 );
array.push( _programLayers.mask );
_programLayers.disableAll();
File: src/renderers/webgl/WebGLPrograms.js (L613-651)
javascript
function acquireProgram( parameters, cacheKey ) {
let program = programsMap.get( cacheKey );
if ( program !== undefined ) {
++ program.usedTimes;
} else {
program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
programs.push( program );
programsMap.set( cacheKey, program );
}
return program;
}
function releaseProgram( program ) {
if ( -- program.usedTimes === 0 ) {
// Remove from unordered set
const i = programs.indexOf( program );
programs[ i ] = programs[ programs.length - 1 ];
programs.pop();
// Remove from map
programsMap.delete( program.cacheKey );
// Free WebGL resources
program.destroy();
}
}
File: src/renderers/webgl/WebGLShader.js (L1-10)
javascript
function WebGLShader( gl, type, string ) {
const shader = gl.createShader( type );
gl.shaderSource( shader, string );
gl.compileShader( shader );
return shader;
}
File: src/renderers/webgl/WebGLShaderCache.js (L1-40)
javascript
let _id = 0;
class WebGLShaderCache {
constructor() {
this.shaderCache = new Map();
this.materialCache = new Map();
}
update( material ) {
const vertexShader = material.vertexShader;
const fragmentShader = material.fragmentShader;
const vertexShaderStage = this._getShaderStage( vertexShader );
const fragmentShaderStage = this._getShaderStage( fragmentShader );
const materialShaders = this._getShaderCacheForMaterial( material );
if ( materialShaders.has( vertexShaderStage ) === false ) {
materialShaders.add( vertexShaderStage );
vertexShaderStage.usedTimes ++;
}
if ( materialShaders.has( fragmentShaderStage ) === false ) {
materialShaders.add( fragmentShaderStage );
fragmentShaderStage.usedTimes ++;
}
return this;
}
remove( material ) {