[Unity]カメラに映る画面をおしゃれにシルエット化する

Newポケモンスナップ、いろんなフィルタがあって現代の写真遊びに則ったカメラゲームらしさがあって凄く素敵ですね。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/ff232dea-540b-5baf-d501-cdefe9ee2b75.jpeg">　<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/5571538c-f007-0bb8-be99-87207ae3fbd5.jpeg">

いろんなフィルタがある中、シルエットフィルタはゲームらしいフィルタで意外と他のゲームでは見ないような気がするものだったのでいいなぁと思いました。
深度テクスチャを使えば簡単に実現できそうで、入れるだけで結構印象的な写真になるのでちょっと真似してみました。

# こんな感じ

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/44cdecd2-dd49-d3c3-ea71-9e08bdfc619e.png">　<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/7c6afda8-aef9-c1d2-91d9-ef4785554d52.png">

そのまんまですね。
空いてる空間にロゴでも入れればデスクトップ壁紙とかに出来そう。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/ef5195d8-e6cb-6435-b442-08fe7fa5f471.png">

こんな感じ？
この記事とは直接関係ないですが、ロゴの自動生成ツールで面白いサイトがあったのでここで作ってみました↓

https://www.777logos.com/


# 実装

ポストエフェクトでカメラの深度テクスチャを取得し、任意の色を乗算して表示しています。

```ShowDepthTexture.shader
Shader "ShowDepthTexture"
{
    SubShader
    {
        Cull Off
        ZTest Always
        ZWrite Off

        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            
            sampler2D _CameraDepthTexture;
            float _Start;
            float _End;
            float4 _Color;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                // 深度テクスチャ
                half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
                depth = LinearEyeDepth(depth);
                // Start〜Endの距離で濃淡の幅を使う
                depth -= _Start;
                half max = _End - _Start;
                depth = lerp(0, 1, depth / max);
                // 色を乗算
                return depth * _Color;
            }
            ENDCG
        }
    }
}
```

シェーダーをカメラに反映して、濃淡の範囲と色をInspectorから指定する`DepthTexture.cs`を作ります。

```DepthTexture.cs
using UnityEngine;

public class DepthTexture : MonoBehaviour
{
    private static readonly int PROPERTY_COLOR = Shader.PropertyToID("_Color");
    private static readonly int PROPERTY_START = Shader.PropertyToID("_Start");
    private static readonly int PROPERTY_END = Shader.PropertyToID("_End");

    [SerializeField]
    private Shader _shader;
    private Material _material;

    [SerializeField]
    private Color _color= Color.white;
    [SerializeField]
    private float _start = 0f;
    [SerializeField]
    private float _end = 20f;

    void Start()
    {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;

        _material = new Material(_shader);
        OnValidate();
    }

    private void OnRenderImage(RenderTexture source, RenderTexture dest)
    {
        Graphics.Blit(source, dest, _material);
    }

    private void OnValidate()
    {
        if(_material == null)
        {
            return;
        }
        _material.SetColor(PROPERTY_COLOR, _color);
        _material.SetFloat(PROPERTY_START, _start);
        _material.SetFloat(PROPERTY_END, _end);
    }
}
```

`DepthTexture.cs`コンポーネントをカメラにアタッチして、`Shader`メンバに`ShowDepthTexture.shader`の参照を入れればOKです。

![スクリーンショット 2021-07-07 21.52.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/1cfd0ba9-eca8-9a09-8605-3c39c786bbb6.png)

`Start`以下の距離だとオブジェクトが真っ黒になり、`End`以上の距離だとオブジェクトが真っ白になります。
その範囲のオブジェクトは深度に合わせた濃さの色に`Color`で指定した色が乗算されます。

# ちょっと改良する

ひとまずこれでいい感じのシルエット写真は作れるようになりましたが、ちょっと困りごとがあります。
`Start`と`End`のパラメータさえ調整すればどうにでもなるんですが、そのぶん被写体との距離に合わせて適宜距離や２値の範囲を調整しないと全体的にコントラストが低く、ぼやっとした絵になってしまいます。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/3d6c11bf-bb97-d8c3-9b33-40d0fb9f781a.png">
▲もっとメリハリをつけたい

とりあえず被写体（今回だと人物）が風景と比べて十分濃くなっていれば雰囲気が出そうなので、被写体オブジェクトを渡せば対象との距離を見て動的に深度をコントロールしてくれるようなコンポーネントになるよう調整してみます。


```ShowDepthTexture.shader
half _TargetDistance; // 被写体までの距離
half _TargetThickness; // 被写体の厚み
half _NearMax; // _Start〜被写体間の最大濃度
half _FarMin; // 被写体〜_End間の最小濃度

// ~~~略~~~

fixed4 frag(v2f i) : SV_Target
{
    half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    depth = LinearEyeDepth(depth);

    // 被写体の背面までの距離
    half back = _TargetDistance + _TargetThickness;

    // _Start〜被写体間のdepth値を、0〜_NearMaxの値に調整
    half near = lerp(0, _NearMax, (depth - _Start) / (back - _Start));
    // 被写体〜_End間のdepth値を、_FarMin〜1の値に調整
    half far = lerp(_FarMin, 1, (depth - back) / (_End - back));
    // この位置の深度がbackより前か後ろかで反映する値を仕分ける
    depth = near * clamp(sign(depth - back), 0, 1) + far * clamp(sign(back - depth), 0, 1);

    return depth * _Color;
}
```

被写体までの距離を閾値として表示する濃度を指定できるようして、被写体と背景のコントラストをコントロールしやすいようにしてみました。
`clamp(sign(~~~`の係数は if を使わずに`near`と`far`の値の使い分けをするための雑な計算です（もっといいやり方があるはず…）

コンポーネントもシェーダーに合わせて適宜メンバーと処理を追加します。

```DepthTexture.cs
private static readonly int PROPERTY_TARGET_DISTANCE = Shader.PropertyToID("_TargetDistance");
private static readonly int PROPERTY_TARGET_THICKNESS = Shader.PropertyToID("_TargetThickness");
private static readonly int PROPERTY_NEAR_MAX = Shader.PropertyToID("_NearMax");
private static readonly int PROPERTY_FAR_MIN = Shader.PropertyToID("_FarMin");

[SerializeField]
private Transform _target;
[SerializeField]
private float _targetThickness = 0.1f;

[SerializeField, Range(0f, 1f)]
private float _nearMaxDepth = 0.2f;
[SerializeField, Range(0f, 1f)]
private float _farMinDepth = 0.6f;

// ~~~略~~~

private void Update()
{
    if(_target == null)
    {
        return;
    }
    float distance = Vector3.Distance(transform.position, _target.position);
    _material.SetFloat(PROPERTY_TARGET_DISTANCE, distance);
}

private void OnValidate()
{
    if(_material == null)
    {
        return;
    }
    _material.SetColor(PROPERTY_COLOR, _color);
    _material.SetFloat(PROPERTY_START, _start);
    _material.SetFloat(PROPERTY_END, _end);
    _material.SetFloat(PROPERTY_START, _start);
    _material.SetFloat(PROPERTY_TARGET_THICKNESS, _targetThickness);
    _material.SetFloat(PROPERTY_NEAR_MAX, _nearMaxDepth);
    _material.SetFloat(PROPERTY_FAR_MIN, _farMinDepth);
}
```

Updateで被写体`_target`とカメラの距離を計算しています。

### 設定例
![スクリーンショット 2021-07-08 0.22.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f2187f33-ef80-e092-5d6b-e114ec30d87b.png)

* `Near Max Depth` = 0.1
* `Far Min Depth` = 0.3

となっています。
`Target`に被写体として設定しているPlayerから前の表示の濃度は`0.1`以下になり、それより後ろは`0.3`以上になるため、被写体がくっきりとしたシルエットを表示しやすくなっています。

今回はここまで。