【Unity】【シェーダ】スクリーンに対してテクスチャをマッピングする方法を完全解説する

f:id:halya_11:20180613234553p:plain

スクリーンに対してテクスチャをマッピングする方法です。

結論

ちょっと解説が長くなりそうなので、結論だけ知りたい方のためにまず最終的なコードを載せます。

Shader "MappingToScreen"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

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

            sampler2D _MainTex;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = ComputeScreenPos(o.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2Dproj(_MainTex, i.pos);
            }
            ENDCG
        }
    }
}

レンダリング結果は次のようになります。

f:id:halya_11:20180613234553p:plain

使用したテクスチャはこちら。

f:id:halya_11:20180613234628p:plain

解説1 基本的な処理

スクリーンに対するテクスチャマッピングには地味に罠が潜んでいるので順序だてて解説していきます。
まず、上述のソースコードの内部処理をもっとわかりやすくしてみます。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
            
#include "UnityCG.cginc"

struct appdata
{
    float4 vertex : POSITION;
};

struct v2f
{
    float4 vertex : SV_POSITION;
    float3 pos : TEXCOORD0;
};

sampler2D _MainTex;
            
v2f vert (appdata v)
{
    v2f o;

    // MVP行列に掛ける
    // mul(UNITY_MATRIX_MVP, v.vertex) と同じ処理だけどパフォーマンス良い
    o.vertex = UnityObjectToClipPos(v.vertex);

    // zは使わないのでxyzをfloat3に格納する
    o.pos = o.vertex.xyw;

    // プラットフォームの違いを吸収
    o.pos.y *= _ProjectionParams.x;

    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    // 0~1に変換
    half2 uv = i.pos.xy / i.pos.z * 0.5 + 0.5;

    fixed4 col = tex2D(_MainTex, uv);
    return col;
}
ENDCG

(CGPROGRAM部分のみ)

基本的な考え方として、MVP行列に頂点座標を掛けたものをwで割れば、xとyは-1~1の範囲を取ります。
これを0~1に変換してやればスクリーンに対してテクスチャがマッピングできるというわけです。
この考え方の解説は下の記事が詳しいです。

light11.hatenadiary.com

これを実現するため、上記ではまずUnityObjectToClipPos()でMVP行列に頂点座標を掛け、これをフラグメントシェーダに渡しています。

しかしMVP行列をただ掛けただけでは、プラットフォームによっては結果が上下反転します。
これはプラットフォームによって射影行列が反転しているためです。
y座標に_ProjectionParams.xを掛ければこのプラットフォームの違いを吸収できます。

_ProjectionParamsに関するマニュアルはこちら
Unity - Manual: Built-in shader variables

この点に注意すればあとはこれをフラグメントシェーダに渡してビューポート座標に変換するだけです。

解説2 ComputeScreenPos()

ここから上記の処理を最適化していきます。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
            
#include "UnityCG.cginc"

struct appdata
{
    float4 vertex : POSITION;
};

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

sampler2D _MainTex;
            
v2f vert (appdata v)
{
    v2f o;

    o.vertex = UnityObjectToClipPos(v.vertex);

    // ComputeScreenPosでxyを-w~wの値に変換する
    o.pos = ComputeScreenPos(o.vertex);

    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    // w除算すれば0~1の値が得られる
    half2 uv = i.pos.xy / i.pos.w;

    fixed4 col = tex2D(_MainTex, uv);
    return col;
}
ENDCG

まず、MVP行列を掛けた値をComputeScreenPos()に渡します。
ComputeScreenPos()はUnityCG.cgincに定義されている関数で、下記のような処理をしています。

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    // この時点でxyは-wからwの値を取る(MVP変換後だから)
    // xyzwに0.5をかけるのでxyは-0.5w~0.5wになる
    float4 o = pos * 0.5f;

    // この処理でxyが0~wになる
    // yがプラットフォームにより上下反転する問題も吸収
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

inline float4 ComputeScreenPos(float4 pos) {
    // 通常はこの処理だけが行われる
    float4 o = ComputeNonStereoScreenPos(pos);

// こっちはVR用の処理っぽい
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

いろいろコメントを書きましたが、要するにxyが0~wに変換されて、ついでにyの上下反転問題も吸収されます。
xyは0~wなので、あとはこれをフラグメントシェーダでw除算するだけで0~1の値が得られるというわけです。

解説3 tex2Dproj()

さて上記の処理はもう少し簡単に書けます。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
            
#include "UnityCG.cginc"

struct appdata
{
    float4 vertex : POSITION;
};

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

sampler2D _MainTex;
            
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.pos = ComputeScreenPos(o.vertex);
    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2Dproj(_MainTex, i.pos);
    return col;
}
ENDCG

w除算をなくしてtex2D()をtex2Dproj()に置き換えました。
tex2Dproj()は与えられた座標のxyをw除算をしてからtex2D()のuvとして使う、という処理をします。

特に早くなるというわけではなさそうですが、こんな方法もあるということで紹介しました。

この辺りは下記のフォーラムが参考になります。
https://forum.unity.com/threads/what-does-the-function-computescreenpos-in-unitycg-cginc-do.294470/

スクリーン座標を求める

ちなみにですが、ビューポート座標をスクリーン座標に変換するには_ScreenParams.xyを掛けるだけです。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
            
#include "UnityCG.cginc"

struct appdata
{
    float4 vertex : POSITION;
};

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

sampler2D _MainTex;
            
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.pos = ComputeScreenPos(o.vertex);
    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    half2 uv = i.pos.xy / i.pos.w * _ScreenParams.xy;
    fixed4 col = tex2D(_MainTex, uv);
    return col;
}
ENDCG

_ScreenParamsのマニュアルはこちら。
Unity - Manual: Built-in shader variables

【2019/4/24追記】VPOSを使う方法

VPOSというセマンティクスを使ってスクリーンにテクスチャをマッピングする方法もあります。

light11.hatenadiary.com

いろんなプラットフォームに対応させる場合はこれを使っておいたほうが変な不具合に悩まされない気がします。

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

https://forum.unity.com/threads/what-does-the-function-computescreenpos-in-unitycg-cginc-do.294470/