【Unity】Unity シェーダーの基礎

# はじめに
こんにちは、[株式会社G-Blossom](https://www.g-blossom.com/) エンジニアのそーだすいです。

この記事は [G-Blossom Advent Calendar 2024](https://qiita.com/advent-calendar/2024/g-blossom) の25日目の記事となります。

今回はUnityのシェーダーについて基礎的な内容をご紹介したいと思います！

本記事で使用したUnityのバージョンは **Unity 2022.3.40f1** になります。

# シェーダー
シェーダーは、3Dモデルが画面にどのように表示されるかを制御するGPU上で実行されるプログラムです。


オブジェクトの色や質感、光の当たり方や影のつき方などをシェーダーで定義します。

例えば以下の画像のような、「金属（鎧）の光の反射」や「水面」、「アニメ調な陰影、輪郭線」など様々な視覚表現に利用されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/6ce12382-97af-21a8-a547-97710041277a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/7a42f7c9-673e-198a-fc6c-5aab70451bdc.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/df8b914a-a68b-4382-0abe-437610fc3012.png)

# Unityにおけるシェーダー
Unityでシェーダーを利用する場合、以下のような方法があります。

1. ShaderLabと呼ばれるUnity独自のシェーダー言語でカスタムシェーダーを作成して利用
1. Shader Graphと呼ばれるノードベースのGUIツールでシェーダーを作成して利用

今回は「ShaderLab」について、記載していきたいと思います！

# ShaderLab
ShaderLabはUnity独自のシェーダーを書くための言語で、3Dモデルの座標変換、色や質感などの見た目をシェーダファイル（.shader）に記述していきます。

ShaderLabは以下のような基本構造になっています。
```hlsl
Shader "Shader Name"
{
    Properties
    {
        // 外部から変更可能なプロパティ
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        // レンダリング設定
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            // 実際のシェーダ処理
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            sampler2D _MainTex;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texColor = tex2D(_MainTex, i.uv);
                return texColor * _Color;
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}
```

1. **Shader**
    * シェーダー全体の定義で、"Shader Name" にシェーダー名を指定
    * 指定した名前は、エディタでシェーダーを選択する際のパスになる
1. **Properties**
    * シェーダーで使用する変数を定義
    * インスペクタで値を設定したりスクリプトから参照可能
1. **SubShader**
    * シェーダーの主要な処理を記述する
1. **Tags**
    * レンダリング設定
1. **Pass**
    * 描画パスを定義
    * 1つのSubShader内に複数のPassを記述可能
    * CGPROGRAM　～　ENDCG の間にCG／HLSLコードを記述
1. **FallBack**
    * ハードウェアがシェーダーをサポートしていない場合に使用する代替シェーダーを指定

以下の処理はバーテックスシェーダーと呼ばれるもので、3Dモデルを最終的に2D画面に表示するために必要な頂点の座標変換を行っています。
```hlsl
v2f vert (appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
}
```

以下の処理はフラグメントシェーダーと呼ばれるもので、最終的に表示される2D画面の各ピクセルの色を決定するための光の計算などを行っています。
```hlsl
fixed4 frag (v2f i) : SV_Target
{
    fixed4 texColor = tex2D(_MainTex, i.uv);
    return texColor * _Color;
}
```

ここからは実際にShaderLabで 以下のようなアニメ調の陰影、輪郭線がついたトゥーンシェーダーと呼ばれるシェーダーを作成してみます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/327318d9-3eca-9da2-3df1-9804b657957b.png)


# トゥーンシェーダー
※トゥーンシェーダのアルゴリズムや実装方法の詳細にはあまり触れないので、あくまてシェーダーでこんなこともできるんだなという感じで見ていただければと思います!

**シェーダーファイル（.shader）の作成**
まずは、プロジェクトの任意の場所に **Create > Shader > Unlit Shader** でシェーダーファイルを作成します。ファイル名は **ToonShader** にします。
![シェーダーファイル作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/5052acc4-215d-c327-0531-64dcd978dc4e.png)

作成されたシェーダーファイルを開くといろいろと書かれていると思いますが、シンプルなコードから始めたいので中身をすべて削除して以下のコードで上書きしてください。

```hlsl
Shader "Custom/ToonShader"
{
    Properties
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

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

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            sampler2D _MainTex;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texColor = tex2D(_MainTex, i.uv);
                return texColor * _Color;
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}
```

**マテリアルファイルの作成**
シェーダーをオブジェクトに適用するためには以下の手順が必要です。
1. マテリアルを作成
1. マテリアルにシェーダーを設定
1. マテリアルをシーンのオブジェクトに適用

**Create > Material** でマテリアルファイルを作成します。ファイル名は **ToonMaterial** にします。マテリアルを選択して表示されるインスペクタのShader項目でシェーダーの設定が可能なので、**Standard　→　Custom/ToonShader** に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/ff5790b8-ac41-d85f-89ce-64f8cba6091b.png)

シーンに球オブジェクトを作成して、そのオブジェクト上にマテリアルファイルをドラックアンドロップしてください。

以下のような陰影がついていない真っ白な見た目になったと思います。
![ToonShader_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/f6df8930-d346-aaf3-9bc0-bbf9082b072b.png)

次にシェーダにアニメ調の陰影をつける処理を実装します。
TooShaderを以下のように書き換えてください。
```diff_hlsl
Shader "Custom/ToonShader"
{
    Properties
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

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

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
+               float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
+               float3 normal : TEXCOORD1;
            };

            sampler2D _MainTex;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.normal = UnityObjectToWorldNormal(v.normal);// 追加
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
-               fixed4 texColor = tex2D(_MainTex, i.uv);
-               return texColor * _Color;
+               fixed4 color = tex2D(_MainTex, i.uv) * _Color;
+               float NdotL = saturate(dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
+               if (NdotL > 0.5)
+                   return color;
+               else
+                   return color * 0.5;
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}
```
物体の表面がどの向きを向いているかを表す情報（**法線（normal）**） を追加し、光がどの向きを向いているかを表す情報（**_WorldSpaceLightPos0**）との内積を計算して物体の表面がどのくらい光に照らされているかを計算しています。

以下のアニメ調の陰影のような見た目になったと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/a68b7e42-59e8-a6db-118b-b0da0551964f.png)

次はシェーダーに輪郭線をつける処理を実装します。
TooShaderを以下のように書き換えてください。

```diff_hlsl
Shader "Custom/ToonShader"
{
    Properties
    {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
+       _OutlineColor ("Outline Color", Color) = (0,0,0,1)
+       _OutlineWidth ("Outline Width", Float) = 0.02
    }

+   SubShader
+   {
+       Tags { "RenderType"="Opaque" }
+       LOD 100
+
+       Pass
+       {
+           Cull Front
+
+           CGPROGRAM
+           #pragma vertex vert
+           #pragma fragment frag
+           #include "UnityCG.cginc"
+
+           struct appdata_t
+           {
+               float4 vertex : POSITION;
+               float3 normal : NORMAL;
+           };
+
+           struct v2f
+           {
+               float4 pos : SV_POSITION;
+               float4 color : COLOR;
+           };
+
+           float _OutlineWidth;
+           fixed4 _OutlineColor;
+
+           v2f vert(appdata_t v)
+           {
+               v2f o;
+               float3 offset = normalize(v.normal) * _OutlineWidth;
+               o.pos = UnityObjectToClipPos(v.vertex + float4(offset, 0));
+               o.color = _OutlineColor;
+               return o;
+           }
+
+           fixed4 frag(v2f i) : SV_Target
+           {
+               return i.color;
+           }
+           ENDCG
+       }

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

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;// 追加
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD1;// 追加
            };

            sampler2D _MainTex;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.normal = UnityObjectToWorldNormal(v.normal);// 追加
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 color = tex2D(_MainTex, i.uv) * _Color;
                float NdotL = saturate(dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
                if (NdotL > 0.5)
                    return color;
                else
                    return color * 0.5;
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}
```
元々あった通常の描画パスとは別に輪郭線用のPassを追加し、物体の表面（法線）方向にオブジェクトを少し拡大して輪郭線となるような描画処理を追加しました。

オブジェクトに輪郭線がついた見た目になったと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/5e10ccbb-d948-b7a2-5961-361e936e327b.png)

また、シェーダーのインスペクタでは輪郭線の色や太さを変更できるので、値を変更して見た目を変えてみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/01c6ddda-8ba0-cf98-7552-27ad1ae8fa5c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/1f9b8fcc-0526-7918-1b5a-f2216f6adaa5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3941883/01d4d7c0-560f-8f03-488f-070d0b44edb5.png)

# まとめ
今回はシェーダーの基礎的な内容についてご紹介しました。シェーダーを触るきっかけになれば幸いです。
上記のトゥーンシェーダーもシェーダーで実現できる表現のほんの一部に過ぎません。ぜひ、シェーダーを使ってゲームの様々なビジュアル表現を実装してみてください！！