WebGL & Three.js

Three.jsライトの使い方|Ambient・Directional・Point Lightを比較

Three.jsのAmbientLight・DirectionalLight・PointLightの違いと使い方を解説します。Materialとの関係、色・intensity、target、distance・decay、candela・lumen、LightHelper、組み合わせ方、暗い・反映されない原因まで確認できます。

この記事の目次
  1. Three.jsの基本Light比較表
  2. Lightへ反応するMaterialを使う
  3. AmbientLightの使い方
  4. AmbientLightを強くしすぎない
  5. HemisphereLightとの違い
  6. DirectionalLightの使い方
  7. rotationではなくtargetで方向を変える
  8. DirectionalLightHelperで方向を確認する
  9. PointLightの使い方
  10. powerはlumenで設定する
  11. distanceとdecayを設定する
  12. PointLightHelperで位置を確認する
  13. 3種類を組み合わせる基本scene
  14. Lightを動かす
  15. Lightの色とintensityを変更する
  16. 背景色とenvironment lightingは別物
  17. shadowを作れるLight
  18. Lightとperformanceの注意点
  19. LightHelperを削除・disposeする
  20. Lightが反映されない・暗い原因
  21. MaterialがMeshBasicMaterial
  22. Lightを作ったがsceneへ追加していない
  23. DirectionalLightのrotationを変えている
  24. PointLightが非常に暗い
  25. AmbientLightを足すと平坦になる
  26. PointLightの途中で急に暗くなる
  27. helperの向き・色が更新されない
  28. 床の向きが合わない
  29. 90度回転にならない
  30. Light選択チェックリスト
  31. まとめ

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を順に追加すると調整しやすくなります。