ほげたつブログ

プログラムとアニメーションをかじって生きてる

UE 中間ファイルとの整合性チェック

こちらは「Unreal Engine (UE) Advent Calendar 2021」6日目の記事です。✌︎('ω'✌︎ )
qiita.com

もはやアドカレでしか記事を書いてないし今回はアニメーションですらない。

前書き

UE に限らず多くのゲームエンジンはアセットを作る際に他のDCCツールで中間ファイルを書き出し、それをゲームエンジンが定義するフォーマットでインポートしてアセット化します。例えばモーションは Maya 等から fbx 形式の中間ファイルを書き出し、uasset としてインポートしますね。つまり作業を行う時は中間ファイルを更新し、同時に uasset を更新します。してくれるはずです。まさか中間ファイルだけ更新して uasset の更新を忘れたり、どちらかだけコミットするのを忘れたりする人なんていませんよね?います。3人いたら1人はやります。ではどうすれば良いでしょうか?

まず思い付くのは定期的に中間ファイルから再インポートを行うことです。uasset の更新が忘れられても Jenkins が代わりに再インポートして綺麗な状態に戻してくれます。では毎日全てのアセットを再インポートすれば解決するでしょうか?しません。解決しそうですがしません。それは再インポートにかかる時間が時として膨大になることがあるからです。数万を超える中間ファイルと向き合ったことがありますか?僕はあります。ではどうすれば良いでしょうか?

次に思い付くのが更新があった中間ファイルのみ再インポートを行うことです。数万を超える中間ファイルだろうが人の手によって日々更新される数には上限があります。仮に全員が示し合わせたかのように中間ファイルだけ更新する嫌がらせがあったとしても時間をかけずに綺麗な状態に戻すことができるでしょう。では更新があったというのはどう判定すれば良いでしょうか?

いくつか方法はありますが、信頼性が高いのは中間ファイルのハッシュ値の比較です。

shuntaendo.hatenablog.com


uasset としてインポートする際に中間ファイルのハッシュ値を保存しておき、次回の再インポート時に中間ファイルのハッシュ値と保存済みのハッシュ値を比較することで更新があったかどうかを確認することができます。ちなみに UE はインポート時に必ず中間ファイルのハッシュ値を Asset Import Data に保存してくれています。これならもし「不具合が出るかもしれないから勝手に再インポートしてほしくない」と言われてもハッシュ値の比較だけ行ってエラーがあれば担当者に通知というフローも取れますね。

また、少し特殊な事例ですが中間ファイルが複数ファイルに分かれることもあります。例えばボディモーションに物理アニメをベイクする時は、ベイクした結果をボディモーションに合成するよりも別の fbx に出力しておいて後から合成した方が何かと取り回しがしやすいです。ただし別の uasset としてインポートしてしまうとランタイムでロードされるアセットが倍になってしまうので、インポート時にボディと物理の fbx をマージしてインポートするというフローにする場合は中間ファイルが二つになります。この場合はボディと物理どちらかの fbx に更新があった場合は再インポートした方が良いので、このケースにおいてもそれぞれのハッシュ値で個別に比較ができるので問題になりません。では実際にハッシュ値の比較について見ていきます。

実装

UE5 になっても人々の記憶から消えないブルーマンの画像をテクスチャとしてインポートしました。

f:id:hogetatu:20211205012828p:plain


そしてテクスチャのハッシュ値をチェックして同じであれば true を返す関数を作ります。

bool UAC2021FunctionLibrary::CheckTextureSourceFileHash(UTexture2D* Texture)
{
    check(Texture);

    UAssetImportData* AssetImportData = Texture->AssetImportData.Get();
    check(AssetImportData);

    // 全てのSourceFileをチェック
    for (const FAssetImportInfo::FSourceFile& SourceFile : AssetImportData->SourceData.SourceFiles)
    {
        FString SourceFilePath = SourceFile.RelativeFilename;

        // 相対パス解決
        if (FPaths::IsRelative(SourceFilePath))
        {
            const FString PathRelativeToPackage = FPaths::GetPath(FPackageName::LongPackageNameToFilename(Texture->GetOutermost()->GetPathName())) / SourceFilePath;
            SourceFilePath = FPaths::ConvertRelativePathToFull(PathRelativeToPackage);
        }

        // ハッシュ値チェック
        FMD5Hash SourceFileHash = FMD5Hash::HashFile(*SourceFilePath);
        if (SourceFileHash != SourceFile.FileHash)
        {
            return false;
        }
    }

    // 全て同じハッシュ値だった
    return true;
}


適当にエディタから呼び出すようにして実行します。

f:id:hogetatu:20211205015136p:plain


無事にブルーマンチャレンジ(ハッシュ値チェック)に成功しました。

f:id:hogetatu:20211205015156p:plain


ハッシュ値が同じことを確認できたのでブルーマンを適当に黄色にします。

f:id:hogetatu:20211205015807p:plain


ハッシュ値チェックに失敗し、中間ファイルだけが更新されたことを検出できました。

f:id:hogetatu:20211205015914p:plain


後書き

今回はサンプルとしてボタンから実行するものを作りましたが、実際に運用する時は Commandlet から起動して全ての FAssetData を取得し、アセットの型によって処理を分岐させるという感じになると思います。FAssetData は GetAsset しない限りはパッケージのロードを行わないので、数万アセットだろうが現実的な速度で処理できます。

プロジェクトが大規模になればなるほどこういった小技が活きてきますね。それではこのツイートをもって締めたいと思います。


明日は @moko さんの「煙に背後のオブジェクトのシルエットを写す [前編] | Draw silhouettes of objects behind smoke Part.1」です。本記事のような薄味じゃなく濃い情報になりそうで楽しみです。