ユニティちゃんトゥーンシェーダー2.0のアウトラインがAndroidで異常に太くなるのを解決する

# ユニティちゃんトゥーンシェーダー2.0 Androidでアウトラインが異常に太くなる問題を解決する

ユニティちゃんトゥーンシェーダー2.0を使っていると、**iOSでは問題ないのに、Androidでアウトラインが異常に太くなる** という問題に直面しました。
この記事では、この問題の原因と解決までの過程を記録します。



# 問題の概要

## 症状  

以下は、問題が発生している様子のGIFです。

![問題の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/b045f34a-7097-292c-1766-5f8c835c0c8b.gif)

カメラがSDユニティちゃんから遠くなるほど、スカートの部分が黒くなっているのが確認できます。肩の布地や胸のUnityロゴも同様に黒くなっています。

どうやらこの黒いのはアウトライン（輪郭線）のようでして、異常に太く描画されています。
iOSだとこの問題は発生しないのですが、Androidだと問題が発生します。

他のプラットフォームも調べたところ、WebGLでも症状が発生しました。

| プラットフォーム | 症状 |
|---|---|
| Windows | なし |
| macOS | なし |
| iOS | なし |
| Android | **あり** |
| WebGL | **あり** |



## 動作環境

私が試した環境は以下の通りです。

| ソフトウェア | バージョン |
|---|---|
| Unity | 2019.4.29f1<br>2022.3.24f1 |
| UnityChanToonShader | 2.0.9 |

SDユニティちゃんだけでなく、他のキャラクターでも同様の現象が発生しました。


# 調査過程

ここからは調べたことを書いていきます。結論だけ知りたい方は読み飛ばして大丈夫です。


## `_Outline_Width` を確認

まずは素直にアウトラインの太さを計算するコードを確認しました。

アウトラインの太さに関わるパラメーターとして `_Outline_Width` があります。
Inspectorでは以下のように `Outline Width` と表示されています。

![Outline Width](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/f8b36b69-d9ca-6d2e-3c55-b85b8edf4cd2.png)

試しにこのパラメーターを0にしてみました。
これでアウトラインの太さは0になるはずなので、症状が改善することを期待しましたが、実際には改善しませんでした。
そのため、 **_Outline_Widthは原因ではない** と結論付けました。


## `_Offset_Z` の影響を確認

次に、 `_Offset_Z` というパラメーターを調べました。
Inspectorでは以下のように `Offset Outline with Camera Z-axis` と表示されているものです。

![Offset Outline with Camera Z-axis](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/92cb4c27-f1d8-9d97-d930-61ad377f13d5.png)

これはアウトラインを深度方向（カメラの正面方向）へオフセットするときの量を調整するパラメーターのようです。

この値をいろいろ調整したところ、 **_Offset_Zを0に近づけると、症状が軽減する** ことがわかりました！
アウトラインのオフセット処理に問題がありそうです…！


## クリップ空間の深度範囲の違いが原因

更に調べたところ、どうやら以下の計算結果が、プラットフォームによって全く異なる値になることがわかりました。

```hlsl
float4 _ClipCameraPos = mul(UNITY_MATRIX_VP, float4(_WorldSpaceCameraPos.xyz, 1));
```

最終的に、問題の原因は **グラフィックスAPIの深度範囲の違い** にあることが判明しました。

Unity公式ドキュメントに以下の記述があります。

https://docs.unity3d.com/ja/2018.4/Manual/SL-PlatformDifferences.html

> - Direct3D 類：クリップ空間の深度の範囲は、ニアクリップ面の 0.0 からファークリップ面の +1.0 までになります。これは Direct3D、Metal、コンソールに適用されます。
> - OpenGL 類：クリップ空間の深度の範囲は、ニアクリップ面の –1.0 からファークリップ面の +1.0 までになります。これは OpenGL と OpenGL ES に適用されます。

つまり、

- Windows、mac、iOSは、深度範囲が `[0, 1]`
- Android、WebGLは、深度範囲が `[-1, 1]`

ということがわかりました。
これが原因で、深度計算の結果がプラットフォームによって異なっていたのです。



# 修正内容

深度計算を統一するため、以下の関数を追加しました

```hlsl
// Computes the depth value based on the platform's graphics API
float compute_depth(float4 clip_pos) {
#if defined(SHADER_API_D3D11) || defined(SHADER_API_METAL)
    return clip_pos.z;
#else
    return (clip_pos.w - clip_pos.z) * 0.5;
#endif
}
```

そして、オフセットの処理でこの関数を使用するように修正しました

```c
o.pos.z = o.pos.z + _Offset_Z * compute_depth(_ClipCameraPos);
```

これにより、症状が改善しました！

以下は修正後の正常な表示です。WebGL、Android、iOSすべてで正しく表示されることを確認しました！

| 修正前 | 修正後 |
|---|---|
| ![問題の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/b045f34a-7097-292c-1766-5f8c835c0c8b.gif) | ![修正後の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/5206fd0a-7103-4c2c-3c55-625da6d25586.gif) |



# さいごに

今回のように、プラットフォームの違いによる予期せぬ問題は、エンジニアをやっていると度々遭遇します。
同様の現象に直面した場合、この解決法が参考になれば幸いです。

本記事作成にあたり、以下を参考にさせていただきました。ありがとうございました！

- https://docs.unity3d.com/ja/2018.4/Manual/SL-PlatformDifferences.html