はじめに
Jater (Ruohao) Xuによる『逆コーラップス:パン屋作戦』の開発経験における3部構成のブログで、今回は第1回目となります。本記事ではWwise Time Stretchプラグインを使用して、ゲーム中のシネマティックスをWwiseで駆動する方法について解説します。パート2、パート3については、順次掲載予定です。
Jater (Ruohao) XuはPaul Ruskayと共に、Audiokineticのブログページ向けに『逆コーラップス:パン屋作戦』 | リモート協業におけるWwiseの重要な役割を執筆したばかりで、キャラクターやカスタムアニメーションのサウンドデザインの管理方法や、ゲームのインタラクティブミュージックシステムの制作などが紹介されました。
Wwise Time Stretchプラグインを使用して、ゲームのシネマティックスをWwiseで駆動する
Techブログ|パート1
プログラムのウィンドウ間を猛スピードでAlt-Tab操作した時や、性能上の限界のためフレームレートのスタッタリングが発生した時など、オーディオ・ビデオの同期問題に対応するため、私たちは『逆コーラップス』のゲーム内クロックだけを頼りにすることを避け、Wwiseクロックを使いオーディオとビデオの同期コードを実装しました。さらにシネマティックスの感動的な瞬間をゲームで際立たせるために重要なスローモーションやファストモーション効果を支えるため、追加措置をとりました。具体的には柔軟性の高いWwise Time Stretchプラグインを使いました。このアプローチは前述の課題を軽減する効果的な方法となりました。ところで「シネマティックス」という用語はUnity TimelineやUnreal Sequencerなどさまざまなゲームエンジンで使われますが、『逆コーラップス』をUnityで構築したため、本記事の例はUnity環境のものです。
基本
基本的なところからはじめますが、本アプローチが成功するための基盤となる哲学は意外にもシンプルであり、各種ゲームエンジンにおいて応用可能であると思います。
現在のオーディオ再生時間が現在のビデオ再生時間より遅い場合、ビデオを一時停止させます。
現在のオーディオ再生時間がビデオ再生時間よりはやい場合、オーディオ再生時間に合わせてビデオを再生します。
両者の時間値が等しい場合、完璧に同期した状態にあるため何もしません。
本ゲームはUnityを使用しており、上記の概念を解釈して以下のようなC#の疑似アルゴリズムで表現することができます:
private void AudioVideoSyncLogic()
{
if (audioTime < videoTime)
{
video.Pause();
}
else if (audioTime > videoTime)
{
video.Play();
}
else
{
continue;
}
}
この初歩的なコードスニペットを使い、この関数をデフォルトのUnityUpdate() 関数に入れ込み、シネマティックスの再生中に各フレームで実行させます。有効にするために、TimelineのUpdate Methodをデフォルトの"Game Time"から "Manual"に変更することも忘れないでください。
上図はRCAudioClockSync.csスクリプト用の設定です。この場合、再生するイベントやアンビエンスを文字列入力で渡すこともできます。
問題を解決する
上記コードをスクリプトに統合した際、いくつかの問題が発生しますが、シネマティックスがゲームのインタラクティブな各種シナリオにおいて引き続き再生できることを保証するため、修正や拡張が必要です。特に以下のような問題に対応する必要があります:
- 現在の再生オーディオ時間の取得:オーディオの進行状況をスクリプトが正確に把握できるようにします。
- シネマティックスを停めた時の、ビデオとオーディオの同期:シネマティックスの終了時に、ビデオがオーディオと共に正しく停止するようにします。
- フェールセーフ手法の実装:シネマティックスが適切に設定されない場合に備え、安全対策を提供します。
- オーディオを同期させるためのQA機能:テストチームが非同期のデフォルト設定とオーディオ同期を比較するために使える、追加の関数を開発します。
これらの問題に対応することにより、ゲーム内のシネマティックス場面を処理するスクリプトの堅牢性と信頼性を高めることができます。
現在の再生オーディオ時間の取得
プログラマー用の大変便利なWwise機能があります。GetSourcePlayPosition()です(リンク:GetSourcePlayPosition (audiokinetic.com))。
GetSourcePlayPosition()を使い、特定のplaying IDのオーディオ再生時間を取得することができます。便利なことに、この関数の元のC++の呼び出しよりもアクセスしやすい、以下のバージョンがWwise Unityインテグレーションにあります:
public static AKRESULT GetSourcePlayPosition(uint in_PlayingID, out int out_puPosition, bool in_bExtrapolate)
この手法を活用するためには、まず提供されたplaying IDを使って呼び出した時のreturn結果を代入します。Ak_Success結果を受領し、現在のオーディオ時間をパラメータモディファイアout_puPositionへ送信します。何らかの理由によりAk_Failを受領した場合、代わりに -1を出力させます。以下の疑似コードが本ソリューションを表していますが、この例においてはplayingID変数を取得する部分が省略されています。
public int GetSourcePlaybackPositionInMilliseconds(uint playingID, bool extrapolated)
{
int returnPos = 0;
AKRESULT returnResult = AkSoundEngine.GetSourcePlayPosition(playingID, out returnPos, extrapolated);
if (returnResult == AKRESULT.AK_Success)
{
return returnPos;
}
else
{
return -1;
}
}
このようにGetSourcePlaybackPositionInMilliseconds()を呼び出し、return値をaudioTime変数に代入することにより、現在のオーディオ再生時間をリアルタイムに取得することができます。変数が-1となりオーディオビデオ同期ロジックがスキップされる状況についても、対応を忘れないでください。
シネマティックスを停めた時のビデオとオーディオの同期
前述のAudioVideoSyncLogic()関数を思い出してください。オーディオが終了した際、ビデオはシームレスにそれに追いついて最後のアクションを継続、つまり現在のステートをそのまま維持します。理想的にはオーディオとビデオが同時に終了した時、両者が停止されるべきです。例えばオーディオが終了した時にビデオが一時停止していた場合、そのまま一時停止の状態が維持され、ゲームがフリーズするかもしれません。逆にオーディオファイルの終わりに数秒の無音状態がある場合、ビデオの再生がそのまま続き、オーディオが停まるまで画像アーチファクトが発生するかもしれません。ゲームを壊すようなバグを回避するため、このような問題を実装段階において解消する必要があります。
シネマティックスゲームオブジェクト自体を破壊することでこの問題を解決できることもありますが、オーディオ部門がコントロールできない多くの要因に依存してしまいます。自明で普遍的な解決策として、オーディオが終わりに達した時にビデオを明示的に停止させる実装が可能です。AudioVideoSyncLogic()関数の末尾に以下の疑似コードを追加するだけであり、以下の例においてはvideoDuration変数を取得する部分が省略されています:
if (audioTime > videoDuration)
{
video.Stop();
}
フェールセーフ手法の実装
開発中はシネマティックスのオーディオにプレースホルダが使われていたり、オーディオが存在しなかったりする状況がよくみられます。ゲームプレイ側ではまれにこれらの手法が失敗し、ビデオがフリーズしてゲームプレイが停まってしまうことがあります。このため前述の手法のみにソフトウェアの安定性を託すわけにはゆきません。シネマティックスの開発中でEditorを使用している時や、ゲーム実行中でプレイヤーがプレイする時など、上記の手法が失敗した時のためのフェールセーフ手法を実装する必要があります。このアプローチは単にUnityのデフォルトの更新手法を使用することとは違い、ゲーム実行中などは適用される更新方式をプレイヤーが制御できないため特に状況が異なります。シネマティックスの最中にデフォルトの更新方式に切り替えた場合、予想以上の連鎖反応が発生する可能性があります。
以下の関数はシネマティックスの手動の更新方法例であり、ここではフォールバック方式となっており、videoTime変数を取得する部分が省略されています:
private void AudioVideoSyncFallbackLogic()
if (videoTime < videoDuration)
{
videoTime += deltaTime
video.Play();
}
else
{
video.Stop();
}
}
AudioVideoSyncLogic()は有効なplaying IDと有効なオーディオ時間の両方が取得された時に実行され、それ以外の状況ではAudioVideoSyncFallbackLogic()が効力を発揮します。
以上をまとめ、擬似コードを用いた例として、updateメソッドやtickメソッドでは以下のようなコードセグメントと関数呼び出しとなります:
private void LateUpdate()
{
int audioTime = GetSourcePlaybackPositionInMilliseconds((uint)playingID, true);
if (playingID != -1 && audioTime != -1)
{
AudioVideoSyncLogic();
}
else
{
AudioVideoSyncFallbackLogic();
}
}
ここでオーディオとビデオを同期させる前に、LateUpdate()を使いシネマティックス内(Unity Timelineなど)のすべてのアニメーションやその他の要素が完全に更新されたことを確認することに注目してください。まれに適切に更新されないことがあるため、こちらのアプローチの方がよい結果に繋がります。通常のUpdate()関数を使用してもよいのですが、カスタマイズされたスクリプトやビジュアル機能を多く含むタイムラインにおいては、LateUpdate()をおすすめします。実装方法はプロジェクトによって異なり、Timelineのプレイアブルやその他のアセットを参照するために、関数にカスタム入力パラメータを追加するメリットがあるかもしれません。
オーディオ同期のQA機能
シネマティックス関連のバグに関してQAチームがビデオのAB比較をしやすいように、トグルを追加しました。これはオーディオ・ビデオの同期と同期フォールバック手段をバイパスするだけのトグルであり、ビデオ録画の前にGame Time更新のデフォルト手法に戻します。バグの原因が追加したオーディオ・ビデオ同期機能にあるのか、Timeline内のアセットまたはスクリプトにあるのかを簡単に判断することができます。
Wwise Time Stretchプラグインで、スローモーションとファストモーションを実現
以下に説明する手順をすべて行うことにより、多くのゲームで効果的に機能するオーディオ・ビデオ同期システムを獲得することができます。ただしプロジェクトによっては、さらなるカスタマイズ機能が必要となる特例があるかもしれません。例えば『逆コーラップス』においてはシネマティックス中に起きるエキサイティングな場面を強調するため、同期システムをスローモーションやファストモーションの機能で支える必要がありました。
オーディオ・ビデオ同期システムが作動している傍らでこの目標を達成するためには、既存のしくみに新しい機能を統合する必要があります。そこでWwise Time Stretchプラグインの登場です(リンク:Time Stretchプラグイン(Time Stretch (audiokinetic.com))。このプラグインはWwiseで再生するボイスのピッチを変えずに再生スピードを変更するもので、今回のユースケースにぴったりです。
Time Stretchを設定するためには、プロジェクトエクスプローラの階層で該当するミキサー、コンテナ、またはオーディオソースのEffectsタブを開きます。今回の例ではシネマティックスSFXのActor-Mixerを設定します。これはシネマティックスSFXの再生トラックをグローバルに担当するミキサーであり、Time Stretchで行った変更が、下のすべてのシネマティックスSFXに適用されます。ゲーム内のすべてのシネマティックスのタイムラインを制御するしくみであり、シネマティックスのタイムラインは1つずつ再生されます。(このトラックを使い、Unity Timelineを駆動します)。
プラグインの大部分のプロパティがデフォルト設定のままですが、Time Stretchプロパティを、コードに入れるRTPCを介して変化させます。
Wwiseで提供されるTime Stretch機能のY軸の範囲は、Wwiseの公式ドキュメントにある通り、25~1600です。この値は元の音の長さに対する割合を表し、25%は4倍はやい再生、1600%は16倍遅い再生を意味します。計算を単純化するため、私たちはX軸の範囲を0.25~16とするRTPC(リアルタイムパラメータコントロール)を作成しました。これは実際の再生時の、元のオーディオ速度に対する乗数の「逆数」を表しています。この数値で1を割ることにより、分かりやすい乗数が得られます。例えば1を1/4で割ると4となり、再生が4倍速となることを意味し、1を16で割ると0.0625となり、再生が16倍遅くなることを意味します。
このセットアップの唯一の欠点は、タイムストレッチの乗数が0.25~16に制限されることです。上限または下限に達した時、再生をそれ以上はやくすることも遅くすることもできません。しかしほかの多くのゲームでこれが支障となるかもしれませんが、今回の私たちの特定のユースケースにおいては、はやい動きにもスローな動きにも充分に対応できる範囲です。私たちは『逆コーラップス』のゲーム設計やアニメーションチームからの要請に応じ、0.25~4の最大乗数しか取り入れていません。
以下の例ではパラメータモディファイアの出力値を抽出する小さなラッパー関数を作成し、この機能を使用する領域にオンデマンドで適用します。
public float GetGlobalRTPC(string rtpcName)
{
int rtpcType = 1;
float acquiredRtpcValue = float.MaxValue;
AkSoundEngine.GetRTPCValue(rtpcName, null, 0, out acquiredRtpcValue, ref rtpcType);
if(acquiredRtpcValue >= 0.25f && acquiredRtpcValue <= 16.0f)
{
return acquiredRtpcValue;
}
else
{
return 1.0f;
}
}
上記の関数はRTPCをグローバルに設定するだけでなく、不正な値が検出された場合、設定予定のRTPCを無視してデフォルト値の1.0fにリセットします。
ようやくこの関数をオーディオ・ビデオの同期ロジックが起きるコードセグメントに追加する段階となりました。シネマティックスを再生するロジックと、ビデオ終了後にシネマティックスを停止するロジックの間に、以下の行を挿入して算出した値をUnity timeScale変数に代入します。
Time.timeScale = 1.0f / GetGlobalRTPC(“TimelineTimeDilation”);
以上の手順で、ゲーム中のシネマティックスタイムラインをWwiseで駆動する実装に成功しました。さらにスローモーション(最大16倍まで遅く)やファストモーション(最大4倍速)などの機能を、任意のDurationやフレームタイムに利用できるようになりました。UnityのTimelineウィンドウでRTPCを自由に設定できます。
上図はフレーム1071からフレーム1078までを0.1倍のスローモーションとした例です。この実装では倍数を再度1に設定するために、手作業でスローモーションの最終フレームの後、つまり例ではフレーム1079の後に別のRTPCを作成します。
最終的にWwiseがサウンドとビジュアルの両方を正しくスローダウンさせ、オーディオとビジュアルの同期の懸念を払拭してくれます。このアプローチによりスローモーションとファストモーションのフレーム用にオーディオアセットを作る必要が減り、サウンドデザイナーの開発時間が短縮されます。アニメーターにとってもスローモーションやファストモーションの曲線を微調整する必要がなくなります。
こちらが最終的な成果を示す動画ですが、機能していることを確認できるように、動画全体を通して意図的なフレームのスタッタリングが複数あります。Wwise Time Stretchで駆動するスローモーションを、0:23’においても聞くことができます。
注:今回引用したコードスニペットは、この記事で紹介するために改めて再構築した汎用バージョンです。基礎にあるロジックが正しく機能することを確認済みであり、プロジェクト固有のAPIコールや関数は、著作権に抵触する可能性があるため省略しました。
コメント