news 2026/9/8 23:09:19

three.js PointLightShadow 深度解析:点光源全方位阴影的配置、六面深度贴图与渲染原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three.js PointLightShadow 深度解析:点光源全方位阴影的配置、六面深度贴图与渲染原理

three.js PointLightShadow 深度解析:点光源全方位阴影的配置、六面深度贴图与渲染原理

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

PointLightShadow是 three.js 中PointLight(点光源)专用的阴影配置对象,负责管理点光源在世界中全方向投射阴影所需的内部相机、深度贴图尺寸以及各类阴影采样参数。本文以 PointLightShadow 官方文档页 为骨架,结合当前仓库中 PointLightShadow.js、LightShadow.js、PointLight.js 与 WebGLShadowMap.js 等源码实现,完整讲解它的构造函数、属性体系、实际使用步骤与底层六面立方体贴图渲染原理。读完本文,你将掌握点光源阴影的完整配置方法、性能瓶颈的成因,以及在不同渲染器/阴影类型下的兼容性边界。

概述与类继承关系

官方文档原句:Represents the shadow configuration of point lights.(表示点光源的阴影配置。)

由于点光源从单个位置向四周 360° 发光,它的阴影也与方向光(DirectionalLightShadow)、聚光灯(SpotLightShadow)截然不同:必须记录以光源为球心、全方向各个方向上的深度信息,才能正确判断场景中任意位置是否处于点光源的阴影中。

继承链:LightShadow → PointLightShadow

从源码 PointLightShadow.js 可以看出它的继承关系:

import { LightShadow } from './LightShadow.js'; import { PerspectiveCamera } from '../cameras/PerspectiveCamera.js'; class PointLightShadow extends LightShadow { constructor() { super( new PerspectiveCamera( 90, 1, 0.5, 500 ) ); this.isPointLightShadow = true; } } export { PointLightShadow };

也就是说:

  • LightShadow(src/lights/LightShadow.js)是所有光阴影的抽象基类,承载了biasnormalBiasmapSizeradiusmapautoUpdate等通用阴影属性以及矩阵更新、序列化等通用方法;
  • PointLightShadow只做两件事:向基类传入一台透视相机作为点光源观察世界的视角,并标记自身的类型标志isPointLightShadow = true

使用入口:Light 上的 shadow 属性

PointLightShadow不会由你手动实例化,而是在创建PointLight时自动挂载到其.shadow属性上。见 PointLight.js:

/** * This property holds the light's shadow configuration. * @type {PointLightShadow} */ this.shadow = new PointLightShadow();

因此,日常使用时的访问链是pointLight.shadow,例如pointLight.shadow.mapSize.set( 512, 512 )

构造函数:new PointLightShadow()

签名如下:

new PointLightShadow()

构造函数不接受任何参数。在 PointLightShadow.js 中,构造过程的核心是调用基类构造函数并传入:

super( new PerspectiveCamera( 90, 1, 0.5, 500 ) );

这行代码揭示了点光源阴影的本质:渲染器为点光源创建了一台专用的透视相机,参数含义如下:

参数作用
fov90(度)视锥体垂直张角。配合立方体贴图的每个面(每面恰好覆盖 90°×90° 的立体角)使用
aspect1纵横比 1:1,保证每个立方体面是正方形视口
near0.5近裁剪面,小于该距离的物体不会被记录进深度
far500远裁剪面,超出该距离的物体不产生阴影

值得注意的是,这个默认far = 500只在不设置PointLight.distance时生效——当PointLight实例设置了最大照明距离distance后,渲染器会用light.distance替换相机far(详见下文"渲染原理"一节),这与 PointLight 源码中distance = 0表示"无限远不衰减"的语义是对应的。

属性

.isPointLightShadow : boolean(只读)

这是PointLightShadow最重要的一个自有属性:

属性说明
isPointLightShadowtrue只读类型标志,用于类型测试

官方文档原句:This flag can be used for type testing.(该标志可用于类型测试。)在 PointLightShadow.js 中被赋值为true

它并非摆设,而是被底层渲染路径反复使用的关键分支依据,例如:

  • ShadowNode.js 中用shadow.isPointLightShadow !== true判定是否走VSM(方差阴影贴图)采样分支;
  • WebGLShadowMap.js 中同样用它决定是否对阴影贴图执行 VSM 模糊 pass;
  • ShadowNode.js 中决定阴影深度纹理取普通深度纹理还是 VSM 水平模糊纹理。

继承自 LightShadow 的核心属性

PointLightShadow的属性主要继承自基类 LightShadow.js,下列表格完整列出对实际效果影响最大的配置项(默认值均以当前仓库源码为准):

属性类型默认值说明
cameraPerspectiveCamera见上文光源观察世界的相机,直接决定阴影记录范围
intensitynumber1阴影强度,取值区间[0, 1]
biasnumber0深度偏移,微调量级约0.0001,可减轻阴影痤疮等伪影
biasNode?Node<float>nullbias的节点版本,仅WebGPURenderer支持;一旦定义,bias失效
normalBiasnumber0沿法线偏移采样位置,适合大场景浅入射角下的阴影痤疮,代价是阴影可能变形
radiusnumber1大于 1 时模糊阴影边缘;类型为BasicShadowMap时无效
blurSamplesnumber8VSM 阴影贴图模糊的采样数
mapSizeVector2(512, 512)阴影贴图宽高,需为 2 的幂,越大越清晰但越耗性能
mapTypenumberUnsignedByteType阴影纹理的类型
map?RenderTargetnull渲染期间内部生成的深度图(立方体贴图渲染目标)
mapPass?RenderTargetnullVSM 路径下内部生成的分布图
matrixMatrix4新的Matrix4模型到阴影相机空间矩阵,渲染期间内部计算
autoUpdatebooleantrue是否自动更新阴影
needsUpdatebooleanfalse手动置true并调用一次render以强制更新

与方向光/聚光灯阴影最大的不同是:mapSize的语义是每个立方体面的尺寸(六个面共享),因此总纹理开销约等于 6 ×mapSize²(详见性能小节)。

点光源阴影的 far 联动:light.distance

点光源阴影相机默认near = 0.5far = 500,但far会被PointLightdistance覆盖。逻辑在 WebGLShadowMap.js:

const far = light.distance || camera.far; if ( far !== camera.far ) { camera.far = far; camera.updateProjectionMatrix(); }

即:light.distance非 0 时用distance作为阴影远裁剪面(保证只有灯光实际照射范围内的物体投影),distance = 0(无限远)时退回相机默认far = 500。这意味着如果希望无限远点光源的阴影覆盖更大范围,需要显式放大相机 far,例如:

light.shadow.camera.far = 2000; // 或在受支持时配合 renderer.shadowMap

在场景中启用点光源阴影(完整实战示例)

与 three.js 所有阴影一致,点光源阴影需要三处配合才能生效:渲染器开关、光源与投影物体属性、接收物体属性。以官方示例 examples/webgl_shadowmap.html 的用法为参照,一个最小可运行示例为:

import * as THREE from './build/three.module.js'; const renderer = new THREE.WebGLRenderer(); renderer.shadowMap.enabled = true; // 1. 打开渲染器阴影总开关 renderer.shadowMap.type = THREE.PCFShadowMap; // 2. 点光源推荐 PCFShadowMap(见"兼容性边界") const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 60, innerWidth / innerHeight, 0.1, 100 ); // 地面:接收阴影 const ground = new THREE.Mesh( new THREE.PlaneGeometry( 20, 20 ), new THREE.MeshStandardMaterial( { color: 0x888888 } ) ); ground.rotation.x = - Math.PI / 2; ground.receiveShadow = true; scene.add( ground ); // 投影物体 const box = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), new THREE.MeshStandardMaterial( { color: 0xff0000 } ) ); box.castShadow = true; // 3. 物体投射阴影 scene.add( box ); // 点光源(灯丝/灯泡式全向光源) const light = new THREE.PointLight( 0xffffff, 1, 0, 2 ); // distance=0 无限远,decay=2 light.position.set( 2, 3, 2 ); light.castShadow = true; // 4. 光源投射阴影 light.shadow.mapSize.set( 512, 512 ); // 5. 每个立方体面 512×512 light.shadow.bias = 0.0001; // 轻微偏移抑制痤疮 scene.add( light );

注意第四步中light.castShadow = true会在渲染帧内把该光源加入阴影渲染列表;随后renderer.shadowMap(其类型见 WebGLShadowMap.js 的this.enabledthis.autoUpdatethis.needsUpdatethis.type等开关)才开始工作。

渲染原理:点光源为什么需要六面深度贴图

这是点光源阴影区别于方向光、聚光灯的核心机制。官方文档的继承页只描述了"LightShadow →",而真正的六面渲染逻辑位于 WebGLShadowMap.js。

1. 创建立方体深度渲染目标

当检测到light.isPointLight时(WebGLShadowMap.js),渲染器创建的shadow.map不是普通 2D 渲染目标,而是WebGLCubeRenderTarget,并配套CubeDepthTexture

shadow.map = new WebGLCubeRenderTarget( _shadowMapSize.x ); shadow.map.depthTexture = new CubeDepthTexture( _shadowMapSize.x, UnsignedIntType );

一个立方体贴图共有 6 个面(+X/-X/+Y/-Y/+Z/-Z),分别记录点光源朝六个方向观察到的深度。

2. 逐个面摆好"影子相机"并渲染

渲染循环对每个光源先求出渲染面数(WebGLShadowMap.js):

const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : shadow.getViewportCount();

点光源命中"6 个面"的分支。随后在 WebGLShadowMap.js 中,每一帧对 6 个面分别执行:

  1. 把内部透视相机放到点光源的世界位置(camera.position.copy( _lightPositionWorld ));
  2. 通过预置的_cubeDirections[ face ](六组单位方向)与_cubeUps[ face ](六组上向量)让相机lookAt朝向对应面;
  3. camera.updateMatrixWorld()后用投影矩阵 × 视图逆矩阵生成该面的投影矩阵与视锥体,用于物体剔除;
  4. renderer.setRenderTarget( shadow.map, face )把渲染目标切到立方体贴图的第face个面并clear()(WebGLShadowMap.js);
  5. MeshDistanceMaterial(内部距离材质,见下节)把所有castShadow的网格渲染进该面深度。

3. 帧率与性能的第一来源:6 倍开销

从上述流程可以推断:每帧点光源会把场景中的投射物渲染 6 次(6 个立方体面各一次)。这就是文档注释里mapSize"越高越好但越耗计算时间"在点光源上被放大 6 倍的原因。若同时存在最大纹理尺寸限制,渲染器会按shadowFrameExtents换算并钳制每个面的大小(WebGLShadowMap.js),不会无限放大。

兼容性边界:哪些阴影类型可用、哪些不可用

围绕isPointLightShadow这个标志,当前仓库对点光源阴影的类型支持存在两个明确的硬性边界(均有源码警告为证):

  1. VSM(VSMShadowMap)不支持点光源。WebGLShadowMap.js 会输出:WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.并跳过该光源。因为 VSM 的两次水平/垂直模糊 pass 是针对单一 2D 深度图设计的,与 6 面深度图结构冲突(WebGLShadowMap.js 中也明确以shadow.isPointLightShadow !== true作为执行 VSM pass 的前提)。

  2. PCFSoftShadowMap在当前仓库的 WebGL 路径中已回退为PCFShadowMap(WebGLShadowMap.js 输出PCFSoftShadowMap has been removed. Using PCFShadowMap instead.)。

因此,在 WebGL 渲染器下点光源阴影实际可用的是BasicShadowMap(硬阴影、无滤波)与PCFShadowMap(PCF 软阴影);radius > 1的边缘模糊只在 PCF 下有效。

而 TSL/WebGPU 节点路径中,点光源阴影走的是 PointShadowNode.js 的pointShadow( light, shadow )节点,同样会用到shadow.isPointLightShadow来判断是否采用立方体深度采样(见 ShadowNode.js),从而在底层代码中复用"点光源不适用 VSM 两趟模糊"的结论。

用 MeshDistanceMaterial 自定义点光源的投影材质

为了让点光源记录"到光源的距离"而非普通深度,渲染器内部使用 MeshDistanceMaterial(官方文档描述其为A material used internally for implementing shadow mapping with point lights)。在 WebGLShadowMap.js 中可以看到选取规则:

const customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial; result = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;

这带来一个实用的扩展点:把某个网格的customDistanceMaterial替换成自定义的MeshDistanceMaterial实例,即可控制该网格在点光源阴影中如何投影。例如保留透明贴图细节、让透明区域不投影,正如 MeshDistanceMaterial.js 顶部注释所示(把实例赋给Object3D.customDistanceMaterial以保证物体透明部分不投影)。需要复制一份默认材质修改时,可在该物体的onBeforeShadow/onAfterShadow钩子中按需调整(钩子在 WebGLShadowMap.js 中被调用)。

序列化与资源释放

PointLightShadow本身只新增了类型标志,序列化工作由继承方法完成:

  • 序列化:PointLight的 toJSON 会将shadow.toJSON()写入data.object.shadow,而 LightShadow.js 的toJSON()输出intensitybiasnormalBiasradiusblurSamplesmapSize以及内部相机(剔除matrix)——这正是编辑器导出与ObjectLoader.parse重建点光源阴影配置的依据;
  • 拷贝:LightShadow.copy(LightShadow.js)会克隆相机并逐项复制上述配置,PointLight的 copy 则通过this.shadow = source.shadow.clone()复制整个阴影对象;
  • 释放:PointLight.dispose 会调用this.shadow.dispose(),进而释放 GPU 上的map/mapPass渲染目标(LightShadow.js)。在你的应用移除点光源时调用light.dispose()即可避免 GPU 资源泄漏。

点光源阴影调优速查

综合源码语义,实践中针对PointLightShadow的常用调优手段可归结为:

  1. 控制 mapSize 与数量mapSize每增大 1 倍,6 个面合计开销增大 4×6 倍;多盏点光源叠加时要格外克制,优先用light.shadow.mapSize.set( 256, 256 )起步;
  2. 根据场景范围设置相机范围:对distance = 0的无限远点光源,按需调大light.shadow.camera.far;对设置了distance的点光源则无需手动设置(渲染器自动联动);
  3. 对抗伪影:出现阴影痤疮时把light.shadow.bias0.0001起逐步微调;大场景浅入射角痤疮优先考虑normalBias
  4. 选择正确的类型:点光源在 WebGL 渲染器下只使用PCFShadowMapBasicShadowMap;需要柔和边缘时用radius > 1(PCF),需要最高性能时用BasicShadowMap
  5. 静态场景省电:阴影静止时设置light.shadow.autoUpdate = false,需要更新时再置needsUpdate = true并触发一次渲染(两个开关均继承自 LightShadow.js);
  6. 透明物体的投影细节:通过给网格赋customDistanceMaterial = new THREE.MeshDistanceMaterial(...)定制其点光源投影行为。

小结

PointLightShadow虽然代码量极少(仅 31 行的 PointLightShadow.js),却在 three.js 阴影体系中扮演了承上启下的角色:它通过向LightShadow基类注入PerspectiveCamera( 90, 1, 0.5, 500 )与只读标志isPointLightShadow,把"点光源 = 全方向光源"的几何事实转化为"每帧渲染 6 面立方体深度图"的渲染管线,并依托isPointLightShadow标志在各渲染路径中正确禁用不兼容的 VSM、启用立方体深度采样。理解了它的构造与这一套底层分支逻辑,你就能精准地预测并控制点光源阴影的画质与性能开销。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:09:03

51单片机嵌入式入门:硬件-寄存器-调试四层穿透教学

1. 这不是“又一套单片机课”&#xff0c;而是嵌入式入门的临界点突破“尚硅谷51单片机视频教程&#xff08;2026新版&#xff09;”——光看标题&#xff0c;你可能以为又是那种“点亮LED→延时→流水灯→串口打印”的标准三板斧。但实测完全部78讲、142个实操案例、配套的32套…

作者头像 李华
网站建设 2026/9/8 23:08:41

FPGA多通道同步数据采集系统设计:从Verilog到调试实战

简介&#xff1a;基于FPGA的多通道数据采集与UART传输完整工程包&#xff0c;定位于电子工程与嵌入式开发学习者&#xff0c;用于解决8通道16位模拟信号同步采集、AD转换及串口回传的实际设计问题。压缩包共112个文件&#xff0c;以Verilog源码&#xff08;.v&#xff09;、Qua…

作者头像 李华
网站建设 2026/9/8 23:08:36

插墙式电源适配器热设计可靠性实战指南

1. 项目概述&#xff1a;为什么插墙式电源适配器的“热”不是小问题你拆开过家里那台给路由器、机顶盒、智能音箱供电的插墙式电源适配器吗&#xff1f;大概率没有——它太不起眼了&#xff0c;就静静插在墙插上&#xff0c;外壳温温的&#xff0c;甚至有点烫手。但就是这个巴掌…

作者头像 李华
网站建设 2026/9/8 23:06:53

STM32G431KBU6:高性价比混合信号控制SoC深度解析

1. 为什么STM32G431KBU6不是“又一款Cortex-M4单片机”&#xff0c;而是意法在混合信号控制领域埋下的关键棋子你手头那块标着“STM32G431KBU6”的小芯片&#xff0c;表面看只是意法半导体&#xff08;STMicroelectronics&#xff09;G4系列里一个带USB-CDC、64KB Flash、32KB …

作者头像 李华
网站建设 2026/9/8 23:04:24

OpenCode 完全指南:安装配置、模型接入与实战技巧

如果你在 2025 年还在用纯手动方式改 bug、写测试&#xff0c;那你大概率已经被周围同事的 AI 编程助手甩开几条街了。最近终端工具圈里讨论度最高的&#xff0c;除了 Claude Code、Codex CLI&#xff0c;就是 OpenCode——一个由 Anysphere&#xff08;Cursor 团队&#xff09…

作者头像 李华