[Unity]平行投影(Orthographic)でリムライト

法線ベクトルと視線ベクトルを使った一般的な計算でリムライトを実装したとき、平行投影モード（Orthographic）のカメラで上手く機能しない問題にぶつかりました
![rim_light.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a6621462-ed42-6532-7c9d-7cab52d970da.jpeg)
▲同じシーンの透視投影モード (左) と平行投影モード (右)
平行投影だと、画面中央から離れた位置のものほどリムライトの付く方向が外向きになっています。

![fix.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a1e94507-b693-aa10-3da7-725624971ae2.jpeg)
▲リムライトの計算をビュー空間を使ったものに変えることで解決しました

**参考にした記事**
* [Fresnel effect does not work with orthographic cameras | Unity Forum](https://forum.unity.com/threads/fresnel-effect-does-not-work-with-orthographic-cameras.807240/)
* [Shader - Convert Normals to View-Space Normals | Unity Forum](https://forum.unity.com/threads/shader-convert-normals-to-view-space-normals.285262/)

# 原因

一般的に、リムライトは法線ベクトルと視線ベクトルの内積の結果を使って表現されます。
そして、透視投影モードにおいて視線ベクトルは`◯◯SpaceViewDir`などを使って **カメラから特定の頂点へ向かうベクトル** を取得して使います。

しかし、平行投影モードにおける視線方向は **カメラから特定の頂点へ向かうベクトル** では無い、ということが原因です。
平行投影モードでは全ての頂点は**ビュー空間におけるZの正方向**から投影されるため、
* ビュー空間の法線ベクトル
* Zの正方向ベクトル = (0, 0, 0)

この２つのベクトルを使って内積を計算することで、正しいリムライトの表現ができます。

# 解決方法

下記、透視投影モードでは正しく動作するシンプルなリムライトのシェーダーを元に解説します。
<details><summary><b>----------------------------------------<br> RimLight.shader（折りたたみ）<br> ------------------------------------------</b></summary><div>

```hlsl:RimLight.shader
Shader "Custom/RimLight" {
    Properties {
        _Color ("Color", Color) = (0, 0, 0)
        _RimColor ("RimColor", Color) = (1, 1, 1)
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
            "LightMode"="ForwardBase"
        }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f {
                float4 vertex : SV_POSITION;
                float3 normal : TEXCOORD1;
                half3 viewDir : TEXCOORD2;
            };

            float4 _Color;
            float4 _RimColor;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.normal = normalize(UnityObjectToWorldNormal(v.normal));
                o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // リムライトの計算。法線と視線のベクトルの内積を取り
                // 0~1の範囲で外側ほどリムライトの色が付くようにしている
                half rimRate = 1 - dot(i.normal, i.viewDir);
                return lerp(_Color, _RimColor, rimRate);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}
```
</div></details>


## ビュー空間の法線ベクトルを計算

```diff_hlsl:RimLight.shader
v2f vert (appdata v) {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

-   o.normal = UnityObjectToWorldNormal(v.normal);
+   o.normal = normalize(mul((float3x3)UNITY_MATRIX_MV, v.normal));
    o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
    return o;
}
```

`NORMAL`セマンティクスで取得される法線ベクトルはオブジェクト空間のものなので、モデル * ビュー行列を使ってビュー空間に変換します。


## ビュー空間の視線ベクトルを計算
```diff_hlsl:RimLight.shader
v2f vert (appdata v) {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    o.normal = normalize(mul((float3x3)UNITY_MATRIX_MV, v.normal));
-   o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
+   o.viewDir = half3(0, 0, 1);
    return o;
}
```

平行投影モードにおける視線方向はカメラや頂点の位置に依存せず、ビュー空間におけるZの正方向なので直接値を代入します。

これで完了です。
![fix.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a1e94507-b693-aa10-3da7-725624971ae2.jpeg)
リムライトはオブジェクトを強調するような使い方が多いと思うので、平行投影＋引きの絵になりやすいクォータービューの戦略シミュレーションゲームなどでは、きっちり正しく描画したいところですね。

# 補足

今回は説明しやすいよう２つのベクトルをそのままビュー空間のものに置き換えましたが、法線も視線も他のいろんな表現に使うものなので、ビュー空間用とそうでないものを別で変数に格納して、表現ごとに使い分けたほうがいいかもしれないです。
また、ビュー空間の視線方向は一律同じ値なので`#define`で定数を定義すべきですね。

普段3Dのビューを作るときは透視投影にすることがほとんどですが、いざ平行投影に切り替えるとこういう問題にぶつかることが多そうなので、日頃から平行投影でもビューをチェックしておこうと思うのでした。