Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影

作者:闭包旅人时间:2026-11-02 10:24:15浏览:327

详细讲解在 Three.js 中使用平行光 DirectionalLight 产生真实自然阴影的完整配置步骤、正交相机视锥体优化、分辨率调节与法线偏置抗瑕疵技巧。

Three.js 核心渲染实战 • 图形与光影调试 • 深度图优化指南
在 Three.js 中,要让平行光(DirectionalLight)产生清晰自然、贴合几何体的真实投影,核心不是提高光源强度,而是打通并调谐整条阴影管线:开启 WebGLRenderer 阴影贴图并指定柔化滤镜、在光源和网格物体上分别赋予投射与接收权限、将方向光内部正交相机的视锥范围精确贴合场景,并配置适当的法线偏置(normalBias)来消除黑斑条纹与漏光悬浮。
Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影
图 1:DirectionalLight 阴影投影的物理模型与正交相机边界关系

一、平行光阴影的核心运作机制

在现实物理世界中,太阳照射大地形成平行光,影子不会随着距离拉远而呈放射状放大,而是保持柱状或斜向平行投射。在 Three.js 中,THREE.DirectionalLight 就是这种模拟远距离光源的平行光对象。

计算机图形学要计算阴影,通常使用“阴影贴图(Shadow Map)”技术。整个流程可以拆分为两个渲染通道(Pass):

  1. 深度记录通道(Depth Pass):引擎把虚拟相机放置在光源位置,朝目标点观察。对于平行光,这个相机是一部正交相机(OrthographicCamera)。它把所有能投射阴影的物体渲染进一张纯深度纹理中。
  2. 主场景渲染通道(Color Pass):正常从主摄像机角度渲染场景,当计算地面或墙面某一像素的光照时,将该点的三维坐标变换到光源正交相机坐标系下,比较该点到光源的距离与深度贴图记录的最近距离。如果该点距离更远,说明光线被中途遮挡,该像素就处于阴影中。

因此,很多初学者“加了平行光却看不到阴影”或者“阴影模糊成巨大马赛克”,本质上都是因为这条阴影管线中的某几个关节被遗漏或正交相机范围失衡。

二、产生投影的 5 个关键配置步骤

在 Three.js 中让阴影真正生效,必须按顺序开启 5 个核心开关。缺少其中任意一步,阴影都不会呈现。

Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影
图 2:Three.js 阴影渲染的 5 项必选配置链路

下面是一份完整的最小可行基础配置代码:

// 1. 渲染器开启阴影计算
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// 采用软阴影算法,消除生硬死黑与严重马赛克
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// 2. 场景与光源设置
const scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(20, 40, 20);
dirLight.castShadow = true; // 允许光照产生阴影

// 平行光朝向默认为 (0, 0, 0),如果目标点变更必须将 target 加入场景
dirLight.target.position.set(0, 0, 0);
scene.add(dirLight);
scene.add(dirLight.target);

// 3. 投射阴影的模型(以立方体为例)
const boxGeo = new THREE.BoxGeometry(4, 4, 4);
const boxMat = new THREE.MeshStandardMaterial({ color: 0x394957 });
const boxMesh = new THREE.Mesh(boxGeo, boxMat);
boxMesh.position.set(0, 2, 0);
boxMesh.castShadow = true; // 允许该物体产生投影
scene.add(boxMesh);

// 4. 接收阴影的地面
const floorGeo = new THREE.PlaneGeometry(50, 50);
const floorMat = new THREE.MeshStandardMaterial({ color: 0xe5e5e5 });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.receiveShadow = true; // 允许表面接收投影
scene.add(floorMesh);

三、核心调优:DirectionalLight 阴影正交相机的边界包围

完成上述配置后,很多开发者发现阴影要么模糊不堪,要么移动一段距离后阴影凭空被切断。这是 DirectionalLight 最核心的技术细节:它的 light.shadow.camera 是一个正交相机(OrthographicCamera)。

点光源(PointLight)和聚光灯(SpotLight)使用透视相机(PerspectiveCamera),光线从一个点扩散,视野是由角度(FOV)控制的椎体;而平行光没有扩散角,它的视锥体是一个长方体盒子(Bounding Box)。这个盒子的长宽高由以下 6 个属性决定:

  • dirLight.shadow.camera.left 与 dirLight.shadow.camera.right:正交相机左右可视边界;
  • dirLight.shadow.camera.top 与 dirLight.shadow.camera.bottom:正交相机上下可视边界;
  • dirLight.shadow.camera.near 与 dirLight.shadow.camera.far:正交相机前后裁剪面。
Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影
图 3:正交相机视锥体尺寸与投影细节利用率的关系对比

为了精准调节视锥体,切忌“凭空猜数字”。Three.js 提供了专门的辅助对象 CameraHelper。在开发阶段务必将其添加到场景中:

// 调节平行光阴影相机的正交范围
const d = 15;
dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = -d;

// 调节近裁剪面与远裁剪面,越紧贴近目标,深度精度越高
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 80;

// 开启辅助可视化调试线框(关键调试工具)
const shadowCameraHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowCameraHelper);

// 注意:修改了 camera 属性后,需要更新投影矩阵
dirLight.shadow.camera.updateProjectionMatrix();
调试提示:CameraHelper 会在场景中绘制出一个黄黑相间的长方体线框。观察你的模型和影子是否完全落在该线框内。如果线框空余区域很大,适当缩小 d 的数值;如果模型的影子越过了黄色外边框,则需要相应调大。

四、阴影贴图分辨率与边缘柔和度选型

阴影的锐利度与锯齿感主要受 shadow.mapSize(贴图分辨率)以及 renderer.shadowMap.type(滤波算法)控制。

1. 分辨率设置与显存权衡

默认情况下,Three.js 的 shadow.mapSize.width 和 height 均为 512。这在较小场景尚可,但一旦场景略大,就会出现像素化。推荐将其设置为 1024 或 2048:

// 提升阴影贴图分辨率(数值须为 2 的幂)
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;

不建议盲目拉高到 4096 或 8192。分辨率翻倍意味着显存占用翻 4 倍,且在移动端或核显设备上会导致明显的每帧渲染掉帧。优先收紧正交相机的视锥范围,比盲目增加 mapSize 能获得更好的清晰度和更高的性能。

2. 四种阴影滤波算法的对比

算法类型 性能开销 视觉特征 适用场景
THREE.BasicShadowMap 极低 未滤波,纯像素阶梯锯齿,边缘死硬 低配设备性能兜底,或像素风格复古游戏
THREE.PCFShadowMap 低 百分比渐进滤波(Percentage-Closer Filtering),简单柔化 对性能要求苛刻的中低端移动端场景
THREE.PCFSoftShadowMap 中等(推荐) 多重泊松盘采样,边缘自然过渡,无生硬阶梯感 绝大多数商业 Web 3D 展厅与标准产品演示的首选
THREE.VSMShadowMap 较高 方差阴影贴图(Variance Shadow Map),极柔和边缘 大场景漫反射阴影,但需要处理光渗(Light Bleeding)

五、根治阴影瑕疵:消除自遮挡黑斑(Acne)与漏光悬浮(Peter Panning)

在开启阴影后,最常出现的两大致命视觉瑕疵是:模型表面出现像百叶窗或斑驳杂纹一样的黑色条纹(Shadow Acne,阴影自遮挡),或者调整偏移后影子与模型底部断开分离,看起来像悬浮在空中(Peter Panning,彼得潘现象/漏光悬浮)。

Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影
图 4:自遮挡黑斑、漏光悬浮与 normalBias 优化机制对比

正确的偏置配置策略

很多旧教程教大家设置 dirLight.shadow.bias = -0.001。但在斜面和曲面上,单纯调 bias 很容易顾此失彼:调小了黑斑依然在,调大了影子与物体分离。

现代 Three.js 的推荐做法是:优先使用 normalBias,保持 bias 在极小数值或为 0:

// 保持标准偏置接近 0,防止物体与阴影脱节
dirLight.shadow.bias = -0.0001;

// 沿顶点法线方向进行偏移,专治表面条纹与黑斑(通常取 0.02 ~ 0.08)
dirLight.shadow.normalBias = 0.05;

当你在高曲率模型(如球体、汽车外壳、人物角色头部)上看到波纹状黑斑时,只需将 normalBias 调到 0.02 到 0.05,黑斑通常会立刻消失,同时模型在地面上的接触阴影(Contact Shadow)依然保持紧密贴合。

六、生产环境高真实感配置代码整合

结合上述所有分析,以下是一套经过实战检验、兼顾真实度、柔和度与渲染性能的 DirectionalLight 阴影配置模板:

import * as THREE from 'three';

// 1. 初始化渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// 阴影总开关与柔化算法
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 2. 构造主场景与平行光
const scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight(0xfff8ee, 1.5); // 略带微暖阳光色
dirLight.position.set(30, 50, 30);
dirLight.castShadow = true;

// 3. 阴影贴图分辨率调优
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;

// 4. 正交相机边界优化(根据主体场景包围盒精确定制)
const frustumSize = 25;
dirLight.shadow.camera.left = -frustumSize;
dirLight.shadow.camera.right = frustumSize;
dirLight.shadow.camera.top = frustumSize;
dirLight.shadow.camera.bottom = -frustumSize;
dirLight.shadow.camera.near = 1.0;
dirLight.shadow.camera.far = 100;

// 5. 偏置与抗瑕疵
dirLight.shadow.bias = -0.0001;
dirLight.shadow.normalBias = 0.04;

// 6. 添加光照与其目标点
dirLight.target.position.set(0, 0, 0);
scene.add(dirLight);
scene.add(dirLight.target);

// 7. 开发调试模式下可挂载辅助器
if (process.env.NODE_ENV === 'development') {
  const cameraHelper = new THREE.CameraHelper(dirLight.shadow.camera);
  scene.add(cameraHelper);
}

七、动态场景中的阴影相机跟随策略

在角色漫游、第三人称控制器或开放大场景中,如果将正交相机设置得巨大无比以覆盖整个地图,阴影分辨率会被严重稀释。

实战技巧:让 DirectionalLight 动态跟随主角移动。因为 DirectionalLight 代表无限远处的太阳,它的空间位置只要和主角保持相对恒定的偏移向量,正交视锥体就能始终紧紧罩在主角和周围小范围区域内,既获得了极高的阴影清晰度,又无需消耗海量显存:

// 在每帧动画循环中同步光照与角色位置
function animate() {
  requestAnimationFrame(animate);

  // 假设 player 是你的主角 Mesh
  const lightOffset = new THREE.Vector3(25, 40, 25);
  dirLight.position.copy(player.position).add(lightOffset);
  dirLight.target.position.copy(player.position);

  // 保证光照矩阵刷新
  dirLight.target.updateMatrixWorld();

  renderer.render(scene, camera);
}

八、常见排错排查路径

如果在配置过程中阴影效果不符合预期,可以按照以下路线图快速定位原因:

Three.js 光照与阴影配置:怎么用 DirectionalLight 产生真实投影
图 5:Three.js 平行光阴影常见问题排查与优化对策总结

总结

要在 Three.js 中让 DirectionalLight 产生真实投影,关键并不在于某一个神秘参数,而是掌握整条管线的协同法则:

  • 确保渲染器、光源、投射网格和承接平面的 5 个布尔值开关完整闭环;
  • 始终通过 CameraHelper 直观校准正交相机的 6 个方向范围,让视锥体像紧身衣一样包裹场景,把每一像素贴图利用到极致;
  • 使用 PCFSoftShadowMap 获得柔和过渡的边缘;
  • 配合 normalBias 沿法线抵消深度精度误差,在彻底抹除表面黑斑的同时保持紧密自然的接地接触阴影。
作者最新文章
相关文章
更多