three.js 级联阴影映射(CSM)底层剖析:CSMShader 的 GLSL 注入机制与内建着色器扩展

发布时间:2026/9/10 21:21:51
three.js 级联阴影映射(CSM)底层剖析:CSMShader 的 GLSL 注入机制与内建着色器扩展 three.js 级联阴影映射CSM底层剖析CSMShader 的 GLSL 注入机制与内建着色器扩展【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js级联阴影映射Cascade Shadow Maps, CSM是大型开放场景中让平行光阴影保持清晰的核心技术它将视锥体按深度切分为多级级联每级使用独立的阴影贴图分辨率。在 three.js 中CSM 的实现分为两层负责级联划分、灯光管理、材质配置的 CSM 管理类以及承载全部着色器增强逻辑的 CSMShader 模块。本文围绕 CSMShader 展开讲解它如何通过注入 GLSL 代码覆盖 three.js 内建材质着色 chunk实现带级联选择的阴影采样与级联间混合并结合源码说明其在 WebGL 渲染管线中的真实调用链与工程用法。CSMShader 是什么CSMShader 并不是一个可实例化的类而是一个静态的、只包含 GLSL 源码字符串的模块级常量对象。它的职责在源码注释中定义得非常清楚The object that holds the GLSL enhancements to enable CSM. This code is injected into the built-in material shaders by CSM.examples/jsm/csm/CSMShader.js。它与 three.js 的着色器模块系统紧密相关WebGL 渲染器在编译内建材质MeshStandardMaterial、MeshPhongMaterial 等时会拼接位于 src/renderers/shaders/ShaderChunk.js 中的一系列 GLSL 片段chunk其中负责逐光源计算光照的片段就是lights_fragment_begin负责声明光照相关结构体与函数的片段是lights_pars_begin。CSMShader 提供的两段 GLSL 字符串正是用于整体替换这两个系统 chunk从而在不改动任何内建材质类的前提下为所有受 CSM 管理的材质加入多级联阴影的分级采样逻辑。导入方式CSMShader 是 three.js 的 addon附加模块不在核心包内必须显式导入官方安装指南称此为 Installation#Addons。你通常不需要直接导入它——它由 CSM 在内部引用但了解其导入路径有助于理解模块边界// 手动导入一般仅用于学习或自行扩展 import { CSMShader } from three/addons/csm/CSMShader.js; // 实际开发中更常见的做法只导入 CSMCSMShader 由它内部引用 import { CSM } from three/addons/csm/CSM.js;在 CSM.js 顶部可以看到它确实通过import { CSMShader } from ./CSMShader.js完成引用同时该模块也一并收录在 addon 总入口 examples/jsm/Addons.js 中。CSMShader.js 内部只依赖 three 核心暴露的ShaderChunk源码第 1 行用于在lights_pars_begin中拼接系统原有的ShaderChunk.lights_pars_begin确保注入是增强而不是丢弃。两个核心属性注入的 GLSL 片段CSMShader 对象本身只有两个成员全部是字符串常量结构非常精简const CSMShader { lights_fragment_begin: /* glsl */ ..., lights_pars_begin: /* glsl */ ... };从代码组织结构看examples/jsm/csm/CSMShader.js这两个片段的作用分别是属性注入位置职责lights_fragment_begin替换ShaderChunk.lights_fragment_begin在片元着色器中重建几何体信息遍历点光源/聚光灯/平行光/RectArea 光源并累积直接光照当定义了USE_CSM与CSM_CASCADES时走级联分支逻辑按线性深度选择级联并对方向光阴影做分级处理lights_pars_begin替换ShaderChunk.lights_pars_begin声明 CSM 所需的三个 uniformCSM_cascades、cameraNear、shadowFar并拼接保留系统原有 chunk 内容lights_pars_beginCSM uniform 声明该片段是所有 CSM 着色器代码的地基只有在材质定义了USE_CSM与CSM_CASCADES宏时才会把三个 uniform 编入着色器examples/jsm/csm/CSMShader.js#if defined( USE_CSM ) defined( CSM_CASCADES ) uniform vec2 CSM_cascades[CSM_CASCADES]; uniform float cameraNear; uniform float shadowFar; #endif各 uniform 的语义与取值来源需要与 CSM 配合理解CSM_cascades[CSM_CASCADES]长度为级联数的vec2数组。每个元素x/y表示该级联在归一化深度轴上覆盖的区间如cascade.x更近、cascade.y更远。它由 JS 侧的级联分界值breaks扩展而来——见 CSM.js 的_getExtendedBreaks对第 i 个级联target[i].x breaks[i-1] || 0、target[i].y breaks[i]。该数组由setupMaterial写入onBeforeCompile的shader.uniforms.CSM_cascades。cameraNear相机近裁剪面用于把视空间深度换算成归一化线性深度。shadowFar实际参与 CSM 的远裁剪面距离取min(camera.far, maxFar)。它出现在深度归一化分母中也是确定级联划分范围的依据。这里有一个容易忽略的设计细节lights_pars_begin是通过字符串拼接保留系统 chunk 的第 304 行 ShaderChunk.lights_pars_begin。也就是说 CSM 的 uniform 声明只是前缀插入后面的点光/聚光结构体、getShadow系列函数等都来自 three.js 内建的原始 chunk这也是为什么 CSM 能无缝兼容标准 WebGL 材质体系。lights_fragment_begin级联阴影的核心算法这是 CSMShader 真正的心脏。它与系统原始 chunk 大体同构都先重建geometryPosition/geometryNormal/geometryViewDir等几何量再依次处理各类光源关键差异在于方向光DirectionalLight处理分支被一分为三examples/jsm/csm/CSMShader.jsUSE_CSM CSM_CASCADES定义的分支L118-L221启用 CSM逐级联做阴影采样与光照累积。!USE_CSM的分支L224-L248完全复刻系统原始的方向光循环保证注入该 chunk 却未启用 CSM 的着色器行为不变。光源数多于阴影级联数时多余的平行光不投阴影会走补充循环L204-L219。在启用 CSM 且开启阴影贴图USE_SHADOWMAP时片段级算法大致如下深度归一化float linearDepth (vViewPosition.z) / (shadowFar - cameraNear);把视角空间深度归一化到[0,1]区间作为后续级联判定的统一标尺。级联选择遍历方向光数组读取CSM_cascades[i]当linearDepth落在[cascade.x, cascade.y)区间最后一层级联右边界开放时才对该级联采样directionalShadowMap[i]并调用getShadow得到阴影因子。级联边界混合CSM_FADE当材质定义了CSM_FADE时启用平滑过渡。算法以级联中心为基准取深度更近侧的边界作为closestEdge按margin 0.25 * pow(closestEdge, 2.0)计算混合带宽再把linearDepth与带宽的相对位置映射为ratio。随后使用mix()对前后两个级联的directDiffuse/directSpecular/indirectDiffuse/indirectSpecular逐通道混合避免级联切换处出现肉眼可见的阴影断层对最后一级级联还额外处理了深度越过级联中心后阴影自然淡出的情形shouldFadeLastCascade。该带宽公式与 CPU 侧_updateShadowBounds中因fade而扩展阴影包围盒的计算相互呼应examples/jsm/csm/CSM.js保证 GPU 采样范围与 CPU 阴影相机范围一致。透射/平行光信息同一循环内仍调用getDirectionalLightInfo填充directLight最终进入RE_Direct完成直接光照的物理计算因此 CSM 不影响原有 PBR 光照模型。值得留意的是这段 GLSL 中还完整保留了点光源、聚光灯与 RectArea 光源的遍历逻辑以及USE_CLEARCOAT、USE_IRIDESCENCE、RE_IndirectDiffuse/RE_IndirectSpecular等间接光照分支L15-L41、L44-L116、L250-L296。这说明 CSMShader 的注入不是另起炉灶而是在完整兼容内建物理光照框架的前提下只对方向光阴影部分做手术式替换。它如何进入渲染管线与 CSM 的协作CSMShader 自身不会产生任何效果真正的驱动者是 CSM。从源码结构看两者的协作包含三条关键链路1. chunk 全局注入构造时执行一次。CSM 构造函数末尾调用私有方法_injectInclude()examples/jsm/csm/CSM.js_injectInclude() { ShaderChunk.lights_fragment_begin CSMShader.lights_fragment_begin; ShaderChunk.lights_pars_begin CSMShader.lights_pars_begin; }它直接把 two 个全局 chunk 指针替换为 CSMShader 的两段 GLSL。此后任何内建材质首次编译时拼接出来的着色器都将包含 CSM 分支代码。2. 宏与 uniform 注入对每个目标材质。应用需要对每个希望受 CSM 影响的材质调用csm.setupMaterial(material)。它做三件事examples/jsm/csm/CSM.js写入material.defines.USE_CSM 1与material.defines.CSM_CASCADES this.cascades若开启fade再写入material.defines.CSM_FADE ——这些宏正是 CSMShader 的 GLSL 中分支判断与 uniform 声明的开关覆写material.onBeforeCompile把CSM_cascades、cameraNear、shadowFar三个 uniform 挂到着色器对象上把material - shader的映射记录进内部shadersMap供每帧更新 uniform 使用。3. 逐帧 uniform 刷新。相机或 CSM 参数改变时调用csm.updateFrustums()其内部_updateUniforms()examples/jsm/csm/CSM.js会遍历shadersMap把最新级联分界、camera.near与far min(camera.far, maxFar)同步给 GPU 侧的 uniform。由此可以看出 CSMShader 在整个方案中的位置它是静态的 GLSL 模板由 CSM 负责把它注进去、配好宏、填好值。在工程中如何启用以官方示例为参照仓库中的 examples/webgl_shadowmap_csm.html 是这一机制最直接的实战演示其用法骨架如下import { CSM } from three/addons/csm/CSM.js; import { CSMHelper } from three/addons/csm/CSMHelper.js; csm new CSM( { maxFar: params.far, // CSM 覆盖的最大阴影距离 cascades: 4, // 级联数量示例为 4 mode: params.mode, // uniform | logarithmic | practical shadowMapSize: 1024, // 每级级联阴影图分辨率 lightDirection: new THREE.Vector3( params.lightX, params.lightY, params.lightZ ).normalize(), lightFar: 5000, lightMargin: 200, camera: camera, parent: scene } ); // 对每个需要 CSM 阴影的材质调用 csm.setupMaterial( floorMaterial ); csm.setupMaterial( material1 ); csm.setupMaterial( material2 ); // 动画循环中渲染前更新 csm.update();工程实践要点可归纳为以下几条均可从示例或源码确认必须逐材质setupMaterial只有调用过的材质才会写入USE_CSM/CSM_CASCADES宏并接收级联 uniform未调用则走 chunk 中的非 CSM 分支行为与普通阴影一致。每帧渲染前调用csm.update()它按当前相机姿态把每个级联视锥体变换到光源空间、做纹素对齐并摆放方向光examples/jsm/csm/CSM.js。相机/参数变化后调用csm.updateFrustums()示例中 GUI 修改far、mode、fade、lightMargin、lightDirection后都会触发它从而重新划分级联并刷新 uniform。级联分档模式示例的 GUI 提供uniform、logarithmic、practical三档practical为默认即对数/均匀按 0.5 比例混合的经验分档对应 CSM.js 中_getBreaks的三种实现若选用custom模式则需提供customSplitsCallback(cascades, near, far, breaks)。清理资源不再使用 CSM 时调用remove()移除场景中的灯光与 target调用dispose()删除材质上的宏、uniform 与onBeforeCompile回调并触发重编译examples/jsm/csm/CSM.js。使用限制与注意点从源码注释examples/jsm/csm/CSM.js可以确认一个重要边界本模块的 CSM 只能配合WebGLRenderer使用若使用WebGPURenderer应改用同目录下的CSMShadowNode即 examples/jsm/csm/CSMShadowNode.js属于基于 TSL/节点体系的实现因为 WebGPU 渲染器不经过上述ShaderChunk拼装管线CSMShader 的字符串注入对它不生效。同理凡是没有走 three.js 内建 chunk 拼装的材质如纯自定义 RawShaderMaterial也不会自动获得 CSM 能力。此外还应留意由于_injectInclude()是全局替换了ShaderChunk中的两个片段因此同一 WebGL 渲染上下文内创建多个 CSM 实例并不会让注入翻倍替换动作本身是幂等的真正需要管理的是每个材质上的宏与 uniform 生命周期这正是setupMaterial/dispose成对设计的原因。小结CSMShader 以极简的模块形态封装了 three.js 级联阴影在 GPU 侧的全部增强代码lights_pars_begin负责条件化声明三个级联 uniformlights_fragment_begin负责在保持内建 PBR 光照框架完整的前提下将方向光阴影替换为按线性深度选级联 可选平滑过渡的实现。它与 CSM 管理类负责 CPU 侧级联划分、阴影相机包围盒与 uniform 同步配合构成了 WebGL 渲染器下大范围平行光阴影的高质量解决方案而掌握这套定义宏 → 挂 uniform → 注入 chunk的协作范式也能帮助你举一反三地理解 three.js 其余着色器扩展类 addon 的底层原理。延伸阅读着色器增强常量对象完整源码examples/jsm/csm/CSMShader.jsCPU 侧级联管理与注入调度examples/jsm/csm/CSM.js被替换的系统 chunk 定义src/renderers/shaders/ShaderChunk.js级联视锥体剖分实现examples/jsm/csm/CSMFrustum.js可视化调试辅助类CSMHelper 文档 与 examples/jsm/csm/CSMHelper.jsWebGPU 渲染器下的等效实现examples/jsm/csm/CSMShadowNode.js官方实战示例examples/webgl_shadowmap_csm.html【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询