【Unity】アウトラインを出してみる【Shader : 3】

## はじめに
この記事は連載記事です。
記事を読むにあたって**レンダリングパイプラインでどのような処理を行っているのか？**を知っている事は前提になります。
非常に分かりやすい資料がありますので、レンダリングパイプラインを知らない方は読む前にご覧ください。

<a target="blank" href="https://www.youtube.com/watch?v=wUx_Y9BgC7k">Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう</a>
<a target="blank" href="https://www.youtube.com/watch?v=-Ki1Q2CAqEc">Unity道場 2019.2 シェーダを書けるプログラマになろう #2 GPUの神秘</a>

**アーカイブ**
<a target=”_blank” href="https://qiita.com/Nekomasu/items/6620ab19bfdaeb7743f2">Unity、Shaderことはじめ【Shader : 0】</a>
<a target=”_blank” href="https://qiita.com/Nekomasu/items/2fbe8ef5ecfa5f81947a">Unity、UnlitShader/textureのコードを追いかける【Shader : 1】</a>
<a target=”_blank” href="https://qiita.com/Nekomasu/items/d50a4409e48ad77bc7f2">Unity、UVScroll(UVの処理)をやってみる【Shader : 2】</a>

## 導入
ちょうど最近、技術書典がありレイマーチングの季節になりました。
なので、「ジュリア集合とかマンデルブロ集合を書いていきます」といいたい所ですが、この記事書いてる人は「フラクタル？アニメじゃないの？」という感じなので、地に足ついて基本的なShaderの書き方をまとめてゆきます。

## アウトライン概要
感覚的には「１オブジェクトあたり１回のレンダリング」ですが、実際の所複数レンダリングできるみたいです。
ポピュラーな例？として、トゥーンレンダリングの輪郭線なんかがそれに該当します。

トゥーンレンダリングの輪郭線の前に…オブジェクトの裏面を描画するケースについて話します。
おそらく、メジャーな例ではSkydomeが該当すると思います。
![skydome.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149449/bd0a9fe7-8f09-ba43-61ca-d9cfca1ef10a.png)
球体のオブジェクトがSkydomeで、球体の中が表・球体の外が裏側となっています。
この状態でSkydomeの中にカメラを入れて描画すると、球体は空のように機能します。
**プラネタリウムも同じ仕組み**というと分かりやすそう？

![sky.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149449/5902fe9f-1dcc-d496-bc32-2fa52c9085ff.gif)
そして、Skydomeの中にもう１つ球体を置くとこのようになります。
**Skydome側は表からは遮るものがないので、中のオブジェクトがレンダリングできます。**
さらに、Skydomeのテクスチャを黒にしてみると…

![rinkaku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149449/1ec438a9-a73d-1614-f5fd-d8141c26b95a.png)
**球体に黒い輪郭がついてるように見えます。**
これが(リアルタイムレンダリングで)トゥーンシェーダーで輪郭線をレンダリングする仕組みです。
>プリレンダの場合、ちゃんと輪郭線がどこなのか？のシュミレーションを行っている。複雑な形状だと輪郭を誤検知する事もある。

今回の場合、Skydomeと中の球体を２つ用意したわけですが、１つのオブジェクトでも**「頂点を外側に拡大→裏面をレンダリングする→テクスチャの色ではなく輪郭線にしたい色で塗りつぶす」**そして、そのあとに通常通り表面をレンダリングするという事をShader側でやってやれば、輪郭線が表示できるわけです。
というわけで**「輪郭線のあるトゥーンシェーダーは２回分(２パスに分けて)レンダリングしている」**というわけです。

## 複数Passの書き方

```c:MultiPath.shader
Shader "MultiPath"
{
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		// 1回目のレンダリング
		Pass
		{
			//裏面
			Cull Front
		    
			CGPROGRAM
			//（略）
			ENDCG
		}

		// 2回目のレンダリング
		Pass
		{
			//表面
			Cull Back

			CGPROGRAM
			// (略)
			ENDCG
		}
		
		// 3回目、4回目以降は下に Pass{} を増やしてゆく
	}
}
```

単純に `Pass{}` でくくって `CGPROGRAM` と `ENDCG` の間に追加のレンダリング内容を書けば、複数回のパスでレンダリングされます。レンダリングは上から順に行われます。

上記のサンプルはよくあるパターンを想定して、１回目のパスでは裏面を。２回目のパスで表面をレンダリングしてますが、２回表面のパスを描く事もできますし、３回目以降も下に続けてゆけば書けます。
（その分、処理が増えますが）

`Cull Back` と `Cull Front` はカメラの向きを基準に表を描画するか、裏を描画するか？のプロパティです。
表面に関しては、省略しても問題ないですが、２パスある場合はあえて明示しておくと分かりやすいので書きました。
<a target=”_blank” href="https://docs.unity3d.com/ja/2018.1/Manual/SL-CullAndDepth.html">ShaderLab ：Culling と Depth Testing</a>

## UnlitShaderにアウトラインの表示を追加したSample

```c:UnlitOutLine.shader
Shader "Unlit/UnlitOutLine"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_LineColor("Line Color", Color) = (1, 1, 1, 1)
        _LineWidth("Line Width", Range(0.001, 0.2)) = 0.01
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		//Line
		Pass
		{
			//Back Side
			Cull Front
		    
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float _LineWidth;
			fixed4 _LineColor;
			
			v2f vert (appdata v)
			{
			    //offset方向の計算
				float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
				float2 offset = TransformViewToProjection(normal.xy);
			    
			    //OutLineとしてレンダリングされるポリゴン
			    v2f o;	
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertex.xy = o.vertex.xy + offset * _LineWidth;
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = _LineColor;
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}

		//Unlit
		Pass
		{
			//Surface
			Cull Back
		    
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
		
	}
}
```

## 解説

```c:UnlitOutLine.shader

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

struct v2f
{
	UNITY_FOG_COORDS(1)
	float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _LineWidth;
fixed4 _LineColor;
			
v2f vert (appdata v)
{
	//offset方向の計算
	float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
	float2 offset = TransformViewToProjection(normal.xy);
			    
	//OutLineとしてレンダリングされるポリゴン
	v2f o;
	o.vertex = UnityObjectToClipPos(v.vertex);
	o.vertex.xy = o.vertex.xy + offset * _LineWidth;
	UNITY_TRANSFER_FOG(o,o.vertex);
	return o;
}

```

アウトラインのレンダリングでポイントとなるのは、**アウトラインを描画すべき裏面の大きさを計算しているバーテクスシェーダー**です。
そこさえわかれば、線の色に工夫しなくてよいなら、ピクセルシェーダーはプロパティで指定されたカラーを渡すだけなので簡単です。

まずここの行。

```c:
float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
```
v.normal は**モデル原点を基準とした法線方向**なので、これを**カメラから見た時の法線方向**に変換する必要があります。
そこで、(float3x3)UNITY_MATRIX_IT_MV (モデルビュー行列の逆行列の転置行列) を mul()関数で掛けると、**カメラから見た法線ベクトル**が求まるそうです。

※mul()は掛け算、HLSLでは行列の掛け算で演算子を使わないらしい。
※UNITY_MATRIX_IT_MVを掛けると何故、カメラから見た時の状態になるのか？はそもそも行列とベクトルを理解する必要があります（なんもわからん…）

```c:
float2 offset = TransformViewToProjection(normal.xy);
```

次の行で、３次元ベクトルの法線情報を、２次元ベクトルに変換しています。
`TransformViewToProjection()` の関数がよしなにやってくれるそうですが、この関数は`UnityCG.cginc`のライブラリに定義されています。
UnityEditorにコンパイルされてるので、何もせずとも使えますが、ライブラリの中身を見たければダウンロードする必要があります。

<a target=”_blank” href="https://unity3d.com/jp/get-unity/download/archive">Unity ダウンロード アーカイブ</a>
**※ドロップダウンから「ビルトインシェーダー」を選択してダウンロードし、その中にある「CGIncludes」の .cginc を読むと、関数の中身が分かる。**

<a target=”_blank” href="https://docs.unity3d.com/jp/460/Manual/SL-BuiltinIncludes.html">ビルトイン シェーダ includeファイル</a>

```c:
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.xy = o.vertex.xy + offset * _LineWidth;
```
そして、最後はいつもどおりの処理で、オブジェクト座標をクリップ座標に変換。
クリップ座標に「offsetで求めたベクトル(外側の方向)×ラインの太さ(ベクトルの長さ)」
を足してやることで、実際のモデルよりも大きくなった場合のメッシュが求まります。

あとは、フラグメントシェーダーに渡してやって、そこのメッシュを塗った後…
２パス目でUnlitShaderをレンダリングするとアウトラインのあるUnlitShaderになります。

![Outline.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149449/e9f0eab6-1663-8ebf-c55e-7336262945bd.gif)

## おわり
行列ちゃんとやろう…Unityなんもわからん…

## 次
プロのエンジニアに馬鹿にされたりする事があるので更新しません。