阴影系统
阴影是真实感渲染的重要组成部分,Three.js 提供了完整的阴影系统,支持多种光源类型的阴影渲染。
概述
阴影能够增强场景的立体感和真实感。Three.js 使用阴影贴图(Shadow Mapping)技术实现阴影,支持实时计算和预烘焙阴影。
阴影系统架构
code
阴影渲染流程
┌─────────────────────────────────────────────────────────────┐
│ 1. 阴影贴图生成 │
│ ├── 从光源视角渲染场景 │
│ ├── 将深度值存储到阴影贴图 │
│ └── 每个投射阴影的光源需要一张阴影贴图 │
├─────────────────────────────────────────────────────────────┤
│ 2. 主渲染 pass │
│ ├── 正常渲染场景 │
│ ├── 对每个片元: │
│ │ ├── 变换到光源空间 │
│ │ ├── 比较深度值 │
│ │ └── 决定是否在阴影中 │
│ └── 应用阴影到着色结果 │
└─────────────────────────────────────────────────────────────┘阴影类型对比
| 阴影类型 | 质量 | 性能 | 特点 | 适用场景 |
|---|---|---|---|---|
| BasicShadowMap | 低 | 最高 | 硬阴影,锯齿明显 | 性能优先场景 |
| PCFShadowMap | 中 | 中 | 边缘平滑,默认选项 | 通用场景 |
| PCFSoftShadowMap | 高 | 低 | 更柔和的阴影边缘 | 高质量渲染 |
| VSMShadowMap | 高 | 中 | 方差阴影,支持模糊 | 高级效果 |
阴影基础
启用阴影
阴影需要四个组件协同工作:
javascript
import * as THREE from 'three';
// 1. 渲染器启用阴影
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 软阴影
// 2. 光源启用阴影
const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
scene.add(light);
// 3. 物体投射阴影
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
scene.add(mesh);
// 4. 物体接收阴影
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.receiveShadow = true;
scene.add(ground);阴影类型详解
javascript
// BasicShadowMap - 基础阴影
// - 最快的阴影类型
// - 锯齿明显,不进行边缘过滤
renderer.shadowMap.type = THREE.BasicShadowMap;
// PCFShadowMap - PCF 阴影(默认)
// - Percentage-Closer Filtering
// - 对阴影边缘进行平滑处理
renderer.shadowMap.type = THREE.PCFShadowMap;
// PCFSoftShadowMap - PCF 软阴影
// - 更高质量的边缘平滑
// - 支持阴影半径(shadow.radius)
// - 性能消耗最大
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// VSMShadowMap - 方差阴影贴图
// - Variance Shadow Mapping
// - 可以实现模糊效果
// - 需要特殊的着色器支持
renderer.shadowMap.type = THREE.VSMShadowMap;平行光阴影
基础配置
javascript
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 10);
directionalLight.castShadow = true;
// 阴影贴图尺寸
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
// 阴影相机配置(正交相机)
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;
// 阴影偏移(消除阴影瑕疵)
directionalLight.shadow.bias = -0.0001;
// 阴影半径(软阴影)
directionalLight.shadow.radius = 2;
scene.add(directionalLight);DirectionalLightShadow 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| mapSize | Vector2 | (512, 512) | 阴影贴图尺寸 |
| camera | OrthographicCamera | - | 正交阴影相机 |
| bias | number | 0 | 阴影偏移 |
| radius | number | 4 | 软阴影半径 |
| normalBias | number | 0 | 法线偏移 |
阴影相机原理
code
平行光阴影相机工作原理:
光源位置
↓
┌─────────────────┐ ← 阴影相机视角
│ │
│ 阴影范围 │ left, right, top, bottom
│ │
│ │
└─────────────────┘
↑ ↑
near far
光线方向:从光源位置指向目标点
阴影相机:正交相机,定义阴影投射范围阴影相机调试
javascript
// 可视化阴影相机范围
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
// 动态调整阴影相机
function updateShadowCamera(light, objects) {
// 计算包围盒
const box = new THREE.Box3();
objects.forEach(obj => {
box.expandByObject(obj);
});
// 更新阴影相机范围
light.shadow.camera.left = box.min.x;
light.shadow.camera.right = box.max.x;
light.shadow.camera.top = box.max.z;
light.shadow.camera.bottom = box.min.z;
// 更新投影矩阵
light.shadow.camera.updateProjectionMatrix();
}点光源阴影
点光源阴影使用立方体贴图(Cube Map),从六个方向渲染场景:
javascript
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(0, 10, 0);
pointLight.castShadow = true;
// 阴影贴图尺寸
pointLight.shadow.mapSize.width = 1024;
pointLight.shadow.mapSize.height = 1024;
// 阴影相机(透视相机)
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 50;
pointLight.shadow.camera.fov = 90;
// 阴影偏移
pointLight.shadow.bias = -0.001;
scene.add(pointLight);PointLightShadow 特点:
code
点光源阴影渲染:
┌───┐
│ +Y│
┌───┬───┬───┬───┬───┐
│-X │+Z │+X │-Z │ │
└───┴───┴───┴───┴───┘
│ -Y│
└───┘
六个方向各渲染一张深度图
消耗是平行光的 6 倍性能警告
点光源阴影消耗较大(需要渲染 6 次),请谨慎使用,特别是在移动端。
聚光灯阴影
javascript
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.1;
spotLight.castShadow = true;
// 阴影贴图尺寸
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
// 阴影相机(透视相机)
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 50;
spotLight.shadow.camera.fov = 30;
// 阴影偏移
spotLight.shadow.bias = -0.001;
// 聚光灯特有:聚焦参数
spotLight.shadow.focus = 1;
scene.add(spotLight);SpotLightShadow 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| mapSize | Vector2 | (512, 512) | 阴影贴图尺寸 |
| camera | PerspectiveCamera | - | 透视阴影相机 |
| bias | number | 0 | 阴影偏移 |
| radius | number | 4 | 软阴影半径 |
| focus | number | 1 | 聚焦程度 |
阴影优化
阴影贴图尺寸选择
javascript
// 根据场景需求选择合适尺寸
// 低质量 - 性能优先
light.shadow.mapSize.width = 512;
light.shadow.mapSize.height = 512;
// 中等质量 - 平衡选择
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
// 高质量 - 视觉优先
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
// 超高质量 - 近距离细节
light.shadow.mapSize.width = 4096;
light.shadow.mapSize.height = 4096;尺寸与内存关系:
| 尺寸 | 内存占用 | 适用场景 |
|---|---|---|
| 512 | 0.25 MB | 移动端、远景 |
| 1024 | 1 MB | 通用场景 |
| 2048 | 4 MB | 高质量需求 |
| 4096 | 16 MB | 近距离特写 |
阴影相机范围优化
javascript
// 只包含必要的场景区域
const light = new THREE.DirectionalLight();
light.castShadow = true;
// 紧凑的阴影相机范围
light.shadow.camera.left = -5;
light.shadow.camera.right = 5;
light.shadow.camera.top = 5;
light.shadow.camera.bottom = -5;
light.shadow.camera.near = 1;
light.shadow.camera.far = 20;
// 更新投影矩阵
light.shadow.camera.updateProjectionMatrix();阴影偏移调整
阴影偏移解决阴影瑕疵(Shadow Acne):
code
阴影瑕疵原因:
物体表面
════════════════════
↓ ↓ ↓ ↓
████████ ← 阴影贴图采样
↑ 部分像素误判为在阴影中
解决方案:添加偏移量javascript
// 基础偏移
light.shadow.bias = -0.0001;
// 大场景需要更大偏移
light.shadow.bias = -0.001;
// 小场景使用更小偏移
light.shadow.bias = -0.00001;
// 也可以使用法线偏移
light.shadow.normalBias = 0.02;级联阴影(CSM)
对于大场景,使用级联阴影贴图(Cascaded Shadow Maps):
javascript
import { CSM } from 'three/addons/csm/CSM.js';
const csm = new CSM({
maxFar: 1000,
cascades: 4,
mode: 'practical',
shadowMapSize: 1024,
lightDirection: new THREE.Vector3(1, 1, 1).normalize(),
camera: camera,
parent: scene,
lightIntensity: 1
});
// 更新 CSM
function animate() {
requestAnimationFrame(animate);
csm.update(camera);
renderer.render(scene, camera);
}
animate();CSM 原理:
code
级联阴影贴图:
相机视锥体
├──────┼──────┼──────┼──────
↓ ↓ ↓ ↓
阴影0 阴影1 阴影2 阴影3
(近) (远)
分辨率高 分辨率低
每个级联使用独立的阴影贴图
近处高分辨率,远处低分辨率高级阴影技术
软阴影
javascript
// 使用 PCFSoftShadowMap
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 调整阴影半径
light.shadow.radius = 4;
// 或使用 VSMShadowMap
renderer.shadowMap.type = THREE.VSMShadowMap;阴影透明度
javascript
// 半透明物体的阴影
const material = new THREE.MeshStandardMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.5
});
mesh.material.transparent = true;
mesh.material.opacity = 0.5;
mesh.castShadow = true;自定义阴影材质
javascript
import { MeshDepthMaterial } from 'three';
// 自定义阴影材质
const shadowMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
side: THREE.DoubleSide
});
// 对特定物体使用自定义阴影材质
mesh.customDepthMaterial = shadowMaterial;接触阴影
javascript
import { ContactShadow } from 'three/addons/objects/ContactShadow.js';
const contactShadow = new ContactShadow({
width: 10,
height: 10,
blur: 3.5,
darkness: 0.5,
opacity: 0.5
});
scene.add(contactShadow);阴影贴图调试
可视化阴影贴图
javascript
// 创建一个平面显示阴影贴图
const shadowHelper = new THREE.PlaneGeometry(5, 5);
const shadowHelperMaterial = new THREE.MeshBasicMaterial({
map: directionalLight.shadow.map.texture
});
const shadowHelperMesh = new THREE.Mesh(shadowHelper, shadowHelperMaterial);
shadowHelperMesh.position.set(5, 5, 0);
scene.add(shadowHelperMesh);阴影相机辅助器
javascript
const helper = new THREE.CameraHelper(light.shadow.camera);
scene.add(helper);
// 更新辅助器
helper.update();完整示例
基础阴影场景
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xa0a0a0);
// 相机
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(5, 10, 10);
// 渲染器
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 controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
// 环境光
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// 平行光(主光源)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 1;
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;
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(50, 50);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 立方体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 1, 0);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);
// 球体
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(4, 1, 0);
sphere.castShadow = true;
sphere.receiveShadow = true;
scene.add(sphere);
// 动画
function animate() {
requestAnimationFrame(animate);
// 旋转立方体
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// 更新控制器
controls.update();
renderer.render(scene, camera);
}
animate();多光源阴影
javascript
// 主光源
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(10, 10, 10);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
scene.add(mainLight);
// 补光
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.5);
fillLight.position.set(-10, 5, -10);
fillLight.castShadow = true;
fillLight.shadow.mapSize.width = 1024;
fillLight.shadow.mapSize.height = 1024;
scene.add(fillLight);
// 点光源
const pointLight = new THREE.PointLight(0xff6600, 1, 20);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
scene.add(pointLight);常见问题解答
Q: 为什么没有阴影?
A: 检查以下四个条件:
javascript
// 1. 渲染器是否启用阴影
renderer.shadowMap.enabled = true;
// 2. 光源是否投射阴影
light.castShadow = true;
// 3. 物体是否投射阴影
mesh.castShadow = true;
// 4. 物体是否接收阴影
ground.receiveShadow = true;
// 5. 阴影相机范围是否正确
// 使用 CameraHelper 检查
const helper = new THREE.CameraHelper(light.shadow.camera);
scene.add(helper);Q: 阴影有条纹/斑纹怎么办?
A: 这是阴影瑕疵(Shadow Acne),需要调整阴影偏移:
javascript
// 增加阴影偏移
light.shadow.bias = -0.0001;
// 或者使用法线偏移
light.shadow.normalBias = 0.02;
// 增加阴影贴图分辨率
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;Q: 阴影边缘锯齿严重?
A: 使用软阴影或提高分辨率:
javascript
// 使用软阴影类型
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 增加阴影半径
light.shadow.radius = 4;
// 提高阴影贴图分辨率
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;Q: 阴影被截断/不完整?
A: 调整阴影相机范围:
javascript
// 扩大阴影相机范围
light.shadow.camera.left = -20;
light.shadow.camera.right = 20;
light.shadow.camera.top = 20;
light.shadow.camera.bottom = -20;
light.shadow.camera.far = 100;
// 使用 CameraHelper 检查范围
const helper = new THREE.CameraHelper(light.shadow.camera);
scene.add(helper);Q: 阴影效果太硬/太柔和?
A: 调整阴影半径和类型:
javascript
// 更柔和的阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
light.shadow.radius = 4;
// 更硬的阴影
renderer.shadowMap.type = THREE.BasicShadowMap;
// 或
light.shadow.radius = 1;Q: 性能太差怎么办?
A: 优化阴影设置:
javascript
// 降低阴影贴图分辨率
light.shadow.mapSize.width = 512;
light.shadow.mapSize.height = 512;
// 减少投射阴影的光源数量
// 只对主光源启用阴影
mainLight.castShadow = true;
fillLight.castShadow = false;
// 缩小阴影相机范围
light.shadow.camera.left = -5;
light.shadow.camera.right = 5;
// 使用基础阴影类型
renderer.shadowMap.type = THREE.BasicShadowMap;性能优化建议
- 合理设置阴影贴图尺寸:根据物体大小和距离选择
- 优化阴影相机范围:只包含必要区域
- 减少阴影光源数量:避免每个光源都投射阴影
- 使用正确的阴影偏移:避免阴影瑕疵
- 移动端谨慎使用:实时阴影消耗较大
- 静态场景使用光照贴图:避免实时阴影
- 使用 LOD 思想:远处使用低分辨率阴影