25
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

QualiArtsAdvent Calendar 2020

Day 11

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

Last updated at Posted at 2020-12-10

はじめに

Gif画像のようなポストエフェクトを制御するタイムラインを作ります
oaiwjfoiwjofifweewew.gif

比較

タイムラインが一番左のクリップの上にいる時は、目立つポストエフェクトはかかりません
Volume1

タイムラインが真ん中のクリップの上にいる時は、Bloomエフェクトがかかり、後ろの花火が綺麗に見えます
Volume2

タイムラインが右のクリップの上にいる時は、Depth of Fieldエフェクトがかかり、Unityちゃんがフォーカスされ、後ろの背景がボケて見えます
Volume3

環境

  • Unity 2020.1.15f
  • Universal Render Pipeline

UniversalRenderPipelineについて

この記事の環境はUniversalRenderPipeline(以下URP)を使用します。
URPはUnityが新たに設計してるグラフィクスパイプラインで、従来のグラフィクスパイプラインとは互換性がありません。
この記事と同じ手順でタイムラインを作成する場合はプロジェクト作成時にUniversal Render Pipelineを選択してください。
スクリーンショット 2020-12-02 17.59.26.png

検証用のポストエフェクトを用意する

Unityではポストエフェクトの設定データをVolumeと呼びます。
今回は3つのVolumeを用意しました
Volume1@2x.png
Volume2@2x.png
Volume3_2@2x.png

それぞれのパラメーターについては触れませんが、Volume1よりもVolume2が、Volume2よりもVolume3が華やかに見えるような設定にしました。
これらのVolumeは同名のゲームオブジェクトを作成しヒエラルキーに配置します。
スクリーンショット 2020-12-02 18.55.32.png

タイムライントラックを自作する

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

VolumeTimelineAsset.cs
[Serializable]
public class VolumeTimelineAsset : PlayableAsset
{
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        return Playable.Null;
    }
}
VolumeTimelineBehaviour.cs
public class VolumeTimelineBehaviour : PlayableBehaviour
{
}
VolumeTimelineTrack.cs
[Serializable]
[TrackClipType(typeof(VolumeTimelineAsset), false)]
public class VolumeTimelineTrack : PlayableTrack
{
}

これらのクラスを作成することでタイムラインにVolume Timeline Trackを作成できるようになります。
スクリーンショット 2020-12-02 18.39.43.png

それぞれのクラスの役割

現時点ではクリップの配置やVolumeの設定はできませんが、それぞれのクラスの役割を完成したタイムラインを使って説明します。
アセット 9@2x.png

VolumeTrack

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

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

VolumeTimelineBehaviour

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

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

VolumeTimelineAsset

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

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

実装方針

これらの役割を踏まえて、以下のような実装方針を建てます。

  1. トラック上にクリップを作れるようにする
  2. クリップを再生している時にVolumeをアクティブにし、それ以外は非アクティブにする
  3. タイムラインで制御するVolumeを指定できるようにする

1. トラック上にクリップを作れるようにする

これはすでにTrackClipTypeアトリビュートで設定されているため、追加の実装は不要です。

2. クリップを再生している時にVolumeをアクティブにし、それ以外は非アクティブにする

この動作はActivationTrackとまったく同じものになります。
そのためActivationControlPlayableを継承することで実装が可能です。

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

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の詳しい仕組みはこちらの記事を参考にしてください。
動的に取得したオブジェクトにアクセスする為にはResolveメソッドを使用します。

VolumeTimelineBehaviourにVolumeを渡す

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

ScriptPlayableを生成して返す

ScriptPlayabeはPlayableGraphでグラフのノード生成に必要なクラスです。
タイムラインはPlayableGraphを利用するため、ScriptPlayableを生成する必要があります。TimelineとPlayableGraphの詳細についてはUnityのマニュアルを参考にしてください。

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

自作トラックを再生してみる

VolumeTimelineTrackを作って、Volumeを配置します。
画面収録 2020-12-02 21.52.25.gif

再生してみます。
画面収録 2020-12-02 21.59.59.gif
Volumeを切り替えるトラックを作成することができました。

フェードを実装する

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

実装方針

画像の通り、VolumeはWeightを操作することで、2つのパラメーターをブレンドできます。
アセット 6@2x.png
アセット 8@2x.png
アセット 7@2x.png
以下の実装方針を建てます

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

1. VolumeTimelineBehaviourからVolumeのWeight設定できるようにする

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

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;
        }
    }
}
  1. タイムラインのクリップの重なり合いを取得しVolumeのWeightに設定する
    タイムラインのクリップの重なり合いを取得する為に VolumeTimelineMixerBehaviourクラスを作成します。
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
VolumeTimelineMixerBehaviourはトラック全体の振る舞いを設定します。
VolumeTimelineMixerBehaviourはトラック全体の振る舞いを制御するクラスで、具体的には以下のような処理を担当します。

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

VolumeTimelineMixerBehaviourを使うように設定する

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

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

再生終了時に変更したパラメーターを元に戻す

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

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

補足)最終的なコード

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);
    }
}
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;
        }
    }
}
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);
            }
        }
    }
}
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);
    }
}
25
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?