关于 threejs-impl-shadows
threejs-impl-shadows 是面向 Three.js 开发者的实时阴影实现参考,介绍阴影贴图、各类光源配置、问题诊断和性能取舍。
主要功能
- 01三步启用阴影
- 02阴影贴图类型指南
- 03各类光源配置
- 04使用 CameraHelper 诊断问题
- 05静态场景阴影优化
适用场景
- 01在场景中启用阴影
- 02调整平行光视锥
- 03排查阴影伪影
- 04减少静态场景的阴影更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-shadows安装后在 Claude Code 中输入/threejs-impl-shadows确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitkeepreferences/anti-patterns.mdexamples.mdmethods.md详细介绍
threejs-impl-shadows
快速参考
启用阴影的三个步骤
Three.js 中的阴影需要显式完成三个启用步骤。遗漏任何一个步骤都会导致没有阴影。
import * as THREE from 'three';
// Step 1: Enable shadow maps on the renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Step 2: Enable shadow casting on the light
light.castShadow = true;
// Step 3: Enable per-mesh shadow behavior
mesh.castShadow = true; // this mesh casts shadows
ground.receiveShadow = true; // this mesh receives shadows
阴影贴图类型
| 类型 | 常量 | 质量 | 开销 | 支持 .radius |
|---|---|---|---|---|
| 基础 | THREE.BasicShadowMap | 硬边缘、有锯齿 | 最低 | 否 |
| PCF | THREE.PCFShadowMap | 边缘略柔和 | 中等 | 否 |
| PCF 柔和 | THREE.PCFSoftShadowMap | 柔和的半影 | 较高 | 否(忽略该属性) |
| VSM | THREE.VSMShadowMap | 高斯模糊 | 中等 | 是 |
- r160+ 中的默认值:
THREE.PCFShadowMap - PCFSoftShadowMap:最适合通用场景的选择;会忽略
shadow.radius属性 - VSMShadowMap:支持
shadow.radius和shadow.blurSamples,但在薄几何体上可能出现光线泄漏
关键警告
绝不要遗漏三个启用步骤中的任何一个——哪怕只遗漏一个,也会导致完全没有阴影,且不会显示错误消息。
绝不要让 DirectionalLight 的阴影相机视锥体保持默认大小——它几乎总是不适合你的场景。务必手动配置。
绝不要使用超过 1 到 2 个投射阴影的 PointLight——每个 PointLight 的阴影每帧都会渲染 6 个立方体贴图面。
务必将 shadow.mapSize 设为 2 的幂次值(512、1024、2048、4096)。
务必在重新定位 DirectionalLight 或 SpotLight 的目标时,将 light.target 添加到场景中。
阴影贴图类型——详细比较
BasicShadowMap
不应用任何过滤。会产生边缘生硬且有锯齿的阴影。仅用于调试,或用于有意采用硬阴影的风格化渲染。
PCFShadowMap(默认)
采用固定大小的核进行百分比渐近过滤。会产生边缘略柔和的阴影。在质量和性能之间取得了良好平衡。shadow.radius 属性不起作用。
PCFSoftShadowMap
使用可变核模拟更柔和的半影。质量高于 PCF,但开销也更大。shadow.radius 属性会被忽略——柔和程度由过滤器自动确定。
VSMShadowMap
方差阴影贴图使用高斯模糊通道。支持 shadow.radius(模糊大小)和 shadow.blurSamples(采样数量)。可生成平滑的柔和阴影,但当薄几何体与阴影接收体相接时,会出现光线泄漏伪影。场景中有许多互相重叠的薄阴影投射体时,绝不要使用 VSM。
各光源的阴影配置
DirectionalLight 阴影
使用正交阴影相机。必须手动调整视锥体大小,使其覆盖需要产生阴影的区域。
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;
// Shadow map resolution
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
// Orthographic frustum -- MUST size manually
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 500;
dirLight.shadow.camera.left = -50;
dirLight.shadow.camera.right = 50;
dirLight.shadow.camera.top = 50;
dirLight.shadow.camera.bottom = -50;
// Anti-acne bias
dirLight.shadow.bias = -0.0001;
dirLight.shadow.normalBias = 0.02;
scene.add(dirLight);
scene.add(dirLight.target); // REQUIRED if repositioning target
视锥体尺寸规则:尽可能紧密地框住需要阴影的区域。视锥体过大会浪费阴影贴图分辨率;过小则会裁切阴影。
SpotLight 阴影
使用透视阴影相机,会根据 SpotLight 的 .angle 和 .distance 自动配置。通常不需要手动配置视锥体。
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.3;
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.bias = -0.0001;
scene.add(spotLight);
scene.add(spotLight.target);
SpotLightShadow 提供:
.focus(数字,默认值为1)——调整阴影相机视场相对于聚光灯视场的大小,范围为[0, 1]
PointLight 阴影
使用立方体贴图(6 个透视相机,每个面各一个)。这是开销最高的阴影类型——每个投射阴影的 PointLight 每帧都会渲染场景 6 次。
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;
// Lower resolution to offset the 6x render cost
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
pointLight.shadow.bias = -0.001;
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 50;
scene.add(pointLight);
性能规则:始终优先使用 SpotLight 阴影,而不是 PointLight 阴影。PointLight 阴影的开销是 SpotLight 阴影的 6 倍。仅在绝对需要全方向投射阴影时才使用 PointLight 阴影。
使用 CameraHelper 调试阴影
配置阴影时,务必使用 CameraHelper 可视化阴影相机的视锥体:
const shadowHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowHelper);
该辅助对象会将视锥体渲染为线框。如果阴影被裁切、缺失或分辨率过低,辅助对象会显示视锥体是否过小、过大或未对齐。
务必在生产构建中移除 CameraHelper。
阴影伪影诊断流程图
Shadows not visible?
├── Check renderer.shadowMap.enabled === true
├── Check light.castShadow === true
├── Check mesh.castShadow / ground.receiveShadow === true
├── Check shadow camera frustum encompasses the scene (use CameraHelper)
└── Check shadow.camera.far is large enough
Striped lines on surfaces? (Shadow Acne)
├── Increase shadow.bias (start at -0.0001, go to -0.005)
├── Increase shadow.normalBias (0.02 to 0.1) for curved surfaces
└── Increase shadow.mapSize for more depth precision
Shadows float away from objects? (Peter Panning)
├── Reduce shadow.bias (you over-corrected for acne)
├── Use shadow.normalBias instead of shadow.bias
└── Increase shadow.mapSize resolution
Shadow edges shimmer when camera moves? (Shadow Swimming)
├── Increase shadow.mapSize resolution
├── Snap shadow camera to texel-aligned positions
└── Use Cascaded Shadow Maps (CSM) for large scenes
Light leaks through thin geometry? (Light Bleeding -- VSM only)
├── Switch from VSMShadowMap to PCFSoftShadowMap
├── Increase geometry thickness
└── Reduce shadow.radius value
Shadows on transparent/alpha-tested materials incorrect?
├── Set material.alphaTest threshold
├── Assign customDepthMaterial with matching alpha map
└── For fully transparent objects: use baked shadows or ContactShadows
透明材质阴影
默认情况下,阴影会将所有几何体视为完全不透明。对于 alpha 测试材质(例如树叶),请指定 customDepthMaterial:
import * as THREE from 'three';
const alphaMap = new THREE.TextureLoader().load('leaf-alpha.png');
const leafMaterial = new THREE.MeshStandardMaterial({
map: texture,
alphaMap: alphaMap,
alphaTest: 0.5,
side: THREE.DoubleSide,
});
// Custom depth material for correct shadow casting
const depthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: texture,
alphaMap: alphaMap,
alphaTest: 0.5,
});
leafMesh.material = leafMaterial;
leafMesh.customDepthMaterial = depthMaterial;
leafMesh.castShadow = true;
切勿期望透明对象(opacity < 1 且未使用 alphaTest)能够投射出正确的实时阴影。请改用烘焙阴影或屏幕空间技术。
静态阴影优化
对于灯光和投射阴影的物体都不会移动的场景,请禁用自动阴影更新:
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render shadows once
仅在有变化时设置 renderer.shadowMap.needsUpdate = true。对于静态场景,这可以完全避免逐帧渲染阴影贴图。
按灯光控制:
light.shadow.autoUpdate = false;
light.shadow.needsUpdate = true; // update this light's shadow once
ContactShadows 替代方案(Drei / React Three Fiber)
对于地面阴影,Drei 的 <ContactShadows> 能以低于阴影贴图的开销提供高质量的柔和阴影:
import { ContactShadows } from '@react-three/drei';
<ContactShadows
position={[0, 0, 0]}
opacity={0.5}
scale={10}
blur={1.5}
far={1}
/>
限制:仅适用于地面阴影,不会投射到任意几何体上,也不能替代复杂场景中的阴影贴图。
性能预算
| 阴影类型 | 每帧开销 | 建议上限 |
|---|---|---|
| DirectionalLight 阴影 | 1 次渲染通道 | 1-2 个灯光 |
| SpotLight 阴影 | 1 次渲染通道 | 2-4 个灯光 |
| PointLight 阴影 | 6 次渲染通道 | 最多 1 个灯光 |
阴影贴图分辨率的影响:将 mapSize 加倍会使 GPU 内存用量增加到 4 倍。在移动设备上切勿超过 4096x4096;建议使用 2048x2048 或更低的分辨率。
务必使用 renderer.info.render.calls 进行性能分析,确认阴影通道数量不会超出帧预算。
参考链接
- references/methods.md -- LightShadow API 签名和阴影贴图类型常量
- references/examples.md -- 各类灯光的完整阴影设置示例
- references/anti-patterns.md -- 阴影瑕疵、成因及修复方法
官方来源
- https://threejs.org/docs/#api/en/lights/shadows/LightShadow
- https://threejs.org/docs/#api/en/lights/shadows/DirectionalLightShadow
- https://threejs.org/docs/#api/en/lights/shadows/SpotLightShadow
- https://threejs.org/docs/#api/en/lights/shadows/PointLightShadow
- https://threejs.org/docs/#api/en/renderers/WebGLRenderer (shadowMap 属性)
- https://threejs.org/docs/#api/en/helpers/CameraHelper