［Unity］イメージエフェクトを特定のモデルだけにかける

# イメージエフェクトを特定のモデルだけにかける

イメージエフェクトは基本的にはスクリーン全体に対してかけるものなので、部分的にかけるにはその部分を判別できるようにする必要があります。そこで、ステンシルバッファの出番です。ステンシルバッファの値を元にエフェクト処理の有無を切り替えます。ググった感じだと、Unityでイメージエフェクトを実装する場合、MonoBehaviourのOnRenderImageでGraphics.Blitを使うことが定番のようですが、この方法ではステンシルバッファを使えなかったので、CommandBufferを使う方法を紹介します。

## CommandBufferを使用したイメージエフェクト

https://docs.unity3d.com/ja/current/Manual/GraphicsCommandBuffers.html
マニュアルにあるようにCommandBufferを使うと、グラフィックスパイプライン中の様々なタイミングで処理を挟み込むことができます。イメージエフェクトをかける場合はCameraEvent.BeforeImageEffectsで行うのが良さそうです。（AfterImageEffectsだと上下反転しており、ステンシルバッファも使えませんでした。AfterImageEffectsとAfterEvrythingのタイミングだとレンダーターゲットに何か処理を施した後のようです。）

手順は
1. CommandBufferを作成
2. イメージエフェクト用マテリアルとともにBlitコマンドをセット
3. カメラに作成したCommandBufferを追加
です。この処理をOnEnableで行うようにして、カメラオブジェクトにアタッチします。

<details><summary>具体的なコードはこちら</summary><div>

```c#:ImageEffect.cs
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ImageEffect : MonoBehaviour
{
    /// <summary>
    /// コマンドバッファ名
    /// </summary>
    private const string CommandBufferName = "StencilImageEffect";

    /// <summary>
    /// イメージエフェクトで使用するマテリアル
    /// </summary>
    [SerializeField] private Material material;

    /// <summary>
    /// イメージエフェクト用コマンドバッファ
    /// </summary>
    private CommandBuffer commandBuffer;


    private void OnEnable()
    {
        if (material == null) return;
        if (commandBuffer != null) return;

        var cam = GetComponent<Camera>();
        var cbs = cam.GetCommandBuffers(CameraEvent.BeforeImageEffects);
        foreach (var cb in cbs)
        {
            // 多重登録を回避するため、名前でチェック
            if (cb.name == CommandBufferName) return;
        }

        commandBuffer = new CommandBuffer();
        commandBuffer.name = CommandBufferName;

        // Blitでmaterialを適用してイメージエフェクトをかける
        commandBuffer.Blit(
            BuiltinRenderTextureType.CameraTarget,
            BuiltinRenderTextureType.CameraTarget,
            material);

        // カメラにコマンドバッファを登録
        cam.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }

    private void OnDisable()
    {
        if (commandBuffer == null) return;

        var cam = GetComponent<Camera>();
        cam.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
        commandBuffer = null;
    }
}
```

</div></details>

## ステンシルバッファの使用

ステンシルバッファに値_Valueを書き込む場合

```
Stencil {
    Ref [_Value]
    Comp Always
    Pass Replace
}
```

ステンシルバッファの値が_Valueのところだけフラグメントシェーダを実行する場合

```
Stencil {
    Ref [_Value]
    Comp Equal
}
```
をシェーダコードに追加します。（詳しくは[リファレンス](https://docs.unity3d.com/ja/540/Manual/SL-Stencil.html)を参照）

モデルのシェーダでステンシルバッファに値を書き込んで、イメージエフェクトのシェーダでステンシルバッファの値を参照し、値書き込み済みのところにだけフラグメントシェーダを実行させます。

## 結果

![スクリーンショット 2018-04-01 1.31.57.png](https://qiita-image-store.s3.amazonaws.com/0/99169/6b084324-f8fa-a099-c471-3da156b8af7c.png)
色反転のイメージエフェクトを適用しています。Cube３つをUnlitシェーダで表示し、真ん中のCubeだけステンシルバッファに書き込むようにしています。

## まとめ

CameraEventのBeforeImageEffectのタイミングでBlitすることで、ステンシルバッファを参照しつつイメージエフェクトを適用できます。以前のバージョンだとOnRenderEmageでも同様のことができたみたいですが、Unity2017.3.0ｆ3ではできなかったので、色々調べた結果、この方法が分かりました。OnRenderEmageを定義した時点でテンポラリのレンダーターゲットが作成されたり等内部で色々行うようになるっぽいので、CommandBufferを使うほうが良さそうです。

2018/04/03 追記
BlitのRenderTargetはプラットフォームによって変更する必要がありそうです。WindowsとMacではCameraTargetからCameraTargetへのBlitでうまくいきましたが、AndroidではCurrentActiveからテンポラリにBlitし、テンポラリからCurrentActiveにBlitする時にマテリアルを設定する手順としないとうまく動作しませんでした。