three.jsにおける2D描画方法

Webブラウザ向けの3DCGライブラリであるthree.jsで、3D描画に2D描画*1を織り交ぜる方法を紹介します。これにより、前景と背景に自由に2D画像を描画できるようになり、ゲームのHUD表示などが可能になります。

基本的な考え方

three.jsに限った話ではありませんが、3DCGで2D描画をしたい機会は多い割に、意外と簡単ではないことが多いです。

3DCGで2D描画を行う場合、大きく分けて2つの方法が考えられます。

1. 3D描画と全く別個に描画を行う。
2. 3D描画で適切にカメラやオブジェクトをセッティングする。

1はWebGLなど3DCG支援機能を一切使わない方法です。Webならcanvas要素の前後に適当にimg要素などを配置するだけで擬似的に実現できます。欠点としては、ちゃんと要素が重なるように位置を調整する必要がある点でしょうか。

2は3DCG支援機能を利用した上で上手くやる方法です。今回はこっちの方のやり方を紹介します。

three.jsでの実現方法

とりあえずthree.jsにフォーカスを絞ってポイントを説明します。

サンプルコード

以下、サンプルコードです。各ファイルは適当に配置して下さい。

<title>Practice of Three.js</title>
<style>
body { margin: 0; }
</style>
<body>
	<script src="three.min.js"></script>
	<script src="main.js"></script>
</body>
// main.js
(function() {

  var WIDTH = 640, HEIGHT = 480;
  var renderer, scene, camera, scene2d, camera2d;

  init();
  animate();

  function init() {
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(WIDTH, HEIGHT);
    renderer.setClearColor(0x000000, 1);
    renderer.autoClear = false;
    document.body.appendChild(renderer.domElement);
    
    (function init3D() {
      camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 0.001, 1000);
      camera.position.set(0, 0, 10);
      scene = new THREE.Scene();
      scene.add(new THREE.AmbientLight(0x303030));
      var light = new THREE.DirectionalLight(0xFFFFFF);
      light.position.set(1, 1, 1);
      scene.add(light);
      var mesh = new THREE.Mesh(
        new THREE.SphereGeometry(1, 20, 20),
        new THREE.MeshLambertMaterial({ambient: 0xFFFFFF})
      );
      mesh.position.set(0, 0, 0);
      scene.add(mesh);
    })();
    
    (function init2D() {
      camera2d = new THREE.OrthographicCamera(0, WIDTH, 0, HEIGHT, 0.001, 10000);
      scene2d = new THREE.Scene();
    
      THREE.ImageUtils.loadTexture("cat1.jpg", undefined, function(texture) {
        var material = new THREE.SpriteMaterial({map: texture, color: 0xFFFFFF});
        var sprite;
        var w = texture.image.width, h = texture.image.height;

        sprite = new THREE.Sprite(material);
        sprite.position.set(w*0.5, h*0.5, -9999);
        sprite.scale.set(w, -h, 1);
        scene2d.add(sprite);
    
        sprite = new THREE.Sprite(material);
        sprite.position.set(w, h, -1);
        sprite.scale.set(w, -h, 1);
        scene2d.add(sprite);
      });
    })();
  }
  
  function animate() {
    requestAnimationFrame(animate);
    renderer.clear();
    renderer.render(scene2d, camera2d);
    renderer.render(scene, camera);
  }
  
})();

これを実行すると↓のような感じになります。

f:id:s-shin:20131211013614p:plain

ポイント

ポイントは以下の通りです。

  • 平行投影なカメラTHREE.OrthographicCameraを利用する。
  • sprite.scaleに幅と高さをセットする。
    • これによりピクセル単位で位置を指定できる。
  • 通常、画像は左上が原点で、テクスチャ座標は左下が原点。なので上下を反転する必要がある。
    • 2Dの位置指定は、画像と同様、左上からピクセル単位で指定できたほうが直感的ということもあり、sprite.scale.y = -height_of_imageすると上手くいく。
    • 詳しくはこちらの中程を参照。
  • THREE.Spriteは基準点が中央なので、sceneに配置する際に少々直感的ではない。サンプルでは妥協。
    • スプライトの左上を基準にするには、画像サイズに依存するのが厄介。何とかしたいのであればラッパークラスなどで対応するのが良さそう。
  • 配置するのに画像の幅と高さを使いたいので、THREE.ImageUtils.loadTextureを使う場合は、コールバックを利用する。
    • 戻り値は第三引数のコールバックの引数と同じtextureなので、サイズが予め分かっているなら、わざわざコールバックを使う必要はない*2
  • 前景と背景に関しては、nearクリップに近いと前景に、farクリップに近いと背景になる。
    • なるべくnear/farクリップ近くに描画しないと3D描画の方に重なってしまったりするので上手く調整する。
  • 3D用のシーンと2D用のシーンを別個で描画するためにrenderer.autoClear = falseする。
    • デフォルトではrenderer.render(...)すると内部的にrenderer.clear()が呼ばれる。renderer.autoClear = falseしておけば呼ばれなくなる。その代わり、render前に手動でclearする必要がある。

おわりに

コミットも頻繁でかなりクオリティの高いthree.jsですが、ドキュメントは相当不足気味です。なので、基本的には、ドキュメントもしくは該当部分のソースコードを流し見しながらサンプルコードを見るのが良いです。今回であれば

らへんをスタート地点として解読していけば大体なんとかなるかと思います。

*1:ここでは画像のみを対象とします。文字の場合はバッファとして確保した画像に描画するなどの工夫をすれば、この方法でも対応できると思います。

*2:同期的風に書ける