{T}

阴影系统

阴影是真实感渲染的重要组成部分,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 属性

属性类型默认值说明
mapSizeVector2(512, 512)阴影贴图尺寸
cameraOrthographicCamera-正交阴影相机
biasnumber0阴影偏移
radiusnumber4软阴影半径
normalBiasnumber0法线偏移

阴影相机原理

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 属性

属性类型默认值说明
mapSizeVector2(512, 512)阴影贴图尺寸
cameraPerspectiveCamera-透视阴影相机
biasnumber0阴影偏移
radiusnumber4软阴影半径
focusnumber1聚焦程度

阴影优化

阴影贴图尺寸选择

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;

尺寸与内存关系

尺寸内存占用适用场景
5120.25 MB移动端、远景
10241 MB通用场景
20484 MB高质量需求
409616 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;

性能优化建议

  1. 合理设置阴影贴图尺寸:根据物体大小和距离选择
  2. 优化阴影相机范围:只包含必要区域
  3. 减少阴影光源数量:避免每个光源都投射阴影
  4. 使用正确的阴影偏移:避免阴影瑕疵
  5. 移动端谨慎使用:实时阴影消耗较大
  6. 静态场景使用光照贴图:避免实时阴影
  7. 使用 LOD 思想:远处使用低分辨率阴影

相关链接