关于 lights
Lights 是面向 Three.js 场景的灯光配置技能,介绍环境光填充、方向光太阳与低强度环境光的组合,并说明如何保留可读阴影。它也涵盖可见天空的设置,以及避免运行时增删灯光造成材质重编译。
主要功能
- 01组合 HDR 图像光照与方向光
- 02使用 CSM 生成级联阴影
- 03设置全景天空背景
- 04平衡环境填充光与太阳光
- 05固定灯光数量以避免材质重编译
适用场景
- 01为户外场景配置太阳光和清晰阴影
- 02调节填充光以避免阴影褪淡
- 03显示随相机转动的全景天空
- 04实现闪光效果而不运行时新增灯光
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill lights安装后在 Claude Code 中输入/lights确认已加载。
详细介绍
灯光
逼真的场景照明由一套在启动时确定、固定的三部分灯光配置构成:基于图像的补光(设置在 scene.environment 上的 HDR,是主要补光源)加上一盏“太阳”——这里是 CSM 级联阴影贴图方向光,用来塑造表面形态并投射阴影——再加少量环境光。平衡原则:所有补光(先是 IBL,再是环境光)都必须明显弱于太阳,否则每个表面的阴影面都会被照亮,投射的阴影也会变淡。以下各节介绍可用的灯光组合、配置方案(IBL 补光 + 一盏太阳光 + 低强度环境光)、带阴影的太阳光(CSM)、环境与可见天空、平衡补光与太阳光以清晰呈现阴影、强度参考,以及为什么要在运行时保持灯光配置固定。
灯光组合
常见的灯光配置及其效果:
| 配置 | 效果 |
|---|---|
| 仅 AmbientLight | 均匀平坦的照明,没有阴影 |
| 仅 DirectionalLight | 对比强烈,阴影纯黑 |
| Ambient + Directional | 逼真的户外照明(太阳光 + 天空补光) |
| Ambient + PointLights/SpotLights | 带局部光源的室内场景 |
环境光会影响阴影的显示效果——没有环境光时,阴影是纯黑色。即使是夜景,也要保留足够的光线(例如 AmbientLight),确保观察者仍能看清环境。
配置方案:IBL 补光 + 一盏太阳光 + 低强度环境光
仅使用环境光或半球光会显得平坦、灰暗,因为没有光线能塑造表面形态。要呈现出“真实场所”的感觉,需要基于图像的补光 + 一盏太阳光 + 有纹理的材质:
- 使用 HDR 设置
scene.environment(Marketenvironment资源自带 HDR),让材质获得真实的基于图像的反射和补光。 - 添加一盏方向光/CSM“太阳光”来塑造形态并投射阴影。
- 保持环境光强度较低(
0.2–0.4,也可以不加)。
平衡补光与太阳光,否则阴影会变淡(见下文“平衡补光与太阳光以清晰呈现阴影”)。平坦的环境光加上纯色(无纹理)材质,正是“灰色黏土”的效果——使用 IBL、太阳光和真实纹理来改善(见 materials 技能)。
带阴影的太阳光(CSM)
推荐将 CSM(级联阴影贴图)用于太阳光。CSM 开箱即用,阴影质量优于 DirectionalLight,无需手动配置阴影相机边界。单独使用 DirectionalLight 时,只有一张阴影贴图覆盖一个固定视锥:在大型户外地图中,阴影要么很模糊(单张贴图铺满数百米),要么在较短距离之外直接消失。对于任何户外或大型场景,都使用 CSM——它的级联能让阴影从紧邻摄像机的位置一直到地平线都保持清晰。在大地图上使用小阴影视锥的单独 DirectionalLight 是错误做法。
CSM 看起来需要很多配置,但它是只需粘贴一次的样板代码——将这个辅助函数原样添加,然后调用一次 createCsmSun(...),并在每一帧调用 csm.update()。这就是全部开销;不要为了省事而换成普通的 DirectionalLight。
import { CSM } from 'three/examples/jsm/csm/CSM.js'
import { Material, MeshStandardMaterial, Vector3 } from 'three'
// Paste-once: an outdoor sun with cascaded shadows. Returns the csm; call csm.update() each frame.
function createCsmSun(scene, camera, renderer, lightDirection = new Vector3(-1, -2, -1), intensity = 2, maxFar = 200) {
renderer.shadowMap.enabled = true // do NOT also set shadowMap.type = PCFSoftShadowMap with CSM
const csm = new CSM({
cascades: 4, lightDirection: lightDirection.clone().normalize(),
camera, parent: scene, lightIntensity: intensity, maxFar, mode: 'practical',
})
// fade is ignored by the constructor and only takes effect after a frustum rebuild,
// so set it then rebuild — otherwise cascade seams stay hard.
csm.fade = true
csm.updateFrustums()
const biases = [-0.00001, -0.0001, -0.0003, -0.0006]
const normalBiases = [0.02, 0.06, 0.16, 0.3]
csm.lights.forEach((light, i) => {
light.shadow.bias = biases[i] ?? -0.0006
light.shadow.normalBias = normalBiases[i] ?? 0.3
})
// CSM injects its shader chunks into materials lazily at render time. Apply it ONLY to the lit
// PBR materials that actually receive shadows (MeshStandardMaterial — terrain, characters, props).
// Splicing CSM's shadow chunks into materials with no lighting pipeline — additive sprites, points,
// MeshBasicMaterial, and other transparent VFX/UI — breaks their shaders so they render as dark
// quads (the classic "dark planes" over muzzle flashes and particles); those never receive shadows
// anyway. setupMaterial OVERWRITES material.onBeforeCompile, so only re-chain a material's existing
// hook when it actually has one — stock materials don't, and calling .apply on a missing prior hook
// would throw.
const done = new WeakSet<Material>()
const original = renderer.renderBufferDirect.bind(renderer)
renderer.renderBufferDirect = function (cam, sc, geom, material, object, group) {
if (material instanceof MeshStandardMaterial && !done.has(material)) {
done.add(material)
const prev = material.onBeforeCompile
csm.setupMaterial(material)
const csmHook = material.onBeforeCompile
if (typeof prev === 'function' && prev !== csmHook) {
material.onBeforeCompile = (...args) => { prev.apply(material, args); csmHook.apply(material, args) }
}
}
return original(cam, sc, geom, material, object, group)
}
return csm
}
请使用它,并在每一帧调用 csm.update(),且要在 renderer.render(...) 之前调用。忘记调用 csm.update() 是唯一容易犯的错误——这样阴影就不会跟随摄像机移动。
scene.add(new THREE.AmbientLight(0xffffff, 0.4)) // fill, so shadows aren't pure black
const csm = createCsmSun(scene, camera, renderer)
// in the render loop, BEFORE renderer.render(...):
csm.update()
环境与可见天空
使用 HDR 设置 scene.environment,提供基于图像的补光(见上文);可见天空是另一个相关步骤,但需要单独处理。可见天空必须使用等距柱状天空盒,而不是平面图像。若要在场景后方显示天空,请将等距柱状全景图指定给 scene.background,并设置其映射方式,这样 Three.js 才会将其环绕世界,作为一个随摄像机转动而保持位置不变的天空盒:
const sky = await new TextureLoader().loadAsync('/environment/<name>-background.webp')
sky.mapping = EquirectangularReflectionMapping // without this it renders as a flat 2D screen image
sky.colorSpace = SRGBColorSpace
scene.background = sky
切勿将天空图像设为摄像机的子级——那样会把一张平面图片固定在视野中,而不是为世界呈现真实的地平线。
平衡补光与阳光,让阴影清晰可见
平衡补光与阳光,否则阴影就会淡得看不清。 明亮的 IBL 环境(尤其是晴朗的正午天空)再加上较强的 AmbientLight,会让每个表面的背光面都充满光线,因此即使阴影投射设置正确,阳光投下的阴影也会淡到几乎看不见。
IBL 通常是比 AmbientLight 更强的补光来源——明亮的天空环境即使单独使用,也会冲淡阴影——因此只平衡 AmbientLight 还不够:还要调低 scene.environmentIntensity(通常设为 ~0.3–0.6),并确保阳光明显强于这两者,否则阴影仍会显得平淡。环境光也要保持较弱(0.2–0.4,或完全关闭),然后用肉眼确认物体能投下清晰可见的阴影。
如果设置正确的 CSM 在屏幕上看不到任何阴影,几乎总是因为补光太强,而不是出了故障——先调低补光(优先调低 IBL),再怀疑阴影管线。平淡的环境光加上无纹理材质,会呈现“灰色黏土”效果——可通过 IBL、阳光和真实纹理来改善(参见 materials 技能)。
强度参考
- AmbientLight:
0.3–0.6(补光) - CSM lightIntensity:
1–3(类似阳光,会影响整个场景) - PointLight / SpotLight:
1–5,并设置合理的distance(例如10–50)
使用 renderer.toneMapping = ACESFilmicToneMapping 将 HDR 范围映射为色调,并将曝光设为约 1。
固定灯光数量(运行时开销)
Three.js 会将灯光数量(以及投射阴影的灯光数量)烘焙进每个材质的着色器中。运行时添加或移除灯光,或切换灯光的 castShadow,都会导致重新编译所有材质,使帧卡顿。每次触发效果(例如枪口闪光、爆炸)都创建一盏灯,会让每次射击都出现明显卡顿。
- 在启动时一次性确定灯光配置,并保持灯光数量不变。
- 对于枪口闪光、爆炸和脉冲,不要添加灯光。为预先创建的常暗灯光设置动画,改变其
intensity/color;也可以用自发光材质或加色精灵模拟辉光。如果确实需要移动光源,可预先创建几盏灯光并重复使用。 - 材质也一样:更改材质的 defines(例如添加或移除贴图)也会触发重新编译——最好改为为 uniform 或
color设置动画。