今回もThree.jsの記事です。ゆひゃです。2022年は「ゆひゃってThree.jsの記事の人ね。」言われたいと思ってます。
さて、今回はThree.jsのライト(Light)についてまとめます。「Three.js lighting」と検索するとサジェストに「tutorial」や「examples」、「not working」など、悩んでると思わしき単語が出てきます。
それではライトの使い方を解説いきます。
ライト(Light)
Three.jsの3Dオブジェクトは、ジオメトリとマテリアルによって成り立っていると以前の記事で解説しました。
マテリアルは多くの種類が用意されています。マテリアルはライトがないと見えないものとライトがなくても見えるものに分けることができます。
ライトがないと見えないマテリアルクラスは下記の5種類です。
- MeshLambertMaterial
- MeshPhongMaterial
- MeshStandardMaterial
- MeshPhysicalMaterial
- MeshToonMaterial
これらはそれぞれマテリアルごとに特徴を持ちますが、その違いについてはまた別の記事にてまとめようと思います。
ともあれ、これらのマテリアルを使うためにはライトをシーンに追加しなければいけません。
ライトの使い方
まずは一番基本的で直感的なスポットライトで、ライトの使い方を説明していきます。
使うテンプレートプログラム
import * as THREE from "three"
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import GUI from 'lil-gui'
const gui = new GUI()
init()
function init () {
const width = window.innerWidth
const height = window.innerHeight
const renderer = new THREE.WebGLRenderer()
document.body.appendChild(renderer.domElement)
renderer.setPixelRatio( window.devicePixelRatio )
renderer.setSize( width, height )
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera( 45, width/height, 5, 1000 )
camera.position.set( 150, 50, 60 )
camera.lookAt( 0, 0, 0 )
scene.add( camera )
const boxGeo = new THREE.BoxGeometry( 10, 10, 10 )
const boxMat = new THREE.MeshStandardMaterial()
const box = new THREE.Mesh( boxGeo, boxMat )
scene.add( box )
const planeGeo = new THREE.PlaneGeometry( 1000, 1000, 50, 50 )
const planeMat = new THREE.MeshStandardMaterial({
color: 0x333333,
})
const plane = new THREE.Mesh( planeGeo, planeMat )
plane.rotation.set( -Math.PI/2, 0, 0 )
scene.add( plane )
const controls = new OrbitControls( camera, renderer.domElement )
const animate = delta => {
requestAnimationFrame( animate )
box.position.y = 4 * ( Math.cos( delta * .002 ) + 1 ) + 10
box.rotation.x += .01
box.rotation.y += .008
box.rotation.z -= .005
controls.update()
renderer.render( scene, camera )
}
animate()
}
以前のコードとほとんど一緒ですが、一部改変してあります。
まず、26行目と32行目を見てみるとMeshStandardMaterialを使っていることがわかります。これは、光源がないと見えないマテリアルです。
この状態を試しにブラウザーに表示してみるとただ真っ黒な画面が表示されるだけだと思います。
ライトの追加
単純なライトの追加は簡単です。カメラの追加と同じようにインスタンス化と位置の調整、シーンへの追加をするだけで光源を設置することができます。
テンプレートコード/18行目の下にライトを追加するコードを書きます。
...
const scene = new THREE.Scene()
const light = new THREE.SpotLight( 0xf0f0f0 )
light.position.set( -10, 30, 40 )
scene.add( light )
...
これで光源が設置できました。5行目のSpotLightクラスは引数で色を指定することができます。指定しない場合は、「0xffffff」になります。"0x"は16進数であることを示すプレフィックスです。
デモを作ってたので見てみます。
回転しながら上下にゆらゆらしているボックスが表示されていると思います。
地面に影を追加する
デモにはまだ影が表示されていません。Three.jsにとって影の描写は処理的に重いものであるので、デフォルトでは影を描写しないようになっています。
影を表示させるためには、以下の4つの手順を行う必要があります。
- レンダラーに影の表示許可をする
- ライトに影を描画させる許可をする
- 3Dオブジェクトに形の影を映す許可をする
- 地面に影を映させる許可をする
...
renderer.setSize( width, height )
renderer.shadowMap.enable = true // 追加
...
const light = new THREE.SpotLight( 0xf0f0f0 )
light.castShadow = true // 追加
...
const box = new THREE.Mesh( boxGeo, boxMat )
box.castShadow = true // 追加
...
const plane = new THREE.Mesh( planeGeo, planeMat )
plane.receiveShadow = true // 追加
...
4つの手順で設定しました。テンプレートコードとライトのコードに対応した部分を追加コードの上に書いておいたので、コードを追記してください。
これで影を落とせるようになったはずです。デモを見てみます。
地面に影が映っています。動きに合わせて影も対応してるのがわかりますね。
応用編〜複数の影
もう1つボックスを追加してみます。
...
scene.add( box )
const secondBoxGeo = new THREE.BoxGeometry( 3, 3, 3 )
const secondBoxMat = new THREE.MeshStandardMaterial()
const secondBox = new THREE.Mesh( secondBoxGeo, secondBoxMat )
secondBox.castShadow = true
secondBox.position.y = 20
...
const animate = delta => {
...
secondBox.position.z = 10 * (Math.sin(delta * .004) + 1) + 10
secondBox.rotation.x += .01
secondBox.rotation.y -= .008
secondBox.rotation.z -= .005
controls.update()
renderer.render( scene, camera )
}
...
2つ目のボックスは1/3サイズで、1つ目より上を前後に動いてるようにしました。ボックス作成時にちゃんとcastShadowも設定しました。デモで確認してみます。
カメラの位置を少し調節しました。コントロールを有効にしていろんな角度から見てみてください。
少し違和感があります。
castShadowを設定したので地面に小さいボックスの影も映し出されています。ですが、大きいボックスの上に小さいボックスの影が出てません。
影を受け取る設定は、上の説明でplaneに設定したreceiveShadowでした。この設定を大きいボックスにも適用させて上げる必要があります。
...
const box = new THREE.Mesh( boxGeo, boxMat )
box.castShadow = true
box.receiveShadow = true // 追加!
scene.add( box )
...
これで大きいボックスの上にも影を表示することができるはずです。見てみましょう。
ボックス同士が近づいた時にちゃんと影が乗っていますね!
このように3Dオブジェクトには影を映し出すパラメータと影を受け取るパラメータの2種類があります。何も考えずにどちらも設定しておけば影に悩むことはありませんが、その分パフォーマンスが低下する可能性があります。
絶対に影を乗せないという場所には影を受け取る設定をしないようにすることが大事です。
応用編2〜ライトのターゲット
上の例ではボックスを回転させたり移動させたりしています。ライトも同様に動かすことができます。
その前にまずライトのヘルパーを表示してみます。
スポットライトヘルパー
ただライトを置いただけでは、実際そのライトはどの角度でどれくらいの距離までなのかがわかりにくいと思います。
そこで使うのがスポットライトヘルパー(SpotLightHelper)です。
ライトのコードの下にヘルパーを追加するコードを追加します。
...
const lightHelper = new THREE.SpotLightHelper( light )
scene.add( lightHelper )
...
これだけでスポットライトのヘルパーを表示することができます。lil-guiでライトヘルパーをオンオフできるデモを作ってみたので見てみましょう。
ライトの角度がわかりました。ライトはボックスではなく地面のある点の方を向いていることがわかったと思います。
ライトをボックスの方に向ける
ヘルパーでライトの向きが地面に向いていることがわかったので、大きいボックスの方に向けてみます。
...
scene.add( box )
light.target = box // 追加
...
const animate = delta => {
...
lightHelper.update() // 追加
}
...
大きい方のボックスをシーンに追加した直下にライトのターゲットパラメータにboxを追加するコードを書きます。
ここで注意なのが、上で追加したヘルパーはライトの最初の位置で固定してしまっているので、animate関数の中でupdateメソッドを使って常にライトをくれるようにする必要があります。
ライトがボックスの動きに合わせて動いているのがわかります。真ん中の線が常にボックスを貫いているので、ライトの中心にボックスがあるのもわかると思います。
スポットライトを使ったサンプル
ライトとボックスを使ったサンプルを作ってみました。
このコードは、GitHubにindex.jsファイルとして公開しています。
さてこのコードを見ていただけると3種類のライトのうち、赤いライトだけ設定して、青と緑はコピーしてるのがわかると思います。
...
const redLight = new THREE.SpotLight( 0xf00000 )
redLight.position.set( 0, 60, 0 )
redLight.angle = Math.PI/7
redLight.castShadow = true
...
const blueLight = redLight.clone()
blueLight.color.set( 0x0000f0 )
...
const greenLight = redLight.clone()
greenLight.color.set( 0x00f000 )
...
このように定義したライトのcloneメソッドを使うことで、全く同じパラメータを持った3Dオブジェクトを作成することができます。
その後color.setメソッドを使って色を設定すれば色のみ変更した同じライトを作ることができます。
cloneメソッドはもちろんライトだけでなく、あらゆる3Dオブジェクトでも使うことができます。ゆくゆく使う機会の多いメソッドなので覚えておいて損はないと思います。
あとはanimate関数内で、2π/3ずつずらして移動させてあげたらこのサンプルは完成します。
シンプルなコードで作れる割にはすごい3D感が出てますね。
まとめ
今回はスポットライトを使って、ライトの基本的な使い方を解説しました!
光源が必要になるマテリアルと光源の設置方法、影の落とし方を理解できたでしょうか。
今回説明に使ったスポットライトは他にも光の淡さや強さ、角度などまだまだ設定できるパラメータがあります。しかもライトはスポットライト他にも5種類くらいあり、それぞれ違うパラメータを持ちます。
他のライトの使い方はまた別の記事で解説しますが、どんどん複雑だと感じてくるかもしれません。
そんな時はまずは初心に立ち戻ってこの記事を見にきてくれたらと思います。
次の記事はこちら