Unityタイムライン拡張入門 ~自作トラックの作り方~

# はじめに
Gif画像のようなポストエフェクトを制御するタイムラインを作ります
![oaiwjfoiwjofifweewew.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/194784e2-e16d-8709-e423-9c2f671de3da.gif)


## 比較
タイムラインが一番左のクリップの上にいる時は、目立つポストエフェクトはかかりません
![Volume1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/5f85f3e0-c90c-cf4d-5c98-ffae3ae64bda.png)

タイムラインが真ん中のクリップの上にいる時は、Bloomエフェクトがかかり、後ろの花火が綺麗に見えます
![Volume2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/bcc6f434-c5b5-9151-3110-04e8a310542c.png)

タイムラインが右のクリップの上にいる時は、Depth of Fieldエフェクトがかかり、Unityちゃんがフォーカスされ、後ろの背景がボケて見えます
![Volume3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/9df2b350-192c-4f35-0d05-cca650cbc470.png)


# 環境
 * Unity 2020.1.15f
 * Universal Render Pipeline

## UniversalRenderPipelineについて
この記事の環境はUniversalRenderPipeline(以下URP)を使用します。
URPはUnityが新たに設計してるグラフィクスパイプラインで、従来のグラフィクスパイプラインとは互換性がありません。
この記事と同じ手順でタイムラインを作成する場合はプロジェクト作成時にUniversal Render Pipelineを選択してください。
![スクリーンショット 2020-12-02 17.59.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/c506906c-ab2d-c536-c973-c18c60603889.png)

# 検証用のポストエフェクトを用意する
Unityではポストエフェクトの設定データをVolumeと呼びます。
今回は3つのVolumeを用意しました
![Volume1@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/6e70f384-b314-322d-be85-febe23f2a600.png)
![Volume2@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/52f6605f-3a60-15bd-69a3-a57547ffd041.png)
![Volume3_2@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/a7ca607c-3eae-863d-52af-d3de5de7fdcf.png)

それぞれのパラメーターについては触れませんが、Volume1よりもVolume2が、Volume2よりもVolume3が華やかに見えるような設定にしました。
これらのVolumeは同名のゲームオブジェクトを作成しヒエラルキーに配置します。
![スクリーンショット 2020-12-02 18.55.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/f8aba95b-50f7-f2ba-1be9-58d16235f76b.png)


# タイムライントラックを自作する
`VolumeTimelineTrack`という名前のトラックを作成します。このトラックを使ってポストエフェクトを制御します。
タイムラインを作るには3つのクラスが必要になります。

```c#:VolumeTimelineAsset.cs
[Serializable]
public class VolumeTimelineAsset : PlayableAsset
{
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        return Playable.Null;
    }
}
```

```c#:VolumeTimelineBehaviour.cs
public class VolumeTimelineBehaviour : PlayableBehaviour
{
}
```

```c#:VolumeTimelineTrack.cs
[Serializable]
[TrackClipType(typeof(VolumeTimelineAsset), false)]
public class VolumeTimelineTrack : PlayableTrack
{
}
```

これらのクラスを作成することでタイムラインに`Volume Timeline Track`を作成できるようになります。
![スクリーンショット 2020-12-02 18.39.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/bd0e91fe-9ba5-825a-f417-ca2efb8e9f1e.png)

## それぞれのクラスの役割
現時点ではクリップの配置やVolumeの設定はできませんが、それぞれのクラスの役割を完成したタイムラインを使って説明します。
![アセット 9@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/e3b36cc5-f91e-d531-88d0-fc8ba4c81925.png)


### VolumeTrack
`VolumeTrackは`トラックの設定に関するクラスで、具体的には以下のような処理を担当します。

 * トラック上に作れるクリップの種類を制限する
 * トラック上にクリップが作られた時の挙動を設定する
 * トラックの名前を変更する

### VolumeTimelineBehaviour
`VolumeTimelineBehaviour`はクリップの振る舞いを制御するクラスで、具体的には以下のような処理を担当します。

 * クリップが再生された時の挙動を設定する
 * クリップが再生中の挙動を設定する
 * クリップが再生終了した時の挙動を設定する

### VolumeTimelineAsset
`VolumeTimelineAsset`は`VolumeTimelineBehaviour`を作るためのクラスで、具体的には以下のような処理を担当します。

 * クリップが参照するオブジェクトを設定する
 * 参照したオブジェクトをVolumeTimelineBehaviourに渡す

## 実装方針
これらの役割を踏まえて、以下のような実装方針を建てます。
 1. トラック上にクリップを作れるようにする
 2. クリップを再生している時にVolumeをアクティブにし、それ以外は非アクティブにする
 3. タイムラインで制御するVolumeを指定できるようにする

### 1. トラック上にクリップを作れるようにする
これはすでに`TrackClipType`アトリビュートで設定されているため、追加の実装は不要です。

### 2. クリップを再生している時にVolumeをアクティブにし、それ以外は非アクティブにする
この動作はActivationTrackとまったく同じものになります。
そのため`ActivationControlPlayable`を継承することで実装が可能です。

```c#
public class VolumeTimelineBehaviour : ActivationControlPlayable
{
    public void SetTarget(Volume volume, PostPlaybackState postPlaybackState)
    {
        gameObject = volume.gameObject;
        postPlayback = postPlaybackState;
    }
}
```
`SetTarget`メソッドを作り以下の情報を外部から渡せるようにします

 * アクティブ・非アクティブを切り替えるゲームオブジェクト
 * クリップの再生終了時にオブジェクトをどの状態にするか

### 3. タイムラインで制御するVolumeを指定できるようにする
画像のVolumeの指定欄を作るためのスクリプトを作成します。
![スクリーンショット 2020-12-02 20.27.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/1f1c37f3-a955-780a-87b9-6c94042b0dfc.png)

```c#:VolumeTimelineAsset.cs
[Serializable]
public class VolumeTimelineAsset : PlayableAsset
{
    public ExposedReference<Volume> volume;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        Volume resolvedVolume = volume.Resolve(graph.GetResolver());
        if (resolvedVolume == null)
        {
            return default;
        }

        VolumeTimelineBehaviour behaviour = new VolumeTimelineBehaviour();
        behaviour.SetTarget(resolvedVolume, ActivationControlPlayable.PostPlaybackState.Inactive);
        return ScriptPlayable<VolumeTimelineBehaviour>.Create(graph, behaviour);
    }
}
```
#### ExporsedReferenceについて

 `ExporsedReference`はシーンにあるオブジェクトをタイムラインから参照する仕組みです。
似たような仕組みとして `[SerializeField]`がありますが、Timelineはアセットのためシーンのオブジェクトを参照できません。
（Prefabの`[SerializeField]`にシーン上のオブジェクトを指定できない挙動と同じものになります）
そのため実行時にシーンにあるオブジェクトを動的に取得する必要があり`ExporsedReference`はそれを行うための仕組みです。
`ExposedReference`の詳しい仕組みは[こちら](https://qiita.com/hadashiA/items/566f0d0222cb9a4e209b#exposedreferencet--iexposedpropertytable)の記事を参考にしてください。
動的に取得したオブジェクトにアクセスする為には`Resolve`メソッドを使用します。

#### VolumeTimelineBehaviourにVolumeを渡す
自作した`SetTarget`メソッドにVolumeを渡します。ここで`ActivationControlPlayable.PostPlaybackState.Inactive`を指定することで、クリップの再生が終了時にgameObjectが非アクティブになります。

#### ScriptPlayableを生成して返す
ScriptPlayabeはPlayableGraphでグラフのノード生成に必要なクラスです。
タイムラインはPlayableGraphを利用するため、ScriptPlayableを生成する必要があります。TimelineとPlayableGraphの詳細については[Unityのマニュアル](https://blogs.unity3d.com/jp/2018/09/05/extending-timeline-a-practical-guide/)を参考にしてください。

これで、タイムラインが再生されている時だけVolumeがアクティブになります。

## 自作トラックを再生してみる
VolumeTimelineTrackを作って、Volumeを配置します。
![画面収録 2020-12-02 21.52.25.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/82f07b75-383a-1b44-7bd0-ebc5caecea38.gif)

再生してみます。
![画面収録 2020-12-02 21.59.59.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/94d5ac5b-710d-540b-14cf-975ddb45b88f.gif)
Volumeを切り替えるトラックを作成することができました。

## フェードを実装する
これまでの実装でVolumeを切り替えることができたので、次はクリップが重なってる部分をフェードする処理を実装します。

### 実装方針
画像の通り、VolumeはWeightを操作することで、2つのパラメーターをブレンドできます。
![アセット 6@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/8549c4ad-f255-31cd-ef96-4a6279800f6a.png)
![アセット 8@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/21f3fd81-991b-656a-199e-57c45ae94376.png)
![アセット 7@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/17b47c54-e611-bacc-c063-c17e17ecaa0b.png)
以下の実装方針を建てます

1. VolumeTimelineBehaviourからVolumeのWeight設定できるようにする
2. タイムラインのクリップの重なり合いを取得しVolumeのWeightに設定する


### 1. VolumeTimelineBehaviourからVolumeのWeight設定できるようにする
`VolumeTimelineBehaviour`を以下のように変更して`VolumeTimelineBehaviour`からVolumeのWeightを設定できるようにします。


```c#:VolumeTimelineBehaviour.cs
public class VolumeTimelineBehaviour : ActivationControlPlayable
{
    private Volume _volume;

    public void SetTarget(Volume volume, PostPlaybackState postPlaybackState)
    {
        _volume = volume;
        gameObject = volume.gameObject;
        postPlayback = postPlaybackState;
    }

    public void SetWeight(float weight)
    {
        if (_volume != null)
        {
            _volume.weight = weight;
        }
    }
}
```

2. タイムラインのクリップの重なり合いを取得しVolumeのWeightに設定する
タイムラインのクリップの重なり合いを取得する為に `VolumeTimelineMixerBehaviour`クラスを作成します。

```c#:VolumeTimelineMixerBehaviour.cs
public class VolumeTimelineMixerBehaviour : PlayableBehaviour
{
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        int inputCount = playable.GetInputCount();
        for (int i = 0; i < inputCount; i++)
        {
            var inputPlayable = (ScriptPlayable<VolumeTimelineBehaviour>) playable.GetInput(i);
            float inputWeight = playable.GetInputWeight(i);
            if (inputWeight > 0)
            {
                inputPlayable.GetBehaviour().SetWeight(inputWeight);
            }
        }
    }
}
```
### ProcessFrame内の処理について
ProcessFrameはタイムライン再生中に毎フレーム呼び出されます。
`playable.GetInput()`メソッドにより、トラックに存在するクリップの情報を取得し、これを`ScriptPlayable<VolumeTimelineBehaviour>`にキャストする事で、`VolumeTimelineBehaviour `を取得します。
取得したクリップのWeightを`VolumeTimelineBehaviour.SetWeight()`メソッドに渡す事で、VolumeのWeightを設定します。

#### VolumeTimelineMixerBehaviourの役割
![アセット 10@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/083fb004-6950-8fd1-b7e0-d6639ff79fbd.png)
VolumeTimelineMixerBehaviourはトラック全体の振る舞いを設定します。
`VolumeTimelineMixerBehaviour`はトラック全体の振る舞いを制御するクラスで、具体的には以下のような処理を担当します。

 * クリップのWeightを取得する
 * 再生中に毎フレーム呼ばれるメソッドで処理を実装する

#### VolumeTimelineMixerBehaviourを使うように設定する
`VolumeTimelineTrack`クラスを変更して、`VolumeTimelineMixerBehaviour`が呼び出されるように設定します。

```c#:VolumeTimelineTrack.cs
[Serializable]
[TrackClipType(typeof(VolumeTimelineAsset), false)]
public class VolumeTimelineTrack : PlayableTrack
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        return ScriptPlayable<VolumeTimelineMixerBehaviour>.Create (graph, inputCount);
    }
}
```


## 再生する
クリップの切り替わり時にフェードするトラックが実装できました！
![oaiwjfoiwjofifweewew.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/66a6b032-f981-3a81-fb69-71a961f261f3.gif)

## 再生終了時に変更したパラメーターを元に戻す
最後に、再生終了後にタイムラインが変更したパラメーターが自動で戻るように`GatherProperties`を設定します。

```c#:VolumeTimelineAsset.cs
public class VolumeTimelineAsset : PlayableAsset, IPropertyPreview
{
    public ExposedReference<Volume> volume;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        Volume resolvedVolume = volume.Resolve(graph.GetResolver());
        if (resolvedVolume == null)
        {
            return default;
        }

        VolumeTimelineBehaviour behaviour = new VolumeTimelineBehaviour();
        behaviour.SetTarget(resolvedVolume, ActivationControlPlayable.PostPlaybackState.Inactive);
        return ScriptPlayable<VolumeTimelineBehaviour>.Create(graph, behaviour);
    }

    public void GatherProperties(PlayableDirector director, IPropertyCollector driver)
    {
        // Volumeのweightとenabledをタイムライン終了時に戻す
        Volume resolvedVolume = director.GetReferenceValue(volume.exposedName, out var isValid) as Volume;
        if (isValid)
        {
            driver.AddFromName(resolvedVolume, "weight");
            if (resolvedVolume != null)
            {
                driver.AddFromName(resolvedVolume.gameObject, "m_IsActive");
            }
        }
    }
}
```

### GatherPropertiesについて
GatherPropertiesを使用するために`IPropertyPreview`インターフェイスを実装します。`driver.AddFromName`メソッドは元に戻すパラメーターのシリアライズ名を入力します。このメソッドを作成する事で、タイムラインが変更を加えたパラメーターが自動で元に戻ります。
また`GatherProperties`を設定したパラメーターは青色になり、インスペクターから確認する事ができます。
![スクリーンショット 2020-12-10 22.37.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213392/57854952-9220-2abc-1ec2-8da60e709d29.png)

# 補足）最終的なコード
```c#:VolumeTimelineAsset.cs
[Serializable]
public class VolumeTimelineAsset : PlayableAsset
{
    public ExposedReference<Volume> volume;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        Volume resolvedVolume = volume.Resolve(graph.GetResolver());
        if (resolvedVolume == null)
        {
            return default;
        }

        VolumeTimelineBehaviour behaviour = new VolumeTimelineBehaviour();
        behaviour.SetTarget(resolvedVolume, ActivationControlPlayable.PostPlaybackState.Inactive);
        return ScriptPlayable<VolumeTimelineBehaviour>.Create(graph, behaviour);
    }
}
```

```c#:VolumeTimelineBehaviour.cs
public class VolumeTimelineBehaviour : ActivationControlPlayable
{
    private Volume _volume;

    public void SetTarget(Volume volume, PostPlaybackState postPlaybackState)
    {
        _volume = volume;
        gameObject = volume.gameObject;
        postPlayback = postPlaybackState;
    }

    public void SetWeight(float weight)
    {
        if (_volume != null)
        {
            _volume.weight = weight;
        }
    }
}
```

```c#:VolumeTimelineMixerBehaviour.cs
public class VolumeTimelineMixerBehaviour : PlayableBehaviour
{
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        int inputCount = playable.GetInputCount();
        for (int i = 0; i < inputCount; i++)
        {
            var inputPlayable = (ScriptPlayable<VolumeTimelineBehaviour>) playable.GetInput(i);
            float inputWeight = playable.GetInputWeight(i);
            if (inputWeight > 0)
            {
                inputPlayable.GetBehaviour().SetWeight(inputWeight);
            }
        }
    }
}
```

```c#:VolumeTimelineTrack.cs
[Serializable]
[TrackClipType(typeof(VolumeTimelineAsset), false)]
public class VolumeTimelineTrack : PlayableTrack
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        return ScriptPlayable<VolumeTimelineMixerBehaviour>.Create (graph, inputCount);
    }
}
```