しゅみぷろ

プログラミングとか

投影手法について

はじめに

Shaderで投影を行う手法についてメモ用に書き残しておこうと思います。 投影の手法や原理を理解出来れば、例えば

  • 背面をGrabしオブジェクト背面の絵を取得しエフェクトを掛ける
  • Projectorの作成
  • DepthShadowによる影の描画
  • 反射の描画

みたいなことが出来るようになります。


f:id:es_program:20180112121206g:plain:h200
弾痕(投影+背景歪み)
f:id:es_program:20180109233123g:plain:h200
反射
f:id:es_program:20180112005401g:plain:h200
反射(水面)



投影の考え方は、グラフィックスの基礎知識さえあればそれほど難しくありません。 数式云々よりもイメージが大切です。座標変換とその座標系がどういった意味を持つのかをしっかりとイメージできれば理解しやすいと思います。 なので、投影について勉強する前にもう一度基礎を固めておくことをオススメします。

esprog.hatenablog.com

本記事では

  • GrabTextureのマッピング
    • 主にGrabPassを用いてオブジェクトの背面の絵を取得するのに使う
  • 投影マッピング
    • 主にProjectorやDepthShadow、反射の描画に使う

といった順番で纏めていきます。

どちらも最終的にやっていることはScreen Spaceで空間を統一し、その空間の座標で描画を行っているだけです。 GrabPassを用いてオブジェクトの背面の絵を取得して描画する処理も、DepthShadowのように別の視点からレンダリングされた画像を使って影を投影するのも本質的には同じことです。 後に紹介するShaderコードを見比べてみるとわかりやすいと思いますが、コード自体はほぼ同じになります。

GrabTextureのマッピング

GrabPassを用いてそれまでにレンダリングされたバッファを取得し、色を反転してオブジェクトに投影してみます。

f:id:es_program:20180221205230g:plain:w600

Sphereに以下のShaderを適用して動かしています。

Shader "Grab"
{
    Properties
    {
    }

    SubShader
    {
        Tags { "Queue" = "Transparent" }

        GrabPass
        {
            "_BGTex"
        }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 grabPos : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.grabPos = o.vertex;
#if !UNITY_UV_START_AT_TOP
                o.grabPos.y *= -1;
#endif
                return o;
            }

            sampler2D _BGTex;

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.grabPos / i.grabPos.w * 0.5 + 0.5;
                fixed4 col = tex2D(_BGTex, uv);

                col = 1 - col;
                return col;
            }
            ENDCG
        }
    }
}

まずGrabPassでバッファを_BGTexという名前で取得します。 で、今回の場合「Sphereの後ろにある部分の色を反転したい」ので、「後ろにある部分」の取得をどうにかする必要があります。 これはSphereの各頂点がスクリーン(_BGTex)のどこにあたるのかがわかれば良いので、Projection変換してUV座標に直せばOKです。

vertex shaderで頂点をProjection空間へ持っていきます。これをo.grabPosに保持してfragment shaderに渡します。

fragment shaderに渡されたProjection空間の座標は-1~1の値をとる同次座標です。 同次座標を通常座標に直すためにはw要素で各要素を割る必要があります。そして、値をUV座標として使うために0~1に変換する必要があるので、0.5を掛けて0.5足します。

これが投影の基本となるShaderです。 「背面の色を変えている」と考えるのではなく、「背面をテクスチャとして取得し、色を変えてオブジェクトに投影している」と考えて下さい。

因みに、上記のShaderコードには少し無駄があります。 「0.5を掛けて0.5足す」という処理をフラグメントシェーダーで行っている点です。 「0.5を掛けて0.5足す」は、言い換えれば「大きさを0.5倍し、0.5平行移動する」ということです。 なので、頂点シェーダーで

$$ \begin{bmatrix} 0.5 & 0 & 0 & 0.5 \\ 0 & 0.5 & 0 & 0.5 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ w \\ \end{bmatrix} = \begin{bmatrix} 0.5x + 0.5w \\ 0.5y + 0.5w \\ z \\ w \\ \end{bmatrix} $$

の計算を行うことで、多少最適化出来ます。

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.grabPos = o.vertex;
                o.grabPos.x = o.grabPos.x * 0.5 + o.grabPos.w * 0.5;
#if !UNITY_UV_START_AT_TOP
                o.grabPos.y = o.grabPos.y * -0.5 + o.grabPos.w * 0.5;
#else
                o.grabPos.y = o.grabPos.y * 0.5 + o.grabPos.w * 0.5;
#endif
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float4 uv = i.grabPos;
                fixed4 col = tex2Dproj(_BGTex, uv);

                col = 1 - col;
                return col;
            }

これを行ってくれるのが、UnityCG.cgincのComputeGrabScreenPosです。 これを使うと以下のように簡略化出来ます。

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.grabPos = ComputeGrabScreenPos(o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float4 uv = i.grabPos;
                fixed4 col = tex2Dproj(_BGTex, uv);

                col = 1 - col;
                return col;
            }

今回は実装例を簡単にするために色を反転するだけにしていますが、例えば法線マップで歪ませたり、それにリムライトをプラスして泡や水っぽいものを表現したりも出来ます。

投影マッピング

さて、本題。 といっても、Grabでの投影が理解できていれば難しくはありません。投影時の「視点」が変わるだけです。 具体的にはgrabPosを算出する時の変換行列が別の視点からのものに変わります。

esprog.hatenablog.com

上記の水面反射の記事で「頂点を反射用カメラの変換行列を用いて変換」の部分がこれに該当します。 反射のコードを一部抜粋してみると

        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.ref = mul(_RefVP, mul(_RefW, v.vertex));
            return o;
        }
        
        fixed4 frag (v2f i) : SV_Target
        {
            return tex2D(_RefTex, i.ref.xy / i.ref.w * 0.5 + 0.5);
        }

となっています。 _RefVP_RefWは、反射描画用カメラの変換行列です。

このコード、GrabTextureの投影と似ていませんか? というより、同じです。同じ。vertex shaderで使っている変換行列が変わっている以外で全く差はありません(変数名等は別として)。

つまり、GrabTextureの投影では「同じ視点から見て特定の場所を投影する」のに対し、反射の投影では「別視点でのレンダリング結果を反映するために、一度それと同じ視点からみて、描画必要な箇所を投影する」形になっています。

自分の中でのイメージの話ですが、投影は「頂点の座標をKey、視点から見たViewSpaceへの変換行列をハッシュ関数としたテクスチャのマッピング」のような感覚を持っています(わかりにくい?)。

計算だけ見るとなんてことないですが、イメージ出来るといざ使う時に役立ちます。

最後に

とりあえず記事が長くなりそうなので、今回はここまでで。 GrabTextureで基礎を、反射の描画で実際の投影処理について書いてみました。

ここらへんのイメージを字で伝えるのはちょっとむずかしいなぁと思ったので、暇があればDepthShadowの自作を通してもっと詳細に解説する記事書きたいと思います。 といっても、実装自体はライトを視点としてレンダリングした結果(ShadowMap)を今回のように投影すれば終わりです(深度値の比較などもありますが..)。

その時に、一緒にUnity標準のShadowCast/ShadowReceiveといった、影を描画するための2パス構成(ShadowMapへ描画するためのパスと、ShadowMapを投影するパス)について再度、より詳細に掘り下げられたら良いなとも思います。SRPで自作パイプライン作る時にここらへんの知識は使えるかもしれないので。

esprog.hatenablog.com

兎にも角にも重要なのは「同じ視点から同じものを眺めてみる」ことです。