【Unity】Timelineを拡張してポストプロセスのカスタムトラックを作成する

# 概要
Timelineでポストプロセス専用のカスタムトラックを作成する実装例をご紹介します。
![TimelinePostProcessing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/878f4a4e-ca54-e0f2-52ef-d888825f8c29.gif)

# 環境
* Unity2019.4.1f1
* UniversalRP 7.3.1

# Timelineでカスタムトラックを作成して連携
TimelineのクリップごとにProfileを指定します。
![2020-09-13_19h59_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/b67296f4-a98f-0626-d9b3-659d721f80f6.png)

ポストエフェクトのかかり具合はWeight Curveで調整するようにしました。
WeightCurveが１ならよくかかる、０ならかかりません。
![2020-09-13_19h59_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148284/20a14273-335c-b5d1-f4bc-f1597ace02eb.png)

# コード
４つのC#ファイルを作成します。

| クラス            |       説明 |
|-----------------|------------------|
| PostProcessBehaviour.cs | クリップごとの振る舞いを表します |
| PostProcessMixerBehaviour.cs | トラック内で複数のクリップがある場合の振る舞いを表します。inputWeightが正のものが現在のクリップです |
| PostProcessTrack.cs | トラックを表します |
| PostProcessClip.cs | クリップを表します。クリップのパラメータが変更されるたびにCreatePlayableが呼ばれます。 |

```csharp:PostProcessBehaviour.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Rendering;
using System;

[Serializable]
public class PostProcessBehaviour : PlayableBehaviour
{
    [HideInInspector]
    public Volume volume;
    public VolumeProfile profile;
    public int layer;
    public AnimationCurve weightCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);

    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        if (profile != null)
        {
            QuickVolume();
        }
    }

    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        if (volume != null)
        {
            GameObject.DestroyImmediate(volume.gameObject);
        }
    }

    void QuickVolume()
    {
        if (volume == null)
        {
            volume = new GameObject().AddComponent<Volume>();
            volume.gameObject.layer = layer;
            volume.gameObject.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
            volume.gameObject.name = $"PostProcessBehaviourVolume [Profile {profile.name}]";
            volume.weight = 0;
            volume.priority = 1;
            volume.isGlobal = true;
            volume.profile = profile;
        }
    }

    public void ChangeWeight(float time)
    {
        if (volume == null) { return; }
        volume.weight = weightCurve.Evaluate(time);
    }
}
```
```csharp:PostProcessMixerBehaviour.cs
using System;
using UnityEngine;
using UnityEngine.Playables;

[Serializable]
public class PostProcessMixerBehaviour : PlayableBehaviour
{
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        int inputCount = playable.GetInputCount();
        for (int i = 0; i < inputCount; i++)
        {
            var playableInput = (ScriptPlayable<PostProcessBehaviour>)playable.GetInput(i);
            PostProcessBehaviour input = playableInput.GetBehaviour();
            float inputWeight = playable.GetInputWeight(i);
            if (Mathf.Approximately(inputWeight, 0f))
            {
                continue;
            }
            float normalizedTime = (float)(playableInput.GetTime() / playableInput.GetDuration());
            input.ChangeWeight(normalizedTime);
        }
    }
}
```
```csharp:PostProcessTack.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[TrackColor(0.98f, 0.27f, 0.42f)]
[TrackClipType(typeof(PostProcessClip))]
public class PostProcessTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        var scriptPlayable = ScriptPlayable<PostProcessMixerBehaviour>.Create(graph, inputCount);
        return scriptPlayable;
    }
}

```
```csharp:PostProcessClip.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public class PostProcessClip : PlayableAsset, ITimelineClipAsset
{
    public PostProcessBehaviour template = new PostProcessBehaviour();

    public ClipCaps clipCaps
    {
        get { return ClipCaps.Extrapolation | ClipCaps.Blending; }
    }

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        var playable = ScriptPlayable<PostProcessBehaviour>.Create(graph, template);
        PostProcessBehaviour clone = playable.GetBehaviour();
        return playable;
    }
}

#if UNITY_EDITOR

[CustomEditor(typeof(PostProcessClip))]
public class PostProcessClipEditor : Editor
{
    PostProcessClip postProcessClip;
    Editor profileEditor;
    SerializedProperty profileProperty;
    SerializedProperty curveProperty;

    void OnEnable()
    {
        postProcessClip = target as PostProcessClip;
        profileEditor = Editor.CreateEditor(postProcessClip.template.profile);
        profileProperty = serializedObject.FindProperty("template.profile");
        curveProperty = serializedObject.FindProperty("template.weightCurve");
    }

    void OnDisable()
    {
        DestroyImmediate(profileEditor);
    }

    public override void OnInspectorGUI()
    {
        postProcessClip.template.layer = EditorGUILayout.LayerField("Layer", postProcessClip.template.layer);
        serializedObject.Update();
        EditorGUILayout.PropertyField(profileProperty);
        EditorGUILayout.PropertyField(curveProperty);
        serializedObject.ApplyModifiedProperties();

        profileEditor?.OnInspectorGUI();
    }
}
#endif
```

# プロジェクトデータ
[以前の記事](https://qiita.com/t-matsunaga/items/09343ae7c683269374c4)と同じ場所にサンプルプロジェクトデータを[GitHub](https://github.com/t-macnaga/UniversalRPPostProcessing)アップしましたので
ご自由にお使いください。

# 参考サイト様
- [unity-post-processing-playable](https://github.com/spencersteers/unity-post-processing-playable)

# まとめ
- [以前の記事](https://qiita.com/t-matsunaga/items/09343ae7c683269374c4)ではコンポーネントを作ってアニメーターと連携して
パラメータをいじってました。
- 今回Timelineを拡張してポストプロセス用のカスタムトラックを作って設定できるようにしました。