Unityシェーダーチュートリアル すりガラスの軽量表現
シェーダー
「室内からすりガラスを通して室外を見る」場合、どうすれば良いか?
PCで動作させるのであれば、GrabPass を利用し、
画面全体をの画をテクスチャとして取得して料理する、という手法が採れます。
今回はモバイル環境で作らねばならなかったので、
Reflection Probe を利用した軽量な手法でやってみました。
Reflection Probe とは
言わずもがなですが、、簡単に言いますと、
指定の位置で周囲をキューブマップとしてキャプチャし、
オブジェクトの法線方向によって、映り込むであろうキューブマップのテクセルをサンプリングして、
あたかも、リアルに反射しているかのように見せる技術、、です。
そして、Reflection Probe には Box Projection と言うモードがあり、
オブジェクトがキャプチャ位置からずれている場合でも、いい感じに補正してくれます。
特に四角い室内で効果を発揮します。
つまり、Box Projection を利用すると窓の内側に反射する四角い室内は綺麗に再現できる訳です。
考察
「窓に四角い室内が反射する」という事は、言い換えると、
「四角い部屋が内側にあるかのように、窓に色を乗せて反射に見せる」と言う事である。
では、「四角い風景が外側にあるかのように、窓に色を乗せて透過に見せる」と言う処理に変えれば、
窓の外を Reflection Probe で表現することができ、
さらに、ぼかしを入れればすりガラス風になるのではないか?
Box Projection を自前でやってみる
とりあえず、Lambertでやってみます。(質感は関係なく色が欲しいだけだから)
Standard Surface Shader を新規作成して Lambertライティングモデルに変更し、
必要部分を追記していきます。
Box Projection をやるには、位置と反射ベクトルの情報が必要なので、
struct Input{} 内で使用できるように定義しています。
struct Input {
float3 worldPos;
float3 worldRefl;
}
ビルトインシェーダーの UnityStandardUtils.cginc 255行目あたりに、
Box Projection する場合のベクトルの取得方法が記述されているのでコピペして整理します。
[インストールフォルダ → Editor → Data → CGIncludes → UnityStandardUtils.cginc]
ここでいう「ベクトル」と言うのは、
「カメラからポイントに向かってレイを飛ばした時、どちらの方向に反射して飛んでいくか」もとい、
「どこから飛んで来たレイが、そのポイントを経由・反射してカメラに飛び込んでくるか」つまり、
「そこには何が反射して映っているか」という事です。
half3 BoxProjection (half3 worldRefl, float3 worldPos, float3 cubemapCenter, float3 boxMin, float3 boxMax) {
half3 nrdir = normalize(worldRefl);
half3 rbmax = (boxMax - worldPos) / nrdir;
half3 rbmin = (boxMin - worldPos) / nrdir;
half3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin;
half fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z);
worldPos -= cubemapCenter;
worldRefl = worldPos + nrdir * fa;
return worldRefl;
}
Unityでは unity_SpecCube0 というキューブマップがあらかじめ定義されていて、
Reflection Probe をシェーダー内で取得することができます。
unity_SpecCube0_ProbePosition
unity_SpecCube0_BoxMin
unity_SpecCube0_BoxMax
unity_SpecCube0_HDR
も同様にあらかじめ定義されています。
そして、UNITY_SAMPLE_TEXCUBE_LOD という関数で、
ベクトル方向に対応するキューブマップの色をサンプリングする事ができます。
ベクトルには前述の BoxProjection で計算したものを使用します。
最後の引数はボケ具合で大きくすればするほど Reflection Probe がボケます。
DecodeHDR という関数で、HDR モードの Reflection Probe にも対応できるようです。
float3 worldRefl = BoxProjection (
IN.worldRefl,
IN.worldPos,
unity_SpecCube0_ProbePosition,
unity_SpecCube0_BoxMin,
unity_SpecCube0_BoxMax
);
o.Emission = DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, 0), unity_SpecCube0_HDR);
Emission に繋いで、自前 Reflection Probe 100% のシェーダーができました。まるで鏡のようです。
反射処理を透過処理に変える
反射は面がどちらの方向を向いているかによって、
どの色が反射するのか(キューブマップのテクセルのどれをサンプリングするのか)が決定します。
前述コードで言うところの以下の部分です。
float3 worldRefl = BoxProjection (
IN.worldRefl,
IN.worldPos,
unity_SpecCube0_ProbePosition,
unity_SpecCube0_BoxMin,
unity_SpecCube0_BoxMax
);
コレをどうにかして、「反射」ではなく「透過」的な見え方にする事ができないだろうか?
worldRefl は分解すると以下の様に記述できます。
※抜粋
struct Input {
float3 worldNormal;
float3 viewDir;
}
void surf (Input IN, inout SurfaceOutput o) {
float3 worldRefl = reflect(-IN.viewDir, IN.worldNormal);
}
IN.worldNormal が面がどちらの方向を向いているかに大きくかかわっている事は一目瞭然です。
試しに、コレを 0 に置き換えてみます。
float3 worldRefl = BoxProjection (
reflect(-IN.viewDir, 0),
IN.worldPos,
unity_SpecCube0_ProbePosition,
unity_SpecCube0_BoxMin,
unity_SpecCube0_BoxMax
);
何と、それらしきものができてしまいました。
本来は「法線ベクトル」を入力すべきところに「0」を 入力すると、
透過的なサンプリングになってしまうようです。
原理はさておき、こうすればそうなるのです。
Reflection Probe の配置
前述の画では Reflection Probe は以下の配置になっています。
室内の反射を取るのであれば、自ずとこうなるはずです。
しかし、Reflection Probe による透過表現が完成した今、
これではただ窓に張り付いた写真のようです。
外に広がる空間として透過させたいのであれば、
きっと以下のような配置になるはずです。
その結果が以下です。
上段:Reflection Probe による透過表現
下段:ガラスを外して実際の3Dモデルをレンダリング
見え方としては全く悪くありません。
Reflection Probe にぼかしを入れる
インスペクターでボケ具合を調整できるように、_Blur というプロパティを設定しています。
値を大きくするほどボケが大きくなります。
今まで Surface Shader の Lamberライティングモデルでやっていましたが、
ライトの影響を受ける必要のないシェーダーなので、
本当は Vertex/Fragment Shader で記述した方が良いと思います。
おわりに
GrabPass を使わずにすりガラス的な表現ができました。
同じやり方で外から部屋の中を見る場合の擬似表現にも使えます。
中身空っぽのビルで平面の窓オブジェクトなのに室内に奥行きがあるアレです。
しかし、窓の数だけ Reflection Probe を配置するのは現実的ではないので、
そういう場合は、Cubemap をマテリアルのプロパティとして設定できるようにする、
Box Projection の計算方法をワールドからローカルにする、
UVを活用して別の手法にするなど、もっと工夫が必要そうです。
[…] すりガラスの軽量表現 | Tsumiki Tech Times […]