mcpField
L
lights

lights

github.com/drawcall-ai设计与 UI137 次安装
代码仓库

关于 lights

Lights 是面向 Three.js 场景的灯光配置技能,介绍环境光填充、方向光太阳与低强度环境光的组合,并说明如何保留可读阴影。它也涵盖可见天空的设置,以及避免运行时增删灯光造成材质重编译。

主要功能

  1. 01组合 HDR 图像光照与方向光
  2. 02使用 CSM 生成级联阴影
  3. 03设置全景天空背景
  4. 04平衡环境填充光与太阳光
  5. 05固定灯光数量以避免材质重编译

适用场景

  1. 01为户外场景配置太阳光和清晰阴影
  2. 02调节填充光以避免阴影褪淡
  3. 03显示随相机转动的全景天空
  4. 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(Market environment 资源自带 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 设置动画。

评论

支持 Markdown