超現実的な3D没入型恋愛インタラクティブスマホゲーム『恋与深空』は、2024年1月18日のサービス開始以来、全世界のプレイヤー数が7000万人を突破し、コロンゲームショー2025で「最優秀モバイルゲーム賞」を受賞した。
本日開催された「Unite Shanghai 2025」イベントにおいて、『恋与深空』のエンジンエンジニアたちが初めて詳細な技術プレゼンテーションを行い、『恋与深空』の制作舞台裏を明かしました。映画級のレンダリングパイプラインや、リアルで繊細な物理効果を通じて、いかにしてプレイヤーにリアルで臨場感あふれる世界を創り出しているのかを紹介しました!
△イベント会場は満席だった
今年のUnityカンファレンスで最も話題を集めたセッションとなった
ディエジでは、知識の共有と創作が日常業務の一部となっています。この1年間で、ディエジは約40回の社内シェア会を開催し、参加した社員の累計人数は5000人に達しました!そのうち、技術関連のシェアが50%以上を占めています!発表者は各制作チームや技術部門から集まり、内容は多岐にわたる実践経験や最先端の探求を網羅しています。オープンな交流の雰囲気の中で、専門知識やアイデアが自由に流れ、最大限に刺激し合っています!
以下は、『恋与深空』の技術共有会の全内容の振り返りです:
『恋与深空』のレンダリング基盤フレームワークに関する共有内容は、主に【シーンレンダリングの最適化、照明ソリューションとパイプライン設計、影の最適化】の3つの部分で構成されています。開発過程において、私たちはUnity 2019をベースにエンジンのソースコードを大幅に修正し、独自のSRPパイプラインを開発しました。現在、Androidの正式リリース版はGLES 3.1ですが、将来的にはVulkan版もリリースし、パフォーマンスを継続的に向上させ、プレイヤーが高品質なゲームを求めるニーズに応えていきます。
1. シーンのレンダリングの最適化
シーンのレンダリング最適化において、我々は「RendererGroupRenderer」という名のシーンレンダリングシステムを開発し、各レンダリングバッチを「RenderGroup」と呼ぶようにしました。このシステムにより、以下の機能を実現しました:
- カスタム静的シーンの説明:GameObjectを削除することで、大量のGameObjectを更新する際に生じるパフォーマンスの低下を回避しました。
- CPU→GPUへのアップロード頻度の最適化:主にInstanceDataとConstantBufferのアップロードが含まれます。InstanceDataのアップロード最適化については、プロジェクト初期段階において、屋内の小規模なシーンを対象に、InstanceDataBufferを静的に生成し、BVHによる分割・クリッピングを組み合わせる方式を採用していました。プロジェクトの進行に伴い、シーンの精度要件がますます高まったため、後期にはGPU側でクリッピングとインスタンスの埋め込みを行う方式へと移行しました。ConstantBufferのアップロード最適化については、後述の【単一DrawCallのパフォーマンス最適化】のセクションで詳しく説明します。
- CPU側におけるBurst+Job Systemによる並列粗切り出し:静的オブジェクトに対しては、Burst+Job Systemを用いて高度に並行化された切り出しシステムを実現しました。同時に、粗切り出しはCPU側のみで実行し、細粒度の切り出しタスクはGPUに委ねています。
InstanceDataのデータ形式
業界で一般的に使用されているConstant Buffer形式のInstanceDataには、64KBというサイズ制限や、定数バッファの容量が小さいこと、動的インデックス化時にキャッシュブレイクスルーが発生しやすく、パフォーマンスが低下しやすいといった欠点があります。もう一つの一般的な形式は、SSBOを使用してInstanceDataを渡す方法ですが、この方法では、キャッシュブレイクスルーが発生していない場合のConstantBufferに比べて読み取り性能が一般的に劣ります。また、一部のAndroid端末ではGLES環境下で頂点シェーダー内でのSSBOの読み取りがサポートされていないため、互換性にも制限があります。また、これら2つの方式には共通の問題があります。それは、動的インデックスに依存しているため、低スペックのスマートフォンではパフォーマンスが低下しやすいという点です。
上記の問題に対し、我々は「古い酒を新しい瓶に詰める」ようなアプローチ、すなわち「Vertex Stream based Instance Data」を提案する。
- PerInstance StepのVertex StreamをInstance Bufferとして使用する;
- Vertex Fetchキャッシュを利用するため、動的インデックスが不要で、キャッシュヒット率が高く、互換性の問題もありません。
- ComputeShaderを介してInstance VertexBufferに出力することで、GPU駆動を実現する。
PerInstance StepのVertex StreamをInstance Bufferとして使用する――これは、GPUインスタンス化が登場した当初からサポートされていたインスタンス化手法であり、動的インデックスによるパフォーマンスの問題を回避できるだけでなく、SSBOの互換性問題も回避できます。また、ComputeShaderを介してInstance VertexBufferに出力することで、GLES下での互換性の高いGPU Drivenを実現することも可能です。
最後に、Unityエンジンは低レベルでPerInstance StepのVertex Streamをサポートしていないため、エンジンに対して適切なカスタマイズを施しました。その結果、上位層に対して公開されたのは、CommandBufferに追加されたDrawMeshInstancedTraditionalインターフェースであり、これには別のメッシュをインスタンスデータとして渡す必要があります。また、インスタンスメッシュ内の各データセグメントに対応する頂点セマンティクスを設定するためのインターフェースも追加しました。
GPU駆動型
Groupの数とInstanceの数に基づいて、IndirectParameter BufferとInstance Data Bufferを事前に割り当てます(ここでInstance Data Bufferは単にスペースが事前に割り当てられているだけであり、実際のデータはGPUによるカリング時に格納されます)。
同時に、各グループのインスタンスオフセットを事前に計算し、それをパラメータの「InstanceStart」項目に格納します。このプロセス全体を通じて、インスタンスバッファは1つだけバインドされます。
さらに、GPUでのクリッピング時に各オブジェクトの属性を取得するために、オブジェクトごとの情報バッファ(GroupID、LOD Distance Range、Bounds、Transformなどの情報を含む)を生成する必要があります。
- CPUによるトリミング:GPUによるトリミングを行う前に、まずCPUで粗いトリミングを1回実行し、グループ全体が可視範囲内にあるかどうかを判断します。ルートバウンディングボックスを起点として、オブジェクトのバウンディングボックスの体積の合計と、統合後のバウンディングボックスの体積との比率を比較し、閾値を下回る場合は再帰的にバウンディングボックスを分割します(主な目的は、2つのオブジェクトの距離が遠すぎて、巨大な総バウンディングボックスが生成されるのを防ぐことです)。同時にPVSを組み合わせてグループの可視性をさらに判断します。DX12のようなIndirectExecuteがないため、GPUクリッピングではインスタンス数を減らすことしかできず、グループ全体のドローコールを完全に排除することはできないからです。そのため、CPUクリッピングを通じて、完全に不可視なグループを可能な限り正確に除外する必要があります。
- GPUクリッピング:GPUクリッピングでは、1回のディスパッチにより、すべてのグループに対してオブジェクト単位で3段階のクリッピング(視錐台クリッピング、LODクリッピング、Hizオクルージョンカリング)を行います。クリッピング処理を通じて、パラメータのインスタンスカウントを1増やし、インスタンスデータを出力します。
- 影の除去:『Dragon's Dogma』で紹介されていた手法を参考に、画面の深度を影の空間に再投影して「Shadow Receiver Mask」を作成しました。Shadow Casterが投射するボリュームとこのマスクが交差しない場合、その部分の影のレンダリングを省略することで、不要な影の生成を防ぐことができます。
また、「なぜCluster/Meshletを実装しなかったのか」という点についてですが、第一にモバイル端末では基礎的なオーバーヘッドが大きいため、第二にGLES環境下でのClusterの実装には互換性の問題も存在します。これらの点を総合的に考慮した結果、1回のDrawCallあたりのパフォーマンスを最適化することを優先した方が、コストをかけずに直接的なパフォーマンス向上につながるという判断に至りました。
単一のDrawCallにおけるパフォーマンス最適化
これまでの観察から、レンダリングにおけるCPU処理時間の最適化において、多くの場合、DrawCallの数に過度に注目し、個々のDrawCall自体の処理時間を軽視しがちであることがわかりました。私たちは、DrawCallの数を減らすことはあくまで一つの最適化手法に過ぎず、最終的なCPU処理時間こそが唯一の評価指標であると考えています。
現代のモバイルデバイスとグラフィックス規格は、実のところ以前から大量のドローコールを処理できる能力を備えており、この点についてはHypeHypeエンジンチームもSIGGRAPH 2023で発表しています。彼らはiPhone 6s上で、1万個の異なるメッシュとマテリアルを用いたドローコールのテストを行い、所要時間はわずか11.27msでした。他の同等のAndroidデバイスも、基本的に60fps以上を維持できています。また、2014年にMetalが誕生した当初、GLESよりも10倍多くのDrawCallを描画できるというスローガンも掲げられていました。
11年が経った今でも、DrawCallの多さに頭を悩ませている主な原因は、PSOの切り替えが多すぎる、バッファの送信やコピー、エンジンのレンダリングロジック、そしてRHIインターフェースの呼び出しが多すぎるなど、多方面にわたるオーバーヘッドにあり、これらがすべてCPUへの負荷を増大させている。したがって、パフォーマンスの最適化においては、DrawCallの数だけに注目するのではなく、これらの要因を総合的に考慮する必要があると考えています。
- PSOの切り替え最適化:主に、各プロジェクトにおけるシェーダーバリアントの数とシェーダーの複雑さのトレードオフによって決まります。RenderGroupのレンダリングキューは、シェーダー、マテリアル、メッシュの優先順位に基づいてソートされます。また、影の処理については特別な処理を施しています。AlphaTestのないマテリアルはすべて同一のシェーダーを使用してShadow Depthをレンダリングし、影のレンダリング時のPSO切り替え頻度を低減しています。
- バッファ送信の最適化:GLES環境下では、バッファのマップ/アンマップに著しいオーバーヘッドが生じます。現代のRHIがサポートする「パーシステントマップ」は、アップロードにかかる時間を大幅に短縮できますが、メインスレッドからレンダリングスレッド、そしてバッファメモリへとデータが複数回コピーされることや、memcmpの処理を依然として回避することはできません。そこで、以下の3つの的を絞った戦略を採用し、バッファのアップロード時間を大幅に短縮しました:
- PerRendererBufferは、Rendererごとのパラメータ(オブジェクトが受ける環境光SHなど)を、Rendererオブジェクトが管理するUniform Bufferに格納し、レンダリング時に直接バインドします。
- PerShaderBufferは、マテリアルごとに変更する必要のないユニフォームバッファを対象としており、シェーダーの切り替え時に1回だけ送信されます。PerRendererBufferと比較して、PerShaderBufferはより柔軟性が高く、異なるシェーダーバリアントに対応可能です。
- PerMaterialBufferについては、SRP Batcherのコードを流用して、マテリアルごとのバッファを事前に生成し、直接バインドしました。
- レンダリングロジックの最適化:商用ゲームエンジンは、柔軟性と安定性を確保するため、レンダリング時に複雑なロジック判定を行います。例えば、Unityエンジン内部では、Drawが呼び出されるたびにまずApplyMaterial関数が呼び出され、この関数はレンダリングの前にすべてのレンダリング状態とパラメータを更新します。DrawCallの数が多くなると、かなりの処理時間がかかることがあります。そこで、以下の最適化を行いました:
- ApplyMaterialインターフェースを独立したモジュールとして分離し、マテリアルやパラメータの切り替えが必要な場合にのみ、上位層から能動的に呼び出されるようにしました;
- PerMaterialBufferを変更する場合のみ、簡略化された専用インターフェースを使用してください。
最適化の結果、同じDrawCall数において、CPUの処理時間が3分の1に短縮されました。
- RHI呼び出しの最適化:RHI呼び出しの最適化の主な目的は、Draw Primitive以外のグラフィックスAPI呼び出しを削減することです。具体的な最適化内容は以下の通りです:
- 同じストライドを持つVertex BufferとIndex Bufferを統合し、Draw CallごとにVB/IBをバインドする処理を回避することで、処理時間を15%短縮;
- Resourceに変更がない場合は、DescriptorSetの設定をスキップすることで、処理時間をさらに30%短縮できます。SetDescriptors自体の処理時間が長い場合、Descriptorを切り替えると次回の描画にかかる時間がさらに増加します。この点については、Armのベストプラクティスガイドでも紹介されています。
低スペックのAndroid端末で、5000回のDrawCallにかかる時間を測定しました。エンジンのネイティブレンダリングを使用した場合、レンダリングスレッドの所要時間は34.79msでした。バッファへのコミットとレンダリングロジックを最適化したところ、所要時間は22.97msに短縮されました。さらにRHIの呼び出し回数を最適化した結果、所要時間は11.8msへと大幅に短縮されました。最終的に、DrawCallの数を変えずに、CPUの所要時間を元の1/3以下に削減することに成功しました。
その他の最適化の試み:ベンチマークシナリオのテスト結果
また、次のようなRHIの新しい機能も試してみました:
- Multi-Draw Indirect(MDI):対応デバイスにおいて顕著な最適化をもたらし、GPUのオクルージョンカリングによって空のDrawCallが発行される可能性をある程度軽減します(CPU側での発行回数が減少します)。
- Bindless:しかし、Bindlessのパフォーマンスは期待外れであり、最新のAndroidデバイス上でも原因不明のパフォーマンス低下が見られました。MDIとBindlessを組み合わせることで、ほぼ1回のDrawCallですべてのオブジェクトをレンダリングすることは可能ですが、CPU処理時間はバッチ処理を行わない場合よりも長くなってしまいます。これは、DrawCallの数を過度に重視した結果生じた反面的な事例でもあります。もちろん、今後のモバイルチップではBindlessへのサポートがさらに向上することを期待している。現段階では、代替案としてUnityのTexture Streamingをベースに、フィードバック不要のSVTシステムを拡張して試みており、この案もまだ検証段階にある。
ベンチマークシーンのテスト結果によると、RenderGroupRendererを使用した場合、インスタンス化を行わない従来のレンダリングと比較して、DrawCallが3分の1に減少し、レンダリングスレッドの処理時間は4分の3も大幅に短縮され、メインスレッドの処理時間も3分の2に短縮されました(C#の処理時間は増加しましたが、エンジンのネイティブクリッピングとGameObjectの更新にかかる時間が短縮されたため、全体としては大幅な最適化が実現されました)。
2. 照明計画
照明計画
- フォワードレンダリングパイプライン:
本プロジェクトでは、フォワードレンダリングパイプラインを採用することにしましたが、これには以下の多角的な考慮事項があります。まず、フォワードパイプラインは、美術面での複雑かつ変化に富んだ要件に対応する上で優位性があり、マテリアル属性の追加によってGBufferが肥大化するかどうかを心配する必要がありません。
また、従来のディファードパイプラインは、モバイルプラットフォームにおいて帯域幅の面であまり適していない。一方、OnePassDeferredには柔軟性の面でいくつかの制限があり、例えばRenderPassの途中でRTのサイズを変更できないほか、現在の位置以外のピクセルコンテンツを取得することもできない。
GLES環境下では、FrameBufferFetchの互換性にも問題があります。チップによってサポートされるフェッチRTの数が異なり、1つのRTしかサポートしていないものもあります。その場合はPLSを介して実装する必要がありますが、テストの結果、PLSのパフォーマンスは芳しくありませんでした。
また、エンジンに標準搭載されているオブジェクトごとの4つの光源では、大きなオブジェクトには不十分であるため、Forward+を試してみました。しかし、Forward+は旧型のデバイスでは処理に時間がかかりすぎます。タイルごとの最大光源数を制限した場合、カメラアングルが変わるとタイル内の光源数が制御できなくなり、上限を超えると表示上の不具合が発生してしまいます。
これらの問題を解決するため、水平方向の世界空間をタイル分割する方式を採用しました。デフォルトでは1タイルあたり2メートルとし、カメラの正面に配置し、1タイルあたり最大4つの光源、128×128のインデックスマップを設定しています。この分割方法により、タイル間の光源の重なり状態が安定し、制作時に制限超過の問題を早期に発見しやすくなります。
- Vulkanのバージョン管理パイプラインの改善
将来のVulkanバージョンのパイプラインに、Subpassに基づくLight Pre-Passを追加しました。
Pre-Z Passでは、簡易なGBuffer RTを出力し、それを保存します。ローカルライトにはフレネル効果のない簡略化されたPBRモデルを使用しているため、GBufferにスペキュラーやアルベドを出力する必要はなく、法線、ラフネス、および一部の特殊なマテリアルIDや属性情報をRGBA8のGBufferにパックするだけで済みます。その後、Deferred Shadingに類似した光源ボリュームのレンダリング処理を実行し、ジオメトリの照明結果をTile Memoryに保存します。
その後、シェーディングパスにおいて、オブジェクトを再度描画し、これらの照明情報を取得します。さらに、レンダリング時に得られたアルベドなどのマテリアル属性を組み合わせることで、最終的な照明結果を導き出します。
TAAに必要なMotionVectorをRGBA8でエンコードします。R + G == 0は有効な速度がないことを表しており、これにより、速度を出力しない一部のマテリアルでは、BAチャネルに他の情報を格納できるようになります。
例えば、単純で大量の植生については、MotionVectorのBAチャンネルにそのUV情報を保存しておきます。そうすることで、シェーディングパスにおいて、後処理でGBuffer内のジオメトリ情報とMotionVector内のUV情報を取得するだけで、植生のマテリアル表現を再現することができます。
Vulkanバージョンのパイプラインの流れは、おおむね以下の通りです。まず、PreZ PassからDepth、GBuffer、MotionVectorが出力され、次に影のオクルージョンカリングが計算されます。続いてシャドウの深度レンダリングを実行し、その後、AOやスクリーンスペースSSSなどの計算が行われます。その後、NativeRenderPassに入り、SubPass内でShadowMaskやLight Pre-Passの計算、および通常のシェーディングパスを実行します。最後にRenderPassを終了し、その他のポストプロセスパスを実行します。
Vulkanのバージョンパイプラインの改良には一定の限界もあります。例えば、Light Pre-Passはデフォルトのライティングモデルのみに置き換えることができ、より多くのGバッファチャネルを必要とするライティングモデルの場合は、依然としてForward+を採用する必要があります。
ただし、光源ごとに設定可能なパラメータを用意しており、特定の光源に対して「Standard Lit Model」を強制的に適用し、すべてのマテリアルを統一して処理することができます。これにより、ライティングモデルの精度を多少犠牲にする代わりに、同じタイル内のピクセルが4つ以上のライトの影響を受けるようにすることが可能です。
- おはようございます
拡散GI(Diffuse GI)の部分では、比較的伝統的なライトマップ+ライトプローブ方式を採用しました。ライトマップには間接光の情報のみが保存され、ライトプローブについては、通常のオブジェクトごとの単一サンプリングポイント方式に加え、各オブジェクトに複数のサンプリングポイントを設定し、線分、三角形、または四面体の重心座標に基づいて補間を行うマルチサンプリングポイント方式も提供しています。
以下の2つの比較図のうち、左の図はサンプリングポイントが1つの場合の効果を示しており、ボックスの底面には均一な環境光が当たっています。一方、右の図はサンプリングポイントを2つ使用した場合の結果であり、左右で異なる間接光が当たっていることがわかります。
スペキュラーGIに関しては、主にAABB補正を施したリフレクションプローブを採用しています。また、特定の床面や水面については、平面反射プロキシも使用しています。これは、おおむね反射を描画するために特化したHLODと捉えることができます。
さらに、『God of War』の手法を参考にし、リフレクションプローブのキューブマップを正規化しました。具体的には、キューブマップのピクセルに基づいて環境光のSH係数を生成し、キューブマップ内のピクセル色をその方向の環境光で割ることで、正規化されたキューブマップを取得します。実際のレンダリング時には、各ピクセルが反射方向から受ける実際の環境光の強度とCubeMapのピクセル値を乗算することで、反射色を復元します。
この手法の利点は、たとえ多数のオブジェクトが同じリフレクションプローブからサンプリングされたとしても、領域ごとに異なる明暗の差を生み出すことができる点にある。
3. シャドウの最適化
機能設計
当社のシャドウシステムの基本的な設計は以下の通りです:
- レベル3のCSM+キャラクターのクローズアップシャドウ/マルチキャラクターのPOSM:レベル3のカスケードのCSMと、レベル1のキャラクター専用のクローズアップシャドウ。特定のマルチキャラクターシーンでは、POSM(オブジェクトごとのシャドウマップ)が使用される。
- 2つのコーンライトの投影に対応しています;
- ScreenSpaceShadowMask:上記の影の処理結果をすべて、RGBA8のScreenSpaceShadowMaskに出力します。
- R:方向性シャドウ、G:ローカルシャドウ 1、B:ローカルシャドウ 2、A:AO:Rチャンネルには主光のシャドウが保存され、GチャンネルとBチャンネルにはコーンライトのシャドウが保存され、AチャンネルにはAO情報が保存されています。
距離による除外
まず、簡単な距離カリングを行い、シャドウ距離に基づいてScreenSpaceShadowポストプロセスで三角形の頂点の深度値を修正した後、ZTest Greaterを使用してレンダリングし、シャドウ距離外のシャドウ計算を排除しました。
影の計算では深度のサンプリングが必要となるため、Test用とSample用の2つの深度バッファが必要となります。NativeRenderPass内で、Memorylessの深度バッファをコピーしてTest用に使用し、余分な読み書きの帯域幅を極力抑えるようにします。
半影領域の検出
半影領域の検出機能を追加しました。まず1/4の解像度でPCFを1回計算し、その後、フル解像度のシャドウパスにおいて1/4のマスクでサンプリングを行い、シャドウ値が中間領域にあるピクセルのみに対してフル解像度のPCFを実行することで、画質を維持しつつ計算量を削減しています。
この処理を行った後に、一部のピクセルにおける詳細な検出精度が不正確になる問題を回避するため、1/4バッファ内のPositionの偏微分と、フル解像度のGatherから得られる4つの深度値に基づいて、それぞれ2組の法線を計算します。法線間の角度が閾値を超える場合、低解像度のピクセルは信頼できないものと判断し、強制的にフル解像度のPCFを実行します。
以下はシーンのデバッグビューです。赤い領域は半影領域と判定されており、これらのピクセルのみ、フル解像度のPCFが実行されます。
ピクセル単位のバイアス
我々は、Receiver Plane Depth Biasアルゴリズムを用いて、ピクセル単位のシャドウバイアスを実装した。その原理も比較的単純で、まず画面空間のシャドウ座標の偏微分に2次元連鎖法則を適用し、シャドウ空間の偏微分を算出する。
偏微分とPCFのサンプリングオフセットを用いて、バイアス値を算出することができます。中心点については、1ピクセル分のオフセットを加えたバイアス結果を初期バイアスとして設定しました。
下図は、固定バイアスとピクセルごとのバイアスの比較結果です:
左の図では固定のバイアス値を使用しており、ボックスの底部に光漏れ領域が見られ、また照明方向に対してほぼ垂直な表面には、自己影によるアーティファクトが一部生じていることがわかります。ピクセルごとのバイアスを適用した後(右の図)では、偏微分が大きい領域にのみバイアスを追加するため、ディテールの投影を維持しつつ、自己影によるアーティファクトの問題を解決することができます。
ただし、画面の奥行きに不連続性がある場合、ピクセル単位のバイアス計算で誤った結果が導き出され、光漏れが発生する可能性があります。この問題を解決するには、グラフィック担当者がバイアスの最大値と最小値を手動で指定する必要があります。
キャッシュされたシャドウマップのスクロール
DrawCallの多いシナリオに対しては、Scrolling Cached Shadow Mapも試みました。具体的には以下の通りです:
- CSMの深度をキャッシュし、前後の2フレームとも陰影錐体内に完全に収まっているオブジェクトについては、前フレームのCSMを現在のフレームの投影位置に転送することで直接陰影深度を取得し、オブジェクトを直接レンダリングすることを回避する。
- Scrollingは最後のカスケードレベルにのみ適用されるため、カスケードの範囲が狭い場合、多数のオブジェクトが視錐と交差することになり、最適化効果が制限されてしまう。
- 数フレームおきにキャッシュを更新し、帯域幅への負荷を軽減する。
将来的には、Local ShadowMap Atlasやキャッシュ機構のサポートも予定しています。2つ以上のローカルライトによる投影に対応し、光源の画面占有率に応じてShadowDepthの精度を動的に調整するほか、遠距離のローカルライトに対しては静的キャッシュのサポートも導入する予定です。
1. キャラクターのライティング案
キャラクターのライティング設定において、皆さんは多かれ少なかれ、以下のような問題に直面したことがあるのではないでしょうか:
これらの問題を分解してみると、以下の3つの要件にまとめられます:
上記の要件に基づき、具体的なキャラクターの照明プランを設計しました。
照明は【直接光】と【間接光】で構成されていますが、通常は平行光が1つだけ存在します。これを、私たちは「メインライト」と呼ぶのが一般的です。メインライトはシーン全体を通常通り照らしますが、キャラクターを照らす際にはその方向を維持したまま、ポストプロセスボックスのような手法を用いて、メインライトの色と明るさを上書きします。具体的な実装方法は以下の通りです:
- シェーダーにキャラクターのメインライトの色をもう1つ渡す。これにより、キャラクターのシェーダーがメインライトを取得する際、その色はキャラクターのメインライトの色となる。
- キャラクターに、輪郭光として使用する、影を落とさない追加の平行光を1つ設定しました;
- 同時に、キャラクター用に2つの追加光源を確保しています。追加光源は、任意の点光源とスポットライトの組み合わせとすることができ、範囲内のキャラクターやシーンオブジェクトを通常通り照らすことができます(2メートルのマスには最大4つの追加光源しか配置できないため、2つの光源をキャラクターに割り当てています)。
間接光については、UnityのLightProbeシステムを使用してプローブを作成し、間接光をプローブに保存する部分については独自に実装しました。シーン用のプローブとキャラクター用のプローブを別々のセットとして分け、それぞれ個別に保存・使用しています。
環境光のハイライトには同じ反射プローブを使用しますが、一部の特殊なマテリアルについては、マテリアル上でCubeMapを入力し、環境の反射プローブを上書きするオプションを用意しています。
これらのキャラクターに影響を与える照明情報をScriptableObjectに保存し、ライトアーティストが調整した後、テンプレートとして保存します。下の右図は、キャラクターのライティング設定として保存された情報です。これには、前述の2つの平行光、2つの追加ライト、プローブによって保存されたSH、さらにポストプロセスボックスで追加調整可能な情報や、カスタム反射プローブの使用有無などが含まれています。
最後に、マネージャーを用いてスタックのような方式で管理します。ここでスタック方式を採用したのは、具体的な使用方法と密接に関連しているためです。通常、新しい照明設定を読み込む以外に最もよく使われる機能は、前の照明設定の効果を復元することであるため、スタック方式を採用しました。
ここまでで、このプランは「キャラクターとシーンの分離」「リアルタイムでの切り替え」「カスタマイズ可能な保持テンプレートのサポート」といった機能を備えるようになりました。最後に、照明プランの切り替えをシナリオエディタ上のイベントアクションとして定義し、照明アニメーションの連続再生に対応させました。
接続可能な光の効果は以下の通りです:
下の図は、このプロジェクト専用のシナリオ編集ツールです。基本的に、照明や影に関連するすべてのパラメータ、および一部のポストプロセスや物理エフェクトは、このシナリオエディタで制御することができます。
2. 影のクローズアップ
光と影は常に切っても切れない関係にあります。前述の通り、当社のシャドウ方式は3段階のCSMとクローズアップシャドウを組み合わせたもので、その実現原理としては、キャラクターの指定可能なボーンを球心として、指定された半径の球を構成し、この球を用いてシャドウマップを構築・生成します。スクリーン空間シャドウの計算時には精度比較を行い、このシャドウマップとカスケードシャドウのうち、精度が高い方をそのピクセルのシャドウマップとして採用します。
以下のGIF画像からわかるように、キャラクターはもともと全体がメインライトの影の中にありましたが、クローズアップシャドウを有効にすると、メインライトによって正常に照らされるようになります。これは、クローズアップシャドウによって近側のクリッピング平面が変更されたためです。つまり、クローズアップシャドウは個別にパラメータを調整可能なシャドウマップであり、具体的なパラメータには、近・遠クリッピング面、最大距離、使用する光源、そして往々にして最も頭を悩ませるバイアスなどが含まれます。
3. 肌のディテール
肌に関しては、細部の表現に焦点を当てます。具体的には、赤面効果と発汗効果を例に挙げます。
- 赤面効果
一般的に、顔が赤くなる過程は徐々に変化し、部位によって赤くなる程度が異なるものです。例えば、多くの人は顔が赤くなる際、まず耳から赤くなり、次に頬へと広がり、まれに顔全体が赤くなることもあります。
このプロセスを再現するため、画面をより生き生きとリアルにするために、以下の手法を採用しました:
- 手描きマスク:マスクテクスチャに基づいて、赤らみの範囲、色のグラデーション、および強度を制御します。
- マルチチャンネル独立:顔、耳、鼻など、各部位の赤み効果を個別に調整可能;
- プリセットの変化過程:赤面する過程が各チャンネルごとに記録され、自然な感情表現を実現しています。
- 発汗効果
当ゲームでは「運動サポート」機能を提供しており、主人公が運動トレーニングのサポートを行うため、それに応じた汗をかくエフェクトも必要となります。具体的な実装は、主に以下の3つの側面から行われます:
- マテリアルとパーティクルの連携:マテリアルシェーダーが肌の表面の光沢と潤いを再現し、汗の粒子のエフェクトについては、肌に付着した状態をマテリアルで表現するか、VFXで表現するかを選択可能です。
- マスクによる発汗部位の制御:マスク画像を使用してマテリアルの発汗部位を特定し、発汗効果のリアリティと芸術性を高める;
- データの自動転送:メインパラメータの変更に応じて、マテリアルおよびパーティクルのパラメータが自動的に更新されます。
下の図は、具体的な計算方法と最終的な結果のイメージを示したものです。
△汗の滴の生成位置を計算し、汗の滴の位置のラフネスを調整する
△UVグリッドIDを用いて乱数を生成する
△汗の滴が落ちる軌跡を再現
△運動サポートシステムによる発汗効果
物理エフェクトに関する共有内容は、主に4つの側面、すなわち、布のシミュレーションの実装、リアルタイムパフォーマンス制御、Unity DOTSに基づく開発、および衝突検出モジュールに焦点を当てています。
1. 布のシミュレーションの実装
プロジェクトにおける特定の課題を解決するため、社内で独自の布地シミュレーションシステムを開発しました。
骨格ベースの布シミュレーションシステム:StrayCloth
StrayClothは、XPBDとサブステップを組み合わせたシミュレーション手法を採用している。PBDと比較して、XPBDの利点は反復回数や時間ステップへの依存から解放される点にあり、サブステップと組み合わせることで、解法の収束性を著しく向上させることができる。
特筆すべき点は、骨格をシミュレーション用の粒子として使用していることであり、つまり各粒子は位置情報に加えて回転情報も持っているということです。
具体的なサブステップの実装においては、さまざまな性能負荷のシナリオに応じて、1/200~1/300の範囲で動的なサブステップ時間を採用しています。また、シーン内の移動オブジェクトに対して実行時補間を行うことで、衝突の効果をより安定させることができます。実際、運動補間はパフォーマンス上のオーバーヘッドがそれほど大きくないものの、静的パーティクル、コリジョンボディ、風場など種類が多いため、実装にあたっては非常に手間がかかります。
なぜプロキシメッシュではなくスケルトンを使用するのでしょうか?主な理由は以下の3つです:
- 『恋与深空』は、ストーリー、戦闘、着せ替えにおいて複雑な表現が求められるが、スケルトン方式を用いることで、アニメーションの移行や演算をスムーズに行うことができる;
- 制御性の要件やモバイル端末の性能上の制約を考慮すると、スケルトン方式は美術面での自由度をより高く保つことができます;
- スケルトンと制約を使用することで、メッシュに似た構造を構築し、同様の効果を得ることができます。
骨格拘束案
既存のスケルトン・クリッピング手法において、スケルトンの拘束は通常、ローカルおよびグローバルな形状拘束に基づく実装が採用されています。これはシンプルで迅速ですが、明らかな欠点もあります。すなわち、クリッピングシミュレーションに用いた場合、その効果はカートゥーン調になりがちで、『恋与深空』が追求する3Dリアリズムスタイルには適合しません。さらに、GlobalとLocalの2つの曲げ強度パラメータが存在するため、パラメータ調整が直感的ではなく、美術チームによる調整や、異なるシーンでの効果の統一が困難である。
そのため、骨格拘束の方式としては、コッセラ・ロッドに基づく骨格拘束を採用しました。その利点としては、以下の点が挙げられます:
- より自然な仕上がりとなり、『恋と深空』全体の写実的なアート表現のスタイルに近づいています。
- パラメータの調整がより直感的になり、3つの軸方向の強度が分離されているため、スカートのシミュレーションなど一部の場面では、異方性の曲げ強度を用いてスカートの下地(ホーン)の効果を近似することができます。
- 髪のシミュレーションではそのまま流用できるため、髪と服で同じ制約セットを共有することも可能です。
具体的な効果については、最新の日カードの実績をご参照ください:
布とキャラクターの接続
布地とキャラクターの接続は、主に以下の2つの方法で行われます:
- 階層:静的スケルトンは、キャラクターのスケルトンアニメーションの影響を直接受け、階層関係に基づいて移動します。
- この方法は比較的簡単で、剛性が高い接続部分では良好な結果が得られます。しかし、複数の骨が交差している部位や、ある程度の伸縮が生じるような比較的複雑な部位(例えば、肘、肩、腰など)では、生地とキャラクターが分離してしまう現象が生じやすくなります。
- 吸着:静的なパーティクルは、キャラクターモデルのアンカー三角形によって制御されます。メッシュを並列でベイクし、重心座標を用いてフレームごとに計算・更新を行います。
三角形が退化した特殊なケースについては、三角形の頂点にあるスキンボーンの変換を用いて、重み付けされた均等化を行い、静的粒子のトランスフォームを更新する。
衝突シナリオ
コリジョン処理に関しては、シーンのコリジョンにおけるブロードフェーズの管理にダイナミックBVHを使用し、各キャラクターをサブツリーとして扱い、その内部のコリジョンオブジェクトをサブツリーノードとして含めています。
また、ロールID、共有可視性、コンポーネントタイプという3つのルールを通じて、異なるロールやコンポーネント間の衝突ルールの共有管理を実現しています。
narrow phase では、コンタクトを直接生成するのではなく、衝突体のペアをキャッシュし、substep において具体的に処理を行います。これは、採用している substep の利点により、多くの場合、DCD を直接使用することで、高速運動時に生じがちな貫通の問題を回避できるため、ccd や predictive contact などの操作を導入する必要がないからです。
- Mesh Colliderの実装
plane、capsule、boxなどのパラメトリックなジオメトリコライダーについては、粒子やエッジとの衝突を比較的簡単に処理できます。一方、肩や首、胸や背中などの複雑な部位では、パラメトリックなジオメトリではキャラクターモデルの形状を正確に表現することが難しく、表示上、貫通現象が発生しやすいため、これらの部位ではメッシュコライダーを多用しています。
しかし、メッシュコライダーは不規則な凹形状であるため、閉じていない場合もあり、パラメトリックなジオメトリに比べて正確な衝突効果を実現するのは比較的困難であり、特にモバイル端末ではその傾向が強いため、三角形の粗い検索方法としてハッシュテーブルを採用し、キャッシュされた近傍の三角形の結果と組み合わせることで、反復処理の開始前に一度だけ粒子と三角形の衝突ペアを生成し、その後の反復処理において、粒子が三角形の範囲内にあるかどうかを判定します。もし三角形の範囲外にある場合は、モデルの三角形間の隣接関係に基づいて、ステップ幅を制限した三角形検索を行い、最も近い三角形を取得し、その結果をキャッシュして次回に利用します。
以下のGIF画像は、プロジェクトにおける具体的な動作例の一部ですが、動作が比較的安定していることがわかります。
フェイス・コライダー
フェイシャルコライダーは、特殊なメッシュコライダーと見なすことができます。基本的なメッシュコライダーに比べ、その形状は比較的固定されており、滑らかです。モデルの中心から見て、三角形の重なりは基本的にないため、16×16のキューブマップを使用して各方向の三角形を事前に計算し、これにより、コリジョン計算時に隣接する三角形を迅速に検索できるようになります。
層間衝突
ゲーム内における布のシミュレーションにおける自己衝突の処理は、最も難しい部分です。パフォーマンスの観点から、我々は以下の解決策を提案します:
- 検索高速化構造としての空間ハッシュの利用
- 美術によってあらかじめレイヤー分けされており、レイヤー間の粒子と三角形の衝突のみを考慮する
- レイヤー間の干渉を防ぐため、粒子と三角形の単一法線方向での衝突のみを計算する
グラフィックス処理の段階で事前に生地をレイヤー分けし、これらのレイヤー間の衝突のみを考慮します。検索を高速化するためにハッシュテーブルを使用し、レイヤー間で詰まる現象を防ぐため、単一法線方向の衝突のみを考慮します。すでに貫通している場合はスキップし、後の処理で修正するようにします。
実際の運用では、前回のサブステップにおける粒子の位置を用いて現在の粒子の位置との衝突判定を行っています。これにより、データを簡単に分離し、依存関係を回避することができます。
層間透過分離
レイヤーの衝突においてすでに貫通している部分については、布の絡まりを解く手法を参考に、軽量な解決策を採用しました。これは、布をレイヤー分けし、布の固定点から出発して、各レイヤーのエッジと三角形の交点を計算するものです。アセットの構造は必ず均一なメッシュであるため、メッシュの交点を用いて他の粒子の三角形を比較的簡単に推測できるため、最終的に貫通した粒子と三角形のペアにスプリング拘束を適用することで貫通問題を解決しています。実際の運用では、サブステップの影響により貫通の確率は比較的低いため、パフォーマンスへの負荷を軽減するために、フレーム単位およびブロック単位での実行を採用しています。
2. ライブパフォーマンスの制御
『恋与深空』のストーリー展開における物理演算の大部分は、カットシーンを通じて実現される各種物理効果の制御と調整に依存しています。当社のツール開発チームは、非常に強力なカットシーンツールを開発・保守しており、それを基盤として、物理効果を具体的に調整するためのさまざまな機能トラックを開発しました。
こちらは、当社の「動卡」における「Cutscene Physcs Track」の例です。美術チームのメンバーが画面表現に非常に細部までこだわっているため、物理トラックの設定全体が非常に複雑になっていることがお分かりいただけるでしょう。
△カットシーン・フィジックス・トラックの例
SmoothBlendPose トラック
演技において、非常に頻繁に見られる問題の一つが、動作の瞬時の切り替えに伴う物理的な揺れであり、ストーリー展開の演技でも衣装替えの場面でも、よく発生します。
我々は、初期の物理姿勢を記録し、切り替え時に初期姿勢と現在の姿勢の間で姿勢補間計算を行うという、比較的汎用性の高い手法を開発しました。これにより、揺れを大幅に軽減することができます。もちろん、これには多少の処理時間(通常は数十ミリ秒程度)がかかりますが、ほとんどの場合、許容範囲内です。また、補間回数や補間のステップサイズなどのパラメータを用意し、アーティストが実際のニーズに応じて調整できるようにしています。
ポーズ追跡
もちろん、SmoothBlendPoseには限界があり、特にストーリー展開における複雑なカットチェンジでは、完全に滑らかな動きを保証することはできません。また、より直接的な解決策として、特定のフレームの物理状態をオフラインで直接保存し、再生時にその保存された物理状態を布に直接適用する方法も提供しています。これにより、カットによる問題を完全に回避することができます。
パラメータトラックの編集
単一の物理アセットだけでは、ストーリー中のさまざまなシーンにおける表現を満足させることは困難です。例えば、布地を柔らかくしたり硬くしたり、ダンピングを強めたり弱めたりしたい場合などがあります。当ツールでは、パラメータを編集するためのトラックを提供しており、このトラックを通じてパラメータをリアルタイムで編集・変更できます。ほとんどのパラメータは上書き可能であり、ごく短いフレーム範囲に対して非常に手軽に修正を行うことができます。このパラメータ変更機能は、特殊なエフェクトの作成にも活用できます。例えば、アニメーション画像において、拘束パラメータの編集を利用して実現される「破断」エフェクトなどが挙げられます。
アニメーション・トラック
物理演算の効果だけでは、実際には画面全体のあらゆる側面を表現するには不十分であり、多くの場合、表現上、アニメーションと物理演算を組み合わせてインタラクションを実現する必要があります。私たちはアニメーショントラックを用いて、アニメーションと物理演算の連携と融合を実現し、異なるフレーム範囲における表現を細かく制御しています。実際の制作プロセスにおいて、DCC内での動作とエンジン上での最終的な挙動には大きな違いがあります。例えば、エンジンのリアルタイムリグシステムで修正を行った後、アニメーションが他のオブジェクトと貫通してしまう場合があります。そのため、アニメーションの統合に加え、物理的な衝突効果を重ねることで、こうした貫通現象を回避しています。
このGIF画像では、ネックレスの物理演算とアニメーションの相互作用の効果を示しており、物理演算からアニメーションへの状態切り替えや、異なるアニメーション間の切り替えなどが含まれています。
コライダー・トラックとウィンド・トラック
「Collider Track」と「Wind Track」を使用すると、カットシーン内でコリジョンボディや風場を動的に生成・削除することができます。シーンの要件に応じて、コリジョンボディや風場の状態を柔軟に変更可能です。キャラクター、パーツの種類、およびファブリックのレイヤーグループ分けを活用することで、影響を与える対象の範囲を詳細に制御できます。
さらに、衝突オブジェクトや風場の軌道のパラメータの大部分には、アニメーションフレームによる制御を追加することができます。これには、衝突オブジェクトの形状やサイズ、風場の方向、範囲、強度、乱流などが含まれ、美術担当者が物理効果を適切に管理し、変化を正確に制御できるようになっています。
このGIF画像には、軌道カプセル本体や風場の様子を示す例がいくつか含まれています。
3. Unity DOTS に基づく開発
仕事 + バースト + 数学
DOTSは非常に強力なツールであり、C#レイヤーだけで高性能なマルチスレッド開発を実現できます。当社の物理システムはDOTSを用いてC#レイヤー上で完全に構築されており、機能のイテレーションやデバッグが非常に容易です。現時点では、最大2000以上のボーンパーティクルのシミュレーションに対応可能です。
もちろん、プロジェクトにおいても、性能をさらに向上させるために、いくつかの最適化を重点的に実施しました。
求人情報を非表示にする
シミュレーションにおけるジョブの数と依存関係は固定されており、ジョブデータも頻繁に変化することはありません。通常、1フレーム内で同じ数と依存関係を持つジョブグループが繰り返し実行されます。Unity Jobsでは、タスクを開始するたびにジョブを新たに作成する必要があります。事前にタスクを開始することでこの問題を緩和することは可能ですが、それでもメインスレッドがブロックされてしまいます。さらに、ジョブの実行完了後にはclearを行う必要があります。以上の観察に基づき、我々は「Cache Job」というソリューションを開発しました。これは、ジョブデータを事前に作成しておき、実行のたびにそれを再利用することで、毎回ジョブを新規作成することに伴うパフォーマンス上のオーバーヘッドを回避するものです。
実装は比較的簡単です。これは専用の構造体であり、特定の使用シナリオのみを考慮しているためです。キャッシュジョブを格納するためにAtomic Queueを追加し、具体的なジョブはfetch and add arrayを使用して格納しています。右側は、ワーカーがキャッシュジョブを実行するフローの概念図です。
ネオン・イントリンシクス
Burstは、プラットフォームごとに高性能なSIMDコードを生成し、Burst Inspectorで非常に簡単に確認することができます。Burst Inspectorでの確認や実機テストを経て、状況によっては、Arm Neon Intrinsicsを手書きすることで、さらにパフォーマンスを向上させることも可能です。
ここでは、ベクトルに0より大きい要素が存在するかどうかを判定する実装例を示します。
Dot(float4)
ドット積については、3つの方法をここに挙げました。テストケースにおいて、mathematics関数と比較して、NEONイントリニクスを使用することで約30%のパフォーマンス向上が得られます。対象機種がarmv8.2に対応している場合は、新たに追加された規約加算命令を使用することで、さらにパフォーマンスを向上させることができます。一般的に、現在市場に出回っている主流の機種のほとんどはarmv8.2に対応しています。
Transpose(float4x4)
転置計算に関しては、mathematicsが生成したアセンブリコードのパフォーマンスが非常に低いことがわかります。NEONイントリンシクスを手書きで記述することで、大幅なパフォーマンス向上を実現できます。
単に転置を行うだけなら、直接「インターリーブ読み取り」を使用すればよい。ここでこのように実装しているのは、私の一般的な実用例では、4つのfloat4を転置することで、スカラー積をベクトル積に変換しているためである。
ここでは、これら2つの項目の中で比較的よく使われる例をいくつか紹介するに過ぎない。mathematicsのコードは通常インライン化されるため、具体的な最適化を行う際にはコードの文脈に応じて調整が必要となる。Burst Inspectorや実機テストを併用して、具体的なパフォーマンステストを行うことができる。
4. 衝突検出モジュール
なぜUnityの成熟した物理エンジンモジュールから離れて、新たに開発するのでしょうか?
Unity自体には、PhysXをベースとした成熟した物理エンジンが備わっていますが、Unityのこの成熟した物理エンジンを離れて新たに開発することには、主に以下の理由があります:
- 『恋与深空』には非常に多くの種類の遊び方があり、各遊び方のレイヤー設定は比較的独立しているため、それぞれ個別のレイヤー設定を維持できることを強く希望しています。
- 戦闘などの一部のモジュールでは、特別なトリガーによる開始・終了メカニズムが必要であり、これを基盤レベルでサポートできるようにしたい。また、実行フローについても、より柔軟な制御が可能であることが望ましい。
- 最後に、パフォーマンスの最適化についてもいくつか考えがあります。具体的には、衝突検出のみが必要な場合、DOTSを活用することでパフォーマンスを向上させることができるでしょうか?
『恋と深空』における実装内容は以下の通りです:
- ネイティブの衝突判定機能のすべてをほぼ実装した
- カスタマイズされたUpdateおよびTriggerロジック
- スレッドセーフなクエリインターフェースであり、上位層から負担なく呼び出すことができる
- DOTSの軽量化構造と組み合わせることで、性能テストにおいて最大で約15%の向上を実現できる
照会プロセスの例
実機上では、実際のスレッド数は4に固定されているため、メモリアロケータについては、あらかじめスレッド数に応じて割り当てておくことができ、割り当て時には現在のスレッドのインデックスに基づいて直接取得することができます。
SAHに基づく動的BVHをブロードフェーズの高速化構造として使用し、挿入、削除、および範囲外への移動の際、現在の操作ノードに隣接する数階層のノードに対して回転のバランス調整を行う。
衝突検出の機能目標は比較的抽象的であり、精度に対する要求もそれほど高くないため、性能を向上させるために、精度をある程度犠牲にして衝突検出アルゴリズムを簡略化しました。
フローのトリガー例
戦闘モジュールの要件を満たすため、特別なトリガーロジックを設計しました。トリガーの「Enter」と「Exit」は必ずペアで出現する必要があります。以下のフロー図に示すように、aがbをトリガーする関数内でbを削除すると、bとオーバーラップするすべてのコライダーがトリガーされます。この点はUnityのネイティブ機能とは異なります。ネイティブのUnityでは、トリガーロジック内でbを削除しても、他のコライダーのトリガーは発動しません。最後に、Historyカウンターを用いてコライダーのバージョンを管理することで、ロジックの再利用によって生じうる潜在的な問題を解決しています。
ぜひご参加ください、
共に技術を通じて、より多くの素晴らしいものを生み出しましょう!
原创文章,作者:游茶妹儿,禁止转载:https://youxichaguan.com/ja/archives/213129
