ライブ制作の課題を解決!Unityで高速化した画像比較ツール
はじめに
株式会社QualiArtsのテクニカルアーティスト室で内定者バイトをしていた中野です。 その期間中に制作したツールの紹介をします。
概要
画像比較ツールは以下の4枚で構成されたテクスチャを連番にして一つの動画にするツールです。
- 変更前のテクスチャ(左上)
- 変更後のテクスチャ(右上)
- PSNR値のグラフ(左下)
- 色差分を出したテクスチャ(右下)
制作経緯
ゲーム開発では描画に関わる開発や、構成するアセットの更新によって日々見た目が変化していきます。 たとえば「IDOLY PRIDE」にはライブが実装されていますが、これは描画システム、アニメーション制御システム、モデル、アニメーション、ライト演出など非常に様々な要素から成り立っています。 そのため、この中の1つで起きた問題によって、ライブ全体として見たときに意図しない大きな差分が生まれてしまうことがあります。 このような差分のチェックをこれまでは人力で行っていたため、コストが高く、見落としも発生してしまっていました。 この問題を解決するためにツールを開発しました。
検知したい差分
ライト・ポストプロセスが適応されていない
ライトやポストプロセスの誤動作で見た目が変わってしまいます。
動画のロードが間に合っていない or 映っていない
動画アセットのエンコード設定によっては、正しく再生されないことがあります。
シェーダーのコンパイルエラー or マテリアルが適用されていない
シェーダーのコンパイルエラーやマテリアルが見つからない時、ピンク色で表示されます。 この状態は絶対NGなので必ず検知したいものになります。
ツール制作時の課題
既に同じ課題を解決するためのツールは存在していましたが、以下のようにいくつかの問題を抱えていました。
-
処理が遅い
- PSNRグラフを作成する際にPythonのライブラリを使用して処理していましたが、単純にロードしていただけなのでマシンによって他の作業ができないような高負荷になっていました。
- PSNRグラフ描画に必要な一時データをファイルとして保存していたためディスクへのアクセスが多発して遅い
- FFmpegで4枚の画像を組み合わせてグリッド状にエンコードする処理が遅い。
-
セットアップが大変
- 使用するデザイナーのPCにPython・FFmpeg等のインストールが必要。
これらの問題を解決するためにUnityでの制作に取り組みました。
実装手順
フローとしては以下のような工程を踏んで最終的なテクスチャを作成しています。
画像の読み込み
まずUnityのプロジェクトとは別の場所にあるPNGファイルを、テクスチャとして読み込む必要があります。 これを行うために、PNG画像をByte配列として読み込みテクスチャにする処理を実装しました。
var bytes = File.ReadAllBytes("Test.png");
var texture2D = new Texture2D(1,1);
texture2D.LoadImage(bytes);
PSNR値の算出
PSNR(Peak Signal Noise Ratio)
画像の一致度を示す尺度であり、画像圧縮などの分野で用いられます。得られる値が大きいほど(高いほど)、一致度が高いことを示します。
※ 今回は圧縮した画像ではなく、2つの画像の差分を検出する指標として利用します。
MAX-輝度信号が8bit表現の場合、0~255の範囲で表現される。
PSNRの計算
[BurstCompile]
public struct ComputePSNRJob : IJobParallelFor
{
[ReadOnly] public Vector2 _texelSize;
[ReadOnly] public NativeArray<float> _sumRGB;
[WriteOnly] public NativeArray<float> _resultPSNR;
public void Execute(int index)
{
_resultPSNR[index] = ComputePSNR(_sumRGB[index], _texelSize);
}
private static float ComputePSNR(float sum, Vector2 texelSize)
{
var mse = sum / (3 * texelSize.x * texelSize.y);
return 20.0f * Mathf.Log10(1.0f / Mathf.Sqrt(mse));
}
}
PSNRグラフの作成方法
計算したPSNRの数値を、グラフにして可視化します。 Unityでグラフ・文字をテクスチャに描画するにあたってCanvasを使用しました。 まず下地となるグラフをuGuiで作成しておいて、そのグラフに1フレーム毎に折れ線・フレーム数・緑の縦線を動かしていきました。
DrawProcedural
Unityで線を描画する時にuGuiでの線描画はあまりにもコストが高いためDrawProceduralを使用しました。
Graphics.DrawProcedural - Unityマニュアル
MeshTopologyをLineStripにしてx軸をフレーム数y軸をPSNR値として適正な位置に点を打ちそれを線にしています。
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
StructuredBuffer<float> PSNRBuffer;
float4 OffsetValue;
float4 vert(uint vertex_id: SV_VertexID) : SV_POSITION
{
return float4(((float)vertex_id / OffsetValue.x - 1) / OffsetValue.z, ((PSNRBuffer[vertex_id] * OffsetValue.y) / OffsetValue.w - 1) * -1, 0, 1);
}
float4 frag(float4 input : POSITION) : COLOR
{
if(input.y < OffsetValue.w )
{
discard;
}
return float4(0, 0, 1, 1);
}
ENDHLSL
比較画像生成
[BurstCompile]
public struct PixelCompareJob : IJobParallelFor
{
[ReadOnly] public NativeArray<Color32> _srcColors;
[ReadOnly] public NativeArray<Color32> _destColors;
[WriteOnly] public NativeArray<Color32> _resultComparisonColors;
public void Execute(int index)
{
_resultComparisonColors[index] = PixelCompare(_srcColors[index], _destColors[index]);
}
private Color PixelCompare(Color srcColor, Color destColor)
{
return Saturate(Color.gray - srcColor + destColor);
}
}
4枚の画像を合体
4枚の画像を1枚にするにあたって、UnityのCanvasに4枚の空画像を用意してその画像にデータを代入して、そのCanvasをカメラでスナップしています。
最適化をするにあたって
これでUnityで実装できましたが。実行してみたところ既存のツールよりも処理時間が長いことが判明しました。
このままでは「処理が遅い」という課題を解決できていないため、最適化を行う必要があります。
以下にその手段について記述します。
PSNR値の算出
差分計算を行う際に採択した技術はBurstとJobSystemを組み合わせて高速に処理を行う方法です。 1ピクセル毎の差分計算を行うのでJobSystemで並列に処理を流しそれをさらにBurstCompileすることで高速化を実現しています。
※この処理ではテクスチャ化はせずあくまで色情報のみを読み込む事で処理の無駄を省きました。
テクスチャのロード
Unityプロジェクトの外にある画像ファイルをテクスチャとしてロードするため、Texture2D.LoadImageを使う必要がありますが、この関数がボトルネックでした。 理由としてLoadImage自体が重たいと言うのもありますが、この関数はメインスイレッドでしか動作せず複数スレッドを立ち上げる事が出来ませんでした。
そこで外部ライブラリである、StbImageSharpForUnityを使用しました。 こちらを非同期で複数スレッド立ち上げて処理を回すことで高速化を実現しました。
private async UniTask LoadImageDataAsync(int index, string[] srcFiles, string[] destFiles)
{
await UniTask.SwitchToThreadPool();
await using (var fileStream = File.OpenRead(srcFiles[index]))
{
var textureResult = ImageDecoder.DecodeImage(fileStream);
_srcImageResults.TryAdd(index, textureResult);
}
await using (var fileStream = File.OpenRead(destFiles[index]))
{
var textureResult = ImageDecoder.DecodeImage(fileStream);
_destImageResults.TryAdd(index, textureResult);
}
await UniTask.SwitchToMainThread();
}
制作した結果
もともとあったツールは冒頭でも紹介した通り以下のような課題を抱えていました。
- 処理が遅い
- セットアップが大変
今回の対応を行うことで、最適化の項目で紹介した通り処理の高速化ができ、処理負荷も削減できました。 またUnity組み込みのMediaEncoderを使用して外部ツールを使わずに動画を書き出せるようになったので、PythonやFFmpegも不要となり、セットアップのコストも削減できました。
最適化前までは20分程かかっていた処理時間が10分前後になりました。
※以前のツールでは14分程かかっていました。
カスタムパッケージ
今回制作したツールはゲームのプロジェクト仕様に依存せず、そのゲームの動画(テクスチャの連番)さえあれば使用できる汎用的なものです。 そこでUnityのカスタムパッケージという機能を使用し、QualiArts内で汎用的に使えるツールとして作成することができました。
さいごに
今回制作した画像比較ツールの実装方法・最適化について紹介しました。このツールを制作するにあたって新しい技術だったり、Unityを今までしっかり触ったことが無かったので完成するか不安でしたが、テクニカルアーティスト室の方々にサポートしていただけ無事完成することが出来ました。