首页
/ three.js CSMHelper 完整指南:可视化级联阴影映射的级联体、阴影边界与主视锥

three.js CSMHelper 完整指南:可视化级联阴影映射的级联体、阴影边界与主视锥

2026-09-06 12:48:17作者:羿妍玫Ivan

CSMHelper 是 three.js 提供的级联阴影映射(Cascaded Shadow Maps, CSM)调试助手,用于把 CSM 实例内部不可见的级联视锥、级联分割平面和每级阴影相机包围盒直接渲染到场景中。读完本文,你将掌握它的完整 API(构造参数、displayFrustum / displayPlanes / displayShadowBounds 三个显示开关、update() / updateVisibility() / dispose() 方法)、它在源码中的几何实现方式,以及如何结合 webgl_shadowmap_csm.html 示例把它接入自己的动画循环,从而快速排查阴影精度、级联划分和阴影范围问题。

three.js CSM 级联阴影示例:多排立方体在地面投射级联阴影

为什么需要 CSMHelper

单个平行光阴影映射只有一张固定分辨率的 shadow map,覆盖整个场景时,近处物体精度不足、远处物体被浪费。CSM 把主相机的视锥按深度拆分为若干“级联”(cascade),每一级对应一个独立的光照阴影相机和独立 shadow map,近处级联分辨率高、远处级联覆盖范围大。three.js 通过 examples/jsm/csm/CSM.js 中的 CSM 类实现 WebGL 路径,通过 examples/jsm/csm/CSMShadowNode.js 中的 CSMShadowNode 实现 WebGPU 路径。

问题在于:级联数量、分割模式、maxFarlightMargin 等参数配错时,症状往往是“某段距离阴影模糊”或“级联交界处闪烁”,而这些几何关系默认完全不可见。CSMHelper 就是为这个调试目的设计的——它把以下三层结构画出来:

  1. 主相机视锥(mainFrustum)的线框;
  2. 每一级级联视锥的包围盒(白色)与级联分割平面(半透明);
  3. 每一级阴影相机的投影包围盒(黄色,即 shadow map 实际覆盖范围)。

导入方式

CSMHelper 是 addon 模块,不在 three 主包内,必须显式导入:

import { CSMHelper } from 'three/addons/csm/CSMHelper.js';

该模块位于 examples/jsm/csm/CSMHelper.js,与 CSMCSMFrustumCSMShaderCSMShadowNode 同目录。它只依赖 three 主包的几何与材质类(GroupMeshLineSegmentsBox3HelperBox3PlaneGeometry 等),自身不引入任何 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 之所以同时接受两类实例,是因为两者暴露了相同的几何字段:cameracascadesmainFrustumfrustumslights(见 CSM.jsCSMShadowNode.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() 的执行流程:

  1. csm 上读取 cameracascadesmainFrustumfrustumslights;若 csm.camera === null 直接返回;
  2. 把 helper 自身的 position / quaternion / scale 复制为相机的变换,并 updateMatrixWorld( true )——helper 整体跟随相机;
  3. 动态增减每级对象:当 cascadeLines.lengthcsm.cascades 不一致时,逐个 remove 或新建。每级新建三件套:
    • cascadeLine:白色(0xffffffBox3Helper,表示该级级联视锥;
    • cascadePlanePlaneGeometry + 半透明 MeshBasicMaterialopacity: 0.1depthWrite: falseside: DoubleSide);
    • shadowLineGroup:内含黄色(0xffff00Box3Helper,表示该级 shadow camera 的投影包围盒;
  4. 逐级更新几何:cascadeLine.box 取该级 frustum.vertices.far 的对角顶点(z 轴加 1e-4 微小偏移避免与平面共面);cascadePlane 定位到远面对角中点、缩放为远面尺寸;shadowLineGroup 对齐 light.shadow.camera 的位姿,boxshadowCamleft / right / top / bottom / near / far 直接写出;
  5. 最后把 mainFrustum.vertices.near / far 的 8 个顶点写入主视锥线框的 position 属性,并置 needsUpdate = true

这套逻辑说明了一个重要细节:helper 每帧从 CSM 实例“拉取”数据,而不是订阅变化。因此 CSM 的级联参数(如 cascadesmaxFarmode)改变后,除了调用 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():切换相机(示例支持正交相机)、修改 maxFarmodefade 后都需触发,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 三层可视化,可以这样定位常见问题:

  • 白色盒子大小不均:反映各级级联的远面跨度,是 modecascades 分配是否合理最直观的证据;'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 实现CSMFrustumCSMShaderCSMShadowNode官方示例

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388