影を付ける

THREE.jsは3Dの敷居を劇的に下げてくれる素晴らしいライブラリなんだけど、影だけは面倒臭い。といっても、そもそもWebGLで影をつけるというのがとてもとても面倒くさい処理という話なので、これでもずいぶんと楽させてもらってるそうだけど。

とりあえず、どのくらい面倒くさいかというと、自分だけの力でゼロから試すことは諦めてサクッとコピペで試してみたくらい。下のが参考にしたコード。

http://jsfiddle.net/6eRzt/11/

一応いくつか設定を変えつつ結果を確認してみたので、以下未来の自分用メモ。

まずは影なしバージョン。

<!doctype html>
<html lang="en">
  <head>
    <title>Shadow</title>
    <meta charset="utf-8">
    <style>
    canvas {
        background-color: #ffffdd;
        margin: auto auto;
        width:  600px;
        height: 400px;
    }   
    </style>
    <script src="js/Three.js"></script>
    <script src="js/RequestAnimationFrame.js"></script>
    <script src="js/jquery-1.7.1.min.js"></script>
    <script>
$(function() {
  var scene, camera, cube, sphere, renderer;
  var width = 600;
  var height = 400;
  var antialias = false;

  function init() {
    scene = new THREE.Scene();

    camera = new THREE.PerspectiveCamera(70, width / height, 1, 10000);    camera.position.y = 150;
    camera.position.z = 400;
    scene.add(camera);

    sphere = new THREE.Mesh(
      new THREE.SphereGeometry(50),
      new THREE.MeshPhongMaterial({color: 0x0000ff})
    );
    sphere.position.set(0, 300, 0);
    scene.add(sphere);

    cube = new THREE.Mesh(
      new THREE.CubeGeometry(100, 100, 100),
      new THREE.MeshPhongMaterial({color: 0x00ff00})
    );
    cube.position.y = 150;
    scene.add(cube);

    var ground = new THREE.Mesh(
      new THREE.PlaneGeometry(1000, 1000),
      new THREE.MeshPhongMaterial({color: 0xe0e0e0})
    );
    scene.add(ground);

    var light = new THREE.SpotLight(0xffffff);
    light.position.set(0, 1000, 0);
    light.angle = Math.PI / 4;
    light.target = cube;
    scene.add(light);

    renderer = new THREE.WebGLRenderer({
      antialias: antialias
    });
    renderer.setSize(width, height);
    document.body.appendChild(renderer.domElement);
  }

  function animate() {    requestAnimationFrame(animate);
    return render();
  }

  function render() {
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.02;
    cube.rotation.x += 0.03;
    sphere.position.x = 100 * Math.sin(cube.rotation.x);
    camera.lookAt(scene.position.clone().addSelf(new THREE.Vector3(0, 200, 0)));
    return renderer.render(scene, camera);
  }

  init();
  animate();
});
    </script>
  </head>
  <body>
  </body>
</html>

こんな感じ。

まだ影はついてないんだけど、すでにいくつかポイントがある。

  1. ライトはTHREE.SpotLightを使うこと。DirectionalLightはダメらしい。
  2. Three.jsは最新を使う。影のあたり結構変更が多そうなので、古いと謎エラーが出たりする。で、この辺はシェーダーが絡んでるのでエラーを解析するのはうちらWebGL素人には割と絶望的。

で、これに影をつけるにはどうするかというと、こうする。

    sphere.castShadow = true;
    cube.castShadow = true;
    cube.receiveShadow = true;    ground.receiveShadow = true;
    light.castShadow = true;
    light.shadowMapWidth = 1024;
    light.shadowMapHeight = 1024;
    light.shadowCameraNear = 100;
    light.shadowCameraFar = 1100;
    light.shadowCameraFov = 30;
    light.shadowCameraVisible = true;
    renderer.shadowMapEnabled = true;

ややこしいすね。で、そうするとこうなる。

球の影が立方体と床に、立方体の影が床に映ってるのが分かる。

ただ、実はさっき色々追加したうち light.shadowXxx 系はそれなりのデフォルト値が設定されているようで、なくてもなんとかなる。

ということでまずはlight.shadowXxx以外の必須項目の説明。

Object3D.castShadow 影を発する物体の場合はtrueに設定
Object3D.receiveShadow 影を映す物体の場合はtrueに設定
Light.caseShadow 影の発生源となるライトの場合はtrueに設定
WebGLRenderer.shadowMapEnabled 影を付ける場合はtrueに設定

とりいそぎこれだけtrueにしておくとたぶん影が表示される。

一応、それぞれの設定の動作を確認すると、例えばsphere.castShadowをfalseにすれば

sphereは影を投げないので、球の影がなくなるし、cube.receiveShadowをfalseにすると

立方体は影を受けないことになって、球の影が床には映るけど立方体には映らなくなる。

問題はオプショナルなlight.shadowXxxについてなんだけど、THREE.jsで用意されている影付けのアルゴリズムシャドウマッピングというものらしく、shadowXxxはそのためのいろんな設定パラメータになる。

大雑把にまとめると、まず光源の位置にカメラを移動して、そこから見える位置を覚えておいて、本来のカメラ位置から画面を描画するときに、光源から見えていなかった部分を影にするみたい。

ただ、バカ正直に空間全体にわたってその処理をやるととにかく処理が重たいのでオプションとして視野角や、どこからどこまでを処理するか指定できる。その指定がlight.shadowXxx。


light.shadowMapWidth たぶんデプステクスチャの横幅
light.shadowMapHeight たぶんデプステクスチャの縦幅
light.shadowCameraNear 四角錐台の上面の位置
light.shadowCameraFar 四角錐台の下面の位置
light.shadowCameraFov 視野角
light.shadowCameraVisible 四角錐台を表示するかどうか

この中でとりあえず何をおいても設定すべきは

light.shadowCameraVisible = true;

こうすると、THREE.jsがシャドウマッピングの設定を目で見える形で表示してくれる。

設定の内容を確認するためにいろいろ値を変えてみる。まずshadowCameraNearを球よりも下にしてみると

light.shadowCameraNear = 750;

球がシャドウマッピングの計算対象から外れて、球の影がどこにも表示されなくなる。

次にshadowCameraNearを元に戻して、shadowCameraFarを床よりも上にすると

light.shadowCameraFar = 950;

床がシャドウマッピングの対象から外れて、床から影が消え、球の影が立方体に映るだけになる。

今度はshadowCameraFovをものすごく小さくしてみる。

light.shadowCameraFov = 5;

そうするとちょうど指定した視野角に入る部分だけシャドウマッピングの対象になるので、その形に影ができる。

最後にshadowMapWidth、shadowMapHeightは影の解像度みたいなものなので、例えばものすごく小さくすると

light.shadowMapWidth = 24;
light.shadowMapHeight = 24;

なんかジャギジャギな感じになる。

他にもいくつか設定があって、場合によっては値を調節する必要もあるみたいなんだけど、まずはこの辺りだけ知ってればなんとかなりそう。