Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

@yomotsuさんが前に作っていたデモWebGL入門者の会 のLTで解説してもらいました。感謝(ㆆᴗㆆ)

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

▼ 動作Gif
動作デモ

ちなみにGithubにコードを上げています。
動作するデモはこちら

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

Depth bufferのシェア

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 関数を実行すると、上記の例では rendertargetDepthBuffer に、mainRendertargetDepthBuffer がシェアされるようになります。

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

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 というプロパティがあり、これになにがしかのマテリアルを設定しておくと、すべてのオブジェクトがそのマテリアルでレンダリングされるようになります。

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

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

▼ 上記のマテリアルでシーンをレンダリングした結果(マスク用バッファ)
depth-buffer
今回のサンプルは、とあるアイテムを選んだときに選択したもの「以外」をぼかすのがやりたい処理でした。
そして今回の例では「ベッド」を選択したことにしています。

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

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

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

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

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

▼ 実際のシーン
rendering

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

コード解説

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

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つのテクスチャを受け取り、マスクのデータを元にどちらのバッファのピクセルを採用するかを決定します。

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 の部分がマスクのデータを取り出しているところです。
そしてその値を使って分岐します。

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

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

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

edo_m18
現在はUnity ARエンジニア。 主にARのコンテンツ制作をしています。 趣味でWebGL/WebXRもいじってます。 Unityに関するブログは別で書いています↓ https://edom18.hateblo.jp/
http://edom18.hateblo.jp/
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした