three.js CSMHelper 完整指南:可视化级联阴影映射的级联体、阴影边界与主视锥
CSMHelper 是 three.js 提供的级联阴影映射(Cascaded Shadow Maps, CSM)调试助手,用于把 CSM 实例内部不可见的级联视锥、级联分割平面和每级阴影相机包围盒直接渲染到场景中。读完本文,你将掌握它的完整 API(构造参数、displayFrustum / displayPlanes / displayShadowBounds 三个显示开关、update() / updateVisibility() / dispose() 方法)、它在源码中的几何实现方式,以及如何结合 webgl_shadowmap_csm.html 示例把它接入自己的动画循环,从而快速排查阴影精度、级联划分和阴影范围问题。
为什么需要 CSMHelper
单个平行光阴影映射只有一张固定分辨率的 shadow map,覆盖整个场景时,近处物体精度不足、远处物体被浪费。CSM 把主相机的视锥按深度拆分为若干“级联”(cascade),每一级对应一个独立的光照阴影相机和独立 shadow map,近处级联分辨率高、远处级联覆盖范围大。three.js 通过 examples/jsm/csm/CSM.js 中的 CSM 类实现 WebGL 路径,通过 examples/jsm/csm/CSMShadowNode.js 中的 CSMShadowNode 实现 WebGPU 路径。
问题在于:级联数量、分割模式、maxFar、lightMargin 等参数配错时,症状往往是“某段距离阴影模糊”或“级联交界处闪烁”,而这些几何关系默认完全不可见。CSMHelper 就是为这个调试目的设计的——它把以下三层结构画出来:
- 主相机视锥(
mainFrustum)的线框; - 每一级级联视锥的包围盒(白色)与级联分割平面(半透明);
- 每一级阴影相机的投影包围盒(黄色,即 shadow map 实际覆盖范围)。
导入方式
CSMHelper 是 addon 模块,不在 three 主包内,必须显式导入:
import { CSMHelper } from 'three/addons/csm/CSMHelper.js';
该模块位于 examples/jsm/csm/CSMHelper.js,与 CSM、CSMFrustum、CSMShader、CSMShadowNode 同目录。它只依赖 three 主包的几何与材质类(Group、Mesh、LineSegments、Box3Helper、Box3、PlaneGeometry 等),自身不引入任何 addon。
构造函数:new CSMHelper( csm )
new CSMHelper( csm : CSM | CSMShadowNode )
csm 是要可视化的 CSM 实例。构造后返回一个 Group 对象(继承链为 EventDispatcher → Object3D → Group),需要手动加入场景:
const helper = new CSMHelper( csm );
scene.add( helper );
从源码看(examples/jsm/csm/CSMHelper.js),构造函数做了两件事:
- 保存传入实例到
this.csm; - 创建主视锥线框:一段 24 个顶点(8 顶点 × 3 分量
Float32Array(24))加 24 条边索引(Uint16Array)的LineSegments,材质为默认LineBasicMaterial,初始加入Group并挂在this.frustumLines上。
每个级联的可视化对象不在构造时创建,而是在第一次 update() 时按 csm.cascades 数量动态生成——这保证了 helper 与级联数量始终同步。
属性
.csm : CSM | CSMShadowNode
要可视化的 CSM 实例。CSMHelper 之所以同时接受两类实例,是因为两者暴露了相同的几何字段:camera、cascades、mainFrustum、frustums、lights(见 CSM.js 与 CSMShadowNode.js 中的同名属性)。update() 依赖这些字段读取每一级级联视锥的远面顶点与对应光照阴影相机,因此无论 WebGL 还是 WebGPU 渲染路径都可用同一个 helper。
.displayFrustum : boolean
是否显示 CSM 视锥线框(包括主视锥线框和每级级联视锥的 Box3Helper)。默认 true。
.displayPlanes : boolean
是否显示级联分割平面(每级远面的半透明平面)。默认 true。注意其可见性受 displayFrustum 联动:只有 displayFrustum && displayPlanes 同时为 true 时平面才可见(见下文 updateVisibility() 实现)。
.displayShadowBounds : boolean
是否显示每级阴影相机的包围盒(黄色线框)。默认 true。
方法
.update()
更新整个 helper 的几何与变换。必须在应用的动画循环中每帧调用,且应在 renderer.render() 之前、与 csm.update() 同处一帧。
源码(CSMHelper.js)中 update() 的执行流程:
- 从
csm上读取camera、cascades、mainFrustum、frustums、lights;若csm.camera === null直接返回; - 把 helper 自身的
position/quaternion/scale复制为相机的变换,并updateMatrixWorld( true )——helper 整体跟随相机; - 动态增减每级对象:当
cascadeLines.length与csm.cascades不一致时,逐个remove或新建。每级新建三件套:cascadeLine:白色(0xffffff)Box3Helper,表示该级级联视锥;cascadePlane:PlaneGeometry+ 半透明MeshBasicMaterial(opacity: 0.1、depthWrite: false、side: DoubleSide);shadowLineGroup:内含黄色(0xffff00)Box3Helper,表示该级 shadow camera 的投影包围盒;
- 逐级更新几何:
cascadeLine.box取该级frustum.vertices.far的对角顶点(z轴加1e-4微小偏移避免与平面共面);cascadePlane定位到远面对角中点、缩放为远面尺寸;shadowLineGroup对齐light.shadow.camera的位姿,box由shadowCam的left / right / top / bottom / near / far直接写出; - 最后把
mainFrustum.vertices.near / far的 8 个顶点写入主视锥线框的 position 属性,并置needsUpdate = true。
这套逻辑说明了一个重要细节:helper 每帧从 CSM 实例“拉取”数据,而不是订阅变化。因此 CSM 的级联参数(如 cascades、maxFar、mode)改变后,除了调用 csm.updateFrustums() 重建级联外,下一帧的 helper.update() 会自动让线框数量与形状跟上。
.updateVisibility()
当运行时修改任一 display* 属性后必须调用。实现(CSMHelper.js)就是把当前三个开关翻译成各子对象的 visible:
cascadeLine.visible = displayFrustum;
cascadePlane.visible = displayFrustum && displayPlanes; // 平面受视锥开关联动
shadowLineGroup.visible = displayShadowBounds;
frustumLines.visible = displayFrustum;
.dispose()
释放该实例占用的 GPU 资源,不再使用时应调用。它会 dispose 主视锥线框的几何与材质,再逐级 dispose 各 Box3Helper(其自带 dispose 方法)、级联平面的几何与材质和阴影包围盒线框。
实战:在 CSM 示例中接入 CSMHelper
examples/webgl_shadowmap_csm.html 是最典型的接入范例,核心片段:
import { CSM } from 'three/addons/csm/CSM.js';
import { CSMHelper } from 'three/addons/csm/CSMHelper.js';
// 1. 先构造 CSM(csmHelper 依赖 csm 实例)
csm = new CSM( {
maxFar: params.far,
cascades: 4,
mode: params.mode, // 'uniform' | 'logarithmic' | 'practical'
parent: scene,
shadowMapSize: 1024,
lightDirection: new THREE.Vector3( params.lightX, params.lightY, params.lightZ ).normalize(),
camera: camera
} );
// 2. 创建 helper 并加入场景
csmHelper = new CSMHelper( csm );
csmHelper.visible = false; // 默认隐藏,调试时再打开
scene.add( csmHelper );
GUI 面板中为三个显示开关绑定 updateVisibility(),这正是官方文档强调的调用时机:
gui.add( csmHelper, 'displayFrustum' ).onChange( () => csmHelper.updateVisibility() );
gui.add( csmHelper, 'displayPlanes' ).onChange( () => csmHelper.updateVisibility() );
gui.add( csmHelper, 'displayShadowBounds' ).onChange( () => csmHelper.updateVisibility() );
动画循环中按“先更新 CSM,再更新 helper,最后渲染”的顺序调用:
function animate() {
camera.updateMatrixWorld();
csm.update(); // 更新各平行光与阴影相机位置
controls.update();
if ( params.autoUpdateHelper ) {
csmHelper.update(); // helper 跟随相机并刷新所有线框/平面
}
renderer.render( scene, camera );
}
几个配套要点(均来自该示例与 CSM.js):
- 相机/CSM 参数变更后调用
csm.updateFrustums():切换相机(示例支持正交相机)、修改maxFar、mode、fade后都需触发,helper 的几何会在后续update()中反映新划分; - 窗口 resize 也要调用
csm.updateFrustums(),因为主视锥由相机投影矩阵决定; - 材质必须通过
csm.setupMaterial( material )注册,否则 CSM 的着色逻辑不会注入该材质(示例中对地面与两色立方体材质各调用了一次); - WebGPU 场景的对应写法见 examples/webgpu_shadowmap_csm.html:把
CSM换成CSMShadowNode(构造签名为new CSMShadowNode( light, data )),new CSMHelper( csm )用法不变。
调试时的判读要点
结合 helper 三层可视化,可以这样定位常见问题:
- 白色盒子大小不均:反映各级级联的远面跨度,是
mode与cascades分配是否合理最直观的证据;'practical'模式按lambda = 0.5对均匀与对数两种划分做线性插值(见 CSM.js),切换uniform/logarithmic/practical对比白盒尺寸差异,能直接看到各策略在远近处的取舍; - 黄色盒子与白色盒子的关系:黄色盒是 shadow camera 的正交包围范围,由该级视锥在光照空间的对角宽度加上
lightMargin决定(见 CSM.js 的_updateShadowBounds())。黄盒过大意味着lightMargin偏大,shadow map 有效分辨率被摊薄;过小则可能出现阴影被裁切; - 级联平面位置:若某级平面离相机过近,近处物体会落入远端低分辨率级联,表现为“近处阴影发糊”,此时应增大该级 break 或改用
practical/custom模式(CSM支持customSplitsCallback自定义划分)。
适用前提与限制
- 适用前提:需要 WebGL 或 WebGPU 渲染器 + shadow map 已启用;helper 仅用于调试可视化,生产场景建议保持
helper.visible = false; - 每级级联对象在
update()中按需创建,级联数变化时自动增减,无需手动重建 helper; - 修改
display*开关后不调用updateVisibility()不会生效;每帧不调用update()则线框停留在旧位姿,与相机脱节; - 移除 helper 时先
scene.remove( helper )再helper.dispose(),同时 CSM 侧按各自约定调用csm.dispose()(或 WebGPU 路径的对应清理)。
相关源码入口:CSMHelper 实现、CSM 实现、CSMFrustum、CSMShader、CSMShadowNode、官方示例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
