【Unity】【シェーダ】VPOSを使ってスクリーンに対してテクスチャをマッピングする

f:id:halya_11:20180719003853g:plain

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

VPOS?

以前、スクリーンにテクスチャをマッピングする方法を紹介しました。

light11.hatenadiary.com

今回のVPOSはこれとは別の方法で、もう少しお手軽です。

VPOSというセマンティクスを付けた変数をフラグメントシェーダに定義しておくだけで
スクリーンスペースのピクセル位置を得ることができます。

docs.unity3d.com

実装

実装はこのようにします。

Shader "Sample"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            // シェーダモデルは3.0以上
           #pragma target 3.0
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };
            
            // 頂点シェーダの出力構造体
            struct v2f_vert
            {
                float4 vertex : SV_POSITION;
            };

            // フラグメントシェーダの入力構造体
            struct v2f_frag
            {
                // UNITY_VPOS_TYPE型の変数をVPOSセマンティクスで定義する
                UNITY_VPOS_TYPE vpos : VPOS;
            };

            sampler2D _MainTex;
            
            v2f_vert vert (appdata v)
            {
                v2f_vert o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f_frag i) : SV_Target
            {
                i.vpos.xy /= _ScreenParams.xy;
                return tex2D(_MainTex, i.vpos.xy);
            }
            ENDCG
        }
    }
}

フラグメントシェーダの入力構造体にUNITY_VPOS_TYPE型の変数をVPOSセマンティクスで定義しています。

頂点シェーダの出力構造体を分けている理由は次節で説明します。

SV_POSITIONとVPOSは一緒に定義できない

さて前節では頂点シェーダの出力とフラグメントシェーダの入力に別の構造体を使いました。
これは、SV_POSITIONとVPOSを一緒に定義するとエラーが出るためです。

例えば以下のように構造体を共通化してみます。

Shader "Sample"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            // シェーダモデルは3.0以上
           #pragma target 3.0
            
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            // 頂点シェーダの出力構造体 かつ フラグメントシェーダの入力構造体
            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_VPOS_TYPE vpos : VPOS;
            };

            sampler2D _MainTex;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                i.vpos.xy /= _ScreenParams.xy;
                return tex2D(_MainTex, i.vpos.xy);
            }
            ENDCG
        }
    }
}

すると次のようなエラーがでます。

Shader error in 'Sample': Duplicate system value semantic definition: input semantic 'SV_POSITION' and input semantic 'VPOS' at line 31

これはGPUの内部構造に起因するもののようですが、幸いにもSV_POSITIONはフラグメントシェーダでは使用せず、
VPOSは頂点シェーダでは使用しないため、前節のように別の構造体を定義すれば解決します。

構造体を2つ定義したくない場合

上述のように構造体を2つ定義すると、頂点シェーダからフラグメントシェーダに受け渡したい値は両方の構造体に定義しなければなりません。

// 頂点シェーダの出力構造体
struct v2f_vert
{
    float4 vertex : SV_POSITION;
    // 両方に定義する必要がある
    float4 color : COLOR;
};

// フラグメントシェーダの入力構造体
struct v2f_frag
{
    UNITY_VPOS_TYPE vpos : VPOS;
    // 両方に定義する必要がある
    float4 color : COLOR;
};

これはメンテナンス性が低下しそうです。
マクロを使う方法なども考えられますが、ここではマニュアルに載っている方法で実装してみます。

Shader "Sample"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {

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

            struct appdata
            {
                float4 vertex : POSITION;
                float4 color : COLOR;
            };
            
            // 頂点シェーダ -> フラグメントシェーダへの構造体
            struct v2f
            {
                // SV_POSITIONとVPOSはこの構造体には定義しない
                float4 color : COLOR;
            };

            sampler2D _MainTex;
            
            // 入力構造体のほかにout修飾子をつけたSV_POSITIONの変数を定義
            v2f vert (appdata v, out float4 vertex : SV_POSITION)
            {
                v2f o;
                vertex = UnityObjectToClipPos(v.vertex);
                o.color = v.color;
                return o;
            }
            
            // 入力構造体のほかにVPOSを定義
            fixed4 frag (v2f i, UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
            {
                vpos.xy /= _ScreenParams.xy;
                return tex2D(_MainTex, vpos.xy) * i.color;
            }
            ENDCG
        }
    }
}

上記のように、頂点シェーダとフラグメントシェーダの引数に、
入力構造体のほかにSV_POSITIONとVPOSを渡すようにするだけです。

これでv2f構造体が共通化されました。

結果

キューブに対して上記のシェーダを用いてテクスチャを貼ってみます。

f:id:halya_11:20180719003853g:plain

スクリーン座標に対してマッピングされていることがわかります。

参考

docs.unity3d.com

関連

light11.hatenadiary.com