WebGLでシャドウマッピングや被写界深度フィルターを実装する場合に、深度値をテクスチャに書き出したものが必要になるようです。
WebGL2からはフレームバッファに使う深度バッファをカラーバッファと同じようにテクスチャを使用できるようになり、これによりカラーバッファと深度バッファを同時に取得できるようになったようなので試してみました。
(正確にはWebGL1でも拡張機能WEBGL_depth_texture
を有効にすることで深度バッファにテクスチャを利用できるようですが、以下で説明する方法と同じようにできるかはわかりません。)
サンプルでは以下のように左が手前、右が後ろになるように2つの三角形を描画しています。
このシーンの深度値を表示すると以下のようになります。
深度値の範囲は[0, 1]で手前が0(黒)、奥が1(白)になります
サンプルコード全体はこちらから、実際に動作しているものはこちらから確認できます。
処理の内容は、次のようなよくあるポストエフェクトの流れです。
- 深度バッファをテクスチャにしたフレームバッファを作成する
- 1で作成したフレームバッファに三角形をオフスクリーンレンダリングする
- canvas全体を覆うポリゴンに深度テクスチャをそのままレンダリングする
ソースコード全体は長くWebGLの定型的な処理も多いので、深度値をテクスチャに書き込む&読み込むことに関係があるところだけを抜粋して解説していきます。
まず、フレームバッファを作成している箇所です。
gl.DEPTH_ATTACHMENT
で設定している深度バッファをカラーバッファと同じようにテクスチャにしています。
// creates framebuffer
const frameBuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
const colorTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);
const depthTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT16, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
depthTexture
を作成する際のgl.texImage2D
の第7引数はgl.DEPTH_COMPONENT
に、第3引数と第8引数のフォーマットの設定には適切な組み合わせを用いる必要があります。(この記事が参考になります。)
例えば、以下のようにしても深度バッファとして使用することができます。このあたりは必要な精度に応じて組み合わせを選択するのだと思います。
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, width, height, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
gl.texParameteri
でフィルター設定にgl.NEAREST
を使用していますが、これはこうしないとテクスチャを参照する際にエラーがでるためです。おそらく後述するようにシェーダーでtexelFetch
関数を用いてテクスチャをサンプリングしているためだと思います。
次に作成したフレームバッファに2つの三角形をオフスクリーンレンダリングします。
レンダリング前に深度値を1.0で初期化しています。
// renders triangles
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(triangleProgram);
gl.bindVertexArray(triangleVertexArray);
gl.uniform3fv(triangleUniformLocations['u_offset'], [-0.3, 0.0, -0.5]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.uniform3fv(triangleUniformLocations['u_offset'], [0.3, 0.0, 0.5]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
オフスクリーンレンダリングした結果のデプステクスチャをuniformでシェーダーに渡して画面全体の覆うポリゴンにレンダリングします。
// renders a depth or color texture
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(quadProgram);
gl.bindVertexArray(quadVertexArray);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.uniform1i(quadUniformLocations['u_colorTexture'], 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.uniform1i(quadUniformLocations['u_depthTexture'], 1);
gl.uniform1i(quadUniformLocations['u_showDepth'], showDepth.checked);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
フラグメントシェーダーは以下のようになっています。
GLSL ES 3.0から使えるtexelFetch
で対応した座標をテクスチャからサンプルして、そのまま出力しています。
u_showDepth
でカラーテクスチャを使うか、デプステクスチャを使うかを変更できるようにしています。
# version 300 es
precision highp float;
out vec4 out_color;
uniform sampler2D u_colorTexture;
uniform sampler2D u_depthTexture;
uniform bool u_showDepth;
void main(void) {
if (u_showDepth) {
float d = texelFetch(u_depthTexture, ivec2(gl_FragCoord.xy), 0).r;
out_color = vec4(vec3(d, d, d), 1.0);
} else {
vec3 c = texelFetch(u_colorTexture, ivec2(gl_FragCoord.xy), 0).rgb;
out_color = vec4(c, 1.0);
}
}
次に以下のように透視プロジェクション行列を使用している場合を考えたいと思います。
このシーンでは左から右に段々と奥になるように三角形を配置しています。
ソースコードはこちらから、実際に動作しているものはこちらから確認できます。
このシーンの深度値をさきほどと同じ方法でそのまま表示すると、次のようなほとんど真っ白な画面が表示されます。
これは透視プロジェクション行列を用いた変換では深度値を非線形に保存しているからです。
(これは手前側ほど精度を高く深度値を保存するためです。)
このままではいろいろと使用しづらいと思うので、この記事を参考に線形に変換してみます。
具体的には以下のような変換をフラグメントシェーダーで行います。
u_near
とu_far
にはそれぞれカメラのニアークリップとファークリップの値となっています。
# version 300 es
precision highp float;
out vec4 out_color;
uniform sampler2D u_colorTexture;
uniform sampler2D u_depthTexture;
uniform bool u_showDepth;
uniform bool u_linearDepth;
uniform float u_near;
uniform float u_far;
void main(void) {
if (u_showDepth) {
float d = texelFetch(u_depthTexture, ivec2(gl_FragCoord.xy), 0).r;
if (u_linearDepth) {
d = (2.0 * u_near) / (u_far + u_near - d * (u_far - u_near));
}
out_color = vec4(vec3(d, d, d), 1.0);
} else {
vec3 c = texelFetch(u_colorTexture, ivec2(gl_FragCoord.xy), 0).rgb;
out_color = vec4(c, 1.0);
}
}
線形変換すると、深度値は以下のように表示されます。