Three.jsで3D objectを照らす基本は、全体の暗さを補うAmbientLightと、主方向を作るDirectionalLightの組み合わせです。電球のような局所光にはPointLightを使います。
const ambientLight = new THREE.AmbientLight(0xffffff, 0.25);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5);
directionalLight.position.set(4, 6, 3);
scene.add(directionalLight);
Lightの見え方はMaterial、色、intensity、objectのnormal、tone mapping、shadow、environmentによって変わります。この記事ではAmbientLight・HemisphereLight・DirectionalLight・PointLightの違い、target、distance・decay、物理単位、helper、組み合わせ方を解説します。
Three.jsの基本Light比較表
| Light | 光の特徴 | position | shadow | 主な用途 |
|---|---|---|---|---|
| AmbientLight | scene全体へ均等 | 影響しない | 不可 | 暗部の底上げ |
| HemisphereLight | sky色とground色を補間 | 方向の基準は上方向 | 不可 | 屋外の環境光 |
| DirectionalLight | 平行な光線 | targetとの方向を決める | 可能 | 太陽・主光源 |
| PointLight | 1点から全方向へ減衰 | 光源位置になる | 可能 | 電球・lamp・局所光 |
SpotLightはcone状、RectAreaLightは面光源です。まず上の基本Lightを理解してから追加すると役割を分けやすくなります。
Lightへ反応するMaterialを使う
Lightを追加しても、すべてのMaterialの見え方が変わるわけではありません。
| Material | Lightへの反応 | 特徴 |
|---|---|---|
| MeshBasicMaterial | 反応しない | 常にbase colorを表示 |
| MeshLambertMaterial | 反応する | matteな拡散反射、比較的軽量 |
| MeshPhongMaterial | 反応する | specular highlightを表現 |
| MeshStandardMaterial | 反応する | PBR、metalness・roughness |
| MeshPhysicalMaterial | 反応する | Standardを拡張したPBR |
Light比較にはMeshStandardMaterialなどを使います。Materialの選択はThree.js Materialの種類で詳しく解説しています。
AmbientLightの使い方
AmbientLightはdirectionを持たず、対象surfaceをscene全体で均等に照らします。
const ambientLight = new THREE.AmbientLight(
0xffffff, // color
0.25 // intensity
);
scene.add(ambientLight);
positionやrotationを変えても見え方は変わりません。directionがないためshadowも作れません。
AmbientLightを強くしすぎない
AmbientLightだけを強くすると、surfaceの向きによる明暗がなくなり、立体が平坦に見えます。完全な主光源ではなく、真っ黒になる暗部を少し持ち上げる補助光として使います。
ambientLight.intensity = 0.15;
ambientLight.color.set(0xdbeafe);
色付きAmbientLightはscene全体のcolor balanceへ影響します。Material色とLight色の関係はThree.jsでMaterialの色を設定する方法も参照してください。
HemisphereLightとの違い
HemisphereLightは上向きsurfaceへsky color、下向きsurfaceへground colorを与え、その間を補間します。
const hemisphereLight = new THREE.HemisphereLight(
0xb1e1ff, // sky color
0x6b4f2a, // ground color
0.8 // intensity
);
scene.add(hemisphereLight);
AmbientLightより上下のcolor差が出ますが、明確な主方向やshadowは作れません。屋外sceneでDirectionalLightと組み合わせる補助光に向きます。
AmbientLightとHemisphereLightを両方強くすると明るく平坦になりやすいため、どちらかを基礎光として少量から調整します。
DirectionalLightの使い方
DirectionalLightは無限遠から平行な光線が届くように振る舞い、太陽光の表現に向きます。
const directionalLight = new THREE.DirectionalLight(
0xffffff,
2.5
);
directionalLight.position.set(4, 6, 3);
scene.add(directionalLight);
PointLightと異なり、objectまでの距離による減衰はありません。同じdirectionの光がsceneへ届きます。
rotationではなくtargetで方向を変える
DirectionalLightのrotationを変更しても照射方向は変わりません。Lightのpositionからtargetのpositionへ向かうvectorで決まります。
directionalLight.position.set(4, 6, 3);
directionalLight.target.position.set(0, 1, 0);
scene.add(directionalLight);
scene.add(directionalLight.target);
default以外のtarget positionを使う場合、targetもscene graphへ追加します。追加しないとworld matrixが期待どおり更新されず、方向が変わらない原因になります。
別のObject3Dをtargetとして設定すれば、そのobjectを追跡できます。
directionalLight.target = character;
scene.add(character);
DirectionalLightHelperで方向を確認する
const directionalHelper = new THREE.DirectionalLightHelper(
directionalLight,
2
);
scene.add(directionalHelper);
Lightやtargetを変更したあとにhelperを同期します。
directionalLight.target.position.set(2, 0, 0);
directionalHelper.update();
毎frameLightを動かす場合は毎frame更新します。staticなら初期設定後だけで十分です。
PointLightの使い方
PointLightは1点から全方向へ放射し、距離に応じて暗くなります。
const pointLight = new THREE.PointLight(
0xffcc88, // color
120, // intensity: candela
20, // distance
2 // decay
);
pointLight.position.set(-3, 4, 2);
scene.add(pointLight);
現行仕様ではPointLightのintensityはcandela(cd)です。旧sampleのintensity = 1をそのまま使うと、scene scaleやtone mappingによって非常に暗く見える場合があります。実世界scaleとexposureを意識し、極端な値を暗記せずsceneごとに調整します。
powerはlumenで設定する
PointLightのpowerはluminous powerをlumen(lm)で表します。powerを変更するとintensityも連動します。
pointLight.power = 1500;
intensityとpowerのどちらか一方を設計上の基準にし、両方を別々に上書きし続けないようにします。
distanceとdecayを設定する
distance = 0: cutoffなし。inverse-square lawで無限遠まで減衰distance > 0: cutoff付近で滑らかに0へ減衰decay = 2: physical renderingで維持すべきdefault
pointLight.distance = 15;
pointLight.decay = 2;
distance cutoffは演出やperformance範囲の制御には便利ですが、物理的には不自然なcutoffです。自然さを優先する場合はdistance 0とdecay 2を基準にし、scene scaleとintensityを合わせます。
PointLightHelperで位置を確認する
const pointHelper = new THREE.PointLightHelper(pointLight, 0.4);
scene.add(pointHelper);
pointLight.position.set(2, 5, -1);
pointHelper.update();
PointLightは方向を持たないため、helperは光源位置をsphereで可視化します。
3種類を組み合わせる基本scene
最初はAmbientLightを弱く、DirectionalLightを主光源にし、PointLightをaccentとして加えます。
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(6, 4, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
controls.update();
const material = new THREE.MeshStandardMaterial({
color: 0x38bdf8,
roughness: 0.45,
metalness: 0.1,
});
const mesh = new THREE.Mesh(
new THREE.TorusKnotGeometry(1, 0.35, 128, 24),
material
);
mesh.position.y = 1.5;
scene.add(mesh);
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x374151, roughness: 0.9 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5);
directionalLight.position.set(4, 6, 3);
directionalLight.target.position.set(0, 1, 0);
scene.add(directionalLight, directionalLight.target);
const pointLight = new THREE.PointLight(0xff8844, 90, 12, 2);
pointLight.position.set(-3, 3, 2);
scene.add(pointLight);
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
この段階ではshadowを有効にしていません。床に明暗は出ますが、Meshの形をした影は落ちません。shadow mapは別設定です。
Lightを動かす
LightもObject3Dなのでpositionを変更できます。PointLightを周回させる例です。
const timer = new THREE.Timer();
timer.connect(document);
renderer.setAnimationLoop((timestamp) => {
timer.update(timestamp);
const elapsed = timer.getElapsed();
pointLight.position.set(
Math.cos(elapsed) * 4,
3,
Math.sin(elapsed) * 4
);
pointHelper.update();
renderer.render(scene, camera);
});
frameごとに固定角度を加算するよりelapsed timeから位置を計算すると、refresh rateによる速度差を避けられます。Object3Dの移動はThree.jsの回転・移動・scaleで詳しく解説しています。
Lightの色とintensityを変更する
directionalLight.color.set(0xfff1dd);
directionalLight.intensity = 3;
pointLight.color.set("#60a5fa");
pointLight.intensity = 140;
Light propertyを変更するだけならMaterialのneedsUpdateは通常不要です。helperを表示している場合は、colorやtransform変更後にhelper.update()を呼びます。
明るさはLight intensityだけで決まりません。Materialのroughness・metalness、environment map、renderer tone mapping・exposure、color managementも結果へ影響します。1つのparameterを極端に上げる前に全体を確認します。
背景色とenvironment lightingは別物
scene.backgroundへ色やtextureを設定しても、それだけではMeshStandardMaterialを照らしません。
scene.background = new THREE.Color(0x111827);
PBR Materialへ周囲の光を与えるには、適切に処理したenvironment mapをscene.environmentへ設定します。AmbientLightとenvironment mapは同じものではありません。
shadowを作れるLight
基本Lightのうち、shadow mapを作れるのはDirectionalLight・PointLight・SpotLightです。AmbientLightとHemisphereLightはdirectionがないためshadowを作れません。
shadowには少なくとも次が必要です。
renderer.shadowMap.enabled = true;
directionalLight.castShadow = true;
mesh.castShadow = true;
floor.receiveShadow = true;
DirectionalLightはshadow camera範囲、PointLightは6方向のshadow render、map size・biasなども調整します。詳細はThree.jsで影を落とす方法に分離しています。
Lightとperformanceの注意点
Lightを増やすほどMaterial shaderの計算が増えます。特にshadow付きLightはsceneを追加renderするためcostが高くなります。
- 役割のないLightを多数追加しない
- PointLightのdistanceで影響範囲を必要に応じて制限する
- shadowを落とすLight数を絞る
- static sceneはbaked lightingやenvironment mapも検討する
- helperはdebug時だけ表示する
- 実機でGPU時間・draw call・shadow mapを計測する
PointLight shadowはcubemapの6方向をrenderするため、DirectionalLight shadowより重くなりやすい点にも注意します。
LightHelperを削除・disposeする
helperをsceneから外すだけでなく、GPU resourceも解放します。
scene.remove(directionalHelper, pointHelper);
directionalHelper.dispose();
pointHelper.dispose();
Light自体にもdispose()があります。shadowなど内部resourceを持つLightが不要になったらsceneから外し、disposeします。
scene.remove(pointLight);
pointLight.dispose();
Lightが反映されない・暗い原因
MaterialがMeshBasicMaterial
MeshBasicMaterialはLightへ反応しません。MeshStandardMaterial、MeshLambertMaterial、MeshPhongMaterialなどへ変更して比較します。
Lightを作ったがsceneへ追加していない
scene.add(light)を確認します。DirectionalLightでcustom targetを使う場合はtargetも追加します。
DirectionalLightのrotationを変えている
DirectionalLightの方向はpositionからtargetへのvectorです。rotationではなくposition・targetを変更し、helperで確認します。
PointLightが非常に暗い
現行PointLight intensityはcandelaです。sceneのunit scale、Lightまでの距離、decay、distance、Material、tone mapping exposureを確認します。Lightの近くへobjectを置く最小sceneから調整してください。
AmbientLightを足すと平坦になる
AmbientLightのintensityが高すぎます。弱くしてDirectionalLightで主方向を作るか、HemisphereLight・environment mapを用途に合わせて使います。
PointLightの途中で急に暗くなる
distance cutoffが設定されています。自然なinverse-square減衰だけを使うならdistanceを0へ戻します。
helperの向き・色が更新されない
Light・target・colorを変更したあとにhelper.update()を呼びます。targetがscene graphへ追加されているかも確認します。
床の向きが合わない
PlaneGeometryはdefaultでXY平面です。XZ floorにするなら一般的にはfloor.rotation.x = -Math.PI / 2を使います。Materialが片面ならfront faceの向きも確認します。
90度回転にならない
degToRad()はdegreeをradianへ変換します。
floor.rotation.x = THREE.MathUtils.degToRad(-90);
radianをdegreeへ変換するmethodではありません。
Light選択チェックリスト
- Lightへ反応するMaterialを使った
- AmbientLightは暗部補助として弱く設定した
- 屋外補助光ならHemisphereLightも比較した
- DirectionalLightはpositionとtargetで方向を決めた
- custom targetをscene graphへ追加した
- PointLightのcandela・lumen・distance・decayを理解した
- helperで位置・方向を確認した
- helperは変更後にupdateした
- backgroundとenvironment lightingを混同していない
- shadowとLight本体の設定を分けた
- Light数・shadow数・実機performanceを確認した
- 不要なhelper・Lightをdisposeした
まとめ
AmbientLightはscene全体の暗部を均等に補い、DirectionalLightは太陽のような平行光で立体の主方向を作ります。PointLightは電球のように1点から全方向へ広がり、inverse-square lawで距離減衰します。
Lightを追加する前にMaterialが照明へ反応するか確認してください。DirectionalLightはrotationではなくtarget、PointLightはcandela・distance・decayが重要です。最初は弱いAmbientLightと1つのDirectionalLightから始め、必要な局所光・environment・shadowを順に追加すると調整しやすくなります。