e.blog

主にUnity/UE周りのことについてまとめていきます

Standard Assetsの「GlassStainedBumpDistort」シェーダを覗いてみた

概要

「Standard Assets」に含まれている「GlassStainedBumpDistort」を覗いてみました。
どういうシェーダかというと、以下のように、オブジェクトの背面を歪ませる効果を実現するものです。

準備するのは歪ませるための法線を持たせたBumpMap用のテクスチャだけなので比較的簡単に利用できます。
(実装もそんなに複雑ではないので色々参考になりそう)

頂点シェーダ

まずは頂点シェーダ。

v2f vert (appdata_t v)
{
    v2f o;
    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
    #if UNITY_UV_STARTS_AT_TOP
    float scale = -1.0;
    #else
    float scale = 1.0;
    #endif
    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
    o.uvgrab.zw = o.vertex.zw;
    o.uvbump = TRANSFORM_TEX( v.texcoord, _BumpMap );
    o.uvmain = TRANSFORM_TEX( v.texcoord, _MainTex );
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

UNITY_UV_STARTS_AT_TOPは、プラットフォームごとに異なるテクスチャ座標系を適切に取り扱うために利用します。
ドキュメントから引用すると以下になります。

Render Texture の座標

垂直方向のテクスチャ座標の表現方法は、Direct3DOpenGL のプラットフォームで異なります。

  • Direct3D、Metal、コンソールでは最上部が 0 の座標位置となり、下方向に行くにしたがって増加します。
  • OpenGLOpenGL ES では、最下部が 0 の座標位置となり、上方向に行くにしたがって増加します。

ほとんどの場合に影響はありませんが、レンダーテクスチャ に対してレンダリングする場合は影響があります。この場合、Unity は OpenGL 以外でレンダリングするときに意図的にレンダリングを上下逆に反転するので、プラットフォーム間のルールは同じままです。シェーダーで処理する必要のある一般的な例は、イメージエフェクトと、UV空間のレンダリングです。

docs.unity3d.com

テクスチャ座標を計算

このシェーダでは、前のパスでレンダリングされた結果をキャプチャし、それを利用して後ろを「透過させているように」見せているため、オブジェクトの後ろを表すテクスチャから色をフェッチしないとなりません。
そのため、オブジェクトのある位置を元に、「後ろの映像のテクスチャ」のUV座標を求める必要があります。

それを行っているのが以下の処理です。

o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;

w は同次座標系で利用するものですね。それを足して、さらに0.5倍しています。
最初これはなにをしているのだろうと思ったのですが、前述のように、オブジェクトの後ろ側のテクスチャの色を適切にフェッチするために正規化している、というわけです。

具体的には、クリップ座標系に変換された時点の x, y の値は -w ~ w の範囲に変換されます。つまり、それに対して w を足すということは 0 ~ 2w の範囲に変換することと同義です。そしてそれを半分( * 0.5 )することで 0 ~ w の範囲にします。

そしてフラグメントシェーダのタイミングでその値をさらに w で割ることで、結果的に 0 ~ 1 の範囲に変換している、というわけです。

詳細は後述しますが、続くフラグメントシェーダでは以下のように色をフェッチしています。

half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));

こちらの射影テクスチャリングの記事を読むとイメージしやすいかもしれません。(射影空間からテクスチャ座標に変換)

NOTE:
tex2Dprojは、該当オブジェクトにテクスチャを投影するような形でテクセルをフェッチします。
つまり、同次座標系で見た場合に、該当のテクセルがどうなるか、を計算しているわけです。

具体的には、以下のように自前で計算することでも同じ結果を得ることができます。

float2 uv = i.uvgrab.xy / i.uvgrab.w;
half4 col = tex2D(_GrabTexture, uv);

要は、Z方向の膨らみを正規化することで2D平面(ディスプレイ)のどの位置に、該当オブジェクトのピクセルがくるのか、を計算しているわけですね。

フラグメントシェーダ

続くフラグメントシェーダ。法線(ノーマルマップ)からフェッチするUVのオフセットを計算し、キャプチャしたテクスチャから色をフェッチしています。

half4 frag (v2f i) : SV_Target
{
    // calculate perturbed coordinates
    half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg; // we could optimize this by just reading the x & y without reconstructing the Z
    float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy;
    i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
    
    half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
    half4 tint = tex2D(_MainTex, i.uvmain);
    col *= tint;
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

ノーマルマップからフェッチするUV座標にオフセットを適用しています。これが歪みを実現している箇所ですね。

そしてtex2Dprojを使っているのは、このシェーダが適用されたオブジェクトに、キャプチャした映像のテクスチャを「投影している」と考えるとイメージしやすいかと思います。
なので頂点シェーダでテクスチャ座標を計算していたんですね。

前に書いた以下の記事も参考になるかもしれないので貼っておきます。