Skip to content
three.js 光照与阴影原理
概述
光照在 three.js 中涉及三个环节:光源对象负责描述光的来源、强度和颜色;材质决定物体表面如何响应这些光照信息;阴影则需要显式地在渲染器、光源和网格体上分别开启。下面从光源类型入手,将这条链路串连起来。
光源类型与参数
three.js 提供了四种主要光源,各自对应一类现实中的发光方式。
环境光(AmbientLight)
环境光没有方向,均匀照亮场景中的所有物体,主要用于模拟间接照明,避免暗面完全变黑。
ts
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);构造函数的第二个参数 intensity 默认为 1,值越大越亮。环境光不参与阴影计算,也不会产生阴影。
平行光(DirectionalLight)
平行光从一个方向发出,可理解为无限远处的光源(如太阳),所有光线平行且没有距离衰减。
ts
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 20, 5);
scene.add(dirLight);平行光支持阴影投射。与 MeshStandardMaterial 搭配可以产生清晰的漫反射和高光。光源的 position 同时决定了光照方向——方向由该点指向 target(默认为原点),因此调整光源位置即调整光线方向。
点光源(PointLight)
点光源从一个点向四周均匀辐射光线,类似灯泡。默认具有距离衰减(decay)和范围限制(distance)。
ts
const pointLight = new THREE.PointLight(0xffaa00, 1, 100);
pointLight.position.set(0, 5, 0);
scene.add(pointLight);intensity:亮度,默认值1。distance:设为0表示光照距离无穷远(但仍受decay影响);大于0时,光照在distance处衰减至零。decay:衰减指数,默认值2(物理正确为2),设为1则为线性衰减。
点光源也可以投射阴影,但开销明显高于平行光和聚光灯:内部会生成 CubeCamera 并渲染六张深度贴图,性能消耗较大。
聚光灯(SpotLight)
聚光灯从一点发出,光线被约束在一个锥体范围内,参数比点光源更细致。
ts
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6; // 半锥角
spotLight.penumbra = 0.2; // 光斑边缘软化
spotLight.decay = 2;
spotLight.distance = 50;
scene.add(spotLight);angle:锥体张角的一半,最大值为Math.PI / 2。penumbra:控制光斑边缘的渐变区大小,0为锐利边缘,1表示从光斑中心到外缘完全渐变。
聚光灯使用透视投影生成阴影贴图,支持投射阴影。
此外还有 HemisphereLight(半球光)用于模拟天空与地面的散射,但不参与阴影,这里不展开。
材质对光照的响应
three.js 中材质是否受光照影响由材质类型决定。MeshBasicMaterial 不参与任何光照计算,无论场景中是否存在光源,其颜色都等于设置值。这种材质常用于 UI 元素、背景或不需要明暗变化的物体;如果与其他受光物体混用,视觉上会显得脱节。
MeshStandardMaterial 基于物理渲染(PBR),外观完全依赖光源。没有光源时,即使指定了颜色,渲染结果也是黑色。通常需要添加低强度的环境光来保证基础可见度。关键属性:
color:固有色,在白色光下呈现的颜色。roughness:表面粗糙度,0为镜面,1为完全漫反射。粗糙度越高,高光越分散。metalness:金属度,0为非金属,1为纯金属,同时会影响反射特性。
示例——金属质感球体:
ts
const geo = new THREE.SphereGeometry(1, 64, 64);
const mat = new THREE.MeshStandardMaterial({
color: 0xccaa88,
roughness: 0.3,
metalness: 1.0,
});
const sphere = new THREE.Mesh(geo, mat);
scene.add(sphere);MeshPhongMaterial 同样响应光照,但采用的是经典的 Phong 光照模型(Blinn-Phong 高光 + 简单漫反射),真实感不如 PBR,可用于性能敏感或需要非写实渲染的场景。
阴影映射的基本原理
three.js 的阴影基于阴影贴图(shadow mapping)技术:对于每个投射阴影的光源,先从光源视角将场景渲染到一张深度纹理上,记录下从光源看去最近表面的深度。在后续从相机视角渲染物体时,把当前片段的坐标变换到光源空间,采样深度纹理并比较深度——若当前片段深度大于存储深度,则说明有物体挡在前面,该片段处于阴影中。
平行光使用正交投影生成阴影贴图(光线平行),聚光灯用透视投影,点光源则用立方体贴图捕获六个方向的深度。投影矩阵和贴图类型由 LightShadow 的子类封装,开发者不需要手动操作,但需要关注阴影相机的可视范围。
启用阴影
要让阴影生效,三个环节都必须显式开启。
渲染器
ts
renderer.shadowMap.enabled = true;该设置会让渲染器在每次渲染前生成并绑定阴影贴图。默认的阴影贴图类型为 PCFSoftShadowMap(带百分比渐近过滤的软阴影),也可以切换为 BasicShadowMap(无过滤,锯齿明显)、PCFShadowMap 或 VSMShadowMap(方差阴影贴图,支持模糊但可能出现光斑瑕疵)。
光源
ts
dirLight.castShadow = true;只有 DirectionalLight、SpotLight 和 PointLight 支持 castShadow。光源上会挂载 shadow 属性(对应的 LightShadow 子类实例),用于配置贴图尺寸、阴影相机范围以及偏差等参数。
网格体
- 投射阴影:
mesh.castShadow = true - 接收阴影:
mesh.receiveShadow = true
例如,地面只接收阴影,立方体投射阴影并也可接收其他物体的投影:
ts
floor.receiveShadow = true;
cube.castShadow = true;
cube.receiveShadow = true;阴影配置的参数调优
阴影贴图分辨率
shadow.mapSize 控制阴影贴图的像素尺寸,默认为 512 × 512。较低分辨率会导致阴影边缘出现块状锯齿。
ts
dirLight.shadow.mapSize.set(2048, 2048);分辨率越高,单帧内存占用和渲染开销越大,需要根据场景复杂度权衡。
阴影相机范围
平行光的 shadow.camera 是正交相机,必须手动设置可视范围以确保覆盖所有需要阴影的物体。
ts
const shadowCam = dirLight.shadow.camera;
shadowCam.left = -20;
shadowCam.right = 20;
shadowCam.top = 20;
shadowCam.bottom = -20;
shadowCam.near = 0.5;
shadowCam.far = 500;视锥若未覆盖投射阴影的物体,这些物体的阴影会消失。缩小视锥可以让相同分辨率的贴图覆盖更小区域,提升像素利用率,从而获得更清晰的阴影边缘。
聚光灯的 shadow.camera 是透视相机,可通过 shadow.camera.fov、shadow.camera.near 和 shadow.camera.far 调整;点光源则自动根据光源的 distance 设置相机的远近平面,无需手动调节。
阴影偏差
阴影粉刺(shadow acne)是深度贴图分辨率有限导致的常见瑕疵:表面错误地认为自身遮挡了自己,产生条纹或点状图案。解决办法是引入深度偏移。
shadow.bias 是最直接的偏移量,会加到深度值上。值太小仍有粉刺,值太大则出现“漏光”——阴影与物体之间出现缝隙。
ts
dirLight.shadow.bias = -0.0005;平行光常用范围为 -0.001 到 -0.0001;点光源需要的 bias 绝对值往往更大。shadow.normalBias 则沿表面法线方向偏移采样点,能在一定程度上缓解 bias 过大导致的偏移问题,但其具体行为依赖几何体朝向。实际使用中两种偏差通常配合调整。
shadow.radius 控制 PCF 阴影的采样半径,值大于 1 会使阴影边缘模糊,同时减弱锯齿和粉刺,默认值 1 对多数场景够用。若使用 VSMShadowMap,radius 无效,需通过其它方式控制。
阴影调试
光照辅助对象可以帮助判断光源方向和范围是否合理。
ts
const dirLightHelper = new THREE.DirectionalLightHelper(dirLight, 1);
scene.add(dirLightHelper);
const spotLightHelper = new THREE.SpotLightHelper(spotLight);
scene.add(spotLightHelper);DirectionalLightHelper 使用平面与线段显示光线方向(第二个参数为辅助对象大小),SpotLightHelper 则绘制光锥的范围。
要检查阴影相机覆盖范围,可使用 CameraHelper 可视化视锥:
ts
const shadowCameraHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowCameraHelper);它会绘制正交相机的视四棱柱或透视相机的视锥体,直观展示哪些物体在阴影贴图覆盖范围内。在调节阴影相机参数时,将这个辅助对象保留在场景中实时预览,能明显减少试错。
综合示例
下面搭建一个带光照和阴影的场景:地板、球体和立方体,平行光投射阴影并辅以环境光。
ts
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
// 基础场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.set(8, 6, 12);
camera.lookAt(0, 1, 0);
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);
// 地面
const floorGeo = new THREE.PlaneGeometry(20, 20);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x888888, roughness: 0.8, metalness: 0.2,
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 球体(投射与接收阴影)
const sphereGeo = new THREE.SphereGeometry(1, 32, 32);
const sphereMat = new THREE.MeshStandardMaterial({
color: 0xdd5533, roughness: 0.4, metalness: 0.6,
});
const sphere = new THREE.Mesh(sphereGeo, sphereMat);
sphere.position.set(-2, 1.5, 0);
sphere.castShadow = true;
sphere.receiveShadow = true;
scene.add(sphere);
// 立方体
const boxGeo = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const boxMat = new THREE.MeshStandardMaterial({
color: 0x3355dd, roughness: 0.3, metalness: 0.8,
});
const box = new THREE.Mesh(boxGeo, boxMat);
box.position.set(2, 0.75, -1);
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);
// 环境光
const ambient = new THREE.AmbientLight(0x404070, 0.6);
scene.add(ambient);
// 平行光(投射阴影)
const dirLight = new THREE.DirectionalLight(0xffeedd, 1);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
dirLight.shadow.bias = -0.0003;
dirLight.shadow.normalBias = 0.02;
scene.add(dirLight);
// 辅助调试
const dirHelper = new THREE.DirectionalLightHelper(dirLight, 0.5);
scene.add(dirHelper);
const camHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(camHelper);
// 轨道控制
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();运行这段代码能得到一个金属质感的地板,红色球体和蓝色立方体在平行光照射下产生相对柔和的阴影——软阴影来自 PCFSoftShadowMap 和默认的 radius。可以逐步注释掉辅助对象,并尝试修改 bias、mapSize 等参数来观察视觉效果变化。
点光源或聚光灯的阴影配置思路相似,但对应的阴影相机属性不同。使用点光源时,要注意 distance 和 decay 对阴影覆盖范围的影响,因为其内部的 CubeCamera 会根据 distance 自动设定远近平面。
