土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity URPでVolumeのDoF(被写界深度)をTimelineで制御するスクリプト

 Unityで動画を作っているフリーの映像作家さんがレンダラーをビルトインからURPに移行する検証をしていて、ちょくちょく起きる問題に土屋が相談に乗っています。現場の方が直近でどんな事で困っていて、何を求めているのかについて知見を得る機会だと感じています。例えばこんなの。

映像作家さん「TimeLineでDoF(被写界深度)をキーフレーム制御したいんですけど」
土屋「え? 出来ないのそれ?」
映像作家さん「PPSv2では出来たんですけど、URP(ボリューム)では出来ないんですよね」
土屋「えっ!? 出来ないのそれっ!?」

 出来ないんですこれが! 知らなかったよ俺! 正確にはPPSv2でもそのままではできません。映像作家さんはDoF制御用にネットで公開されているスクリプトを使っていたのですが、これがURPに対応していなかったのです。これをURP用に簡易ポートしたというのが今回の話。

TimeLineでVolume(PPSv2)を制御する

 TimeLineはオブジェクトにアタッチしたAnimatorコンポーネントを介して、他のコンポーネント(=MonoBehaviorクラス)の値をキーフレームに応じて滑らかに遷移させます。しかし、PPSv2やVolumeが管理しているポストプロセスに関連する値は、TimeLineから直接設定することができません。なんでかというと、これらの値はアセットファイル(=ScritableObjectクラス)に保存されている静的なパラメータだからです*1

 その為、TimeLineとPPSv2(Volumeでも同じです)の間を仲介するスクリプトXを用意し、「AnimatorはXを制御する」「Xが自身の更新を監視し、更新されたらPPSv2に反映する」という流れでDoFの制御を実現します。

コードサンプル

下記の記事にあるスクリプトが、PPSv2の時に映像作家さんが使っていた物。これをDoFに限定してポーティングしたコードになります。
qiita.com

ポーティングしたコードはこちら。コードがわかりやすいように、DoF制御に特化した形に書き直しています(実際の現場で使ってる物とも違います)。「自身とVolumeを監視しあって連動させ続ける」という処理は他にも応用が利きそう。

// zlib/libpng License
//
// Copyright (c) 2021 TSUCHIYA Tsukasa
// Based By Maron_Vtuber https://qiita.com/Maron_Vtuber/items/a60310779d0b790034a4
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
//    If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.

using UnityEngine;
using UnityEngine.Rendering;//volumeを使うのに、この行が必要です。
using UnityEngine.Rendering.Universal;//DepthOfFieldを使うのに、この行が必要です。

// 再生中じゃなくてもスクリプトを適用する
[ExecuteAlways]

// TimelineでパラメータをいじるにはAnimatorが付いている必要があるので、
// このスクリプトを付けると自動でAnimatorがアタッチされるように以下を追記します。
[RequireComponent(typeof(Animator))]

public class URPVolumeController : MonoBehaviour
{
    [SerializeField]
    VolumeProfile volumeProfile;//Volumeコンポーネントへの参照

    VolumeProfile volumeProfileStored;//VolumeProfileの取得・破棄の監視用

    DepthOfField depthOfField; //VolumeCompornent派生クラス

    [SerializeField, Range(0, 10)]
    public float depthOfFieldFocusDistance;//DoFフォーカス距離(この値をTimeLineで制御する)

    float depthOfFieldFocusDistanceStored;//DoFフォーカス距離のバックアップ値(更新差分テストに使用)

    //インスペクタでスクリプトが読み込まれたor値が変わった時に実行される
    void OnValidate()
    {
        //volumeProfileがまだ設定されていないのであれば戻る
        if (volumeProfile == null && volumeProfileStored == null) return;

        //volumeProfileが変更されていない場合は戻る。
        if (volumeProfileStored == volumeProfile) return;

        //volumeProfileが削除された場合、メンバとして格納しているVolumeCompornentへの参照を破棄して戻る。
        if (volumeProfile == null && volumeProfileStored != null)
        {
            depthOfField = null;
            return;
        }

        // DepthOfFieldコンポーネントを探して格納する
        foreach (var item in volumeProfile.components)
        {
            if (item as DepthOfField)
            {
                depthOfField = item as DepthOfField;
            }
        }
    }

    void Update()
    {
        if (depthOfField) // depthOfFieldへの参照が格納されていれば値を操作する。
        {
            //VolumeCompornen上の現在値とバックアップ値が不一致の場合(スクリプトアタッチ時など)
            if (depthOfField.focusDistance.value != depthOfFieldFocusDistanceStored)
            {
                //バックアップ値をVolumeCompornen上の現在値で更新
                depthOfFieldFocusDistanceStored = depthOfField.focusDistance.value;
            }
            //バックアップ値とスライダー値が不一致の場合(Animatorが更新した時など)
            else if (depthOfFieldFocusDistance != depthOfFieldFocusDistanceStored)
            {
                //バックアップ値をスライダー値で更新
                depthOfFieldFocusDistanceStored = depthOfFieldFocusDistance;
            }
        }

        //スライダー値をバックアップ値で更新
        depthOfFieldFocusDistance = depthOfFieldFocusDistanceStored;
        //VolumeCompornen上の現在値をバックアップ値で更新
        depthOfField.focusDistance.value = depthOfFieldFocusDistanceStored;
        //volumeProfileの更新(OnValidate()でやればいい気もする)
        volumeProfileStored = volumeProfile;
    }
}

DoFをTimeLineで制御したい理由

 ちなみに、なんでDoFをTimeLineで制御したいのかという話なんですが、DoFを有効にした状態でキャラモデルAにピントを当てているとして、Aとカメラの距離が変わってもAにピントを当て続けたい時があるのだそうです。そのためにはカメラの動きに合わせてDoFの距離を変えなければならないので、キーフレーム制御できないと困るんだそうです。

 土屋はシェーダープログラミングには多少の知識があるつもりですが、カットシーンの演出などを実際にやっているわけではなく、またカメラやポスプロの知識も無いので、「なるほどこれが現場か」と思ったのでした。他にもいくつかあるのでまたの機会に。

参考リンク

forum.unity.com
TimeLineで直接DoFを操作する方法は無いと説明する公式フォーラムのスレッド

tsubakit1.hateblo.jp
バーチャルカメラを2台用意して被写界深度の滑らかな切り替えを行うアプローチ。細かい制御は難しいでしょうですが、こういう手もあります。

qiita.com
カスタムトラックを実装して、複数のVolumeProfileをフェードさせるアプローチ。

*1:Animatorの制御対象がMonoBehavior派生クラスに限定される為だと思うが詳細は知らない