lights について
Lightsは、Three.jsシーンのライティング設定を扱うスキルです。画像ベースのフィルライト、CSMシャドウ付きの太陽光、弱い環境光の組み合わせに加え、空の設定や実行中のライト数変更によるシェーダー再コンパイルの回避方法を説明します。
主な機能
- 01HDRの画像ベース照明と方向光の組み合わせ
- 02CSMによるカスケードシャドウ
- 03正距円筒画像を使った空の設定
- 04フィルライトと太陽光のバランス調整
- 05シェーダー再コンパイル回避のためライト数を固定
活用シーン
- 01屋外シーンに太陽光と読み取りやすい影を設定
- 02影が薄くならないようフィル光を調整
- 03カメラに固定されないパノラマ空を表示
- 04実行中にライトを追加せずフラッシュ効果を表現
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill lightsインストール後、Claude Code で/lightsを入力して読み込まれたか確認します。
詳細
ライティング
説得力のあるライティングのシーンは、起動時に一度だけ決定する、固定された3要素のリグで構成されます。画像ベースのフィル(scene.environment に設定するHDR。主要なフィル光源)に、「太陽」を1つ加えます。ここではCSMのカスケードシャドウマップを使う指向性ライトで、サーフェスに形を与え、影を落とします。さらに、わずかなアンビエント光を加えます。バランスの原則は、すべてのフィル光(まずIBL、次にアンビエント)を太陽より明確に弱くすることです。そうしないと、すべてのサーフェスの影側が照らされすぎ、落ちる影が薄れてしまいます。以下のセクションでは、利用可能なライトの組み合わせ、基本構成(IBLフィル+太陽1つ+弱いアンビエント)、影を伴う太陽(CSM)、環境と見える空、影がはっきり見えるフィルと太陽のバランス調整、強度の目安、そして実行時にライト構成を固定する理由を説明します。
ライトの組み合わせ
一般的なリグと、それぞれが生み出す結果は次のとおりです。
| 構成 | 結果 |
|---|---|
| AmbientLight only | 平坦で均一なライティング。影はありません |
| DirectionalLight only | 強いコントラスト。影は完全な黒になります |
| Ambient + Directional | リアルな屋外ライティング(太陽+空からのフィル) |
| Ambient + PointLights/SpotLights | 局所的な光源を使った屋内シーン |
アンビエントライトは影の見え方を調整します。アンビエントライトがないと、影は完全な黒になります。夜のシーンでも、見る人が環境を認識できるよう、十分な明るさを保ってください(たとえば AmbientLight を使います)。
基本構成:IBLフィル+太陽1つ+弱いアンビエント
アンビエントライトのみ、または半球ライトのみのライティングは、サーフェスに形を与えるものがないため、平坦で灰色に見えます。「本物の場所」として感じられるのは、画像ベースのフィル+太陽+テクスチャ付きマテリアルです。
- HDRから
scene.environmentを設定します(Market のenvironmentアセットにはHDRが含まれています)。これにより、マテリアルに現実的な画像ベースの反射とフィル光が反映されます。 - 形と影を作る指向性ライト/CSMの「太陽」を1つ追加します。
- アンビエントライトは弱くする(
0.2–0.4、または使わない)ようにします。
フィル光と太陽のバランスを調整してください。そうしないと、影が薄れてしまいます(下記の「影がはっきり見えるフィルと太陽のバランス調整」を参照)。アンビエントライトだけの平坦な光と、単色の(テクスチャのない)マテリアルを組み合わせると、まさに「灰色の粘土」のような見た目になります。IBL、太陽、実際のテクスチャを使って改善してください(materials スキルを参照)。
影を伴う太陽(CSM)
太陽にはCSM(Cascading Shadow Maps)を推奨します。CSMは DirectionalLight を置き換え、特別な設定なしでより優れた影の品質を実現します。シャドウカメラの範囲を手動で設定する必要はありません。単独の DirectionalLight では、1つの固定フラスタムを覆うシャドウマップが1枚だけ使われます。そのため、大規模な屋外マップでは、影がぼやける(1枚のマップを数百メートルに引き伸ばす)か、短い距離を越えると単に表示されなくなります。屋外シーンや大規模なシーンでは、必ず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
}
これを使い、毎フレーム renderer.render(...) の前に csm.update() を呼び出してください。忘れやすい間違いは、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 を組み合わせると、すべての表面の影側が照らされ、シャドウの設定が正しくても、太陽光による影はほとんど見えなくなります。
通常、フィル光としては AmbientLight より IBL のほうが大きな光源になります。明るい空の環境光を最大強度にすると、それだけでシャドウがかすんで見えなくなるため、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:適切な
distance(例:10–50)で1–5
renderer.toneMapping = ACESFilmicToneMapping を設定して HDR の範囲をトーンマッピングし、露出を約 1 にしてください。
ライトの数を固定する(実行時コスト)
Three.js は、ライトの数(およびシャドウを落とすライトの数)を、すべてのマテリアルのシェーダーに組み込みます。ライトの追加や削除、またはライトの castShadow の実行時切り替えは、すべてのマテリアルの再コンパイルを引き起こし、フレームを停止させます。エフェクト(マズルフラッシュや爆発)ごとにライトを生成すると、発生のたびに目に見えるカクつきが生じます。
- ライトの構成は起動時に一度決め、数を固定してください。
- マズルフラッシュ、爆発、パルスには、ライトを追加しないでください。事前に作成した通常は暗いライトの
intensity/colorをアニメーションするか、発光マテリアルや加算スプライトでグローを表現してください。移動する光源が本当に必要な場合は、あらかじめ作成した再利用可能なライトをいくつかプールしてください。 - 同じことはマテリアルにも当てはまります。マテリアルの定義(マップの追加や削除など)を変更すると再コンパイルが発生するため、代わりにユニフォームまたは
colorをアニメーションしてください。