冬休みの工作に最適な49行ゆるふわShader「HibitShader」の作り方

　こんにちはhibitです。この記事は、[VRChat Advent Calender 2018](https://qiita.com/advent-calendar/2018/vrchat)の15日目の記事です。
 
　以下のような自作シェーダを作成しました。GitHubリポジトリは[ここ](https://github.com/hibit-at/HibitShader)。

<img width="1569" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/288125/6fa08cc2-5679-af3e-be44-ed5ae4825bc3.png">

<img width="1339" alt="how to use.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/288125/0c584bff-4e5b-8ed8-b4bd-3015a47a4042.png">

　なんとこのシェーダ、49行で書けますので、**いつかシェーダを写経してみたいけど長すぎて……**とか、**あめりか語をみるとめまいが……**とか、そういった方でも大丈夫！　1行ずつ（ただし私にわかる範囲で）時に丁寧に時に雑に解説します。冬休みの工作気分でレッツトライ！

# 何はともあれソースコード

```GLSL
Shader "Hibit/HibitShader" {
	Properties {
		_Color ("Color", Color) = (.5,.5,.5,1)
		_RimColor ("RimColor", Color) = (.5,.5,.5,1)
		_MainTex ("MainTex", 2D) = "white" {}
		_NormalMap ("NormalMap", 2D) = "white" {}
		_Change ("Change", Range (0,6.28)) = 0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		Cull Off

		CGPROGRAM
		#pragma surface surf Custom
		#pragma target 3.0

		sampler2D _MainTex;
		sampler2D _NormalMap;
		fixed4 _Color;
		fixed4 _RimColor;
		float _Change;

		struct Input {
		float2 uv_MainTex;
		float2 uv_NormalMap;
      		float3 viewDir;
		};

		fixed4 LightingCustom (SurfaceOutput s, fixed3 lightDir, fixed atten) {
		half d = dot(s.Normal, lightDir)*0.2+0.2;
		fixed4 c;
		c.rgb = s.Albedo.rgb * _LightColor0.rgb * ((_Color-.5)/3+d);
		return c;
		}
		void surf (Input IN, inout SurfaceOutput o) {
		fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
		half rim = 1 - saturate(dot(IN.viewDir, o.Normal));
		fixed3 rot = fixed3(saturate(cos(_Change)),saturate(cos(_Change+UNITY_PI*2/3)),saturate(cos(_Change+UNITY_PI*4/3)));
		fixed3 Crgb = fixed3(dot(rot,c.rgb),dot(rot,c.gbr),dot(rot,c.brg));
		half3 environment = ShadeSH9(half4(o.Normal, 1));
     	o.Emission = (2*Crgb+_RimColor)/3 * pow(rim, 3)*environment*2;
		o.Albedo = Crgb;
		o.Normal = UnpackNormal(tex2D (_NormalMap, IN.uv_NormalMap));
		}
		ENDCG
	}
	FallBack "Diffuse"
}
```

　これを各部分ずつ解説していきます。

# プロパティ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/3e0d4ab3-130e-9e8a-d8eb-a33cb38f6d36.png)


　先頭の行でシェーダのカテゴリとUnity上のシェーダ名を指定します。赤マルで区切った部分ですね。

　次のプロパティは大事ですね。ここはUnityのインスペクタでユーザが値をいじったりテクスチャをぶっこんだりする所です。黄色で囲った所の1行が、それぞれUnity上のスライダーやテクスチャに対応する訳です。

　つまり、以下の画像のように対応します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/3078b854-6449-8553-945a-b81e26a7dedc.png)

　後で詳細な数学的作用を説明しますが、

- Color　調整用の色味
- RimColor　リムライト（輪郭光）の色
- MainTex　いわゆるテクスチャ画像
- NormalMap　ノーマルマップ
- Change　色相の変換具合

　を示しています。

　プロパティの種類には`Range`（変数、スライダー）、`Color`（色、カラーピッカーで選ぶ）`2D`（テクスチャ、画像を読み込ませる）などがあります。詳しい種類は[公式リファレンス](https://docs.unity3d.com/jp/current/Manual/SL-Properties.html)へ。黄色で囲ったクォーテーション内の文字がUnity上で表示されるパラメータ名、左のアンダーバーがついたやつ（`_Color`みたいなやつ）が変数名になります。

　変数名は別に何でも良いのですが、テクスチャ画像の変数名を`_MainTex`にしておくと、VRChat上でシェーダブロックされてもスタンダードシェーダとしてそのテクスチャが表示されるというメリットがあります。

#タグとか

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/08d227a1-5140-de1b-7012-6fdcdf3cc3cd.png)

　次はサブシェーダを指定します。赤枠の部分は`"Opaque"`にします。なぜかというと……よくわからないけど普通のシェーダはOpaqueにしろって[公式リファレンス](https://docs.unity3d.com/ja/current/Manual/SL-ShaderReplacement.html)に書いてあるんだもん。ちなみにOpaqueとは「不透明な」という意味です。透過したい時とかは`"Transparent"`とか使うらしいですよ。

　`LOD`というのはLevel of Detailの略で、公式リファレンスによると「カメラとオブジェクトの距離が離れた場合にレンダリングする三角形の数を減らす事ができます」とのことですが、何も考えずに200でいいと思います。

　黄枠の`Cull Off`は結構大事で、この1行を追加すると、シェーダがポリゴンの両面を描画するようになります。デフォルトだと裏面は透過してしまうので、両面を描画したい時は必須のコマンドとなります。

#CGプログラム

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/efee5b6c-1a88-11ba-bdd5-3ec09306fa68.png)

　この部分はライティングのやり方とかを指定します。赤枠の部分は、デフォルトのサーフェスシェーダだと`#pragma surface surf Standard fullforwardshadows`という記述になっています。これはライティングのメソッドをスタンダードライティングでいい感じにやってくれ～ということを示しています。当シェーダではライティングメソッドをいじる必要があるので「いや、そこはオリジナルでやらせてくれよ」という意思表示が必要になります。それが赤枠の部分で、「ライティングはここで指定したメソッドでやるから！」ということになります。コードでは`Custom`としていますが、別に好みのメソッド名で構いません。

　`target 3.0`では何をしているか**よくわかりません**（おい）。でも消すとエラーになるのできっと大事な部分なんでしょう（適当）。pragmaってなんだよ。

#プロパティで指定した変数の定義

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/589b25aa-2ec3-7c99-061c-8b44fa239183.png)

　ここでは、先程プロパティで指定したパラメータを変数として再定義してます。**そんなん自動でやってくれよ**と思わないでもないですが、やりましょう。テクスチャは`sampler2D`で、その他変数は`float`,`fixed`,`half`+`n`で定義します。floatとかhalfとかは数値の精度を示しています。詳しい解説は[公式リファレンス](https://docs.unity3d.com/ja/current/Manual/SL-ShaderPerformance.html)へ（ぶっちゃけ、いちいち悩むより、全部floatでやっても問題ないような気もしますが……）。nの部分は次元数を示しておりまして、float2だと2次元配列、float3だと3次元配列、float2x2（間の文字は**エックス**であることに注意）だと2かける2の行列になります。**ちなみに5次元以上の配列は使えません**。なんだその欠陥言語……。

#構造体に使う変数

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/8432b5a3-9d5c-0207-0ed6-aac5592e00d9.png)

　ここでは構造体に使う変数を指定します。**構造体って何だよ**ってことは聞かないでください。構造体に何を入れられるかは[公式リファレンス](https://docs.unity3d.com/ja/current/Manual/SL-SurfaceShaders.html)をご覧ください。当シェーダでは、

- テクスチャ画像のUV（uv_MainTex）;
- ノーマルマップのUV（uv_NormalMap）;
- 視線の方向（viewDir）

　の3つが必要なのでそれらを指定しています。

# ライティングシェーダ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/14ba23f3-d923-7f6f-adcf-b2be2cac866c.png)

　いよいよ本題に入っていきます。先程、ライティングは独自に書くと言いましたが、その独自メソッドが上になります。赤枠の`Custom`は、CGプログラムの段階で書いた`Custom`と対応しています。この`fixed4{……}`というひとかたまりがひとつの関数を示しており、黄枠で囲った変数を入力して、`return`で示された変数を出力という手順になっております。入力変数の`SurfaceOutput`はサーフェスシェーダの出力で、これはいわゆるメッシュの各点の色とかの情報を持っています。`lightDir`は光の方向、`atten`は光の減衰を示しており、ライティングに使える定義済関数です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/4dd81ea6-f5a4-3616-77d3-f59d4b9e7010.png)

　図で言うとココの部分をいま書いている訳です。サーフェスシェーダと順番が前後しちゃって申し訳ない。別に後で記述するサーフェスシェーダの`void{……}`の後に書いてもいいのですが、コードを書くにあたって参考にした諸先輩方のコードが皆最後にサーフェスシェーダを書いてるので、それに倣っています。何か理由があったりするのだろうか。

　青枠の`half d = dot(s.Normal, lightDir)*0.2+0.2`について説明します。これは、光の当たり方によって色の明るさを変える処理を示しています。`s.Normal`がメッシュの法線、`lightDir`が光の方向で、それらの内積`dot`は、光と法線が垂直になる時0、水平になる時1になります。メッシュの法線が光と垂直になるということは、メッシュと光は平行になるということです。回りくどい説明になりましたが、要は**光があたらないと0で、よくあたるようになるほど1に近づく**ということです。後でこれを発色の値にかけることによって「光の当たり具合」＝「発色の明るさ」となるように調節しています。例えばUnlitではこのような処理をしてないので、光源に関係なく発色が決まる訳です。

　ここで**当シェーダの工夫ポイントその1**が出てきますが、内積をそのまま使うと光の当たり具合をストレートに発色に示してしまい、いわゆる**スタンダードシェーダ特有のツヤツヤ感**が出てしまいます。そこで、当シェーダでは、光が当たらなくてもそこまで暗くならないし、光があたってもそこまで明るくならない、という風に調節することにより、**スタンダードとアンリットの中間**のようなソフトな色合いを出しています。それが`dot(略)*0.2 + 0.2`の部分です。グラフにするとこんな感じです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/e7bdd7fb-cbdf-51f5-8eb3-5d1ad9c15253.png)



　これから更に、プロパティで指定した`_Color`を調整用の色味として足しています。これも生の値を足してしまうと調整を通り越して全部その色になってしまうので、数値が小さくなるような補正を加えています。それらを合わせたのが橙枠の部分になります。日本語と並べるとこんな感じです。

`c.rgb = s.Albedo.rgb * _LightColor0.rgb * ((_Color-.5)/3+d);`
「最終的な出力　＝　メッシュの色　×　光の色　×　（調整用の色味　＋　光のあたり具合）」

　これを意識した上で読むと、コードでやってることがわかりやすいのではないでしょうか。Albedoとは入射光と反射光の比らしいですが、サーフェスシェーダでは色のことをなぜかこう呼びます。

# サーフェスシェーダ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/4f6f1983-6842-602c-75f7-339d0502987a.png)

　シェーダの処理的には前後しますが、今度はサーフェスシェーダの処理に写っていきます。量が多いですが頑張って解説していきます。手続き的には以下の図の部分になりますね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288125/0697e865-3dd5-0a1c-9857-0239fcb1d9e1.png)

　赤枠の部分がインプットとアウトプットの名前を示しています。サーフェス→ライティングの名前、さっきは`s`で定義してたじゃん、なんで変えるの？　と思った方へ。要は関数の中で整合性が取れていればいいので`s`でも`o`でも`hoge`でも何でもいいのですが、名前を変えといた方が同じ関数の中で変数が完結してデバッグしやすいとかですかね？　理由は私にもよくわかりません。

　次の行の`fixed4 c = tex2D (_MainTex, IN.uv_MainTex);`は決まり文句のようなものです。テクスチャ画像から、UVマップに対応したメッシュに着色して、それを`c`と定義しています。

　黄枠の`half rim = 1 - saturate(dot(IN.viewDir, o.Normal));`、これは結構大事な行で、リムライトの強さを指定しています。というか、リムライトがリムライトらしく光るよう、縁だけ光るように指定する、といった方が正しいですね。計算は先程でてきた`dot(o.Normal, lightDir)`と似ていますが、今度は、メッシュの法線と**視線**の方向の内積を取っています。ただ、それだけだと**視線が当たれば当たる程強く発色する**ということになってしまい、リムライトで演出したいものとは逆の効果になっています。そこで、`1 - saturate(dot(IN.viewDir, o.Normal))`という形で**1から引くことによってそれを逆転**させています。

　また、`saturate`という関数が余計についていますが、これは中身を0～1の間に留めるという関数です。これがないと、内積がマイナスになった時（つまり視線の反対側にあるようなメッシュ）が強く発光してしまうという現象が起こってしまうので、それを防ぐためです。

　青枠の部分は、プロパティで指定して`_Change`の値だけ色相を変える、という機能を実装しています。要は、周期的が3分の1ずつずれた三角関数を用意して、それに**赤緑青をかけたものを「新赤」、緑青赤→「新緑」、青赤緑→「新青」**と定義することにより擬似的な色相変換を実現しています。（ここで言う赤緑青は、テクスチャ画像から取得された色の各成分のことを言います）

　あと、ここにも`saturate`が出てきますが、これを噛ませておかないと「青の分の色だけ『新赤』の色がひかれる」みたいな現象が起こってしまうので、それを防ぐためです。というか、要は**ある周期の3分の1をカバーするような曲線**が描ければ何でも良いのですが、このシェーダでは三角関数を使っていて、三角関数そのままだとマイナスにはみ出てしまう部分があるためそれをカットするために`saturate`を噛ませる、というような発想ですね。

　その下の`half3 environment = ShadeSH9(half4(o.Normal, 1));`も決まり文句のようなものですが、これは環境光を取得しています。後でリムライトの発色を指定するのですが、その時にこれがないと**周りは真っ暗で、アバターも真っ黒なのに、リムライトだけは光ってしまう**という現象が起こるため、それを防ぐためです。ここはjoniburnさんにフィードバックいただいた上手く改良できた部分です。ありがとうございました！

　次の橙枠のはリムライトの発色を指定しています。先に`(2*Crgb+_RimColor)/3`の解説をしますが、ここは**当シェーダの工夫ポイントその2**になります。これはリムライトの色を「テクスチャの元々の色と、`_RimColor`で指定した色を混ぜたもの」（正確には2:1の割合で混合）にしています。他のシェーダではリムライトが単一の色になっているものが多いのですが、当シェーダでは**テクスチャの色をリムライトに利用する**ことにより、よりリムライトをソフトな形で演出しています。シンプルですが、なかなか良いアイディアではないでしょうか（自画自賛）。で、そうして決定された最終的なリムライトの色に、リムライトの強さ、環境光による減数をかけて最終的な出力にしている訳です。これも日本語と並べると、

`o.Emission = (2*Crgb+_RimColor)/3 * pow(rim, 3)*environment*2;`
「リムライトの光り具合　＝　（テクスチャ色と調整用の色を混ぜた色）　×　リムライトの強さ　×　環境光による減衰」

　という感じになります。なお、リムライトの強さに`pow`という関数が噛ませてありますが、これは3乗することにより強さのカーブを変化させているだけです。

　次の`o.Albedo = Crgb;`は、ライティングシェーダへのアウトプットの色に今まで計算してきた色を渡しますという意味です。

　次でやっと最後の行ですね。`o.Normal = UnpackNormal(tex2D (_NormalMap, IN.uv_NormalMap));`ですが、これもまた決まり文句です（決まり文句が多いな！）。ノーマルマップ用のテクスチャを指定している場合、この命令によってノーマルマップの凹凸が法線の形として後のライティングに反映されるようになります。詳しくは[公式リファレンス](https://docs.unity3d.com/ja/2018.1/Manual/SL-SurfaceShaderExamples.html)へ。

**※12/16追記**
　ワールドのライティングによっては、シームの切れ目でライティングが急に変わってしまうことがあるようです。その場合は、この行を削除すると直ります（ノーマルマップも適用できなくなりますが）。また、それとは別に、削除した方がよりソフトなライティングとなります。この現象は**ノーマルマップにテクスチャを指定していなくてもなる**ので、ノーマルマップを使わないなら**6,19,26,38行目**を最初から削除しておいた方が無難かつシンプルかもしれません。原因は不明です、すいません。GitHubのリポジトリに、以上の変更を施したHibitShader(Non Normal)というバージョンも併せて上げています。
**※追記ここまで**

# 最後に

　という訳で、49行という短いシェーダではありますが、一応すべての行について解説をつけました。一部不明のままのものや、「決まり文句」で済ませてしまったものもあって申し訳ないですが、皆様のシェーダライフの一助になれば幸いです。しかし疲れた……。

　当シェーダを作るにあたって、Unityの公式リファレンスの他、おもちゃラボ様の[7日間でマスターするUnityシェーダ](http://nn-hokuson.hatenablog.com/entry/2018/02/15/140037)には大変助けられました。というか、コードの基本的な書き方はすべてこのサイトから学ばせてもらったといっても過言ではないです。また、環境光の取得には[Unity の Shader で環境光を得る](https://qiita.com/Santarh/items/0f0980564b53de6e9797)の記事を参考にさせていただきました。この場を借りて感謝申し上げます。

　私もシェーダについて学び始めた初心者ですので、当記事について曖昧な部分、誤認している部分があれば遠慮なくコメント、編集リクエストを送っていただければと思います。

　明日のAdvent Calenderは@jscmla1118さんです。