[Unity]聖剣3TOMのキャラの「アナログ感」シェーダーを模写してみた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/d70b2ac3-ab26-d377-69dd-a5fa6b413861.png)
先月、聖剣伝説3 TRIALS of MANA のスマホ版がリリースされました。
スーファミ版リアルタイム世代なのもあって楽しく遊んでます！

フレームレートの安定ためにコンシューマ版に比べて一部ポストエフェクトを切っていたり色域が違うように見えたりと、細かに調整されていて、スマホでもスムーズに遊べていて感動です・・・。（[コンシューマ版開発時点でスマホ対応も構想に入っていた](https://youtube.com/clip/Ugysp8mdrpdsWPGANVd4AaABCQ)らしい）

聖剣３TOMを遊んでいて印象的だったのが、光沢を感じるような独特の陰影の付け方でした。
![デュランパーティ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/b28fc548-25a6-f920-227a-732189513899.gif)　![ホークアイパーティ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/640bacf5-26e8-951f-7c60-3256632822ce.gif)
「何かグラフィックに関するインタビューとかないかなぁ」と探したところ、　**UNREAL FEST EXTREME 2020 WINTER** でドンピシャな講演をされていたようで、そのアーカイブが見つかりました！（36:07〜）
ありがたや…。
　
<iframe width="560" height="315" src="https://www.youtube.com/embed/NHZRn4OHa1g?start=2173" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
　
講演の中でシェーダーに関する細かいお話があって良い足がかりになったので、聖剣３TOMのキャラクターのシェーダーを模写してみました。

# できたビジュアル

![tom_shader.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/606735e5-62bc-72d1-35b9-8a30d86190c9.gif)
こんな感じになりました。
左がシンプルなテクスチャのみでの表示で、右が聖剣３TOM風シェーダーを反映した表示です。

前述の講演によると、今時の3Dゲームっぽいリッチさとしてモデルの陰影や物理ベースっぽい質感表現など、3Dだからできる塗り方の特徴は活かしつつ、手書きイラスト風の **ハンドメイドっぽさ** や **暖かみ** のある絵を目指していたそうです。
アートのコンセプトとしてそれらを総じて**「アナログ感」**と表現していて、言い換えれば「できるだけ3Dポリゴンっぽく見えないようにした」とのことでした。素敵。

# 聖剣3TOMのシェーダーの機能

講演ではシェーダーの機能として大きく３つの話をされていました。

* シェーディング
* アウトライン
* エミッシブ（ライトの影響緩和）

件の「光沢を感じるような独特の陰影」は１つめの **シェーディング** で表現されているようだったので、今回は主にその部分の再現をやります。

### ■ 描画の工程
1. 陰1
1. 陰2
1. 陰のマスク
1. リムライト
1. シャドウ（ハーフランバート）
1. スペキュラ
1. アウトライン

の7つの工程で描画されています。

|表示|概要|
|---|---|
|<img width="350" alt="1_テクスチャ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/cd41b248-85f1-0815-6ccc-433c95fabe0f.png">|**■工程0 - テクスチャのみ**<br>シェーダーで特別なことをせず、テクスチャの色をそのまま出している状態|
<img width="350" alt="2_陰1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/6cbb144d-0888-f33c-e90d-c3e1bc141909.png">|**■工程1 - 陰1**<br>視線方向に依存して体のフチに色を乗算して陰影を付ける工程です。|
|<img width="350" alt="3_陰2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/2f0d6f66-8d7b-e6f2-9136-23bbe1a0f340.png">|**■工程2 - 陰2**<br>陰1の上から更に色を乗せます。<br>2段階に分けて陰影を付けるために挟んでいる工程です。|
|<img width="350" alt="4_陰のマスク.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/1448a51b-4830-f5ba-7b9d-f260cd40c423.png">|**■工程3 - 陰のマスク**<br>陰1、陰2を外側からマスクして元の色にする工程です。<br>後述する**「稜線」の表現**のために行っています。|
|<img width="350" alt="5_リムライト.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5216bd1-3bd2-a17c-f9f6-bf44f69e9bd3.png">|**■工程4 - リムライト**<br>視線方向とライトの方向を使うシンプルなハイライト付けの工程です。<br>ライト方向に近い体のフチに白っぽい色（ライトに色）が乗ります。|
|<img width="350" alt="6_シャドウ（ハーフランバート）.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/afb7c36b-9be0-e7f6-71b6-9b52e1bc5da4.png">|**■工程5 - シャドウ**<br>ライトの方向を使って塗るシンプルなシャドウです。<br>暗くなりすぎないようによく使われている**Half-Lambert**という手法で濃さを調整しています。|
|<img width="350" alt="7_スペキュラ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5752eda-c77d-6c2d-e35b-588f7d47f928.png">|**■工程6 - スペキュラ（光の反射）**<br>光沢を調整してモデルの材質に合わせた質感表現するための工程です。<br>**Phong鏡面反射**という手法を使います。|
|<img width="350" alt="8_アウトライン.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/4057057c-5da9-3bfd-8bb9-39cf8623edd0.png">|**■工程7 - アウトライン**<br>モデルの輪郭を塗って印象をクッキリさせています。<br>単色ではなく、接する頂点の色を使ったアウトラインを引いています。|

工程ごとにシェーダーを書いていきます。

解説では**パラメータや入力/出力構造体の定義は省略**します。
**記事の最後にシェーダー全体をまとめて載せる**ので、定義周りの確認や、とりあえず試したいという方はそちらをご利用ください！

# 工程0 - テクスチャのみ
<img width="350" alt="1_テクスチャ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/cd41b248-85f1-0815-6ccc-433c95fabe0f.png">
まずはこの表示を作る下記のシンプルなUnlitシェーダーを用意します。

```TOM.shader
Shader "Custom/TOM"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

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

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                // apply fog
                col.rgb = MixFog(col.rgb, i.fogFactor);
                return col;
            }
            ENDHLSL
        }
    }
}
```

これでテクスチャをシンプルに表示するシェーダーになります。

### ノーマルマップの対応
今後陰影に関する処理をいろいろ書きますが、ノーマルマップを使って細かな凹凸があるとよりその効果が強く出るので、ノーマルマップを使えるように処理を追加しておきます。

```TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.normal = TransformObjectToWorldNormal(v.normal);
    o.uvNormal = TRANSFORM_TEX(v.uv, _BumpMap);
    o.tangent = v.tangent;
    o.tangent.xyz = TransformObjectToWorldDir(v.tangent.xyz);
    o.binormal = normalize(cross(v.normal, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // ノーマルマップから法線情報を取得する
    float3 localNormal = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uvNormal), _BumpScale);
    // タンジェントスペースの法線をワールドスペースに変換する
    i.normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;

    //~~省略~~
    return col;
}
```

* 入力頂点構造体をフラグメントシェーダで使えるように出力頂点構造体に適宜変化をして代入
    * `binormal`の計算が独特。セマンティクスで直接取得できないのでこんな感じに
* `UnpackNormalScale`を使ってノーマルマップの法線情報を取得 → 変換して反映

これでノーマルマップが使えるようになります。

# 工程1 - 陰1
|Before|After|
|:-:|:-:|
|<img width="350" alt="1_テクスチャ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/cd41b248-85f1-0815-6ccc-433c95fabe0f.png">|<img width="350" alt="2_陰1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/6cbb144d-0888-f33c-e90d-c3e1bc141909.png">|

視線方向に依存して体のフチに色を乗算して陰影を付ける工程です。
リムライトの陰版だと言ったほうが分かりやすそうですね。

```TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.viewDir = normalize(-GetViewForwardDir());
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // 陰1の計算をする
    float limPower = 1 - max(0, dot(i.normal, i.viewDir));
    float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);
    limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);
    col.rgb = lerp(col.rgb, col.rgb * _LimShadeColor1, limShadePower * _LimShadeColorWeight1);

    //~~省略~~
    return col;
}
```

視線方向`viewDir`と法線の内積`normal`を取り**１からその値を引く**ことで、視点に対して横を向いているところほど値が大きい陰係数`limPower`を作っています。
それを下記コードのように現在色`col`と、`col`に陰色`_LimShadeColor1`をかけた色のlerpのレート値に使えばリム陰自体は完成します。

```
col.rgb = lerp(col.rgb, col.rgb * _LimShadeColor1, limPower);
```

しかし、このシンプルな計算だと**陰によってできる陰影の層**が作れず、視線に対して完全に横を向いてる辺りしか陰色がハッキリ乗りません。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/4c74abf8-8c59-285b-6b0d-6dd2de165e0c.png">
聖剣3TOMの陰は、一種のトゥーンシェーディングっぽい「層のある陰影」を作るための目的があると解釈しているので、

* 陰色`_LimShadeColor1`の影響範囲　→ `_LimShadeMinPower1`
* 完全に陰色`_LimShadeColor1`になる範囲の広さ　→ `_LimShadeColorWeight1`

の２つのパラメータを追加して、細かく陰の層が作れるようにしました。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/73487e35-c92b-8320-ab0b-c865ba2b65e3.png">

```
float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);
```
まず`limPower`を、`_LimShadeMinPower1`の値が開始点0になるように`inverseLerp`にかけて調整しています。
これによって
* `_LimShadeMinPower1`が大きいほど陰の影響が始まるのがフチに近くなる（内側がマスクされるイメージ）
* `_LimShadeMinPower1`が小さいほど陰の影響が始まるのが視線方向に近くなる
という挙動のパラメータとして扱えるようになります。

※`inverseLerp`は、HLSLの標準関数に用意されてないので、自前実装しています。記事末尾の`Custom.cginc`を参照してください

```
limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);
```
その上で`limShadePower`に`_LimShadePowerWeight1`をかけることで陰色が最大まで反映される内積位置が視線方向に近くなります。
つまり、
* `_LimShadePowerWeight1`が大きいほど陰色が完全に反映されている範囲が視線方向の頂点に向かって広くなる
* `_LimShadePowerWeight1`が小さいほど陰色が完全に反映されている範囲がフチに近くなる
という調整に使えるパラメータになります。


# 工程2 - 陰2

|Before|After|
|:-:|:-:|
<img width="350" alt="2_陰1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/6fcef5f9-0f23-adb6-3219-daa0e3f80bf0.png">|<img width="350" alt="3_陰2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/2f0d6f66-8d7b-e6f2-9136-23bbe1a0f340.png">|

陰1の上から更に色を乗せます。
2段階に分けて陰影を付けるために挟んでいる工程です。


```TOM.shader
float4 frag (v2f i) : SV_Target
{
    // sample the texture
    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    // テクスチャから取得したオリジナルの色を保持
    float4 albedo = col;
    
    //~~省略~~

    // 陰2の計算をする
    limShadePower = inverseLerp(_LimShadeMinPower2, 1, limPower);
    limShadePower = min(limShadePower * _LimShadePowerWeight2, 1);
    col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor2, limShadePower * _LimShadeColorWeight2);

    //~~省略~~
    return col;
}
```

陰１と同じ働きをするシェーディングをもうひと枠分設定するだけで、陰1の処理とほとんど変わりません。

特殊な点としては、最後の`lerp`の計算で第二引数に`albedo`のカラーを使っています。
これは、陰1の計算後の色を使うと陰色の調整が難しくなるため、テクスチャのオリジナルの色を使うようにする目的で実装しています。

# 工程3 - 陰のマスク

|Before|After|
|:-:|:-:|
|<img width="350" alt="3_陰2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/2f0d6f66-8d7b-e6f2-9136-23bbe1a0f340.png">|<img width="350" alt="4_陰のマスク.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/1448a51b-4830-f5ba-7b9d-f260cd40c423.png">|

陰1、陰2を外側からマスクして元の色にする工程です。
これは講演によると「稜線」の表現に一役買っているとのことでした。

※稜線とは、デッサン用語における `形の変化する場所を表す線や濃淡の変化` みたいな意味みたいです
https://dessinlaboratory.com/knack/ridge-con.html

```TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // 陰のマスク
    float limShadeMaskPower = inverseLerp(_LimShadeMaskMinPower, 1, limPower);
    limShadeMaskPower = min(limShadeMaskPower * _LimShadeMaskPowerWeight, 1);
    col.rgb = lerp(col.rgb, albedo.rgb, limShadeMaskPower);

    //~~省略~~
    return col;
}
```

陰1,2と同じような要領で塗りの領域を計算し、対象の範囲をオリジナルのテクスチャの色に戻す計算（＝陰のマスク）をしています。

# 工程4 - リムライト

|Before|After|
|:-:|:-:|
|<img width="350" alt="4_陰のマスク.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/1448a51b-4830-f5ba-7b9d-f260cd40c423.png">|<img width="350" alt="5_リムライト.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5216bd1-3bd2-a17c-f9f6-bf44f69e9bd3.png">|

視線方向とライトの方向を使うシンプルなハイライト付けの工程です。
ライト方向に近い体のフチに白っぽい色（ライトに色）が乗ります。

```TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // リムライト
    Light light = GetMainLight();
    float limLightPower= 1 - max(0, dot(i.normal, -light.direction));
    float3 limLight = pow(saturate(limPower * limLightPower), _LimLightPower) * light.color;
    col.rgb += limLight * _LimLightWeight;

    //~~省略~~
    return col;
}
```

ライト方向と視線方向をベクトル合成してリムの計算に使います。
`_LimLightPower`をパラメータとして乗数にすることで、リムのグラデが影響する範囲を調整できるようにしています。

### リムライトは必要？

講演内でリムライトは「陰のマスク」の工程と同一のものとして説明されていたため、もしかしたらこの工程は不要かもしれないです。
ただ、聖剣３TOMをプレイしてる中で「陰のマスク」だけでは説明できない**白く塗られたリムライトっぽい部分**が目についたので、今回は独自にリムライトも別途必要と解釈して追加しました。

▼型や左胸のあたりにリムライトが効いてそうに見える
<img width="350" alt="limlight.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/36e39b63-c937-e2fa-de03-7ca075bde64b.png">

# 工程5 - シャドウ（ハーフランバート）

|Before|After|
|:-:|:-:|
|<img width="350" alt="5_リムライト.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5216bd1-3bd2-a17c-f9f6-bf44f69e9bd3.png">|<img width="350" alt="6_シャドウ（ハーフランバート）.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/afb7c36b-9be0-e7f6-71b6-9b52e1bc5da4.png">|

ライトの方向を使って塗るシンプルなシャドウです。
暗くなりすぎないようによく使われている**Half-Lambert**という手法で濃さを調整しています。

```TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // Half-Lambert拡散反射光
    float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);
    col.rgb *= diffuseLight + _AmbientColor;

    //~~省略~~
    return col;
}
```
```Custom.cginc
// HalfLambert拡散反射光を計算する
float3 CalcHalfLambertDiffuse(float3 lightDirection, float3 lightColor, float3 normal)
{
    // ピクセルの法線とライトの方向の内積を計算する
    float t = dot(normal, lightDirection);
    // 内積の値を0以上の値にする
    t = max(0.0f, t);
    t = pow(t * 0.5 + 0.5, 2);
    // 拡散反射光を計算する
    return lightColor * t;
}
```

ライトへ向かうベクトルに対して外向きのベクトルほど暗くなる`Lambert拡散反射光`に対して、ざっくり言うと陰影の暗い部分の下限を底上げするような手法が`Half-Lambert拡散反射光`です。

|Lambert|Half-Lambert|
|:-:|:-:|
|<img width="350" alt="6_シャドウ（ランバート）.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a3694253-54d6-6782-a00d-0229c8000038.png">|<img width="350" alt="6_シャドウ（ハーフランバート）.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/afb7c36b-9be0-e7f6-71b6-9b52e1bc5da4.png">|

```
t = pow(t * 0.5 + 0.5, 2);
```

`CalcHalfLambertDiffuse`の上記のコードが`Half-Lambert`特有の値の調整計算をしているところです。
（詳しくは下記ページなどを参照）

https://developer.valvesoftware.com/wiki/Half_Lambert

# 工程6 - スペキュラ

|Before|After|
|:-:|:-:|
|<img width="350" alt="6_シャドウ（ハーフランバート）.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/afb7c36b-9be0-e7f6-71b6-9b52e1bc5da4.png">|<img width="350" alt="7_スペキュラ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5752eda-c77d-6c2d-e35b-588f7d47f928.png">|

光沢を調整してモデルの材質に合わせた質感表現するための工程です。
Phong鏡面反射という手法を使います。

```TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.toEye = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(v.vertex.xyz)));
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // Half-Lambert拡散反射光
    float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);

    // 減衰なしのPhong鏡面反射光
    float shinePower = lerp(0.5, 10, _Smoothness);
    float3 specularLight = CalcPhongSpecular(-light.direction, light.color, i.toEye, i.normal, shinePower);
    specularLight = lerp(0, specularLight, _SpecularRate);

    col.rgb *= diffuseLight + specularLight + _AmbientColor;

    //~~省略~~
    return col;
}
```
```Custom.cginc
// Phong鏡面反射光を計算する
float3 CalcPhongSpecular(float3 lightDirection, float3 lightColor, float3 toEye, float3 normal, float shinePower)
{
    // 反射ベクトルを求める
    float3 refVec = reflect(lightDirection, normal);
    // 光が当たったサーフェイスから視点に伸びるベクトルを求める
    toEye = normalize(toEye);
    // 鏡面反射の強さを求める
    float t = dot(refVec, toEye);
    // 鏡面反射の強さを0以上の数値にする
    t = max(0.0f, t);
    // 鏡面反射の強さを絞る
    t = pow(t, shinePower);
    // 鏡面反射光を求める
    return lightColor * t;
}
```

`Phong鏡面反射光`は、ライト方向、法線、対象の頂点から視線位置へのベクトルを使ってハイライトを作る手法です。
※「頂点から視線位置へのベクトル`toEye`」は「視線ベクトル`viewDir`」と異なるものなので注意
滑らかさ`_Smoothness`のパラメータでハイライトの大きさや強さを調整して、**部位ごとの質感を作ることができます**。

# 工程7 - アウトライン

|Before|After|
|:-:|:-:|
|<img width="350" alt="7_スペキュラ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f5752eda-c77d-6c2d-e35b-588f7d47f928.png">|<img width="350" alt="8_アウトライン.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/4057057c-5da9-3bfd-8bb9-39cf8623edd0.png">|

モデルの輪郭を塗って印象をクッキリさせています。
単色ではなく、接する頂点の色を使ったアウトラインを引いています。

**聖剣3TOMではポストエフェクトでアウトラインを作る方法を使っている**と講演で語られていましたが、今回はシェーダーだけで完結させるために**パスを増やして法線方向に拡大する方法**でアウトラインを作ります。

```TOM.shader
v2f vert (appdata v)
{
    v2f o = (v2f)0;
    // アウトラインの分だけ法線方向に拡大する
    o.vertex = TransformObjectToHClip(v.vertex + v.normal * (_OutlineWidth / 100));
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}
            
float4 frag (v2f i) : SV_Target
{
    // sample the texture
    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    // 表面の色にアウトライン色をブレンドして使う
    return col * _OutlineColor;
}
```

ポストエフェクトでのアウトラインの作り方については下記などで紹介されています。

https://light11.hatenadiary.com/entry/2018/05/13/183314

# まとめと課題

![tom_shader_sequence.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/65230367-7647-fe08-d30a-c409e1818e8b.gif)

アートのコンセプトである「アナログ感」の要素として、陰1,2やマスク、リムライトなどでイラストっぽさを出しつつも、シャドウやスペキュラによるPBRっぽさをブレンドして独特のビジュアルを作っていたことが分かって面白かったです。

ただ、講演で話されていた表現で再現できなかった課題がいくつかありました。

### 1. テクスチャがAlbedoになっていない

今回のシェーダーはちょっと物理ベースっぽい思想が入っているので、モデルのメインテクスチャは**陰影が入っていない物質そのものの色**で構成されている、いわゆる Albedo Map であることが望ましいです。
陰影はノーマルマップやモデルそのものの凹凸を使ってシェーディングで表示されるべきです。

今回使ったSDユニティちゃんのモデルは、シェーディングされなくてもそれなりに綺麗に見えるように陰影が入ったテクスチャが用意されていました。
Gimpを使って多少陰影を落としたものを使ったんですが、それでも完全には Albedo Map にはできませんでした。
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f0724ed5-40e3-8ffa-b620-3075b6e351d2.png">
▲シェーディングを一切してなくてもテクスチャに書き込まれた陰影が表示されてしまっている

ライトの角度やキャラの向きで動的に陰影が作られることがこの手のシェーディングの良いところ（リッチに見えるところ）なので、ここは結構大事そうです。
（絵の担保のために一定の陰影を残すことはあると思いますが）

### 2. ノーマルマップにタッチ感を出すフィルター

講演の中で、ノーマルマップに特別なフィルターをかけることで陰影の変化具合にデザインの「タッチ感」を出す工夫が施されてると語られていました。
Gimpでノーマルマップに「油絵化」のフィルタをかけてみたりしましたが、良い感じにならず…。
ここはデザイナーさんに意見を聞いたりして再現したいところです。

### 3. アウトライン

「アナログ感」を出すために、線の太さを一定ではなくムラがあるように調整してるとのことでした。
これも再現できていないので取り入れたいです。

### 4. ライトの影響具合

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/6beef8a8-055f-1aef-efcd-e9ef93ad23ba.png"> <img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/cf03ddaf-c72d-aa9b-4cd2-b886b7fe1e07.png">
ライトの色や明るさは反映されるようになっていますが、ちょっと影響度が強すぎるようです。
聖剣3TOMのキャラクターはライトの影響度を抑えて、色味の変化の範囲がある程度担保されるようになっているようなので、この辺りも重要そうです。

いずれこれらの課題にも対応したいなと思います。
今回はここまで！

# シェーダー全文

## TOM.shader
```TOM.shader
Shader "Custom/TOM"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}

        _BumpMap ("Normal Map", 2D) = "bump" {}
        _BumpScale ("Normal Scale", Range(0, 2)) = 1

        _LimShadeColor1 ("リム陰の色 ベース", Color) = (0,0,0,1)
        _LimShadeColorWeight1 ("リム陰色の影響度 ベース", Range(0, 1)) = 0.5
        _LimShadeMinPower1 ("リム陰のグラデ範囲 ベース", Range(0, 1)) = 0.3
        _LimShadePowerWeight1 ("最濃リム陰の太さ ベース", Range(1, 10)) = 10

        _LimShadeColor2 ("リム陰の色 外側", Color) = (0,0,0,1)
        _LimShadeColorWeight2 ("リム陰色の影響度 外側", Range(0, 1)) = 0.8
        _LimShadeMinPower2 ("リム陰のグラデ範囲 外側", Range(0, 1)) = 0.3
        _LimShadePowerWeight2 ("最濃リム陰の太さ 外側", Range(1, 10)) = 2

        _LimShadeMaskMinPower ("リム陰マスクのグラデ範囲", Range(0, 1)) = 0.3
        _LimShadeMaskPowerWeight ("最濃リム陰マスクの太さ", Range(1, 10)) = 2

        _LimLightWeight ("リムライトの影響度", Range(0, 1)) = 0.5
        _LimLightPower ("リムライトのグラデ範囲", Range(1, 5)) = 3

        _AmbientColor ("Ambient Color", Color) = (0.5,0.5,0.5,1)

        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
        _SpecularRate ("スペキュラーの影響度", Range(0, 1)) = 0.3

        _OutlineWidth ("Outline Width", Range(0, 1)) = 0.1
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
    }
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

        Pass
        {
            // 前面をカリング
            Cull Front

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;

            half _OutlineWidth;
            half4 _OutlineColor;
            CBUFFER_END
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                // アウトラインの分だけ法線方向に拡大する
                o.vertex = TransformObjectToHClip(v.vertex + v.normal * (_OutlineWidth / 100));
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }
            
            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);

                return col * _OutlineColor;
            }
            ENDHLSL
        }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Custom.cginc"

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

                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float fogFactor: TEXCOORD1;
                float4 vertex : SV_POSITION;

                float3 normal : NORMAL;
                float2 uvNormal : TEXCOORD2;
                float4 tangent  : TANGENT;
                float3 binormal : TEXCOORD3;

                float3 viewDir : TEXCOORD4;

                float3 toEye : TEXCOORD5;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            TEXTURE2D(_BumpMap);
            SAMPLER(sampler_BumpMap);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;

            float4 _BumpMap_ST;
            float _BumpScale;

            float3 _LimShadeColor1;
            float _LimShadeColorWeight1;
            float _LimShadeMinPower1;
            float _LimShadePowerWeight1;

            float3 _LimShadeColor2;
            float _LimShadeColorWeight2;
            float _LimShadeMinPower2;
            float _LimShadePowerWeight2;

            float _LimShadeMaskMinPower;
            float _LimShadeMaskPowerWeight;

            float _LimLightPower;
            float _LimLightWeight;

            float3 _AmbientColor;

            float _Smoothness;
            float _SpecularRate;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);

                o.normal = TransformObjectToWorldNormal(v.normal);
                o.uvNormal = TRANSFORM_TEX(v.uv, _BumpMap);
                o.tangent = v.tangent;
                o.tangent.xyz = TransformObjectToWorldDir(v.tangent.xyz);
                o.binormal = normalize(cross(v.normal, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);

                o.viewDir = normalize(-GetViewForwardDir());

                o.toEye = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(v.vertex.xyz)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);

                // ノーマルマップから法線情報を取得する
                float3 localNormal = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uvNormal), _BumpScale);
                // タンジェントスペースの法線をワールドスペースに変換する
                i.normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;

                float4 albedo = col;

                // 陰1の計算をする
                float limPower = 1 - max(0, dot(i.normal, i.viewDir));
                float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);
                limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);
                col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor1, limShadePower * _LimShadeColorWeight1);

                // 陰2の計算をする
                limShadePower = inverseLerp(_LimShadeMinPower2, 1, limPower);
                limShadePower = min(limShadePower * _LimShadePowerWeight2, 1);
                col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor2, limShadePower * _LimShadeColorWeight2);

                // 陰のマスク
                float limShadeMaskPower = inverseLerp(_LimShadeMaskMinPower, 1, limPower);
                limShadeMaskPower = min(limShadeMaskPower * _LimShadeMaskPowerWeight, 1);
                col.rgb = lerp(col.rgb, albedo.rgb, limShadeMaskPower);

                // リムライト
                Light light = GetMainLight();
                float limLightPower= 1 - max(0, dot(i.normal, -light.direction));
                float3 limLight = pow(saturate(limPower * limLightPower), _LimLightPower) * light.color;
                col.rgb += limLight * _LimLightWeight;

                // Half-Lambert拡散反射光
                float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);
                float shinePower = lerp(0.5, 10, _Smoothness);
                float3 specularLight = CalcPhongSpecular(-light.direction, light.color, i.toEye, i.normal, shinePower);
                specularLight = lerp(0, specularLight, _SpecularRate);
                col.rgb *= diffuseLight + _AmbientColor + specularLight;

                // apply fog
                col.rgb = MixFog(col.rgb, i.fogFactor);
                return col;
            }
            ENDHLSL
        }
    }
}
```

## Custom.cginc
```Custom.cginc
float inverseLerp(float a, float b, float value) {
    return saturate((value-a)/(b-a));
}

// HalfLambert拡散反射光を計算する
float3 CalcHalfLambertDiffuse(float3 lightDirection, float3 lightColor, float3 normal)
{
    // ピクセルの法線とライトの方向の内積を計算する
    float t = dot(normal, lightDirection);

    // 内積の値を0以上の値にする
    t = max(0.0f, t);

    t = pow(t * 0.5 + 0.5, 2);

    // 拡散反射光を計算する
    return lightColor * t;
}

// Phong鏡面反射光を計算する
float3 CalcPhongSpecular(float3 lightDirection, float3 lightColor, float3 toEye, float3 normal, float shinePower)
{
    // 反射ベクトルを求める
    float3 refVec = reflect(lightDirection, normal);

    // 光が当たったサーフェイスから視点に伸びるベクトルを求める
    toEye = normalize(toEye);

    // 鏡面反射の強さを求める
    float t = dot(refVec, toEye);

    // 鏡面反射の強さを0以上の数値にする
    t = max(0.0f, t);

    // 鏡面反射の強さを絞る
    t = pow(t, shinePower);

    // 鏡面反射光を求める
    return lightColor * t;
}
```