mcpField
T
threejs-impl-shadows

threejs-impl-shadows

github.com/impertio-studio开发23138 次安装30 周前更新
代码仓库

关于 threejs-impl-shadows

threejs-impl-shadows 是面向 Three.js 开发者的实时阴影实现参考,介绍阴影贴图、各类光源配置、问题诊断和性能取舍。

主要功能

  1. 01三步启用阴影
  2. 02阴影贴图类型指南
  3. 03各类光源配置
  4. 04使用 CameraHelper 诊断问题
  5. 05静态场景阴影优化

适用场景

  1. 01在场景中启用阴影
  2. 02调整平行光视锥
  3. 03排查阴影伪影
  4. 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 外,安装时一起下载的参考资料和脚本。

.gitkeep
references/
anti-patterns.md
examples.md
methods.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硬边缘、有锯齿最低否
PCFTHREE.PCFShadowMap边缘略柔和中等否
PCF 柔和THREE.PCFSoftShadowMap柔和的半影较高否(忽略该属性)
VSMTHREE.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 进行性能分析,确认阴影通道数量不会超出帧预算。


参考链接

官方来源

评论

支持 Markdown