インターン生が描画回帰テストツールの開発を通して学んだこと

インターン生が描画回帰テストツールの開発を通して学んだこと

インターン生が描画回帰テストツールの開発を通して学んだこと

はじめに

はじめまして。早稲田大学大学院基幹理工学研究科 修士1年の脇田知樹です。 2026年7月の1か月間、株式会社QualiArtsのインターンシップ「CA Tech JOB」に参加し、テクニカルアーティスト室(以下、TA室)で業務に取り組みました。 本記事では、インターンシップ期間中に作成した「描画回帰テストツール」と、その開発を通して学んだことを紹介します。

描画回帰テストツールのアイキャッチ

前提:描画回帰テストとは

ゲームのグラフィックは、ライティングや影、反射、ポストエフェクトといった多くの描画処理と、シェーダーキーワードやマテリアルの設定などの組み合わせで作られます。そして、その組み合わせは膨大です。開発では、新たな機能が実装されるたびに描画結果も変化していきます。

プロジェクトの運用の中で生じる変化には、大きく2種類あります。

  • コード実装・デザイナーによる調整等による意図的な変化
  • 新規で追加した処理の不具合等による意図しない変化

プロダクトの品質を保つためには、後者の変化を早期に発見することが重要です。

しかし、すべてのパターンについて、更新のたびに人手で差分を確認するのは、コストの面から現実的ではありません。この確認を自動化することで、保守・運用のコストを下げることが期待できます。

描画回帰テストは、こうした意図しない変化を自動で検知する仕組みです。あらかじめ人が確認・承認した基準画像を保存しておき、コードやアセットを変更したあとに同じシーンを撮影して、基準画像と比較します。差分が出れば「どこかの見た目が変わった」と分かります。なお、ツールが判定するのは基準画像からの差分であり、見た目の正しさそのものではありません。描画差分の考え方については、清原氏による解説記事もご参照ください。

課題

プロジェクトには、UnityのTest Runnerで動く描画回帰テストツールがありました。シーンとカメラの組み合わせごとにスクリーンショットを撮影し、NVIDIAが公開しているFLIP(レンダリング画像の知覚的な差分を評価するツール)を用いて比較するものです。単体のテストを行う用途では十分でしたが、プロジェクトで本格的に運用していく上では、実行時間や検証項目の追加コストなどの課題がありました。 加えて、テスト結果の管理や基準画像の更新など、運用面にも課題がありました。

今回の取り組みでは、課題の整理から解決策の提示、実装、運用フローとドキュメントの整備までを一通り行いました。

解決に向けて

開発当初は、「描画関連のあらゆる不具合を検知できるツール」を目指し、どのパターンを網羅すればよいのかばかり考えていました。 しかし、メンターへのヒアリングを通じて要件・優先度を整理するうちに、プロジェクトにおいて真に必要なのは網羅性ではなく、拡張性であることを理解しました。

描画の条件は膨大で、すべてを網羅すること自体が現実的ではありません。 それよりも、後から検証項目や検証環境を追加したくなったときに、簡単に対応できることのほうが重要でした。

この気づきをもとに、ツールの要件を整理しました。 拡張性・再現性・実行時間の3要件を整理した図

今回の要件は、拡張性、再現性、実行時間の3点です。

作成したツール

今回作成したツールの実行フローは以下のようになっています。

状態生成から撮影・比較・結果表示までのテストフロー

今回作成したツールは、入力として与えるテストケースを変えることで2つの使い方ができます。

実際のシーンを撮影する機能

実際にプロダクトに含まれているアセットをそのまま撮影し、基準画像と比較する使い方です。 リリースするプロダクトに意図しない見た目の変化が紛れ込んでいないかを検出できます。

実際にテストを実行すると、以下のような結果を得られます。今回は、シーンのマテリアルのプロパティを上書きし、意図的に色を変更して撮影しました。

Sponzaシーンのマテリアル変更前のスクリーンショット Sponzaシーンのマテリアル変更後のスクリーンショット

検証用シーンにはSponzaを使用しています。1

これらの画像の違いを、目視だけで判別するのは容易ではありません。

この2枚の画像をFLIPに渡して比較を行うと、次のような差分ヒートマップを得ることができます。

Sponzaシーンの変更箇所を示すFLIPの差分ヒートマップ

今回作成したツールでは、これらの画像を見やすくまとめ、どのような差分が出ているのかを確認しやすくしています。

描画差分の確認画面を表示するアニメーション

テスト後はレビュー画面で参照画像と撮影画像、差分ヒートマップを確認し、FLIPの値を見ながら判定します。意図した変更であることを確認した場合のみ、対象の基準画像を明示的に更新する運用にしました。

今回はマテリアルの色の変更という比較的わかりやすい差分でしたが、実際は発光強度のわずかな違いやピクセル単位の小さなずれ、といった人間の目ではすぐには発見できないような差分が発生することがあります。 このような差分を早期に検知し、意図しない変更の混入を防ぐことが重要です。

テストケースを生成して比較する機能

シェーダーキーワードやマテリアルのパラメータ、ライティングなどを意図的に変化させてテストケースを生成し、各ケースの承認済み基準画像と比較する使い方です。実データでは発生頻度の低い条件を人工的に作り、その条件における描画結果の変化を検出できます。

全組み合わせ(直積)を選んだ場合は、検証したい項目のすべての組み合わせについてテストケースを生成します。

たとえば、

  • Emissionの値として[0.0, 0.5, 1.0]の3通り
  • シェーダーキーワード _FOG_ON

を検証項目として設定すると

(Emission, FOG) = (0.0, ON), (0.0, OFF), (0.5, ON), (0.5, OFF), (1.0, ON), (1.0, OFF)

の6通りすべての組み合わせについてテストケースを生成します。

要件ごとの対応と結果

ここからは、先に述べた3つの要件に沿って具体的にどのような対応を行ったのかをまとめます。

拡張性

拡張性については、検証項目など、将来的に追加が想定される要素をメインの流れから切り出し、後から追加できる構成にしました。

描画回帰テストの構成と各処理を示すパイプライン

  • 検証項目 — キーワード・マテリアルのプロパティ・Volume・ライティング・時刻など
  • 撮影構図(カメラの位置・画角) — 検証に使用する画角。シーンと画角数を指定するだけで、検証に使用する構図を自動で選定できるようにしました。
  • 撮影環境の固定条件 — 撮影のたびに変化する要素を固定する処理。共通のインターフェースを実装するクラスを追加することで拡張できます(詳細は次の「再現性」で触れます)。
  • 比較方式 — 画像の比較処理。比較方式をインターフェースで抽象化し、比較アルゴリズムを差し替えられるようにしました。

再現性

ツール作成の中で特に課題となったのが、この再現性の問題です。

回帰テストは「差分が検出された=何かが変わった」という前提で成り立っています。 しかし、開発初期のツールでは、条件を変えていないにもかかわらず、撮影するたびに差分が出てしまうことがありました。

原因は、描画結果に影響する非決定的な要素(シェーダー時間や乱数など)でした。 そこで、撮影環境を記録する→固定値を適用する→元に戻す の3操作を持つインターフェースとして実装しました。

以下は、考え方を示した簡略化したコードです。

public interface ICaptureCondition
{
    void Capture();  // 事前状態を記録
    void Apply();    // 撮影用の固定値を適用
    void Restore();  // 記録した状態へ復元
}

このインターフェースを実装したクラスのインスタンスを配列に追加することで、記録・適用・復元の実行順序や後始末を一元管理できます。実行順序を明確にし、決定性を担保するためにこのような実装にしています。

public sealed class CaptureConditions
{
    private readonly ICaptureCondition[] _conditions =
    {
        new RenderingResolutionCondition(), // 解像度を固定
        new ShaderTimeCondition(),          // シェーダー時間を固定
        new RandomSeedCondition(),          // 乱数を固定
        // ← 新しい条件はここに追加
    };
}

撮影のあいだだけ環境を固定し、適用した条件を撮影後に逆順で復元するよう一元管理することで、状態を次々に切り替えても前の設定が後続の撮影に残ることを防いでいます。

実行時間

実行時間の削減については、いくつかの対応を行っています。比較処理を非同期化し、基準画像と撮影画像が完全一致した場合には、後続の処理を省略するようにしました。

完全一致判定

差分がないケースでは、基準画像との詳細な比較は必要ありません。 そこで、撮影直後にピクセルデータを独立したbyte[]へコピーし、基準画像のピクセルと比較するようにしました。完全一致なら、撮影画像のPNGエンコード、ファイル保存、FLIPでの比較を省略できます。

// 処理の流れを示す概念コード
var rawPixels = Capture().ToArray();       // 撮影直後にbyte[]へコピー
var reference = LoadReferencePixels(stateId); // 基準画像のピクセル

if (PixelsAreEqual(rawPixels, reference))
{
    // 完全一致 → PNGエンコードもファイル保存もFLIPでの比較も不要
    RecordAsMatched(stateId);
}
else
{
    // 不一致のときだけ、比較フローへ回す
    EnqueueCompare(stateId, rawPixels);
}

この比較で分かるのは「完全一致か否か」だけです。不一致だったものについては、その差がどこに・どの程度あり、許容できるものなのかをFLIPで評価します。 一次判定の後に詳細な比較を行う二段階の構成です。合否判定にはFLIP Meanを使い、閾値はシーンや構図ごとに設定します。単純な検証用構図ではMean 0.001を用い、実データのシーンでは再実行ノイズを考慮して別の閾値を設定します。

非同期比較

このピクセル比較で完全一致しなかった状態だけが、非同期の比較キューに積まれます。撮影ループは比較の完了を待たずに次の状態へ進み、比較は同時実行数を制限しつつワーカースレッドで並行して実行します。実装では、PNGエンコード自体もメインスレッドではなくワーカー側へ遅延させます。撮影条件の復元は、比較の完了待ちの途中で例外やキャンセルが発生した場合にも行われるよう、try/finallyなどで保証しています。なお、ワーカー側で行うのは、PNGエンコードや比較などUnity APIに依存しない処理に限定しています。

// 比較をワーカースレッドへ投げ、完了を待たずに次の撮影へ進む
// 簡略化のため、同時実行数の制限処理は省略
void EnqueueCompare(string stateId, byte[] rawPixels)
{
    _compareTasks.Add(Task.Run(async () =>
    {
        var png = EncodeToPng(rawPixels);   // 独自のエンコード処理をワーカー側で行う
        await CompareAsync(stateId, png);   // FLIPで比較
    }));
}

// 全撮影が終わってから、まとめて完了を待つ
// 簡略化のため、例外時の撮影条件の復元処理は省略
await Task.WhenAll(_compareTasks);

従来ツールと今回のツールで同一のテストケースを同一の開発環境で実行して比較したところ、実行時間を257.4秒から50.7秒に短縮できました。これは約5.1倍の高速化に相当します。

ツール実行時間
従来ツール257.4s
今回のツール50.7s(約5.1倍高速化)

ペアワイズによる状態圧縮

すべての軸を掛け合わせると状態数が組み合わせの数に応じて急増するため、生成方式を選べるようにしました。 すべての組み合わせ(直積)だけでなく、「任意の2つのパラメータについて、取りうる値の組み合わせがすべて最低1回は登場する」ように状態数を圧縮する方式2や、3つのパラメータについて同様に値の組み合わせを網羅する方式も用意しています。 少数のパラメータの組み合わせで不具合が見つかることもあるため、少ない撮影枚数で有効なテストケースを得ることを狙いました。2wayでは3つ以上のパラメータの組み合わせまでは網羅しないため、網羅範囲と撮影枚数のバランスを選択できます。

以下は、各パラメータが2値をとる場合の、生成方式によるテストケース数の一例です。生成器のアルゴリズムや除外条件によって値は変わります。

パラメータ数(個)直積(件)2way(件)3way(件)
532612
8256815
101,024817
1532,7681021

パラメータ数が15の場合、15個のパラメータから2つを選ぶ組み合わせは105組です。2wayでは、これら各ペアの4通りの値の組み合わせをテストケース全体で少なくとも1回ずつ網羅しながら、検証回数を大幅に減らすことができます。

直積では指数関数的に増えていくのに対し、ペアワイズ(2way)では少ないテストケース数で実行できることがわかります。

技術面のまとめ

以上の要素を組み込みながら、状態生成から撮影、比較、結果表示まで一貫して行うテストフローを構築できました。改善の余地は残るものの、今後の改善につながる土台を構築できました。

CA Tech JOBについて

CA Tech JOBでは、技術面・業務面の両方で学ぶ機会がありました。

メンターとは毎日振り返りの時間を設け、進捗の共有やフィードバック、方向性のすり合わせを行いました。その中で、毎日記録を残して振り返り、思考を言語化する習慣が身につきました。

週1回の人事面談では、業務上の悩みに限らず、今後のキャリアについても相談できました。

社員とのランチの機会では、日頃の業務の様子や、サイバーエージェントに入社したきっかけ、AI時代のエンジニアに求められるスキルについて話を伺い、さまざまなことを学びました。

社内勉強会にも参加し、学びや知見を共有する文化を体験しました。

主体的に学ぶ姿勢に応じてさまざまな機会があり、1か月間でも技術力や働き方について多くのことを学べたと感じています。

インターンシップを通して感じたこと

最後に、インターンシップを通して感じたQualiArtsの開発環境について紹介します。

特に印象に残ったのは、アーティストが表現したいものを、技術によって実現していく姿勢です。TA室では、他プロジェクトの知見も活かしながら、ワークフローやパイプライン、ツール開発を通して、アーティストの表現を支えていました。

また、課題に応じて新しい技術や社内の知見を取り入れ、改善を続ける姿勢も印象的でした。既存のやり方にとらわれず、より良い方法を探していく姿勢を身近に感じました。

さらに、担当するプロダクトについて職種を越えて意見を交わしながら、より良いものを目指して改善を続けている姿勢も印象に残りました。

おわりに

1か月という短い期間でしたが、QualiArtsでの開発の進め方や企業文化を知ることができました。 TA室・人事の皆さまをはじめ、関わってくださった皆さまに心より感謝申し上げます。ありがとうございました。

Footnotes

  1. 使用したアセットは、IntelのGPU Research Samplesで公開されている「Sponza Base Scene」と「Colorful Curtains」です。同ページでは、これらのSamplesのダウンロードにCreative Commons Attributionライセンスが適用されると案内されています。本記事の画像は、これらのアセットを用いて検証用にレンダリングしたものです。 ↩

  2. いわゆるペアワイズ法。組み合わせテストのPICTは、この考え方を実装したツールの一例です。今回のツールでは自前の生成処理を使用しています。 ↩

著者

QualiArtsのプロフィール画像

QualiArtsエンジニアブログ編集部です。

この記事をシェア

目次