[WebGL] Three.jsでジオメトリインスタンシングを使ってモデルをレンダリングする


現状、通常のフローでモデルデータを読み込んでそれをそのままジオメトリインスタンシングでレンダリングする方法はないようです。
なので、モデルデータを読み込み、自前で処理する必要があります。

（ただ、ドキュメントも整備されておらず、もしかしたらまだ安定していないかもしれないので、利用する場合は自己責任でお願いします）

ちなみに今回の解説用に作った[サンプルはGithubに上げてあります](https://github.com/edom18/instanced_array_test)。
（[動作デモはこちら](https://edom18.github.io/instanced_array_test/)）

## ジオメトリインスタンシングって？

さて、まずジオメトリインスタンシングとはなにか。
WebGLでの詳細についてはエマさんのこちらの記事（[WebGLにおけるジオメトリインスタンシング（ANGLE_instanced_arrays）を丁寧に説明してみる](http://qiita.com/emadurandal/items/1812198d562bd1216c70)）がとても分かりやすく書かれているので読むことをおすすめします。

ざっくり言うと、プログラムで言うところのクラス・インスタンスの関係をジオメトリにも当てはめて考えよう、というやつです。
3Dコンテンツにおいて、ドローコールの増加は避けるべきものです。
ただ一言にドローコールが重いと言っても理由はいくつかあり、各ドローコールごとに必要な設定を切り替えたり、利用するデータを換える必要があるために、その切り替えコストが重くなる要因のひとつとして挙げられます。

今回のジオメトリインスタンシングを使うと、この切り替え処理を一気に軽減することが可能です。
具体的に言うと、インスタンスごとの設定やデータを事前にまとめてGPUに送っておくことで、この切替コストを最小限にしよう、というものです。

## Three.jsでの実装

Three.jsではすでに、ジオメトリインスタンシング向けの実装がなされており、それを適切に利用することで実現することが可能です。

ちなみに、すでに[example](http://threejs.org/examples/webgl_interactive_instances_gpu.html)が公開されているので、そちらのコードを読むと色々な気付きが得られると思います。

以下のコードは、冒頭に記載したサンプルのコピーです。

## コード

まずはシェーダから。

見てもらうと分かりますが、シェーダ自体はとてもシンプルです。
もちろん、ライティングやらをしっかりやろうとするとがっつり書かないとなりませんが、最低限動くものであればそれほどコード量は必要ありません。

```glsl:vert
// ジオメトリインスタンシングで複製されたそれぞれのモデルに適用される頂点シェーダ
attribute vec3 translation;

varying vec2 vUv;

void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(translation + position, 1.0);
}
```

```glsl:frag
// ジオメトリインスタンシングで複製されたそれぞれのモデルに適用されるフラグメントシェーダ
precision mediump float;
uniform sampler2D map;

varying vec2 vUv;

void main() {
    gl_FragColor = texture2D(map, vUv);
}

```

```js
var initialized = false;

var camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.y = 2;
camera.position.z = 2;
camera.lookAt(new THREE.Vector3(0, 0, 0));

var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x101010);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

// 拡張なので、未サポートの場合は処理しない
if (renderer.extensions.get('ANGLE_instanced_arrays') === false) {
	alert('Don\'t support instanced array extension.');
	return;
}

var scene = new THREE.Scene();

var translations = null;

new THREE.JSONLoader().load('models/table.json', function (geometry, materials) {

	// 読み込んだモデルデータからテクスチャの参照を利用する
    var texture = materials[0].map;
    var material = new THREE.ShaderMaterial({
        vertexShader: vert,
        fragmentShader: frag,
        uniforms: {
            map: { type: 't', value: texture }
        }
    });

	// インスタンスの数
    var instances = 200;

    // Base geometry。読み込んだモデルデータからジオメトリをコピー
    var bgeo = new THREE.BufferGeometry().fromGeometry(geometry);

    // ジオメトリインスタンシング用のバッファを生成
    var igeo = new THREE.InstancedBufferGeometry();

	// 頂点データをコピーし、バッファに設定
    var vertices = bgeo.attributes.position.clone();
    igeo.addAttribute('position', vertices);

	// 実際に手でバッファを作る場合は以下のように`BufferAttribute`を生成する
    // var rawVertices = data.vertices;
    // var vertices = new THREE.BufferAttribute(new Float32Array(rawVertices), 3);
    // geometory.addAttribute('position', vertices);

	// UVデータをコピーし、バッファに設定
    var uvs = bgeo.attributes.uv.clone();
    igeo.addAttribute('uv', uvs);

	// 必要であればインデックスを設定
    // var rawIndices = data.faces;
    // var indices = new THREE.BufferAttribute(new Uint16Array(rawIndices), 1);
    // geometory.setIndex(indices);

    // インスタンス向けの個別のデータ
    translations = new THREE.InstancedBufferAttribute(new Float32Array(instances * 3), 3, 1);

    var vector = new THREE.Vector4();
    for (var i = 0, ul = translations.count; i < ul; i++) {
        var x = Math.random() * 100 - 50;
        var y = Math.random() * 100 - 50;
        var z = Math.random() * 100 - 50;
        vector.set(x, y, z, 0).normalize();
        translations.setXYZ(i, x + vector.x * 5, y + vector.y * 5, z + vector.z * 5);
    }
    igeo.addAttribute('translation', translations); // per mesh translation

	// 通常と同じようにメッシュを生成
    var mesh = new THREE.Mesh(igeo, material);
    scene.add(mesh);

    initialized = true;
});

// レンダリング処理
function render() {

    if (!initialized) {
        return;
    }

    renderer.render(scene, camera);
}

(function loop() {
    requestAnimationFrame(loop);
    render();
}());

document.addEventListener('DOMContentLoaded', function () {
    document.body.appendChild(renderer.domElement);
}, false);
```

`JSONLoader` などのローダから読み込んだモデルデータを使ってジオメトリインスタンシングが比較的簡単に実装できるのが分かってもらえたかと思います。
若干作法が異なるものの、基本的なフローは変わりません。

エマさんの記事をしっかり読んでから見てみると、まったくむずかしいことはやっていないのでぜひ読んでみてください。