Three.js着色器编译机制深度解析

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:内置着色器模板库

ShaderLibShaderChunk 中的完整着色器与 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
避免重复编译 programsMapcacheKey 缓存 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 ) {
相关推荐
丷丩1 小时前
MapLibre GL JS第22课:查看本地GeoJSON
前端·javascript·map·mapbox·maplibre gl js
油炸自行车1 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
丷丩5 小时前
MapLibre GL JS第19课:实时更新要素
前端·javascript·gis·map·mapbox·maplibre gl js
xiaohua0708day6 小时前
Lodash库
前端·javascript·vue.js
突然好热6 小时前
TS 调试技巧
前端·javascript·typescript
h64648564h6 小时前
Flutter 国际化(i18n)全指南:一键切换中/英/日多语言
前端·javascript·flutter
丷丩8 小时前
MapLibre GL JS第8课:禁用滚动缩放
javascript·mapbox·maplibre gl js
kyriewen9 小时前
面试8家前端岗位后,我发现了一个残酷的事实:AI不是加分项,是门槛
前端·javascript·面试
MageGojo12 小时前
做节日活动页时,如何用 API 快速生成对联内容
javascript·python·节日·对联生成