@yomotsuさんが前に作っていたデモを WebGL入門者の会 のLTで解説してもらいました。感謝(ㆆᴗㆆ)
そこで使われていた、DepthBuffer
を複数のレンダリングターゲットで共有してマスクとして使いましょう、という発想を拝借し、ゲームとかでありそうな「選択しているオブジェクト以外にブラーをかける」というのをやってみたのでそのメモです。
ちなみにGithubにコードを上げています。
(動作するデモはこちら)
まず最初にデモで書かれている、 Three.jsでDepthBufferをシェア
するコードを紹介します。
WebGLに詳しい人ならぱっと見てなんとなく分かるのではないかなと思います。
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
がシェアされるようになります。
シェアしているコードは以下の部分です。
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
というプロパティがあり、これになにがしかのマテリアルを設定しておくと、すべてのオブジェクトがそのマテリアルでレンダリングされるようになります。
なのでそれを利用して、マスク用のマテリアルとして以下のマテリアルを設定したのちにレンダリングを行うと、まるでマスクのような見た目の映像がレンダリングできる、というわけですね。
// オブジェクトをただ単に白に塗るだけのマテリアル
var maskMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
depthWrite: false,
fog: false
});
▼ 上記のマテリアルでシーンをレンダリングした結果(マスク用バッファ)
今回のサンプルは、とあるアイテムを選んだときに選択したもの「以外」をぼかすのがやりたい処理でした。
そして今回の例では「ベッド」を選択したことにしています。
そして↑のキャプチャでなにが起こっているかと言うと、シーン内のオブジェクトをベッド「だけ」にした状態で、overrideMaterial
に真っ白に塗るだけのマテリアルを設定し、レンダリングを行っています。
しかし DepthBuffer
を共有した状態でレンダリングを行っているのでベッドの手前にあるテーブルの部分は深度テストで不合格となり、結果としてテーブルの間から見える部分だけがレンダリングされている、というわけです。
つまり、ベッド以外の「前後関係だけ」を利用しつつ、ベッドだけをレンダリングしている、というわけですね。
文章だとちょっとなにが起こっているか分かりづらいですが、下の実行結果と見比べてもらうとなんとなく分かるんじゃないかと思います。
以下に実際のシーンのキャプチャを載せます。
実際のシーンと見比べると、ベッドの白いレンダリング結果と一致するのが分かるかと思います。
コード解説
今回の実装のコードを抜粋しつつ実装について解説します。
// 深度バッファを残すため`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);
}
ループの中はシンプルです。
- 各種バッファのクリア(描画準備)
- 通常シーンをレンダリング(通常シーン用バッファ)
- ベッドだけのマスクをレンダリング(マスク用バッファ)
- (2)のバッファのシーンにブラーのレンダリング(ブラー用バッファ)
- (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);
}
つまりマスク対象外の部分はブラーのシーンが使われ、マスク対象の場合は通常のシーンが使われるため、選択したもの「以外」がぼける、というわけです。
いやー、シェーダはほんとに楽しいです(ㆆᴗㆆ)