Skip to content

Three.js - 光照与阴影

在 3D 图形中,灯光对于使物体可见以及创建逼真或风格化的外观至关重要。Three.js 提供了几种类型的灯光,用于模拟真实世界的照明条件。然而,并非所有材质都对光线有反应。像 MeshBasicMaterial 这样的基础材质是自发光的。你最常使用的材质,作为基于物理的渲染 (Physically-Based Rendering, PBR) 工作流的一部分,例如 MeshStandardMaterial 和 MeshPhysicalMaterial,被设计为以物理上合理的方式对光线做出反应。像 MeshLambertMaterial 和 MeshPhongMaterial 这样的旧版材质也会对光线做出反应,但通常不建议用于新项目。

Three.js 中的所有灯光都共享两个基本属性:

  • color − (可选) 灯光的十六进制颜色。默认为 0xffffff(白色)。可以使用 new THREE.Color(0xffffff) 设置。
  • intensity − (可选) 灯光强度或亮度的数值。有效范围取决于渲染器(renderer)的配置,但默认值 1 是一个很好的起始点。

阴影为场景增添了深度和真实感,但计算成本很高。因此,你必须明确地启用它们。这个过程包括配置渲染器、灯光以及场景中的对象。

首先,你必须在 WebGLRenderer 中启用阴影贴图(shadow mapping)。阴影贴图是一种从光源视角渲染场景以创建“阴影图”的技术。

// 在你的初始化代码中
renderer.shadowMap.enabled = true;
// 用于更柔和、更逼真的阴影(以性能为代价)
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 其他类型:THREE.BasicShadowMap, THREE.PCFShadowMap, THREE.VSMShadowMap

注意

选择 shadowMap.type 是性能和质量之间的权衡。PCFSoftShadowMap 是一个流行的选择,用于提供视觉上令人愉悦的柔和边缘。

并非所有灯光都能投射阴影。DirectionalLight(定向光)、PointLight(点光源)和 SpotLight(聚光灯)是主要的投射阴影的灯光。你需要为你希望生成阴影的每个灯光启用阴影投射。

// 对于定向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
// 为此灯光启用阴影投射
directionalLight.castShadow = true;
// --- 阴影质量 ---
// 设置阴影贴图的分辨率。值越高意味着质量越好,但性能越低。
// 值必须是 2 的幂(例如,512、1024、2048)。
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
// --- 阴影摄像机视锥体 ---
// 定义将渲染阴影的区域。这对于性能和质量至关重要。
// 更小、更紧密的视锥体(frustum)会带来更高质量的阴影。
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
// 用于调试阴影摄像机视锥体
const shadowCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(shadowCameraHelper);

“shadow.camera` 属性定义了一个虚拟盒子。只有在这个盒子内的物体才会投射阴影。将这个盒子尽可能紧密地围绕场景中的投射阴影对象进行调整是关键的优化措施。

最后,你必须指定场景中的哪些对象将投射阴影,哪些将接收阴影。一个对象可以同时投射和接收阴影。

// 投射阴影的对象(例如,一个立方体)
cube.castShadow = true;
// 接收阴影的对象(例如,一个平面/地板)
plane.receiveShadow = true;
// 一个对象可以同时投射和接收阴影
anotherObject.castShadow = true;
anotherObject.receiveShadow = true;

你可能会遇到阴影的视觉故障。以下是两个常见的:

  • 阴影痤疮(Shadow Acne):在接收阴影的表面上出现奇怪的线条图案。这是由于浮点精度误差引起的。通过应用一个小的“偏移(bias)”来修复它。
  • 彼得潘(Peter Panning)现象:阴影与投射它的物体之间出现间隙,使物体看起来像是漂浮着。通过“法线偏移(normal bias)”来修复它。
// 仔细调整这些值。从小处开始。
directionalLight.shadow.bias = -0.0001; // 修复阴影痤疮
directionalLight.shadow.normalBias = 0.02; // 修复彼得潘现象

此示例展示了使用 lil-gui 进行控制的现代设置以及阴影的最佳实践。此代码假设项目是使用像 Vite 这样的打包工具设置的。

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import GUI from 'lil-gui';
/**
* 基础设置
*/
const gui = new GUI();
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a2b34);
/**
* 尺寸
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};
/**
* 摄像机
*/
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.set(4, 5, 6);
scene.add(camera);
/**
* 灯光
*/
// 环境光用于基本照明
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 定向光用于投射阴影
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(5, 6, 2);
directionalLight.castShadow = true; // 启用阴影投射
scene.add(directionalLight);
// 配置阴影属性
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 20;
directionalLight.shadow.bias = -0.001;
// 辅助工具
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 1);
scene.add(directionalLightHelper);
const shadowCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(shadowCameraHelper);
shadowCameraHelper.visible = false; // 初始隐藏
/**
* 对象
*/
// 材质
const material = new THREE.MeshStandardMaterial({ color: 0x0080ff });
material.roughness = 0.6;
material.metalness = 0.2;
// 地板
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
material
);
floor.rotation.x = -Math.PI * 0.5;
floor.receiveShadow = true; // 地板接收阴影
scene.add(floor);
// 球体
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
material
);
sphere.position.y = 1;
sphere.castShadow = true; // 球体投射阴影
scene.add(sphere);
/**
* 渲染器
*/
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 在渲染器中启用阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
/**
* 控制器
*/
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* GUI 控制器
*/
gui.add(ambientLight, 'intensity').min(0).max(3).step(0.01).name('环境光强度');
gui.add(directionalLight, 'intensity').min(0).max(5).step(0.01).name('太阳光强度');
gui.add(directionalLight.position, 'x').min(-10).max(10).step(0.1).name('太阳 X');
gui.add(directionalLight.position, 'y').min(-10).max(10).step(0.1).name('太阳 Y');
gui.add(directionalLight.position, 'z').min(-10).max(10).step(0.1).name('太阳 Z');
gui.add(shadowCameraHelper, 'visible').name('调试阴影摄像机');
/**
* 动画循环
*/
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// 动画化球体
sphere.position.x = Math.sin(elapsedTime) * 3;
sphere.position.z = Math.cos(elapsedTime) * 3;
sphere.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();
/**
* 处理窗口大小调整
*/
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
灯光类型描述与用途
1. AmbientLight (环境光)从各个方向均匀照亮场景中的所有物体。用途:提供基础照明,防止阴影完全变黑。无法投射阴影。
2. DirectionalLight (定向光)以特定方向发射平行光线。用途:模拟非常遥远的光源,如太阳。非常适合投射清晰、平行的阴影。
3. PointLight (点光源)从空间中的一个点向所有方向发射光线。用途:模拟一个裸露的灯泡。阴影从光源位置向外辐射。
4. SpotLight (聚光灯)从一个点以锥形发射光线。用途:模拟手电筒、舞台聚光灯或汽车头灯。可以精确控制阴影区域。
5. HemisphereLight (半球光)位于场景正上方,颜色从天空色渐变为地面色。用途:模拟自然、环境化的户外光照。不投射阴影。
6. RectAreaLight (矩形区域光)沿矩形平面的表面均匀发射光线。用途:模拟明亮的窗户或摄影棚照明。仅适用于 MeshStandardMaterial 和 MeshPhysicalMaterial。阴影支持复杂且有限。