
three.js ShadowMapViewer 完全指南在 WebGL/WebGPU 渲染管线上实时观察阴影贴图【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsShadowMapViewer 是 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 lights 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 节点NodeMaterialDepthTexture见第 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 场景的观察相机单位即屏幕像素坐标独立 Scenenew Scene()与主场景隔离只包含 HUD 网格和可选名称标签深度着色平面PlaneGeometryShaderMaterial采样light.shadow.map.texture的.r通道转成灰度显示名称标签可选CanvasTextureMeshBasicMaterial当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 / size3.1 .enabled : boolean是否显示该 viewer默认true。关闭时render()、updateForWindowResize()等方法内部全部短路不再做任何渲染开销适合在调试面板里做开关。3.2 .position : Objectviewer 在屏幕上的位置语义为距离窗口左上角的偏移量像素结构为position.x // 默认 10 position.y // 默认 10 position.set( x, y ) // 调用后立即生效默认值{ x: 10, y: 10 }来自构造函数的内部frame常量见 ShadowMapViewer.js#L46-L51。坐标换算逻辑在position.set中ShadowMapViewer.js#L165-L178mesh.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-L176dirLightShadowMapViewer.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 内部自动生效无需再 update3.3 .size : Objectviewer 的宽高像素结构为size.width // 默认 256 size.height // 默认 256 size.set( width, height ) // 调用后立即生效默认值{ width: 256, height: 256 }同样来自frame常量。缩放实现是把 HUD 平面网格相对 256×256 基准几何做mesh.scaleShadowMapViewer.js#L142-L153mesh.scale.set( this.width / frame.width, this.height / frame.height, 1 ); // 缩放后位置锚点会偏移因此 size.set 内部还会调用 resetPosition()从源码结构看size.set在缩放后会重新执行position.set以修正锚点偏移——这就是缩放后位置会漂移、必须复位的实现原因。4. 方法详解render / update / updateForWindowResize4.1 .render( renderer ) — 每帧调用lightShadowMapViewer.render( renderer );此方法必须在应用的动画循环中调用且放在主场景renderer.render( scene, camera )之后才能形成前景叠加效果。WebGL 版实现ShadowMapViewer.js#L185-L204this.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 falseclearDepth()的组合保证了 HUD 不会清掉主场景的色缓冲只清除深度避免遮挡冲突渲染完立即恢复用户原设置不产生副作用深度值可视化规则。HUD 片元着色器读取深度纹理.r通道ShadowMapViewer.js#L69-L80float 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-L67const 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.depthTextureWebGPU 后端使用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-L234this.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-L224this.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-L93viewer 布局见 #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#L42WebGPU 项目 HUD 无显示误用了 WebGL 版ShadowMapViewer.js应导入 ShadowMapViewerGPU.js另外两点从源码可确认的行为多 viewer 叠加无冲突每个 viewer 持有独立的 Scene 与正交相机且render()前clearDepth()因此同屏可以放置任意多个 viewer官方示例即同时显示两个它们按调用顺序叠加viewer 不影响主渲染状态autoClear的保存/恢复保证了 viewer 对渲染器是透明的可安全嵌入已有动画循环。7. 参考文件索引内容路径本文档对应的官方 API 文档源docs/pages/ShadowMapViewer.html.mdWebGL 版实现examples/jsm/utils/ShadowMapViewer.jsWebGPU 版实现TSL/NodeMaterialexamples/jsm/utils/ShadowMapViewerGPU.js官方示例双光源 视锥线框 resizeexamples/webgl_shadowmap_viewer.html示例运行截图examples/screenshots/webgl_shadowmap_viewer.jpg阴影状态惰性初始化map nullsrc/lights/LightShadow.jsWebGLRenderer / WebGPURenderer 文档docs/pages/WebGLRenderer.html.md、docs/pages/WebGPURenderer.html.md【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考