リアルな映像たらしめるもの、それが影(陰)です。
やはり影(陰)がないだけで途端にうそ臭い映像になります。
(逆に、影(陰)があるだけで格段にリアリティが増します)
ということで、今回は3Dの世界でどうやって影を生成しているのかを書きたいと思います。
今回のデプスバッファシャドウのデモをjsdo.itに上げました。
デモでは、デプスバッファシャドウをどう使っているのかを分かりやすくするために、デプスバッファを視覚的に表示するオプションを追加しています。
今回の記事はwgldさんのシャドウマッピングを参考にさせて頂いています。
射影テクスチャリング
前回書いた「射影テクスチャリング」。
通常のテクスチャマッピングではなく、あたかもプロジェクターで写しだした画像のような効果を得るものでした。
さて、ここで考えてみてください。
ライトから見た映像を表現できる なら 影がどうなるか分かる 気がしませんか?
デプスバッファシャドウはまさにこの考え方を元に影を計算してレンダリングするテクニックです。
いったんデプスバッファに深度値を書き込む
とはいえ影をレンダリングする、といきなり言われても、どうやるんだよ! っていう話だと思います。
ざっくり手順を書くと、
- ライトを視点とした状態でいったんオフスクリーンレンダリングを行う(射影テクスチャリングのための準備)
- (1)の時点の結果はデプスバッファのみを書き込む(ライトから影になっているかだけが知りたい)
- 実際のレンダリングの際に、計算中の頂点が影になっているかを計算する
という手順です。とても簡単ですね(ぇ
この(1)のオフスクリーンレンダリングがキモです。
オフスクリーンレンダリングということは、これを後々「テクスチャ」として使うということです。
射影テクスチャリング と テクスチャ と 影 がこれでつながりました。
ライト視点のレンダリングとは、いったんライトの位置にカメラを移動し、そこから見える映像の 深度値のみ をレンダリングする、ということです。
そしてそれをテクスチャとしていったん保存しておきます。
デプスバッファに深度値をレンダリング?
いきなり深度値のみをレンダリングと言われても分かりづらいと思います。
まずは通常のオフスクリーンレンダリングを想像してください。
このとき、もし仮に普通にレンダリングを行った場合は、単純にメモリ上にレンダリング結果を保持することになりますね。
ここに深度値のみを書き込むことを考えます。
白黒の、深度値のみで構成された単色の世界。これがデプスバッファです。
ライト視点の映像で、白黒の画面を想像してください。
白黒だけでも前後関係が分かると思います。
ちょうどイメージしやすい画像がwgldの被写界深度の記事にあったので引用させていただきたいと思います。
※ ちなみに、冒頭でも書いた通り今回アップしたデモには、デプスバッファを視覚的に表示するオプションがあるので、そちらを見るとより分かりやすいと思います。(実際のデータの場合は上記サンプルのような感じではありませんが)
レンダリング時に深度値を読み込む
さて、一度深度値のみをオフスクリーンにレンダリングしたら当然、通常のレンダリング時にデプスバッファに書き込まれた値を取り出す必要があります。
この読み込む処理は、冒頭で書いた「射影テクスチャリング」の要領で値を読み込みます。
(射影テクスチャリングのイメージは前回の記事を参照してください)
読み出すコード例は以下になります。
float depth = restDepth(texture2DProj(shadowTexture, vShadowTextureCoord));
restDepth
はシャドウ用テクスチャにいい感じに値を保持・復元する関数です。
(詳細についてはwgldさんの記事を参照してください)
上記のコードで、テクスチャに保持された深度値を読み込みます。
texture2DProj
関数を使って射影テクスチャリングの要領で読み込んでいるのが分かるかと思います。
値を比較する
さて、値を取り出したらそれを比較しないとなりませんね。
どんな値と比較するのか。まずはコード例を。
uniform mat4 lgtMatrix;
varying vec4 vPosition;
// 中略
vPosition = lgtMatrix * vec4(position, 1.0);
vPosition
にはライトから見た行列を掛けて位置をfragment shaderに送ります。
vec4 shadowFactor = vec4(1.0);
if (vPosition.w > 0.0) {
vec4 lightCoord = vPosition / vPosition.w;
if (lightCoord.z - 0.0001 > depth) {
shadowFactor = vec4(0.5, 0.5, 0.5, 1.0);
}
}
// 中略
gl_FragColor = vColor * texColor * shadowFactor;
shadowFactor
の値は影を落とすかどうかで変化します。
最後のgl_FragColor = vColor * texColor * shadowFactor;
の部分で掛けていますが、影でないところでは1.0
を掛けるので色が変化しない、というわけですね。
vPosition.w
が0.0
以上の場合、つまりライト視点から見て視点内に収まっている場合に処理を行います。
まず最初にライトから見た頂点の位置を同次座標空間に変換します。(w
で除算します)
こうすることで、深度値(Z値)が適切な値に変換されます。
そしてその計算した深度値と、テクスチャから取り出した値とを比較し、もしテクスチャから取り出した値より大きい場合は影として計算します。
(vec4(0.5, 0.5, 0.5, 1.0)
なので、色が若干暗くなります)
0.0001
を引いているのは、テクスチャから取り出した値とまったく同じだと問題が起きるからのようです。(ここはまだよく理解できていません)
なぜ値が大きいと影?
深度値、つまりZ値の意味を考えると分かります。
Z値はカメラに「近い」ほど0
に、逆に「遠い」ほど1
に近くなります。
つまり、現在計算中の頂点の深度値(lightCoord
)が、ライト視点でテクスチャに書き込まれた深度値より大きいということは、ライト視点から見て、その頂点よりライトよりに「なにか」がある、となるわけです。
要は「影」、ということですね。
なので該当の部分の色を暗くすることでめでたく影の演出ができる、というわけなのです。
補足
一番最初、wgldさんの記事を読んでいて「???」だったのがこの深度値の比較部分でした。
なぜ一度オフスクリーンレンダリング? 頂点の位置からなんで分かるんだ? と。
実際に自分でコードを書いてみてクリアになったので、参考のために書いておきたいと思います。
フレームバッファには様々なデータがある
画面に出力されるのは最終的に2次元的なものなので分かりづらいですが、フレームバッファには色に関する情報の他に、深度値に関する情報も含まれます。
現在レンダリング中のピクセルに対して深度値があるわけです。
そして、いくつものモデルがレンダリングされていくと当然、その深度値も書き換わっていきます。
後ろにあるはずのものが手前にレンダリングされたらおかしくなっちゃいますからね。
こうした、現実世界では当たり前のことを実現するために「深度値」はなくてはならない情報です。
今回、深度値のみをオフスクリーンレンダリングしたのはまさにこれが理由です。
つまり、 一度全モデルをレンダリングしてみないと、なにが手間にあってなにが奥にあるのか、という深度値の状態が分からない わけです。
だから、オフスクリーンで一度ざっとレンダリングを行い、さらにそのときに計算された深度値をまるまるテクスチャとして保持しておく、という方法を取るわけですね。
オンスクリーンレンダリング時もライト視点からの変換を行う
うん、なんとなくオフスクリーンレンダリングの必要性はわかったけど、じゃあそれをどうやって使うのさ? というのが次の疑問でした。
しかし分かってみるとなんのことはなくて、実際に画面にレンダリングを行うオンスクリーンレンダリング時にも、ライト視点からの座標変換を行うんです。
つまり、
- オフスクリーンでライト視点からの深度値のみを保持
- オンスクリーンで「もう一度」ライト視点からの頂点位置を計算
- (1)と(2)の情報を比較し、影になっている判定になったら色を暗くする処理を入れてオンスクリーンレンダリング
という流れです。
なので都合3回、ひとつの頂点に対して座標変換が行われるわけですね。
(オフスクリーン用に1回、オンスクリーン時の影判定で1回、あとは実際に画面にレンダリングするための「本当の」カメラ位置からの座標変換の1回)
作ってみての感想
このあたりを頭で納得するまで少し時間がかかりました。
やはり、記事を読んだだけでは理解がむずかしいなーと。
ということで、WebGLを学ぶためにはやはり実際に手を動かして書いていくのが一番だと思います。
そういう意味でも、既存コードをコピペしてすぐにブラウザで実行できるWebGLは学習がしやすいですね。
ちなみに、wgldの運営者である@h_doxasさんが開設した「WebGL総本山」というサイトがあります。
こちらもぜひチェックしてみてください。