[Three.js][WebGL] DepthBufferを共有して選択オブジェクト以外にブラーをかけてみる



[@yomotsu](https://twitter.com/yomotsu/)さんが前に作っていた[デモ](https://twitter.com/yomotsu/status/727912128586256384/photo/1)を [WebGL入門者の会](http://web3dj.connpass.com/event/30009/) のLTで解説してもらいました。感謝(ㆆᴗㆆ)

そこで使われていた、`DepthBuffer` を複数のレンダリングターゲットで共有してマスクとして使いましょう、という発想を拝借し、ゲームとかでありそうな「選択しているオブジェクト以外にブラーをかける」というのをやってみたのでそのメモです。

▼ 動作Gif
![動作デモ](https://i.gyazo.com/ac430673f337ea8d5d977c1f8c52a5cd.gif)

ちなみに[Githubにコード](https://github.com/edom18/Focus-blur-sample)を上げています。
（[動作するデモはこちら](https://edom18.github.io/Focus-blur-sample/)）

まず最初にデモで書かれている、 `Three.jsでDepthBufferをシェア` するコードを紹介します。
WebGLに詳しい人ならぱっと見てなんとなく分かるのではないかなと思います。

## Depth bufferのシェア

```js:share-depth-buffer
var shareDepth = (function () {

    var scene  = new THREE.Scene();
    var camera = new THREE.Camera();

    return function (renderer, renderTarget, renderTargetFrom) {
    
        // to force setup RT1, not for rendering.
        renderer.render(scene, camera, renderTargetFrom);

        // to force setup RT2, not for rendering.
        renderer.render(scene, camera, renderTarget);

        var _gl = renderer.context;
        var framebuffer = renderer.properties.get(renderTarget).__webglFramebuffer;
        var renderbufferShareFrom = renderer.properties.get(renderTargetFrom).__webglDepthbuffer;
        _gl.bindFramebuffer(_gl.FRAMEBUFFER, framebuffer);
        _gl.bindRenderbuffer(_gl.RENDERBUFFER, renderbufferShareFrom);

        _gl.framebufferRenderbuffer(
            _gl.FRAMEBUFFER, 
            _gl.DEPTH_ATTACHMENT,
            _gl.RENDERBUFFER,
            renderbufferShareFrom
        );

        _gl.bindFramebuffer(_gl.FRAMEBUFFER, null);
        _gl.bindRenderbuffer(_gl.RENDERBUFFER, null);
    };
}());

// 使い方
var rendertarget = new THREE.WebGLRenderTarget(width, height, {
	minFilter: THREE.LinearFilter,
	magFilter: THREE.LinearFilter,
	depthBuffer: true,
	stencilBuffer: false
});

// DepthBufferをシェアしたいターゲット
var mainRendertarget = new THREE.WebGLRenderTarget(width, height, {
	minFilter: THREE.LinearFilter,
	magFilter: THREE.LinearFilter,
	depthBuffer: true,
	stencilBuffer: false
});

// rendertargetのDepthBufferに、mainRendertargetのものをシェアする
shareDepth(renderer, rendertarget, mainRendertarget);

// このあとに実際のレンダリングを行う
```

`shareDepth` 関数を実行すると、上記の例では `rendertarget` の `DepthBuffer` に、`mainRendertarget` の `DepthBuffer` がシェアされるようになります。

シェアしているコードは以下の部分です。

```js:share-buffer
var _gl = renderer.context;
var framebuffer = renderer.properties.get(renderTarget).__webglFramebuffer;
var renderbufferShareFrom = renderer.properties.get(renderTargetFrom).__webglDepthbuffer;
_gl.bindFramebuffer(_gl.FRAMEBUFFER, framebuffer);
_gl.bindRenderbuffer(_gl.RENDERBUFFER, renderbufferShareFrom);

_gl.framebufferRenderbuffer(
    _gl.FRAMEBUFFER, 
    _gl.DEPTH_ATTACHMENT,
    _gl.RENDERBUFFER,
    renderbufferShareFrom
);

_gl.bindFramebuffer(_gl.FRAMEBUFFER, null);
_gl.bindRenderbuffer(_gl.RENDERBUFFER, null);
```

`renderer.properties.get` を使って生のバッファを取り出し、`_gl.framebufferRenderbuffer` を使って 同じ `depthbuffer` をアタッチしているわけですね。

ちなみに空のシーンをレンダリングしているのはThree.jsのフローを使って深度バッファを生成するためです。
（バッファ生成部分が隠蔽されているため、一度 `render` を実行することで対処しています）


深度バッファのシェア後、`mainRendertarget` に対して通常のシーンをレンダリングしたあとに `rendertarget` に対してレンダリングを行うと `DepthBuffer` にはすでになにがしかの値が入力された状態になっています。（メインシーンのレンダリング時の深度バッファが使われるからですね）

この状態で `rendertarget` に対してレンダリングを行うと、仮にオブジェクトをひとつだけレンダリングする場合でも `mainRendertarget` で描かれたオブジェクトとのZテストが行われることになり、結果としてマスクのような効果を得ることができる、というわけです。（って、文章だと説明むずかしい・・）

### 余談

このデモを見ていて初めて知ったんですが、`renderer` には `overrideMaterial` というプロパティがあり、これになにがしかのマテリアルを設定しておくと、すべてのオブジェクトがそのマテリアルでレンダリングされるようになります。

なのでそれを利用して、マスク用のマテリアルとして以下のマテリアルを設定したのちにレンダリングを行うと、まるでマスクのような見た目の映像がレンダリングできる、というわけですね。

```js:overrideMaterial
// オブジェクトをただ単に白に塗るだけのマテリアル
var maskMaterial = new THREE.MeshBasicMaterial({
    color: 0xffffff,
    depthWrite: false,
    fog: false
});
```

▼ 上記のマテリアルでシーンをレンダリングした結果（マスク用バッファ）
![depth-buffer](https://qiita-image-store.s3.amazonaws.com/0/5616/c6f04bbd-57b1-fcbe-beff-4d3b6e86ff65.jpeg "depth-buffer")
今回のサンプルは、とあるアイテムを選んだときに選択したもの「以外」をぼかすのがやりたい処理でした。
そして今回の例では「ベッド」を選択したことにしています。

そして↑のキャプチャでなにが起こっているかと言うと、シーン内のオブジェクトをベッド「だけ」にした状態で、`overrideMaterial` に真っ白に塗るだけのマテリアルを設定し、レンダリングを行っています。

しかし `DepthBuffer` を共有した状態でレンダリングを行っているのでベッドの手前にあるテーブルの部分は深度テストで不合格となり、結果としてテーブルの間から見える部分だけがレンダリングされている、というわけです。

つまり、ベッド以外の「前後関係だけ」を利用しつつ、ベッドだけをレンダリングしている、というわけですね。

文章だとちょっとなにが起こっているか分かりづらいですが、下の実行結果と見比べてもらうとなんとなく分かるんじゃないかと思います。

以下に実際のシーンのキャプチャを載せます。

▼ 実際のシーン
![rendering](https://qiita-image-store.s3.amazonaws.com/0/5616/f367966c-ce6a-b552-b5cf-564138f679ac.jpeg "rendering")

実際のシーンと見比べると、ベッドの白いレンダリング結果と一致するのが分かるかと思います。


## コード解説

今回の実装のコードを抜粋しつつ実装について解説します。

```js:loop
// 深度バッファを残すため`autoClear = false`に
renderer.autoClear = false;

// ... 中略 ...

// アニメーションループ
function animate(timestamp) {

	// autoClear == falseなので、自分の手でクリアを実行する
    renderer.clear( true, true, true );
    renderer.clearTarget( renderTarget, true, true, true );
    renderer.clearTarget( maskTarget, true, true, true );
    renderer.clearTarget( blurRenderTarget, true, true, true );

    // バックバッファへ通常シーンのレンダリング
    renderer.render(scene, camera, renderTarget);

    // マスク用データを収集
    scene.overrideMaterial = maskMaterial;
    hide(); // ベッド以外を隠す
    renderer.render(scene, camera, maskTarget);
    show(); // 隠したものを戻す
    scene.overrideMaterial = null;

    // ブラー
    renderer.render(blurScene, screenCamera, blurRenderTarget);

    // 描画
    renderer.render(screenScene, screenCamera);

    // アニメーションループ
    requestAnimationFrame(animate);
}
```

ループの中はシンプルです。

1. 各種バッファのクリア（描画準備）
2. 通常シーンをレンダリング（通常シーン用バッファ）
3. ベッドだけのマスクをレンダリング（マスク用バッファ）
4. (2)のバッファのシーンにブラーのレンダリング（ブラー用バッファ）
5. (2), (3), (4)のバッファを利用して最終結果をレンダリング（画面）

という流れです。
(5)を見てもらうと分かりますが、いわゆるポストエフェクト的な感じで最終結果をレンダリングしているわけですね。
それまではひたすらバッファにレンダリングして情報を収集していきます。

最後に、結果を描画しているシェーダは以下になります。
通常シーンの `texture` と、ブラーをかけたシーンの `blurTexture`、そしてマスク用の `maskTexture` の3つのテクスチャを受け取り、マスクのデータを元にどちらのバッファのピクセルを採用するかを決定します。

```glsl
uniform sampler2D texture;
uniform sampler2D blurTexture;
uniform sampler2D maskTexture;
varying vec2 vUV;

void main() {
    float mask = texture2D(maskTexture, vUV).r;
    if (mask > 0.0) {
        gl_FragColor = texture2D(texture, vUV);
    }
    else {
        gl_FragColor = texture2D(blurTexture, vUV);
    }
}
```

`float mask = texture2D(maskTexture, vUV).r` の部分がマスクのデータを取り出しているところです。
そしてその値を使って分岐します。

```glsl
if (mask > 0.0) {
	gl_FragColor = texture2D(texture, vUV);
}
else {
	gl_FragColor = texture2D(blurTexture, vUV);
}
```

つまりマスク対象外の部分はブラーのシーンが使われ、マスク対象の場合は通常のシーンが使われるため、選択したもの「以外」がぼける、というわけです。


いやー、シェーダはほんとに楽しいです(ㆆᴗㆆ)