【Unity】図解で分かる！アニメ調シェーダ基礎

https://qiita.com/advent-calendar/2021/sumzap


この記事は[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap) 12/2の記事です！
昨日の記事はオオバさんによる「[【Unity】失敗しないUI開発3つのポイント](https://shibuya24.info/entry/unity_ui_point_3) 」でした！

### この記事について

この記事ではアニメ調シェーダで使われている輪郭線の仕組みについての解説を挟みながら、画像のような最も簡単な輪郭線を作れるシェーダとモデル（オブジェクト）の話をします。

<img width="500" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393006/78df5891-9eaf-475d-b95c-109c6023af25.png">

### 輪郭線とは

輪郭線とは端的に言えばモデルに対して周囲を取り囲むように表示される線の事です。輪郭線を引くことで造詣がはっきりとし、モデルを際立たせることができます。また、UnityのSceneビューでオブジェクトを選択した際に輪郭に沿って表示されるオレンジ色の線など、身近にありふれている表現方法です。

![スクリーンショット 2021-11-29 152901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393006/3153ec13-c8b8-472b-7aad-ad1bd3473ab5.png)

この輪郭線をUnityで表現する場合は主にシェーダを用いて表現されることがあり、アウトラインシェーダや輪郭線シェーダと呼ばれています。


輪郭線はアニメ調の表現でもよく使われており、例えばUnityちゃんでも使われています。
下の画像ではUnityちゃんの一部の輪郭線の有り無しを比較したもので、Unityちゃんのあご付近を見ると右の画像では輪郭部分に濃い線が引かれていますね。
これがUnityちゃんの輪郭線です。（Unityちゃんの他の部分にもたくさん使われています。）

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/84d5014882db717a18324e73.png">

### もっとも簡単な輪郭線

最も簡単に輪郭線を表示する方法は、モデルを一回り大きく輪郭線の単色で描写しその上にモデルを描写する方法です。
例として、ある四角いアイコンのUIで輪郭線が作られるか見てみましょう。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393006/0c4c9aff-1b2b-ccbf-a7e3-e0f4a72d2be5.png)

このようにベース画像と単色の拡大画像を合わせることで輪郭線を表示させることができます。

それでは早速シェーダに落とし込んでみましょう。コード内でも同様に、1パス目でベース画像を拡大したものを描画し、2パス目でべース画像自体を描画することで輪郭線を表示するようにします。特別なことはしておらず、最小限の構成にしています。

#### シェーダコード

```
NormalOutLine.shader
Shader "Unlit/NormalOutLine"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BaseColor("BaseColor", Color) = (1, 1, 1, 1)
        _OutlineColor("Color", Color) = (1, 1, 1, 1)
        _OutlineWidth("Width", float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        // _OutlineColorの色で_OutlineWidthの分だけ大きく描画を行う
        Pass // 1
        {
            // 裏面のみの描写にする
            cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            half _OutlineWidth;
            half4 _OutlineColor;
            
            v2f vert (appdata v)
            {
                v2f o;
                // _OutlineWidthの分だけ法線方向に拡大させている
                o.vertex = UnityObjectToClipPos(v.vertex +  v.normal * _OutlineWidth / 100);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }
        
        // 特別な処理を行わず、そのまま描画を行う
        Pass // 2
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            half4 _BaseColor;

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

            fixed4 frag (v2f i) : SV_Target
            {
                return _BaseColor;
            }
            ENDCG
        }
    }
}
```

### 法線拡大法の問題点

それではこのシェーダをキューブやスフィアにアタッチしてみましょう。

実際にコードを実行していくつかのオブジェクトにアタッチしてみると分かるのですが、実はこの方法では問題があり、ある基準を満たしたモデルでないと輪郭線が途切れてしまいます。

<img width="500" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393006/9c397dfc-d8d8-7cc7-f978-744b11b1be6a.png">

上記の画像では左のキューブに上記のシェーダをアタッチしたところ右の様になりました。右のキューブは輪郭線が切れてしまっています。
これは**頂点が持っている法線データ**が原因で起こってしまっています。

今回のシェーダでは法線方向に拡大をしているため、モデルの法線の方向が意図したものでないと正常に表示されません。

### 法線データ

頂点が持っている法線データはどう確認すればよいでしょうか？
一度法線データを確認したいですがUnityでは手軽に法線データを確認できないため、今回はBlenderでモデルの法線データ確認してみます。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/ec99e700ee869c47a7bed14e.png">

上記の画像では、各頂点から法線をピンク色の線で表示してみました。確かに面から垂直に法線が引かれているため法線方向に拡大すると角や辺の付近にはアウトラインが生成されることはなさそうです。
Blender上で板ポリをおいて実際に法線が垂直な場合をシミュレートした結果がこちらです。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/b3896703c78e8b7c3acbe2bd.png">

確かにUnity上と同じく、辺や角の付近が切れている状態になりました。

次は頂点の法線データをBlender編集してUnityに持っていき、想定しているアウトラインが出るか確認します。
簡略化のため、一時的にキューブの１面だけで考え、予想されるアウトラインをオレンジの板て表現しています。

現在は下の画像のように面に対して垂直な法線になっているため、アウトラインが角までカバーできていない状態です。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/df20601f2e75c9eaadcb2a14.png">

そこで下記のように角の法線を関連する面の平均ベクトルになるように編集しました。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/a0b727c8a4d815928e382d5a.png">

この法線を使って、法線方向に拡大すれば輪郭線は以下の画像の様に拡大されるため途切れることなく表示されるはずです。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/6658a3a3199d37bf6f3b8d36.png">


それではUnityで確認をしてみましょう。

<img width="500" alt="ファイル名" src="https://storage.googleapis.com/zenn-user-upload/26abc69953ac8212c3a51da0.png">

調整した右のキューブでは、無事に途切れることがない輪郭線を作ることができました。
つまり原因は**法線データが面から垂直になため、角や辺の部分にアウトラインが生成できていない**という事でした。

これで無事にモデルとシェーダの調整を終えたため、輪郭線を描画できるようになりました！

### まとめ

この記事ではもっとも簡単な輪郭線シェーダの解説を行いました。
この方式ではモデル自体に制約を課してしまいますが、その分単純なコードで輪郭線を描画することが可能です。

この記事が初めてアニメ調シェーダを書こうとする方、輪郭線について知りたい方の一助になれば幸いです！

また、日々の出来事や開発記録をこちらのブログで公開しています。
よろしければご覧になって下さい！

https://zenn.dev/solarisb2

次回は@shiimanさんの「**【Golang】OAuth2.0を実装する方法/Google認証**」です！

#### 参考サイト
https://light11.hatenadiary.com/entry/2018/05/13/183314

#### ライセンス表記

© Unity Technologies Japan/UCL