■ 2008年12月20日 [OpenGL][GLSL] 屈折マッピング
日本橋
古いノートパソコンの増設メモリが和歌山で手に入らなかったので,先日,奥さんと一緒に大阪の日本橋にある中古パソコンショップに行きました.
その後,日本橋から難波に向かう途中に,メイド喫茶を見つけました.奥さんは「メイド喫茶がある!」と異様に反応していました.それをちょうど出勤してきた店員さんと思しきお姉さんに見とがめられ,「どうぞ〜,いらっしゃいませ〜」と声をかけられてしまいました.田舎者のオジサンはひきつりながら会釈を返しつつ,テンションが上がったオバサンを引きずって,その場を退散したのでした.しかし,お姉さんすっごくきれいだった.タレントさんみたい.
次に,奥さんのたっての希望で,四つ橋線で肥後橋へ.そして 30 分ほど行列に並んだ後,ついに,
Mon chou chou の堂島ロールゲット!,Get!,げっとぉぉぉ!!!!
和歌山のデパートでも数量限定で販売していたことが何回かあったのですが,いつも売り切れで涙を飲んでいたのでした(奥さんが).さすがに人気だけのことはあります.クリームが一味違います.ミルク風味で,黒沢牧場のソフトクリームを連想しました.
GLSL による環境マッピング
学生さんに「何か説明して欲しいことは無いか?」と聞いたら「無い」というつれない返事だったので,何か適当なことを書くことにします.前に反射マッピング(リフレクションマッピング)については書きましたけど,屈折マッピング(リフラクションマッピング)については書いてませんでしたので,ちょっとまとめておきたいと思います.そのために,まず,以前作ったキューブマッピングによる環境マッピングのプログラムを,GLSL を使うように書き換えました.
単に書き換えるだけでなく,いろいろ細工しています(マウスの左ボタンでオブジェクト,右ボタンで背景を回転できるようになっています).
このバーテックスシェーダプログラム reflect.vert は次のようになっています.
// reflect.vert varying vec3 r; // 視線の反射ベクトル void main(void) { vec4 p = gl_ModelViewMatrix * gl_Vertex; // 頂点位置 vec3 v = p.xyz / p.w; // 視線ベクトル vec3 n = gl_NormalMatrix * gl_Normal; // 法線ベクトル r = vec3(gl_TextureMatrix[0] * vec4(reflect(v, n), 1.0)); gl_Position = ftransform(); }
まず,頂点位置 gl_Vertex にモデルビュー変換行列を乗じて,視点座標系における頂点位置 p を求めます.視点座標系では視点は原点にありますから,視線ベクトル v は,この点の位置ベクトルになります.また,視点座標系における法線ベクトル n も求めておきます.n は正規化する必要がありますが,このプログラムでは形状データ作成の時点で頂点の法線ベクトルを正規化しているので,ここでは正規化していません.
そして,GLSL の組み込み関数 reflect() を使って,視線の反射ベクトルを求めます.これを使ってキューブマップをサンプリングすれば反射マッピングを実現できますが,ここではテクスチャを回転させるために,さらにテクスチャマトリクスを乗じています.この結果を varying 変数 r に格納して,フラグメントシェーダに渡します.
フラグメントシェーダプログラム reflect.frag では,この r を使ってキューブマップをサンプリングします.
// reflect.frag uniform samplerCube cubemap; varying vec3 r; // 視線の反射ベクトル void main(void) { gl_FragColor = textureCube(cubemap, r); }
屈折マッピング
GLSL の組み込み関数には,反射方向を求める reflect() の他に,屈折方向を求める refract() というものがあります.これを使って屈折マッピングを実現してみます.reflect.vert を次のように書き換えてください.
// reflect.vert varying vec3 r; // 視線の反射ベクトル varying vec3 s; // 視線の屈折ベクトル const float eta = 0.67; // 屈折率の比 void main(void) { vec4 p = gl_ModelViewMatrix * gl_Vertex; vec3 v = normalize(p.xyz / p.w); vec3 n = gl_NormalMatrix * gl_Normal; r = vec3(gl_TextureMatrix[0] * vec4(reflect(v, n), 1.0)); s = vec3(gl_TextureMatrix[0] * vec4(refract(v, n, eta), 1.0)); gl_Position = ftransform(); }
視線の屈折ベクトルは,視線ベクトル v と法線ベクトル n を refract() の引数に与えて得ることができますが,refract() は refrect() と異なり,v を正規化しておく必要があります.eta は視線が通過する境界の前後にある媒質の屈折率の比で,視線が大気中からガラスに進入する場合,大気の屈折率 n1 ≒ 1.0,ガラスの屈折率 n2 ≒ 1.5 として,n1 / n2 ≒ 0.67 を設定しています.これらから得られた視線の反射ベクトルにテクスチャマトリクスを乗じて,結果を varying 変数 s に格納します.
フラグメントシェーダプログラム reflect.frag では,この s を使ってキューブマップをサンプリングします.
// reflect.frag uniform samplerCube cubemap; varying vec3 r; // 視線の反射ベクトル varying vec3 s; // 視線の屈折ベクトル void main(void) { gl_FragColor = textureCube(cubemap, s); }
この変更で,次のようが画像が得られます.
反射と屈折の合成
物体の境界面では,視線は反射方向と屈折方向に分かれるので,その点の色は反射方向にあるものの色と屈折方向にあるものの色を合成したものになります.そこで,試しにこれらを合成してフラグメントの色を決定するようにしてみましょう.フラグメントシェーダプログラムにおいて GLSL の組み込み関数 mix() を使って,反射方向にあるテクスチャのサンプル値と屈折方向にあるテクスチャのサンプル値を合成します.
// reflect.frag uniform samplerCube cubemap; varying vec3 r; // 視線の反射ベクトル varying vec3 s; // 視線の屈折ベクトル void main(void) { gl_FragColor = mix(textureCube(cubemap, s), textureCube(cubemap, r), 0.5); }
フレネル反射
前の例では,反射方向にあるテクスチャのサンプル値と屈折方向にあるテクスチャのサンプル値の配分を 0.5 に固定していましたが,この配分は,実際には視線の入射角と屈折率の比 (eta) で決まります.この関係はフレネルの式で求めることができます.以前,シェーダを使わずにこれを実装するために,この式の値を1次元テクスチャに格納しておく手法を用いましたが,シェーダが使えればこれをシェーダで計算することができます.ただし,フレネルの式は少し複雑なので,シェーダでは Shlick による近似(この人は Phong の陰影付けモデルを始め,いろんな式の近似式を提案している人ですね)が用いられるようです.
ここで f は視線が境界面に垂直に入射するときの反射率で,これはフレネルの式において入射角θ= 0 (c = v·h = 1) として求めることができます.V は視線ベクトルですが,ここでは参照点から視点に向かう単位ベクトルです.N は参照点における単位法線ベクトルです.これらをもとに参照点における反射率を求め,varying 変数 t に格納します.これをバーテックスシェーダプログラムに組み込むと,次のようになります.
// reflect.vert varying vec3 r; // 視線の反射ベクトル varying vec3 s; // 視線の屈折ベクトル varying float t; // 境界面での反射率 const float eta = 0.67; // 屈折率の比 const float f = (1.0 - eta) * (1.0 - eta) / ((1.0 + eta) * (1.0 + eta)); void main(void) { vec4 p = gl_ModelViewMatrix * gl_Vertex; // 頂点位置 vec3 v = normalize(p.xyz / p.w); // 視線ベクトル vec3 n = gl_NormalMatrix * gl_Normal; // 法線ベクトル r = vec3(gl_TextureMatrix[0] * vec4(reflect(v, n), 1.0)); s = vec3(gl_TextureMatrix[0] * vec4(refract(v, n, eta), 1.0)); t = f + (1.0 - f) * pow(1.0 - dot(-v, n), 5.0); gl_Position = ftransform(); }
フラグメントシェーダプログラムでは,反射方向にあるテクスチャのサンプル値と屈折方向にあるテクスチャのサンプル値の合成に varying 変数 t を使うようにします.
// reflect.frag uniform samplerCube cubemap; varying vec3 r; // 視線の反射ベクトル varying vec3 s; // 視線の屈折ベクトル varying float t; // 境界面での反射率 void main(void) { gl_FragColor = mix(textureCube(cubemap, s), textureCube(cubemap, r), t); }
これで,こういう結果が得られます.
なんだかちょっと分かりづらいですね.環境のテクスチャが暗いせいかも知れません.そのうち環境のテクスチャを作り直して試してみたいと思います.
オレンジブック
ここまで書いてオレンジブックを見たら,まったく同じことが書いてありました orz.更に,光の分光現象を近似する手法も書いてありました.たいした手間ではなさそうなので,暇があったらこれもやってみたいと思いますが,これも環境のテクスチャを変えないとちょっと分かりづらいかも知れないなぁ.
参考になりました。
Windows版のソースがダウンロードはできるのですが解凍できません。<br>他のページもかなりあるのでできれば対応お願いします。
nanasi さま、コメントありがとうございます。<br>アーカイブを ZIP に転換する必要性は認識しているのですが、対応できておりません。<br>とりあえず Lhaplus http://www.forest.impress.co.jp/library/software/lhaplus/ 等をお使いいただけないでしょうか。<br>よろしくお願いします。
自分が使用しているLhasaで解凍すると途中で書き込みエラーで終了していたのでファイルに問題があるのかと思っていましたが、紹介していただいたLhaplusで問題なく解凍できました。<br>ありがとうございました。
床井先生<br>今回、こちらと半透明処理のページを非常に参考にして作品を作らせていただきました。感謝いたします。<br>https://youtu.be/TzncuBiFxRA<br><br>ただ、もしお時間があればご助言頂きたいことがございます。<br>本作品制作時に、重なる半透明オブジェクトを順番に奥から描いていくピクセルパーフェクトな半透明世界の描画シェーダを作ったのですが、<br>光の屈折の関係上、ピクセルの単純な重なりだけではなく、屈折も考慮した前後の重なりを考慮しないと矛盾した描画になってしまう<br>(zバッファ的には重なっていないが、屈折させると重なるため、単純にzバッファでの重なり順だと描けない背後の物体が出ました)現象が起きました。<br>こちら、解決法をご存じでしょうか。<br>あまり、ピクセルパーフェクトな手法も寡聞にて知りませんので、そちらを示して頂けるだけでも助かります。<br><br>お忙しいところ申し訳ありません、どうぞ宜しくお願い致します。<br><br><br>工藤達郎
工藤様、コメントありがとうございます。お返事が遅くなり、申し訳ありません。言い訳をさせていただきますと、このコメント欄には通知機能とかをつけてないので、書き込みがあっても自分がこのブログを見ない限り気がつきません。すみません。<br><br>ご指摘の通り、透明な物体を表示するには奥から順番に描いていくしかないので(マルチサンプルによる方法もありますが)、デプスバッファとは相性がよくありません。そこで、順序に依存しない手法がいくつか提案されています。ただ、これらも見方によっては選択ソートに準じているので、結局どこかで並べ替えをしなければならないのだと思います。<br><br>ご存知かもしれませんが、これに関しては、OpenGL Tutorial の下記の記事にリンクがあります。<br>http://www.opengl-tutorial.org/jp/intermediate-tutorials/tutorial-10-transparency/<br><br>屈折に関しては、やはりご指摘の通り、テクスチャを遠方から屈折に合わせてずらしてサンプリングし、その結果をブレンドする必要があります。これは非常にコストが高いように思われます。結局、その問題が果たしてユーザーにとって知覚可能であるか、アーティファクトになり得るかというあたりになるのではないかと考えます。
床井先生<br><br>工藤です。<br>お忙しい中、ご返信深謝致します。<br><br>提示頂いたページ(公式にあったのですね・・ありがとうございます)、理解と実装で手間取りそうですが、頑張ってやってみたいと思います。<br><br>また、屈折に関しても丁寧なご回答ありがとうございました。僕個人としてはやはり"想定"が先にあるせいかすごく気になってしまうのですが、アーティファクトたりうるかという視点で少し検証致しました。<br>結果、単純化した空間(ガラスのボックス1つとその奥に2〜3個の半透明オブジェクトのみ)で数人に見せたのですが、誰1人気付きませんでした。<br>一般的に気づくレベルのものではないことが予想されます。<br>もし、何かしら高速処理の方法を思い付いたら実装しようと思います。<br><br>ありがとうございました。<br><br>工藤