見出し画像

Unityでカードの表裏に別々の画像を表示するSpriteRenderer用シェーダ

こんにちは、カキレモンです。

前回に引き続いてカードゲーム用のシェーダの話です。カードゲームを作るとき、大抵は裏と表に別の画像を用意しますよね。カードの表面と裏面で画像を切り替える方法を検索すると「3Dメッシュ(Quad)を使う」「C#スクリプトから角度に応じて画像を切り替える」といった方法がヒットします。別にそれでもいいのですが、今回はシェーダを使った解決法を提案したいと思います。

SpriteRenderer用の基本シェーダ

前回と同様にまずこちらのリポジトリなどを参考にSpriteRenderer用の基本シェーダを書きます。

表裏の判定

カードの表裏の判定のために、カードの法線ベクトルを考えます。ここでは法線が向いている側が表ということにします。また、オブジェクトから視点に向かう方向のベクトルをここでは視線ベクトルと呼びます。

法線ベクトルと表裏の関係

このとき、視線ベクトルと法線ベクトルが「同じ向き」のときは表を、「逆向き」のときは裏を描画すればいいことが分かります。つまり視線ベクトルと法線ベクトルの内積の正負で判定することができます。

法線、視線方向の取得

法線を取得するにはセマンティクスを使います。

もともとの値はオブジェクト空間のものなのでUnityObjectToWorldNormalを使ってワールド空間に直して使います。また、視線ベクトルはWorldSpaceViewDirで取得します。

struct appdata
{
    float4 vertex : POSITION;
    float4 color : COLOR;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL; // 法線ベクトル
};

struct v2f
{
    float2 uv : TEXCOORD0;
    fixed4 color : COLOR;
    float4 vertex : SV_POSITION;
    float3 normalWS : TEXCOORD1; // 法線ベクトル(World Space)
    float3 viewWS : TEXCOORD02; //視線ベクトル(World Space)
};

v2f vert(appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.color = v.color;
    o.uv = v.uv;
    o.normalWS = UnityObjectToWorldNormal(v.normal); // ワールド空間法線ベクトルの計算
    o.viewWS = WorldSpaceViewDir(v.vertex); // ワールド空間視線ベクトルの計算
    return o;
}

次に、取得した法線ベクトルと視線ベクトルの内積を使って描画内容を変更します。試しに裏(内積<0)のときは単純に真っ白で塗りつぶすことにしましょう。

fixed4 frag(v2f i) : SV_Target
{
    // 内積を計算
    float dp = dot(i.normalWS, i.viewWS);
    // dp>=0(表)なら画像、dp<0(裏)なら白色を返す
    fixed4 col = dp >= 0 ? tex2D(_MainTex, i.uv) : fixed4(1,1,1,1);
    col *= i.color;
    col.rgb *= col.a;
    return col;
}

表裏で別々の画像を表示することができました。

Orthographicでの視線方向を正しくとる

さて、一見うまくいってそうですが、まだいくつか問題があります。例えば、カメラの投影がOrthographicの場合はカードを動かしたときに表裏の判定が微妙にずれてしまうことがあります。

一見うまくいってそうだが

カメラの投影がPerspectiveの場合は「オブジェクト座標からカメラ座標へのベクトル」を視線ベクトルとして扱えばよいですが、Orthographicではカメラの位置に関係なくカメラの向きを使用する必要があります。そのため、先ほど視線ベクトルとして使用したWorldSpaceViewDirはOrthographicには不適当です。

カメラの投影法による視線方向の違い

カメラがOrthographicになっているかどうかはunity_OrthoParams.wで取得できるので、視線ベクトルの計算を次のように書き直せばよいです。

o.viewWS = unity_OrthoParams.w > 0 ?
    mul((float3x3)unity_CameraToWorld, float3(0, 0, -1)) : // ワールド空間でのカメラの向き
    WorldSpaceViewDir(v.vertex);

裏面の画像の反転を修正する

また裏面を画像に変えてみると分かるのですが、裏面の表示が反転してしまいます。

裏面の画像が反転している

これを防ぐために、裏面を表示しているときはuv座標を左右反転させることにします。

fixed4 frag(v2f i) : SV_Target
{
    float dp = dot(i.normalWS, i.viewWS);
    // 裏面ならuv座標を反転
    i.uv.x = dp >= 0 ? i.uv.x : 1 - i.uv.x;
    ...
}

これで裏面の表示時にも画像が反転しないようになりました。

コード全体

最後に、完成したシェーダのコード全体を掲載します。

Shader "Custom/Flippable"
{
    Properties
    {
        [PerRendererData] _MainTex("Texture", 2D) = "white" {}
        _BackTex("BackTexture", 2D) = "white" {}
    }
        SubShader
    {
        Tags 
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent"
        }
        Blend One OneMinusSrcAlpha

        Cull Off

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

            struct appdata
            {
                float4 vertex : POSITION;
                fixed4 color : COLOR;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                fixed4 color : COLOR;
                float4 vertex : SV_POSITION;
                float3 normalWS : TEXCOORD1;
                float3 viewWS : TEXCOORD02;
            };

            sampler2D _MainTex;
            sampler2D _BackTex;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.color = v.color;
                o.uv = v.uv;
                o.normalWS = UnityObjectToWorldNormal(v.normal);
                o.viewWS = unity_OrthoParams.w > 0 ?
                    mul((float3x3)unity_CameraToWorld, float3(0, 0, -1)) : 
                    WorldSpaceViewDir(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float dp = dot(i.normalWS, i.viewWS);
                i.uv.x = dp >= 0 ? i.uv.x : 1 - i.uv.x;
                fixed4 col = dp >= 0 ? tex2D(_MainTex, i.uv) : tex2D(_BackTex, i.uv);
                col *= i.color;
                col.rgb *= col.a;
                return col;
            }
            ENDCG
        }
    }
}

おわりに

地味ですが、DOTweenなどで自由に回転させても表裏の表示を自動でやってくれるので便利かもしれませんね。

ちなみに裏面の画像はマテリアルで共通になっていますが、オブジェクトごとに設定したい場合はMaterialPropertyBlockを使うとよさそうです。

ということで今回は以上です。

それでは、また。

この記事が気に入ったらサポートをしてみませんか?