首页
/ three.js ShadowMapViewer 完全指南:在 WebGL/WebGPU 渲染管线上实时观察阴影贴图

three.js ShadowMapViewer 完全指南:在 WebGL/WebGPU 渲染管线上实时观察阴影贴图

2026-09-07 15:52:11作者:幸俭卉

ShadowMapViewer 是 three.js 提供的辅助类(addon),用于将投射阴影光源(DirectionalLight、SpotLight)的阴影贴图实时渲染到屏幕 HUD 上,是调试阴影精度、视锥范围与深度偏置问题的核心工具。本文覆盖其构造参数、全部属性与方法、WebGL 与 WebGPU 双版本的使用差异,并结合 ShadowMapViewer.js 源码剖析 HUD 渲染管线,配合 webgl_shadowmap_viewer.html 官方示例给出可直接运行的完整集成方案。

three.js 官方 ShadowMapViewer 示例运行效果:场景左上方叠加显示了两张实时阴影贴图 HUD

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.

由此可以确定三个关键事实:

  1. 仅支持平行光与聚光灯DirectionalLightSpotLight 的阴影贴图是单张 2D 深度图,HUD 直接以平面纹理方式显示;PointLight 需要 6 面立方体贴图,因此不在支持范围内;
  2. 按渲染器区分导入。该模块只能与 WebGLRenderer 一起使用;使用 WebGPURenderer 时必须从 ShadowMapViewerGPU.js 导入同名类;
  3. 它是一块叠加层(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.jsexamples/jsm/utils/ShadowMapViewerGPU.js 的公共 API 完全一致,差异只在 HUD 材质的实现方式(后者使用 TSL 节点 NodeMaterial + DepthTexture,见第 4 节)。

2. 构造函数:new ShadowMapViewer( light )

new ShadowMapViewer( light : Light )

light:要观察阴影贴图的投射光源,必须是已开启 castShadow = trueDirectionalLightSpotLight

构造函数接收光源后立即构建内部 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#L42ShadowMapViewer.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
);

也就是说:xy 增大 → 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.scaleShadowMapViewer.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;  // 恢复用户设置
  }
};

三个实现细节值得注意:

  1. light.shadow.map 是惰性初始化的。从 LightShadow.js 可以看到构造时 this.map = null,只有渲染器在首次阴影通道渲染后才分配 DepthRenderTarget。这就是 viewer 注释中强调"必须在第一帧渲染之后调用 render(),且每帧重新绑定纹理"的原因,否则 HUD 可能显示错误的(第一个光的)贴图;
  2. autoClear = false + clearDepth() 的组合保证了 HUD 不会清掉主场景的色缓冲,只清除深度避免遮挡冲突,渲染完立即恢复用户原设置,不产生副作用;
  3. 深度值可视化规则。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.jsrender() 逻辑相同,但 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()

重新应用 positionsize 到内部网格。只要直接改写了 position.x/ysize.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

另外两点从源码可确认的行为:

  1. 多 viewer 叠加无冲突:每个 viewer 持有独立的 Scene 与正交相机,且 render()clearDepth(),因此同屏可以放置任意多个 viewer(官方示例即同时显示两个),它们按调用顺序叠加;
  2. 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.mddocs/pages/WebGPURenderer.html.md
登录后查看全文
热门项目推荐
相关项目推荐