three.js ShadowMapViewer 完全指南:在 WebGL/WebGPU 渲染管线上实时观察阴影贴图
ShadowMapViewer 是 three.js 提供的辅助类(addon),用于将投射阴影光源(DirectionalLight、SpotLight)的阴影贴图实时渲染到屏幕 HUD 上,是调试阴影精度、视锥范围与深度偏置问题的核心工具。本文覆盖其构造参数、全部属性与方法、WebGL 与 WebGPU 双版本的使用差异,并结合 ShadowMapViewer.js 源码剖析 HUD 渲染管线,配合 webgl_shadowmap_viewer.html 官方示例给出可直接运行的完整集成方案。
1. 核心能力与适用前提
官方文档(docs/pages/ShadowMapViewer.html.md)对该辅助类的定义如下:
This is a helper for visualising a given light's shadow map. It works for shadow casting lights: DirectionalLight and SpotLight. It renders out the shadow map and displays it on a HUD.
由此可以确定三个关键事实:
- 仅支持平行光与聚光灯。
DirectionalLight与SpotLight的阴影贴图是单张 2D 深度图,HUD 直接以平面纹理方式显示;PointLight需要 6 面立方体贴图,因此不在支持范围内; - 按渲染器区分导入。该模块只能与 WebGLRenderer 一起使用;使用 WebGPURenderer 时必须从
ShadowMapViewerGPU.js导入同名类; - 它是一块叠加层(overlay)。viewer 自身不创建画布,而是在主渲染循环里"追加渲染"一个正交相机场景,把阴影贴图画到窗口前景上。
导入方式
ShadowMapViewer 属于 addon,需要显式导入(详见 安装手册 Addons 章节):
// WebGLRenderer 项目
import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewer.js';
// WebGPURenderer 项目
import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewerGPU.js';
两份源码 examples/jsm/utils/ShadowMapViewer.js 与 examples/jsm/utils/ShadowMapViewerGPU.js 的公共 API 完全一致,差异只在 HUD 材质的实现方式(后者使用 TSL 节点 NodeMaterial + DepthTexture,见第 4 节)。
2. 构造函数:new ShadowMapViewer( light )
new ShadowMapViewer( light : Light )
light:要观察阴影贴图的投射光源,必须是已开启 castShadow = true 的 DirectionalLight 或 SpotLight。
构造函数接收光源后立即构建内部 HUD,从源码 ShadowMapViewer.js#L38-L55 可以看到其初始化了四样东西:
| 内部构件 | 实现 | 作用 |
|---|---|---|
| 正交相机 | new OrthographicCamera(...),覆盖整个窗口,camera.position.z = 2 |
作为 HUD 场景的观察相机,单位即屏幕像素坐标 |
| 独立 Scene | new Scene() |
与主场景隔离,只包含 HUD 网格(和可选名称标签) |
| 深度着色平面 | PlaneGeometry + ShaderMaterial |
采样 light.shadow.map.texture 的 .r 通道转成灰度显示 |
| 名称标签(可选) | CanvasTexture + MeshBasicMaterial |
当 light.name 非空时,在贴图下方绘制红色 Bold 20px Arial 文字 |
其中 doRenderLabel = ( light.name !== undefined && light.name !== '' ) 决定了是否渲染名称标签——这是官方示例里"Spot Light"、"Dir. Light"红色文字的由来,见 ShadowMapViewer.js#L42 与 ShadowMapViewer.js#L91-L115。
3. 属性详解:enabled / position / size
3.1 .enabled : boolean
是否显示该 viewer,默认 true。关闭时 render()、updateForWindowResize() 等方法内部全部短路,不再做任何渲染开销,适合在调试面板里做开关。
3.2 .position : Object
viewer 在屏幕上的位置,语义为距离窗口左上角的偏移量(像素),结构为:
position.x // 默认 10
position.y // 默认 10
position.set( x, y ) // 调用后立即生效
默认值 { x: 10, y: 10 } 来自构造函数的内部 frame 常量,见 ShadowMapViewer.js#L46-L51。
坐标换算逻辑在 position.set 中(ShadowMapViewer.js#L165-L178):
mesh.position.set(
-window.innerWidth / 2 + width / 2 + x, // 从"左上角偏移"换算到正交相机中心坐标
window.innerHeight / 2 - height / 2 - y,
0
);
也就是说:x、y 增大 → HUD 向右/向下移动;换算时以"贴图几何中心"为锚点。若渲染了名称标签,标签会跟随贴图底边居中放置。
注意:直接改写 position.x / position.y 属性本身不会移动网格,必须再调用 .update() 或改用 position.set()(set 内部会自动应用)。官方示例中两种方式都有演示(webgl_shadowmap_viewer.html#L162-L176):
dirLightShadowMapViewer.position.x = 10; // 直接改属性
dirLightShadowMapViewer.position.y = 10;
dirLightShadowMapViewer.size.width = size;
dirLightShadowMapViewer.size.height = size;
dirLightShadowMapViewer.update(); // 必须手动 update
spotLightShadowMapViewer.size.set( size, size ); // 走 .set()
spotLightShadowMapViewer.position.set( size + 20, 10 ); // .set 内部自动生效,无需再 update
3.3 .size : Object
viewer 的宽高(像素),结构为:
size.width // 默认 256
size.height // 默认 256
size.set( width, height ) // 调用后立即生效
默认值 { width: 256, height: 256 } 同样来自 frame 常量。缩放实现是把 HUD 平面网格相对 256×256 基准几何做 mesh.scale(ShadowMapViewer.js#L142-L153):
mesh.scale.set( this.width / frame.width, this.height / frame.height, 1 );
// 缩放后位置锚点会偏移,因此 size.set 内部还会调用 resetPosition()
从源码结构看,size.set 在缩放后会重新执行 position.set 以修正锚点偏移——这就是"缩放后位置会漂移、必须复位"的实现原因。
4. 方法详解:render / update / updateForWindowResize
4.1 .render( renderer ) — 每帧调用
lightShadowMapViewer.render( renderer );
此方法必须在应用的动画循环中调用,且放在主场景 renderer.render( scene, camera ) 之后,才能形成前景叠加效果。WebGL 版实现(ShadowMapViewer.js#L185-L204):
this.render = function ( renderer ) {
if ( this.enabled ) {
// 光源的 shadow map 只在第一次渲染之后才初始化,
// 必须每帧把正确的 map 送进 shader,否则会一直显示
// 场景中第一个添加的投射光的 shadowMap
material.uniforms.tDiffuse.value = light.shadow.map.texture;
userAutoClearSetting = renderer.autoClear;
renderer.autoClear = false; // 允许叠加渲染
renderer.clearDepth(); // 只清深度,保留已绘制画面
renderer.render( scene, camera );
renderer.autoClear = userAutoClearSetting; // 恢复用户设置
}
};
三个实现细节值得注意:
light.shadow.map是惰性初始化的。从 LightShadow.js 可以看到构造时this.map = null,只有渲染器在首次阴影通道渲染后才分配DepthRenderTarget。这就是 viewer 注释中强调"必须在第一帧渲染之后调用render(),且每帧重新绑定纹理"的原因,否则 HUD 可能显示错误的(第一个光的)贴图;autoClear = false+clearDepth()的组合保证了 HUD 不会清掉主场景的色缓冲,只清除深度避免遮挡冲突,渲染完立即恢复用户原设置,不产生副作用;- 深度值可视化规则。HUD 片元着色器读取深度纹理
.r通道(ShadowMapViewer.js#L69-L80):
float depth = texture2D( tDiffuse, vUv ).r;
#ifdef USE_REVERSED_DEPTH_BUFFER
gl_FragColor = vec4( vec3( depth ), opacity );
#else
gl_FragColor = vec4( vec3( 1.0 - depth ), opacity );
#endif
默认(前向)深度缓冲下显示 1 - depth,即靠近相机的物体为亮色、空白区域为暗色;若项目启用了反向深度缓冲则直接显示原值。阅读 HUD 时:白色区域 = 被遮挡物占据的深度,黑色 = 阴影相机视锥内的"天空"。
WebGPU 版 ShadowMapViewerGPU.js 的 render() 逻辑相同,但 HUD 材质换成 TSL 节点(ShadowMapViewerGPU.js#L62-L67):
const material = new NodeMaterial();
const textureDimension = uniform( new Vector2() );
const shadowMapUniform = textureLoad( new DepthTexture(), uv().flipY().mul( textureDimension ) );
material.fragmentNode = shadowMapUniform.x.oneMinus();
每帧绑定的对象也从 light.shadow.map.texture 变为 light.shadow.map.depthTexture(WebGPU 后端使用 DepthTexture 而非 DepthRenderTarget.texture),并对 UV 做了 flipY 校正(ShadowMapViewerGPU.js#L180-L183)。
4.2 .update()
重新应用 position 与 size 到内部网格。只要直接改写了 position.x/y 或 size.width/height,就必须调用一次;用 position.set() / size.set() 则已内置更新,不必再调。实现非常直观(ShadowMapViewer.js#L229-L234):
this.update = function () {
this.position.set( this.position.x, this.position.y );
this.size.set( this.size.width, this.size.height );
};
构造函数末尾会强制执行一次 this.update() 以完成初始定位。
4.3 .updateForWindowResize()
窗口尺寸变化时应调用。它重建正交相机的投影范围(以窗口像素为左右上下边界)再调用 update()(ShadowMapViewer.js#L210-L224):
this.updateForWindowResize = function () {
if ( this.enabled ) {
camera.left = window.innerWidth / - 2;
camera.right = window.innerWidth / 2;
camera.top = window.innerHeight / 2;
camera.bottom = window.innerHeight / - 2;
camera.updateProjectionMatrix();
this.update();
}
};
不调用它的后果:HUD 的像素坐标系与实际窗口失配,贴图会出现在错误位置。
5. 官方示例完整解读:双光源阴影贴图 HUD
官方演示 webgl_shadowmap_viewer.html 完整覆盖了"场景 + 两个 viewer + 窗口自适应"的集成流程,可整体照搬到自己的项目中。
5.1 最小可运行集成代码
import * as THREE from 'three';
import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewer.js';
// 1. 场景与投射光源(关键:castShadow = true 且配置 shadow 相机范围)
const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
dirLight.name = 'Dir. Light'; // 非空 name 会触发 HUD 名称标签
dirLight.position.set( 0, 10, 0 );
dirLight.castShadow = true;
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 10;
dirLight.shadow.camera.left = - 15;
dirLight.shadow.camera.right = 15;
dirLight.shadow.camera.top = 15;
dirLight.shadow.camera.bottom = - 15;
dirLight.shadow.mapSize.set( 1024, 1024 );
scene.add( dirLight );
const spotLight = new THREE.SpotLight( 0xffffff, 500 );
spotLight.name = 'Spot Light';
spotLight.angle = Math.PI / 5;
spotLight.penumbra = 0.3;
spotLight.position.set( 10, 10, 5 );
spotLight.castShadow = true;
spotLight.shadow.camera.near = 8;
spotLight.shadow.camera.far = 30;
spotLight.shadow.mapSize.set( 1024, 1024 );
scene.add( spotLight );
// 2. 渲染器必须开启阴影贴图
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap; // Basic 下 HUD 与最终阴影表现一致,便于对照
// 3. 创建 viewer 并布局
const dirViewer = new ShadowMapViewer( dirLight );
const spotViewer = new ShadowMapViewer( spotLight );
function resizeViewers() {
const size = window.innerWidth * 0.15; // HUD 边长取窗口宽度的 15%
dirViewer.position.x = 10;
dirViewer.position.y = 10;
dirViewer.size.width = size;
dirViewer.size.height = size;
dirViewer.update(); // 直接改属性后必须 update
spotViewer.size.set( size, size ); // .set 自动生效
spotViewer.position.set( size + 20, 10 );
}
resizeViewers();
// 4. 动画循环:先渲染主场景,再叠加 HUD
renderer.setAnimationLoop( () => {
renderer.render( scene, camera );
dirViewer.render( renderer ); // 每帧调用
spotViewer.render( renderer );
} );
// 5. 窗口尺寸变化
window.addEventListener( 'resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
resizeViewers();
dirViewer.updateForWindowResize();
spotViewer.updateForWindowResize();
} );
这段代码与示例源码逐段对应:光源与 shadow 相机参数见 webgl_shadowmap_viewer.html#L63-L93,viewer 布局见 #L162-L176,渲染与 resize 流程见 #L178-L229。
5.2 配套技巧:CameraHelper 同步显示阴影视锥
示例中还做了两件对调试非常有帮助的事:
scene.add( new THREE.CameraHelper( spotLight.shadow.camera ) );
scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );
CameraHelper 在 3D 场景中画出阴影相机的视锥线框,与 HUD 里的深度图互相印证:HUD 中黑色空白区域,正是视锥内没有被物体覆盖的部分。若发现 HUD 大面积空白或物体被裁剪,应优先检查 shadow.camera.near/far/left/right/top/bottom(平行光)或 near/far(聚光灯)的取值——这是 ShadowMapViewer 最主要的实战价值。
6. 调试清单与源码级注意事项
结合 ShadowMapViewer.js 的实现,整理一份常见问题的排查清单:
| 症状 | 可能原因与检查点 |
|---|---|
| HUD 全黑 | 光源未 castShadow = true;或 light.shadow.map 尚未初始化(viewer 的 render() 需在第一帧主场景渲染后调用) |
| 两张 HUD 显示内容相同 | 绑定的 light.shadow.map.texture 未每帧刷新;确认没有复用同一个 viewer 或错误传入光源引用 |
| 物体只显示一半 | shadow 相机视锥(near/far,平行光还有 left/right/top/bottom)未完整覆盖遮挡物,用示例中的 CameraHelper 验证 |
| HUD 位置/尺寸不更新 | 直接改写 position.x 等属性后忘记调用 update() |
| 窗口缩放后 HUD 错位 | resize 事件里漏调 updateForWindowResize() |
| 看不到名称标签 | light.name 为空或未设置(doRenderLabel 判断逻辑,ShadowMapViewer.js#L42) |
| WebGPU 项目 HUD 无显示 | 误用了 WebGL 版 ShadowMapViewer.js;应导入 ShadowMapViewerGPU.js |
另外两点从源码可确认的行为:
- 多 viewer 叠加无冲突:每个 viewer 持有独立的 Scene 与正交相机,且
render()前clearDepth(),因此同屏可以放置任意多个 viewer(官方示例即同时显示两个),它们按调用顺序叠加; - viewer 不影响主渲染状态:
autoClear的保存/恢复保证了 viewer 对渲染器是透明的,可安全嵌入已有动画循环。
7. 参考文件索引
| 内容 | 路径 |
|---|---|
| 本文档对应的官方 API 文档源 | docs/pages/ShadowMapViewer.html.md |
| WebGL 版实现 | examples/jsm/utils/ShadowMapViewer.js |
| WebGPU 版实现(TSL/NodeMaterial) | examples/jsm/utils/ShadowMapViewerGPU.js |
| 官方示例(双光源 + 视锥线框 + resize) | examples/webgl_shadowmap_viewer.html |
| 示例运行截图 | examples/screenshots/webgl_shadowmap_viewer.jpg |
阴影状态惰性初始化(map = null) |
src/lights/LightShadow.js |
| WebGLRenderer / WebGPURenderer 文档 | docs/pages/WebGLRenderer.html.md、docs/pages/WebGPURenderer.html.md |
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
