Unityゲームのメモリ分布概要
メモリはゲームパフォーマンス最適化の非常に重要な側面です。特にモバイルデバイスの場合、ハードウェアデバイスが限られているが、該当ユーザーを配慮する必要があります。ゲームはPCまたはMacで開発されますが、最終的には(ほとんど)モバイルで実行されます(AndroidとiOSのみが考慮されます)。メモリが適切に制御されていない場合、OOMの原因でモバイルのOSによってプロセスを強制終了することが起こしやすくなります。
しかし実際には、異なるオペレーティングシステムでは、メモリ管理戦略はまったく異なります。プラットフォームごとに専門的なメモリ分析ツールがありますが、これらのツールは、プラットフォーム、統計戦略、さらにはシステムバージョンが異なるために統計的な偏差を引き起こします。たとえば、XCodeのMemory Reportページは、Instrumentの下の独自の統計とは異なります。
テクスチャなどのいくつかのwrapのラッピング損失もあり、そのDataメモリのほとんどはNativeに入りますが、ロジックレイヤー呼び出しのためにいくつかのClassをラップする必要があり、この部分はMonoヒープに入ります。また、System FrameworkのConstデータの一部がCleanメモリに入り、別の部分がDirtyに入ります。これは、システムによってSwapメモリにさらに圧縮される場合があります。
一、ユニティの観点からのメモリ
Unityの観点からのメモリ分析は、Unity自体によって管理されるメモリに焦点を当てています(実際には、Unityによって割り当てられておらず、管理できません)が、Unityゲームは実際にはプラットフォーム(AndroidやiOSなど)で実行する必要があります)。)。そして、Unity自体によって割り当てられたメモリの以外に、システムの共有ライブラリからの部分もあります。さらに、複雑なUnityゲームは多くのサードパーティプラグインを参照することが多く、これらのプラグインによって割り当てられるネイティブメモリはUnityの考慮を超えています。
1.1 Unityのメモリの起源
Unityは、実際には、.netのスクリプト仮想マシンを使用するC++を使用して開発するゲームエンジンと見なすことができます。Unityは、仮想メモリからネイティブ(C ++)オブジェクトと仮想マシンにメモリを割り当てます。同様に、サードパーティプラグインのメモリ割り当ても仮想メモリプールにあります。
ネイティブメモリは、モノヒープを含むすべての必要なオブジェクトにメモリページを割り当てるために使用される仮想メモリの一部です。
前述のように、モノヒープは、仮想マシンのニーズに合わせて特別に割り当てられたネイティブメモリの一部です。これには、C#によって割り当てられたすべての種類の管理対象メモリが含まれ、これらのメモリの管理対象は、ガベージコレクター(略してGC)です。
Unity内には、仮想メモリの短期および長期の割り当てニーズの管理を担当する特殊なアロケータがいくつかあります。すべてのUnityアセット(Assets)はネイティブメモリに保存されます。ただし、これらのアセットは、論理アクセスと呼び出しのためにClassに軽くパッケージ化されます。つまり、C#でTextureを作成すると、その生データ(RawData)のほとんどがNativeメモリに存在し、小さなClassオブジェクトが仮想マシン(つまり、モノヒープ)に入ります。
1.2 Reserved/Used
メモリページング(iOSでは主に16K /page)は、メモリ管理の最小単位です。Unityがメモリを申請する必要がある場合、block(数ページ)で申請します。いくつかのGC(iOSの場合は8)を実行してもページがまだEmptyの場合、ページは解放され、実際の物理メモリが物理メモリに戻されます。
ただし、Unityは仮想メモリ内でメモリを管理しているため、仮想空間のアドレスは返されないため、「Reserved」になります。つまり、これらのReservedアドレススペースをUnityで繰り返し割り当てることはできなくなります。
したがって、32ビットオペレーティングシステムでは、仮想メモリアドレスが頻繁に割り当てられて解放されると、アドレススペースが使い果たされ、システムがそれを強制終了します。
1.3 GCとメモリの断片化
Monoヒープによって申請される物理メモリは連続しており、Monoヒープがオペレーティングシステムに拡張メモリを要求するのは非常に時間がかかります。したがって、ほとんどの場合、Monoヒープは、後の使用のため、申請された物理メモリを保持しようとします。したがって、仮想空間アドレス以外に、Monoヒープによって申請されるメモリにもReservedの概念があります。
メモリのアロケーションユニットはページであるため、つまり、ページにint値のみが格納されている場合、ページは引き続きUsedとして表され、物理メモリは解放されません。もちろん、特定のメモリが1ページより大きい場合は、複数の連続したページが申請されます。
ある時点でヒープメモリがGCの場合、この部分の物理アドレスは空になります。
次回ヒープメモリを申請する必要がある場合、Monoヒープはまず、このメモリ申請を収容するために、現在のヒープに連続スペースがあるかどうかを確認します。十分でない場合は、GCが実行されます。そうです、最も嫌なGC操作です。その後、そのようなblockが見つからない場合、Monoヒープはメモリ拡張操作を実行し、オペレーティングシステムに追加のメモリを要求します。
そして、これらは空になりますが、再利用できないメモリはメモリの断片化になります。それらは使用も破壊もできません。
たとえば、上図では、Mono Reserved/Usedの関係は次のとおりです。
Reserved size:256KB + 256KB + 128KB = 640KB
Used:88 562B
1.4 Profiler Simpleビュー
UnityのProfilerをメモリ分析に使用する場合、Simpleモードでは、次のようなスクリーンショットが表示されます。
ここに示されているのは、Unity自体によって管理される仮想メモリです。
これは明らかで、1行目はUsedメモリを示し、2行目はReservedを示しています。
- Total:
- Unity:Unityによって申請および管理されるすべてのメモリから<Profiler>、<FMOD>、および<Video>を差し引いたもの。つまり、Mono Heapが含まれます。
- Mono:ヒープメモリをホストする。
- GfxDriver:GPUメモリのオーバーヘッド。主にTexture、Vertex bufferおよびindex bufferで構成されます。ただし、Render Targetsは含まれていません。(他のプラットフォームのドライバーレイヤーも含まれません)
- FMOD:FMODによって申請されたメモリ。
- Video:ビデオファイルの再生に必要なメモリ。
- Profiler:プロファイラー自体のオーバーヘッド。
ここでTotal Reservedも、ゲームの仮想メモリの正確な値ではありません。理由は次のとおりです。
- ゲームのバイナリ実行可能ファイル、ロードされたlibraries、frameworksのサイズは含まれていません。
- GfxDriverの値には、レンダリングターゲットと、ドライバーレイヤーによって割り当てられたさまざまなバッファーは含まれていません。
- プロファイラーは、Unityコードによって行われた割り当てのみを表示し、サードパーティのnativeプラグインやOSの割り当ては表示しません。
1.5 Profiler Detailedの詳細図
Detailedビューのサンプルは次のとおりです。
仮想メモリの詳細な割り当てを示します。
- Assets――scenes、ResourcesとAsset Bundlesから現在読み込まれているアセットの合計。
- Built-in Resources――Unity EditorアセットまたはUnity defaultアセット。
- Not Saved――DontSaveとしてマークされたGameObjects。
- Scene Memory――GameObjectとそれに付属するComponents。
- Other—上記のカテゴリに含まれないその他。
ほとんどの場合、メモリの問題はAssetsで見つけることができます。たとえば、参照の数によってテクスチャとアセットのリークを見つけます(通常、リークされたアセットには参照の数がありません)。
ここでは、OtherディレクトリのObjectsアイテムに注目したいと思います。
実際、この値はいくつかのBUGが原因です。このアイテムは、テクスチャやMeshなど、Objectから継承されたさまざまなオブジェクトを表します。それらはある時点で実際のオブジェクトから切断され、無視することができます。
- System.ExecutableAndDlls:これはUnityの推測です。ファイルサイズを合計することにより、ロードされたバイナリによって使用されたメモリを推測しようとします。
- ShaderLab:これらはシェーダーのコンパイルに関連する割り当てです。シェーダー自体には独自のobject rootがあり、シェーダーの下に一覧表示されます。
1.6 Unityの視点の制限
Unityのメモリ分析は、ネイティブProfilerという項目があるだけではなく、よく使用されることは以下のように示しています。
- MemoryProfiler
- MemoryProfiler Extension
しかし、それらはすべて同じ問題を抱えています。つまり、Unity自体が提供するProfilerAPIに依存しています。すなわち、ツールはデータの表示方法と操作方法が異なりますが、測定した結果が同じです。
つまり、Unityの観点から見たツールは、Unityコードによって行われた割り当てのみを認識し、サードパーティのnativeプラグインとオペレーティングシステムの割り当ては認識しません。
ただし、完全なUnityプロジェクトは最終的にプラットフォーム上で実行され、プラットフォームのメモリ分析ツールの統計結果に大きなエラーが発生します。さらに、Unityプロジェクトの実際のメモリ分散とオーバーヘッドを把握することは困難です。
この記事では、iOSプラットフォームでのUnityゲームのメモリ状況を紹介します。
二、XCodeの観点から見たメモリ
Unity独自のツールがメモリ分散のパノラマ統計を満たすことができない場合は、最高のデバッグ機能を備えたXCodeツールを使用します。通常、UnityプロジェクトをXCodeプロジェクトにエクスポートしてから、XCodeとそのInstrumentforProfilerを使用する必要があります。
2.1 iOSの観点からのメモリ管理
依然として起源から話します。iOSの観点からのメモリは、Unityの観点からは完全に異なります。概念的、管理的、またはカテゴリ的にも。
オペレーティングシステムとして、iOSはUnityほど詳細ではなく(実際にはそれができません)、オペレーティングシステムレベルでのメモリ管理とサンドボックスAPPレコードのさまざまな基盤となるメモリ操作に関心がありますが、これもスタックからのみ反映できます。実際、オペレーティングシステムからメモリを要求するAPPの記録を記録します。
UnityプロジェクトはiOSプラットフォーム上でAPPとして実行され、iOSシステムに通常のAPPとしてのみ記録および分析されます。実際、Unityにはネイティブアプリと比較して自然な欠点があります。iOSのネイティブAPPやコントロールと比較すると、Unityはメモリを申請する目的を認識していません。つまり、オペレーティングシステムは、APPによって申請されるメモリがどのように使用されるかにまったく注意を払いません。
これは、親(iOS)が子供(Unity)にポケットマネーを渡すようなものです。親は、一昨日、子供が100円を要求し、試験紙を購入したいと言ったと記録します(スタックレコード)。昨日、またクラス費用を出すと50円を要求し、今日は、クラスメートと食事するとさらに100円を要求しました。子供が要求しすぎて親の許容限度を超えた場合、それは終了します(今月のポケットマネーはありません!)。
子供の見方が違います。昨日1000円、国語20円、数学20円、英語20円、往復車8円、残りは32円しかないなあ、まあ、親に返しなくてもいい、万が一また物理学や化学を買うなら、どうしますか。
2.2 iOSで使用されるメモリの種類
仮想メモリのみに注意を払うUnityとは異なり、OSはPhysical Memory(RAM)に注意を払う必要があります。特にモバイルプラットフォームでは、限られたメモリを極限まで使用する必要があります。したがって、多くのPCプラットフォームで使用されているメモリ戦略は、スワップスペースなどのモバイルプラットフォームでは使用できません(iOSはCleanタイプのメモリのPagingのみを実行できます)。
次に、iOSシステムで使用されるメモリタイプを一覧表示します。
Physical Memory:iOSデバイスの物理チップメモリ、よくマシンメモリと呼ばれています。モバイルデバイスでは、物理メモリの実際の使用量は、オペレーティングシステム自体の使用量から差し引かれます。iOSメモリクラッシュしきい値この記事では、さまざまなiOSデバイス上のアプリで使用できる物理メモリの量を記録します。
Virtual Memory(VM):仮想メモリは、OSによって各APPに割り当てられる仮想アドレス空間でもあり、Unityの仮想空間アドレスに似ています。これは、メモリ管理ユニットMMU(memory management unit)によって管理され、実際の物理メモリにマップされます。前述のように、メモリはページごとに割り当てられます。初期のiOSプロセッサは4K /ページを使用します(一部の世代は64Kを使用します)。A9以降は、16K/ページが均一に使用されます。仮想メモリは通常、コードセグメント、ダイナミックライブラリ、GPUドライバー、Mallocヒープ、およびその他の部分で構成されます。
GPU Driver Memory:iOSシステムは、いわゆる統合アーキテクチャを使用します。つまり、GPUとCPUは、ドライバーによって割り当てられるテクスチャやメッシュデータなどのメモリの特定の部分を共有します。さらに、いくつかのGPUドライバーレイヤーとGPU専用メモリタイプ(Video memory)があります。
Malloc Heap:APPが実際にメモリを申請する場所。Unityのメモリ申請の動作はここで発生し、メモリアプリケーションはmallocおよびcalloc関数を介して実行されます。 Appleは、使用できる最大仮想ヒープのアドレスを公開していません。理論的には、ポインタのサイズ(32ビットまたは64ビット)によってのみ制限されますが、実際の経験は理論値よりはるかに低くなります。それに規則はありません。実際の経験は次のように要約されます。
Unity自身の仮想アドレスと同様に、アプリケーションがメモリを頻繁に要求したり解放したりしないことが最善です。
Resident Memory:常駐メモリ。ゲームまたはAppが実際に占有する物理メモリです。一般的に、アプリケーションがシステムにメモリを要求すると、仮想メモリは直接大きくなります。ただし、割り当てられたメモリがデータを書き込まない場合、実際の物理メモリ割り当ては生成されません。したがって、仮想メモリは>=常駐メモリです。
Clean Memory:Cleanメモリは常駐メモリの一部です。しかし、この部分のメモリタイプは読み取り専用です。一般的なCleanメモリには、System frameworksの定数部分、アプリケーションバイナリファイル、メモリマップトファイルなどが含まれます。読み取り専用であるため、アプリケーションのメモリが不足するとPage Outになる可能性があります。
Dirty Memory:Cleanの反対はDirtyメモリです。この部分は、OSがページングできないものを指します。
Swapped Compressed Memory:SwappedCompressedは実際にはDirtyメモリに属しています。アプリケーションメモリが不足している場合、OSはダーティメモリにある使用頻度の低いメモリを圧縮して保存します。使用する必要がある場合は、再度解凍してください。これらのアルゴリズムと戦略は現在公開されていませんが、経験から、OSは積極的に圧縮を行ってダーティメモリの総量を減らします。ここでの圧縮スワップは、従来のオペレーティングシステムのディスクスペーススワップではないことに注意してください。
上の図は、圧縮プロセスを示しています。圧縮と解凍はどちらもCPUに負荷がかかります。
2.3 FootPrint
FootPrintは、アップルが推奨するメモリ測定および最適化の指標です。Memory Footprintの値がLimit lineに達すると、メモリー警告がトリガーされ、さらにOOMにつながります。
Footprintは主にDirtyとCompressedで構成されています。または、ResidentはFootprintとCleanで構成されています。Footprintには統一された基準はありません。デバイス、オペレーティングシステム、さらには現在のランダム環境によっても異なります。
Byteの既存のテストツールGamePrefは、iOSをテストするときにFootprintメモリを取得します。詳細なデータについては、GamePerfユーザーマニュアルを参照してください。
2.4 Xcodeメモリゲージ
これは、XCodeでデバッグするときに最も簡単なインターフェイスです。 「Debug」タブに切り替えると表示されます。
緑はメモリが良好であることを示し、黄色は危険な領域を表示し、即時に処理しないと、OSによってすぐに強制終了されます。
メーターの最大メモリはデバイスの物理メモリを指しますが、実際には、正式な場合でポインタメモリがどのようなメモリを指すかを説明していません。ただし、テスト結果から、VM Trackerツールで測定されたDirty Memory + Swapped Memoryよりも常に10〜15MB大きくなります。
しかし実際には、OSがAPPを強制終了するための基準はこの値だけではありません。通常、アプリを強制終了する場合は、次の手順を実行します。
- Cleanメモリページを削除してみてください。
- APPがDirtyメモリを大量に使用すると、OSはメモリ警告を送信してリソースを解放させます。
- いくつかの警告の後、Dirty使用量がまだ高い場合、強制終了されます。
iOSによるAPPの強制終了戦略も不透明であるため、APPがシステムによって終了されるのを防ぐ唯一の方法は、Dirtyメモリを可能な限り減らすことです。
2.5 VMトラッカー
VM Trackerは、XCodeInstrumentsツールセットの1つです。より詳細な仮想メモリの分散を提供し、Dirty Memory情報を提供するためのツールでもあります。ただし、残念ながら、メモリ割り当ての目的とタイミングは示されていません。
典型的なVM TrackerのProfilerスナップショットは次のとおりです。
ヘッダーは次のとおりです。
Type — メモリタイプ
Resident Size — Resident Memoryメモリ
Dirty Size — Dirty Memoryメモリ
Swapped Size — Compressed Swapped Memoryメモリ
Virtual Size — Virtual Memoryメモリ
Res. % — Resident Memory和Virtual Memoryの比率
次は、さまざまなレベルでのいくつかの実際のタイプの特定の値です。タイプを1つずつ紹介することはしませんが、いくつかの重要なポイントを取り上げて説明します。
*All*—すべての割り当て
*Dirty*— Dirty Memory
IOKit — graphics driver memory、テクスチャ、メッシュ、コンパイルされたシェーダーなどのグラフィックドライバメモリ。
VM_ALLOCATE — 主にMono Heap。この値が大きすぎる場合は、Unity Profilerを使用して特定の割り当てを表示できます
MALLOC_* —主にUnity nativeまたはthird-party pluginsよって割り当てられたメモリ
__TEXT —読み取り専用の実行可能コードセグメントと静的データ
__DATA —書き込み可能な実行可能code/data
__LINKEDIT —シンボル、string、再配布テーブルなど、さまざまなリンクライブラリの実際のメタデータ。
仮想メモリやダーティメモリなどの各グループの情報を比較することで、メモリの根本原因分析を行うことができます。たとえば、上記のスナップショットでは、分析からどのような結論を導き出すことができますか?
2.5.1Regions Map
Regions MapはVM Trackerの別の視点であり、主にメモリページングの表示と仮想アドレス空間の構造を提供します。
たとえば、上の図からわかるように、MonoHeapブロックのメモリは連続していません。
2.6 Allocations
Allocationsツールは、すべてのスレッド(Unityネイティブコード、ガベージコレクター、IL2CPP仮想マシン、サードパーティプラグインなど)によってすべてのスレッドで行われた、アプリケーションのアドレス空間内のすべての割り当てを表示します。
VM Trackerと比較すると、Allocationsの利点は、任意の期間のメモリ割り当てを表示できることです。スタックは、どのコードが割り当てを行ったかを確認することもできます。
しかし、それはまだバグがあり、割り当ての状況を確認できますがことが、常駐の状況を確認することができません。
CallTreeの観点から、スタックを見ることができます。
たとえば、上のスクリーンショットからわかるように、コードはいくつかのPoolerのクローンを作成するPoolerクラスのインスタンスを作成し、その結果、メモリが割り当てられます。
Summaryの下に、仮想マシンのメモリ割り当ての詳細なリストが表示されます。
クリックを選択すると、より詳細な割り当てが表示されます。たとえば、次のメモリ割り当てはJsonの解析によって発生します。
2.7 Memory Debugger
ツール分析に加えて、XcodeはMemory Debugger機能も提供します。プロジェクト設定でMallocStackを開く必要があります。
次に、Debugタブの下のロゴをクリックして、メモリフレームをキャプチャして分析できます。
ここでは、各バイトの割り当てを表示できます。しかし、それはあまりにも詳細です。
2.8 vmmap
Memoryスナップショットをエクスポートすることで、コマンドラインツールを使用してメモリをより詳細に分析することもできます。
エクスポートされたmemgraphファイルは、vmmapのコマンドラインを介して物理メモリの実際の割り当てを表示できます。
もちろん、vmmapツールには、より多くの詳細とコンテンツの表示をサポートするためのより多くのコマンドラインがあります。興味がある場合は、Jiadong氏が書いた「Unity開発者向けiOSメモリデバッグガイド」の後半を確認してください。
メモリリークを分析する場合は、leaksApp.memgraphコマンドを使用することもできます。たとえば、次の循環参照:
2.9Xcode視点の制限
XCode視点にも独自の制限があります。 Unityの主な焦点または管理の重点は、仮想メモリです。また、XCodeは、OSの観点から物理メモリに重点を置いています。
Unityはシステムライブラリとサードパーティのプラグインをカウントできません。XCodeはカウントできますが、区別するのは困難です。 OSの場合、これらのメモリはすべてAPPによって要求され、すべてMalloc Heapの申請であるため、すべて1つのカテゴリに分類されます。本当にライブラリを区別したい場合は、すべての関数割り当てを手動で収集してから、Unity、APlugin、Luaの割り当てなどを手動で分類する必要があります。
もう1つは、XCodeには多くの分析ツールがありますが、これらのツールには、それぞれの統計的次元の追加に関して、依然として異なる問題があります。とはいえ、XCode独自のツールを使用しても、標準を完全に統一することはできません。
最後に、XCodeのツールチェーンの統計は低レベルで詳細すぎます。これを使用してメモリの例外をすばやく特定できますが、すべてのメモリをすばやく分類することは困難です。
三、Androidの観点から見たメモリ
iOSオペレーティングシステムはUnixに基づいており、AndroidオペレーティングシステムはLinuxに基づいており、LinuxはUnixに基づいているため、カーネルの面から見ればAndroidオペレーティングシステムはiOSと非常に似ています。
したがって、Androidのメモリ管理戦略はiOSと非常に似ています。ただし、違いは、iOSシステムが閉じられており、各世代のハードウェアが既知であり、列挙可能でさえあることです。 Androidシステムは、オープンソースであるため、さまざまなハードウェアを備えており、制御が非常に困難ですが、オープンソースシステムによってさらに可能になります。
3.1Androidの視点からのメモリ管理
メモリポリシーは似ていますが、用語と実際の管理プロセスにはわずかな違いがあります。たとえば、Androidはメモリを次の3つのタイプに分類します。
RAM:通常、メモリとも呼ばれますが、そのサイズは制限されています。ハイエンドデバイスは通常、RAM容量が大きくなります。
zRAM:スワップスペースに使用されるRAMパーティションです。メモリが不足している場合、OSはデータの一部をRAMに圧縮し、zRAMに保存します。デバイスメーカーは、zRAMのサイズに上限を設定できます。
Storage:一般にストレージと呼ばれます。通常のAPP、写真、キャッシュファイルはすべてここにあります。
ただし、iOSのFootprintとは異なり、Androidのメモリは別の名前です。
VSS―Virtual Set Sizeの仮想使用メモリ(共有ライブラリが占有するメモリを含む)
RSS―Resident Set Sizeに実際に使用されている物理メモリ(共有ライブラリによって占有されているメモリを含む)
PSS―Proportional Set Sizeによって実際に使用される物理メモリ(プロポーショナル割り当ての共有ライブラリによって占有されるメモリ)
USS―Unique Set Sizeプロセスのみが占有する物理メモリ(共有ライブラリが占有するメモリを除く)
一般的に、メモリ使用量のサイズは次のとおりです。VSS> = RSS> = PSS>=USS。
現在、Android上のUnityのゲームの指標はデフォルトでPSSを使用しています。どういう意味ですか?
たとえば、次のようなメモリページがあります。
それらの1つは、GooglePlayと特定のゲームアプリの両方で使用される位置共有サービスです。これにより、どちらのAPPがより多くを使用するかを定めるのが難しくなります。ロケーション共有サービスのすべてのメモリを2つのアプリケーションに追加すると、コンピューティングの観点はRSSになります。これは確かにより正確な物理メモリ使用量ですが、この方法で、ロケーション共有サービスは2回計算されるようになり、3つのアプリケーションに対しては3回になります。明らかに、これは正しくありません。
では、一応、皆はこの共有サービスメモリを等分すれば?この計算の観点ならPSSです。完全に合理的ではありませんが、現時点で最もバランスの取れたソリューションと言えます。
3.2 LMK(Low Memory Killer)
iOSでは、Footprintは閾値に達するとOSによって強制終了されます。これは、Androidにも当てはまります。ただし、iOSと比較すると、AndroidのLMKプロセスはより透過的です。
LMKは、oom_adj_scoreと呼ばれる「メモリ不足」スコアを使用して、実行中のプロセスに優先順位を付け、これを使用して、強制終了するプロセスを決定します。スコアが最も高いプロセスが最初に終了します。バックグラウンドアプリケーションが最初に終了し、システムプロセスが最後に終了します。次の表に、LMKスコアのカテゴリを高から低まで示します。スコアが最も高いカテゴリ、つまり最初の行のアイテムが最初に終了します。
以下は、上記の表のさまざまなカテゴリの説明です。
Background apps:以前に実行されていて、現在アクティブではないアプリ。 LMKは、oom_adj_scoreが最も高いアプリから始めて、最初にバックグラウンドアプリを強制終了します。
Previous app:最近使用されたバックグラウンドアプリ。ユーザーはバックグラウンドアプリよりも前のアプリに切り替える可能性が高いため、前のアプリの優先度はバックグラウンドアプリよりも高くなります(スコアが低くなります)。
Home app:ランチャーアプリ。アプリを終了すると、壁紙が消えます。
Services:アプリによって開始されるサービス。同期やクラウドへのアップロードが含まれる場合があります。
Perceptible apps:小さなインターフェースを表示する検索プロセスの実行や音楽の再生など、ユーザーが何らかの方法で知覚できる非フォアグラウンドアプリケーション。
Foreground app:現在使用中のアプリ。フォアグラウンドアプリを終了すると、アプリがクラッシュしたように見え、デバイスに問題があることをユーザーに警告する場合があります。
Persistent:これらは、テレフォニーやWLANなどのデバイスのコアサービスです。
System:システムプロセス。これらのプロセスが強制終了された後、携帯は再起動する場合があります。
Native:システムで使用される非常に低レベルのプロセス(例:kswapd)。
デバイスメーカーは、LMKの動作を変更できます。
3.3 Android Profiler
AndroidプラットフォームでUnityパッケージを構築するのに便利であり、Android独自のStudioにはメモリとパフォーマンス分析ツールがないため、多くの場合、AndroidがUnity Profilerに接続する後、メモリデバッグを実行することを選択します。
しかし実際には、Androidにはパフォーマンスを分析できるツールがたくさんあります。例えば:
https://developer.android.com/studio/profile
ほとんどの場合、XCodeは引き続き分析に使用されるため、この部分は現在実行されていませんので、将来的には、ツールの使用法とスキルを詳細に調査する時間を取ります。
さらに、Googleの会議で、Android開発者は最新のパフォーマンス分析ツールPerfettoの使用を推奨しました。
https://perfetto.dev/docs/quickstart/android-tracing
今のところ、AndroidのツールはXCodeと同じく、アプリケーションでのメモリ割り当てがUnityによって行われるのかPluginによって行われるのかを区別する方法がありません。だから彼らの意見も分離測定を実行することです。
四、予想される選択の方向
上記の研究結果を考慮して、2つの解決策が検討されました。
4.1 1つずつ解決する
Unityの観点から考えます。Unityはサードパーティのプラグインの使用量をカウントできないため、「差異法」を使用して、使用されていた各サードパーティのプラグインを1つずつ解決します。
たとえば、miniプロジェクトに基づいて、まずはminiプロジェクトの現在のメモリ指標値を測定します。そして、サードパーティのプラグインに接続し、ユースケースを使用して同じインジケーターを再度テストします。結果として生じる差は、プラグインのメモリ使用量とほぼ同じです。
この方法ですべてのプラグインの指標を測定する上に、Unity独自のReservedメモリを加え、現在のメモリ分布と見なすことができます。
この方法には当然欠点があります。
差異値は、異なるデバイスの現在の環境の共有ライブラリの影響を受けます。これにより、Pluginが異なるマシンでまったく異なる可能性があります。
実にどの客観的条件または内部ロジックの影響を受けるかは特定できません。
モバイルプラットフォームのダーティメモリポリシーにより、実際に測定できるのは仮想メモリの相違だけです。詳細については、次のテストの結論を参照してください。異なるメモリの割り当て方法は実際のメモリに対する影響。
変数を制御することは困難です。したがって、プラグインを十分にカバーできるテストケースを作成する必要があります。
4.2 海から月を釣る
もう一つの方法は、海から月を釣ることです。 最下層のMallocから始めて、Hook関数を記述してメモリ申請を監視し、最後に要約して分析します。たとえば、この記事:『mallochookメモリ割り当てコールバック(glibc-3-memory)』(中国語注意)。
これは、実際にはモバイルプラットフォームのメモリルールモデルと同じです。カスタムの方法でツールの表示ルールをさらにカスタマイズできるということだけが違っています。解決策は実行可能ですが、モバイルプラットフォームツールと同じ問題があり、統計されたスタックをどのように分類すればよいか。どの関数がエンジンに属し、どの関数がサードパーティのプラグインに属し、またはシステム共有ライブラリまたはフレームワークに属しているかを判断するにはどうすればよいですか?
Androidメモリについて:
https://zhuanlan.zhihu.com/p/372883142(中国語注意)
https://zhuanlan.zhihu.com/p/151238164(中国語注意)
5.参考
[1] Fixing Performance Problems – 2019.3 – Unity Learn
[3] https://zhuanlan.zhihu.com/p/87310853
[4] Finding iOS memory | Rambling Llamas
[5] Unity – Manual: Understanding the managed heap
[6] Memory Management in Unity – Unity Learn
[7] iOS Memory Deep Dive – WWDC 2018 – Videos – Apple Developer
[8] Delivering Optimized Metal Apps and Games – WWDC 2019 – Videos – Apple Developer
[9] Gain insights into your Metal app with Xcode 12 – WWDC 2020 – Videos – Apple Developer
[10] https://developer.android.com/topic/performance/memory-management
[11] Memory allocation among processes
[12] Understanding Android memory usage
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
Image Effects拡張——深度ベースのbloom
Unityに付属するImage Effectsのbloomコンポーネントは、フルスクリーンのブルーム効果を実現するために使用されます。
サブカメラBloom
サブカメラにbloom要件がある場合、bloomコンポーネントを直接用いるには問題があります。たとえば、シーン内の特殊効果のみをbloomしたい場合は、bloomコンポーネントを特殊効果カメラに掛けると、メインカメラと特殊効果カメラが同じrender targetを描画するため、シーン全体がbloomされます。
解決策は、render targetを使用してbloomカメラのrender targetをメインカメラから分離し、bloom後のrender textureをメインカメラの描画コンテンツと重ね合わせて効果を実現することです。
以下に、おおよその実装手順の概要を示します。
bloomカメラを作成する
黒の背景を設定し、bloomが必要なlayer(ここではEffectレイヤーを指す)のみを描画し、レンダリングターゲットはrender textureを設定します。
private void SetupGlowCamera()
{
GlowCamera.CopyFrom(GetComponent<Camera>());
GlowCamera.clearFlags = CameraClearFlags.Nothing;
GlowCamera.rect = new Rect(0, 0, 1, 1);
GlowCamera.backgroundColor = new Color(0, 0, 0, 0);
GlowCamera.cullingMask = 1 << LayerMask.NameToLayer("Effect");
GlowCamera.orthographic = false;
GlowCamera.targetTexture = glowTexture;
GlowCamera.enabled = false;
}
Render Textureを描画する
メインカメラのOnRenderImageでbloomカメラのRenderを手動で呼び出して、render textureを描画します。
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
...
glowTexture = RenderTexture.GetTemporary(source.width, source.height, 24, rtFormat);
SetupGlowCamera();
GlowCamera.Render();
...
}
次に、元のメインrender target——sourceをglowTextureに置き換えて、bloom効果がglowTextureだけに有効するようにします。
重畳
最後に、bloom後のrender textureとsourceを重畳します。
half4 fragAdd (v2f i) : SV_Target
{
half4 addedbloom = tex2D(_MainTex, UnityStereoScreenSpaceUVAdjust(i.uv[0].xy, _MainTex_ST));
half4 screencolor = tex2D(_ColorBuffer, UnityStereoScreenSpaceUVAdjust(i.uv[1], _ColorBuffer_ST));
return _Intensity * addedbloom + screencolor;
}
次の図は、立方体を順番にbloom効果を実現させる各ステップの結果を示しています。
立方体bloom効果の分解図
深度ベースのカリング
複雑なシーンでは、サブカメラのBloomにも問題があります。Bloomカメラのrender targetがメインカメラから分離されている場合、Bloomカメラにはメインカメラのdepth buffer(render target はcolor bufferとdepth bufferからなっている)がなくなって、Bloomカメラにオクルージョンされたコンテンツが描画されます。下図参照:
左側の立方体はキャラクターによって遮られておらず、右側の画像は正しい表示です
解決策は、sourceのdepth bufferを使用することです。OnRenderImage関数では:
...
GlowCamera.SetTargetBuffers(glowTexture.colorBuffer, source.depthBuffer);
GlowCamera.Render();
...
GlowCamera.SetTargetBuffers関数は、カラー出力をglowTextureのcolorBufferに設定し、深度出力をメインカメラの深度bufferであるsource.depthBufferに設定します。このように、glowモデルが描画されると、メインカメラの深度bufferがZTestおよびZWrite設定に従って更新されます(OpaqueのデフォルトはZTest LEqual、ZWrite On)。
デプスカリング後の効果の分解図
MSAAをオンにした実装
MSAA(マルチサンプリングアンチエイリアシング)をオンにすると、メインカメラのcolorBufferとdepthBufferが2〜8倍大きくなります(特定のMSAA設定による)。このときglowTextureを作成すると、antiAliasing設定はMSAA設定と同じである必要があります。そうでない場合、深度書き込みが失敗します(エラー報告なし、効果なし)。
OnRenderImage関数では:
...
glowTexture = RenderTexture.GetTemporary(rtW, rtH, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Default, 4);
...
ここで、QualitySettingでの設定はMSAAの4倍であり、これに対応します。
ただし、このようにUnityを実行すると、RenderTexture.GetTemporaryが呼び出されるたびに、次のwarningが表示されます。RenderTexture: Resolving a destination texture that is NULL.。バージョンのbugではないかと思っていますが。
別の実装は次のとおりです。bloomカメラのターゲット深度bufferとして、深度カメラを作成してAnti-Aliasingなしの深度render textureを生成する。
private void SetupDepthCamera()
{
DepthCamera.CopyFrom(GetComponent<Camera>());
DepthCamera.allowMSAA = false;
DepthCamera.targetTexture = depthTexture;
DepthCamera.enabled = false;
}
OnRenderImage関数では:
...
depthTexture = RenderTexture.GetTemporary(rtW, rtH, source.depth, RenderTextureFormat.Depth);
SetupDepthCamera();
// render depth texture first
DepthCamera.Render();
...
GlowCamera.SetTargetBuffers(glowTexture.colorBuffer, depthTexture.depthBuffer);
...
このようにしたら、warningの問題が解決され、アンチエイリアシングのオーバーヘッドが節約されます(ぼかしに使用されるtextureアンチエイリアシングには意味がありません)が、深度を生成するための追加の描画コストがあります。
完全なコードは次を参照してください。
https://github.com/chenyong2github/DepthBasedBloom
参考文献:
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
カラーグレーディング
カラーモデルと相互変換の方法を理解した後、後処理で最終的な画像にカラーグレーディングを行います。カラーグレーディングは比較的幅広いトピックであり、主な目的は、色相、彩度、明るさ、コントラスト、ホワイトバランス、およびその他の多くの操作の調整など、画像のパフォーマンスを向上させることです。次に、いくつかの一般的なカラーグレーディング操作をリストします。
彩度(Saturation)と明るさ(Value)
HSVモデルの変数Sと変数Vはそれぞれ彩度と明るさであるため、彩度と明るさの調整は比較的簡単な操作です。RGBカラーモデルをHSVカラーモデルに変換した後、SとVの値を調整します。
彩度を調整する
コード:
float4 SetPixelShader(float4vertex:POSITION、float2 uv:TEXCOORD0):SV_TARGET
{
float4 color = tex2D(_MainTex、uv * _MainTex_ST.xy + _MainTex_ST.zw);
float3 source = RGBToHSV(color.rgb);
source = float3(source.r、source.g * _saturation、source.b);
color.rgb = HSVToRGB(saturate(source));
return color;
}
元の画像:
各ピクセルのそれぞれの彩度(HSVのS値)に1.8を掛けます。
より特別なものの1つは、彩度を0に調整して、白黒フィルターの効果を取得することです。
明るさを調整する
コード:
float4 SetPixelShader(float4vertex:POSITION、float2 uv:TEXCOORD0):SV_TARGET
{
float4 color = tex2D(_MainTex、uv * _MainTex_ST.xy + _MainTex_ST.zw);
float3 source = RGBToHSV(color.rgb);
source = float3(source.r、source.g、source.b * _value);
color.rgb = HSVToRGB(saturate(source));
return color;
}
明るさを暗くする:
元の画像:
明るさを上げる:
コントラスト
コントラストとは、画像の明るい領域と暗い領域の 最も明るい白と最も暗い黒の間 のさまざまな輝度レベルの測定値を指します。差の範囲が大きいほど、コントラストが大きくなり、差の範囲が小さくなり、コントラストが小さくなります。良好なコントラスト率は120:1です。鮮やかで豊かな色を簡単に表示でき、コントラスト率が300:1と高い場合は、すべてのレベルの色をサポートできます。
HSVカラーモデルの場合、明るさの差(V値)を大きくすることでコントラストを調整できます。
float4 SetPixelShader(float4vertex:POSITION、float2 uv:TEXCOORD0):SV_TARGET
{
float4 color = tex2D(_MainTex、uv * _MainTex_ST.xy + _MainTex_ST.zw);
float3 source = RGBToHSV(color.rgb);
source = float3(source.r、source.g、((source.b --0.5)* _value + 0.5));
color.rgb = HSVToRGB(saturate(source));
return color;
}
コントラストを下げる:
コントラストを上げる:
逆色(Inverse)
逆色とは、原色に重ねると白になる色、つまり白から原色を引いた色のことです。したがって、このフィルターの実装は比較的簡単です。RGBカラーモデルでは、白から原色を引いたものを使用します。
RGBカラーモデルを使用して実装する方が簡単です。
float4 SetPixelShader(float4vertex:POSITION、float2 uv:TEXCOORD0):SV_TARGET
{
float4 color = tex2D(_MainTex、uv * _MainTex_ST.xy + _MainTex_ST.zw);
color.rgb = float3(1-color.r、1-color.g、1-color.b);
return color;
}
逆色効果:
PS:関連用語の定義はbaike.baidu.comから引用されています
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
カラーモデル
6と7の節では、2つのワークフローをまとめました。そうすればシェーダーに入力されたカラーデータは期待どおりであることを確保できます。シェーダーは入力された色データを処理します。この節では、いくつかの色表現と一般的な処理方法について説明します。
RGBカラーモデル
人間には主に3種類の錐体細胞があり、色認識を行うときに赤(波長約600nm)、緑(波長約550nm)、青(波長約450nm)に最も敏感です。これは、私たちが知覚できる色の領域を決定します。この理論に基づいて、RGBカラーモデルが作成されました。RGBカラーモデルは、色を表すために3つのチャネルの線形結合を使用して、私たちが最もよく目にするカラーモデルです。3つのカラーチャネルはそれぞれ、赤(Red)、緑(Green)、青(Blue)です。
(Unity RGB色調整のインターフェース)
これらの3つの変数にはどの色も関連付けられているため、色を継続的に変換するのは直感的ではありません。これらの3つの変数はすべて明るさに関連しているため、明るさが変更されると、それに応じて3つの変数すべてが変更されます。さらに、人間の目は3色の変数に対してさまざまな感度を持っており、人間の目は赤に対して最も感度が低く、青に対して最も感度が高くなっています。これにより、RGBモデルで色の特定の値をより正確に推測することが困難になります。この欠陥に基づいて、アーティストが開発するとき、RGBカラーモデルの代わりに、より直感的なHSVカラーモデルを使用します。
HSVカラーモデル
HSVカラーモデルを説明する前に、まず、スペクトルカラーという概念を理解する必要があります。
多色光が分散系(プリズムやグレーティングなど)で分割された後、分散された単色光は波長順に並べています。波長が約400nm〜770nmの可視光波長範囲の単色光に対応する色をスペクトル色と呼びます。
RGBと比較して、HSVはより直感的なカラーモデルです。各部分の意味は次のとおりです。
H:色相、色合い(Hue)、角度で測定、値の範囲は0°〜360°で、赤から反時計回りに計算されます。対応するスペクトル色は波長に応じて選択されていると理解できます。
S:彩度、飽和度、色の純度(Saturation)、値の範囲は0%〜100%で、スペクトル色に近い度合いを示します。任意の色を特定のスペクトル色を白と混合した結果として見ることができます。スペクトル色の白色光成分が0で、彩度は最も高いのです。
V:明度、色の明るさ(Value)、色のもつ明るさの度合いを示します。値の範囲は0%(黒)〜100%です。
上図に示されているモデルでHSVカラーモデルを示す。円柱の断面は極座標として見ることができます。Hパラメータは極座標の極角で表され、Sパラメータは極座標の極軸の長さで表され、Vパラメータは円柱の中心軸の高さで表されます。
RGBカラーモデルでは、赤はRGB =(255、0、0)によって決定されます。HSVモデルでは、パラメータH=0を表すことができます。
パラメータH=0の場合の半辺断面は次のとおりです。
この図から、次のことがはっきりとわかります。
水平方向は左から右です。パラメータSの値が大きくなると、彩度が高くなるほど色が暗くなり、スペクトル色に近づきます。逆に、パラメータSの値が小さくなると、彩度が小さいほど色が薄くなり、色が白に近くなります。彩度0は真っ白です。
垂直方向は下から上です。パラメータVの値が大きくなるほど、明るさが高くなるほど色が明るくなります。逆に、パラメータVの値が小さくなると、明るさが低くなるほど色が濃くなります。明度0は、純粋な黒を意味します。
HSVカラーモデルを使用すると、単一の色を簡単に取得して、指定したスペクトル色の明るさと彩度を調整できます。
相互変換
基礎的な内容
RGBカラーモデルとHSVカラーモデルの間には特定の変換式があります。
Unityには、変換用の対応するAPIがあります。
public static Color HSVToRGB(float H, float S, float V);
public static void RGBToHSV(Color rgbColor, out float H, out float S, out float V);
Unrealには対応するブループリントノードがあります。
ただし、Shaderにはこのような変換を直接実行できる関連関数はなく、2つのカラーモデル間の変換関係を理解する必要があります。
RGBカラーモデルの場合、デカルト座標系に基づいて表すことができます。3つのチャネルR、G、およびBの量は同じで、デカルト座標系を介して、上の図に示されている幾何学的モデルを構築します。座標の原点で表される色は黒、座標(1、1、1)で表される色は白です。ベクトル(1、1、1)は、明度の向上を表すことができ、その意味は、HSVカラーモデルのパラメーターVの座標軸に相当します。
HSVカラーモデルでは、デカルト座標系のRGBカラーモデルの点が円筒座標系で表されていると考えることができます。2つの座標系には多くの変換方法があり、異なるカラーモデルも生成します。
上の図に示すように、最初に、RGBカラーモデルの原点(0,0,0)から白色点(1,1,1)までのベクトルの延長線を円筒座標系のZ軸と見なされ、原点(黒)は円筒座標系の原点と見なされます。次に、さまざまなルールに従ってさまざまなマッピングを作成します。その中で、HSVモデルは、Red(赤)、Green(緑)、Blue(青)、Cyan(青緑)、Magenta(赤紫)、Yellow(黄)、White(白)の7色をRGBで構成される平面に投影し、六角錐という幾何学的モデルを取得します。そしてこの六角錐を六角柱に拡張し、さらにそれを円柱に拡張すると、得られたモデルはHSV幾何学的モデルです。その結果、RGBカラーモデルとHSVカラーモデルの色の間に1対1のマッピング関係が形成されます。
RGBカラーモデルからHSVカラーモデルへの変換
(r、g、b)をRGBカラーモデルの幾何学的モデルの色のRGB座標とし、それらの値は(0、1)の間の実数です。 maxをr、g、bの最大値、minをr、g、bの最小値とします。これをHSVカラーモデルのポイントに変換します。次のように計算されます。
ここで、hの値は0°から360°の間にあるように正規化されています。
上記の数学モデルに基づいて、Unityでshaderコードをビルドします。まず、数式のデータを0から1の範囲に正規化します。これは、すべてを360°で割ることに相当します。数式を並べ替えて、各関数に必要なデータを取得します。
必要なデータは、lerp関数とstep関数の組み合わせによって選択されます。step関数は、r、g、bの3つの量のサイズを比較するために使用され、対応するベクトルは、サイズの結果に従ってlerp関数によって選択されます。除数が0になるのを防ぐために、1.0e-10を使用して計算に参加します。残りのsとvの計算は比較的簡単です。
シェーダーコードを取得します。
float3 RGBToHSV(float3 c)
{
float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
HSVカラーモデルからRGBカラーモデルへの変換
(h、s、v)をHSVカラーモデルの円筒座標系の色の座標とします。 hの値は0°から360°の間にあるように正規化されます;sの値は0から1の間に正規化され、彩度を表します;vの値は0から1の間に正規化され、明度を表します。これをRGBカラーモデルのポイントに変換します。次のように計算されます。
同じく、変数Hの値を(0,1)の間に正規化します。
上記の数学モデルによれば、UnityでShaderコードを構築します:3つの変数r、g、bの計算式は分解し、異なるhiに応じた計算式の表は次のとおりです。
3つの変数の計算は、オフセットを使用して同じ式に分類できます。変数Rの式の計算に基づくと、2つの変数GとBのオフセットはそれぞれ2/3と1/3です。つまり、コード内のk.xyzです。
観察から、各計算方法はlerp関数の形式で表すことができることがわかります。
変数pの場合、v * lerp(1、0、s)に変換できます。
変数qの場合、v * lerp(1、1-f、s)に変換できます。
変数tの場合、v * lerp(1、f、s)に変換できます。
変数vの場合、v * lerp(1、1、s)に変換できます。
このことから、shaderコードがわかりやすくなります。変数から3を引いて絶対値を求めることで、変数qと変数tの演算を区別することができます。変数pと変数vの計算は、saturate関数によって区別できます。
コードを取得します。
float3 HSVToRGB(float3 c)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
}
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
Linear Spaceワークフロー
Gamma SpaceとLinear Space
Gamma Correctionが存在するため、レンダリングでの計算に少し問題があります。たとえば、sRGB画像の非線形入力があり、操作が線形空間で実行されると、最終的な出力結果が目的の結果から外れます。
実世界の物理法則は線形空間で計算されるため、Shaderでの計算も線形空間で計算されます。たとえば、前の曲線によると、実際の物理的な空間の光の強度が0.2ですが、人間の目はそれを0.5の明るさとして認識します。0.2に従って計算することと0.5に従って計算することによって得られる結果は、当然異なります。
7.1Unityエンジンのさまざまなスペースの照明表示
したがって、現実の世界の色をより現実的かつ正確に復元するために、より良い制作プロセスは、入力、計算、および出力を線形空間に統合させることです。次にGamma Correctionが実行され、最後にディスプレイに表示されます。
まず、入力データが線形空間に配置されていることを確認する必要があります。sRGBテクスチャの場合、Remove Gamma Correction(2.2の累乗の操作を実行)を実行してLinear Spaceに戻す必要があります。
次に、Shaderを介して計算を実行します。この部分の計算は線形空間にあります。
次に、結果に対してGamma Correctionが実行され、最終的にディスプレイから出力されます。これにより、現実の世界に近い結果が得られます。
グレースケール値1を例として取り上げます。Shader計算後の出力結果が入力結果の0.9倍であると仮定します。
Linear Spaceにある場合は、[Remove Gamma Correction] 後の値が1になり、Shaderで計算して値を1 * 0.9 = 0.9とし、Gamma Correctionを実行して0.9 ^ 0.45 = 0.95になり、最後にディスプレイに表示されます:0.95 ^ 2.2=0.89。
Gamma Spaceの下にある場合、Shaderで計算された値は1 * 0.9 = 0.9であり、ディスプレイに表示されます:0.9 ^ 2.2=0.79。
Gamma Spaceでのレンダリング結果が暗いことがわかります。Linear Spaceでのレンダリング結果は少し明るくなります。図7.1の結果に示されているように。
Unityの場合
Linear SpaceとGamma Spaceの選択は、Project Settings->Player ->Color Spaceにあります。
ガンマスペースが選択されている場合:
Unityは入力と出力結果に対して何の処理も行いません。図7.2に示すようなRemove Gamma CorrectionとGamma Correctionは発生しません。必要に応じて、手動で実装する必要があります。
Linear Spaceが選択されている場合:
sRGBテクスチャの場合、Unityはテクスチャサンプリングの前に自動的にRemove Gamma Correctionを実行します。Linearテクスチャにはそのようなステップはありません。出力する前に、Unityは自動的にGamma Correctionを実行してからディスプレイに出力します。
したがって、テクスチャの場合、Unityにテクスチャのタイプと色空間を教える必要があります。
いくつかの特別なテクスチャタイプは、Normal Map、Light Mapなど、デフォルトでLinear Spaceで行います。
Defaultタイプのテクスチャには、sRGBオプションがあります。チェックされている場合は、ガンマ0.45空間にあることを意味し、チェックされていない場合は、Linear Spaceにあることを意味します。
さらに、Linear Spaceでは、Shader Labでの色入力はデフォルトでsRGB色になり、Remove Gamma Correctionが自動的に実行されます。頂点カラー属性データについては、手動でRemove Gamma Correction処理を実行する必要があります。
場合によっては、Float変数でRemove Gamma Correctionもする必要があります。その時、ShaderLabで[Gamma]プレフィックスを使用する必要があります。
モバイルサポート:Androidでは、LinearはOpenGL ES 3.0およびAndroid4.3以降でのみサポートされ、iOSではMetalのみがサポートされます。ハードウェアデバイスの開発に伴い、より現実的な効果を追求するために、ますます多くのプロジェクトがLinear Spaceの下で作業することを選択しています
Unrealの場合
(Unrealデフォルトの照明表示)
Unrealの場合、原理は同じです。
関連するTextureのDetailsページにsRGBオプションがあります。
Unityと同様に、画像のストレージカラースペースを表します。
同様に、画像サンプリングを実行するときは、対応するサンプリングタイプを選択する必要があります。
Colorタイプ。サンプラーがsRGB色空間で画像をサンプリングすることを示します。
Linear Colorタイプ。サンプラーがLinear Spaceで画像をサンプリングすることを示します。
異なる色空間では、効果が異なります(平行光が追加されます)。
sRGBがチェックされていない場合、つまりこの画像がLinear Spaceの画像として扱われている場合、結果はより明るくなることがわかります。 Unityと比較すると、Unrealの設定はより便利であり、Unrealは色入力に対応するスペースを設定することもできます。
まとめ
今日、レンダリングの要件がますます高まっており、よりリアリズムのある物理ベースのレンダリング(PBR)の出現により、より正確な計算結果を得るために、Linear Spaceでのレンダリング計算が徐々に多くのチームに採用されています。 UnityとUnrealの2つの主要なエンジンもLinear Spaceワークフローをサポートしています。開発者は、Linearワークフローを理解し、データの入力、計算、および出力をどのように変更する必要があるかを理解する必要があります。この方法でのみ、データ計算の精度を保証できます。
関連する知識については、以下をお読みください。
「HDRと色彩管理」シリーズ
実際のプロジェクトでは、Gamma SpaceとLinear Spaceの開発経験について、次の記事を参考してください。
「ガンマから線形色空間へのUnityプロジェクトの経験共有」(中国語注意)
推奨:
「UnityのGamma Correctionと線形ワークフローについて話す」(中国語注意)
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
Gamma Spaceの起源
前書き
本節に触れる後処理効果は色の調整に関連しています。特定の後処理効果を勉強する前に、色の最終出力表示に関連する基本的な知識を理解する必要があります。それでは、Gamma Correctionの起源、プロジェクト開発におけるColor Spaceの特定のアプリケーション、UnityおよびUnrealエンジンの知識など、Gamma SpaceとLinear Spaceについて学習します。
知識学習に入る前に、概念を明確にする必要があります。今までGamma Spaceに関する議論は、限られた階調予算に基づいています。具体的な内容については、後ほど詳しく説明します。
CRTディスプレイ理論
それほど遠くない過去には、当時の主流のディスプレイはCRT(Cathode-ray tube)ディスプレイでした。そのフルネームは、よく見られる大きくて重たい物のブラウン管(陰極線管)ディスプレイです。
CRTディスプレイの場合:電圧を2倍にすると、明るさは2倍になりません。出力の輝度と電圧は線形ではなく、輝度の増加は電圧の増加の2.2乗にほぼ等しいため、2.2はCRTディスプレイのガンマ値と呼ばれ、CRTディスプレイの色はGamma2.2スペースにあります。
明るさと電圧の関係
電圧が直線的に変化する場合、現実の世界と比較して、CRTディスプレイの明るさは暗い領域でゆっくりと変化するため、暗いデータの範囲が広くなり、全体的な画像の色が暗くなります。
そして、私たちの目標は、現実世界と同様の明るさを見ることができることです。CRTディスプレイはGamma 2.2空間にあるため、ディスプレイに入力する画像の明るさは、Gamma 2.2の影響を平均化除去する必要があります。この操作はガンマ補正(Gamma Correction)と呼ばれ、実際には0.45乗の操作にほぼ等しいため、色はガンマ0.45空間にあります。
Gamma Correction
これにより、実空間の色に似た色に復元できます。
人間の目の階調知覚の理論
一方、現実の世界では、光の強度が0から1に増加すると、明るさはそれに比例して増加します。しかし、人間の目に知覚される明るさが直線的ではありません。この点は、ウェーバー-フェクナーの法則で示されています。外部刺激に対する人々の知覚は直線的ではなく、感覚量の増加は物理量より遅れ、物理量は等比数列で増加し、心理量は等差数列で増加します。つまり、人間の目で知覚される明るさが1レベル増加すると、光の強度は実際には数倍に増加します。
この現象に簡単な数学的シミュレーションをすると、人間の心理における明るさの知覚は、Gamma 0.45空間曲線に似ています。
人間は闇の知覚に対してより敏感であることがわかります。たとえば、同じ部屋で、暗闇の中でライトを点灯することと、100個のライトをオンにした後で別のライトをオンにすることとは、まったく異なる明るさの変化を人々に感じさせることができます。現実の世界では、光の強度の増加量は同じですが…
今日の主流の画像はチャネルあたり8ビットであるため、各チャネルには256の階調があり、階調数は比較的少なくなります。人間は闇の知覚に対してより敏感であるため、より暗い明るさを保存するためにより多くの階調が必要です。図の対応関係から、実空間での光の強度は0.2と仮定し、人間の目は0.5の明るさとして感じ、このデータを128番目の階調(中間の階調)とします。人間の目で知覚される明るさについては、階調数を均等に分割しますが、実空間では、0〜0.2の光強度間隔で割り当てられる階調数が多くなります。
このように階調が割り当てられます。実空間での強度が0.2の明るさは、人間の目が0.5の明るさとして認識し、人間の目が認識した明るさをデータとして記録します。人間の目で0.5と認識した明るさをディスプレイに送信して表示します。CRTディスプレイの場合、0.5の電圧はGamma 2.2で調整されます。Gamma 2.2は、人間の視覚のニーズを満たす0.2の明るさを示しています。これは、Gamma色空間の生成に関する主流の見解でもあります。すばらしい偶然の一致であり、CRTディスプレイのエラーは人間の目の知覚と一致します。実際、人間の目の明るさの知覚曲線の変換は、Gamma0.45空間曲線ほど単純ではなく、より複雑である可能性があります。この偶然の一致により、人間目の明るさの知覚曲線をシミュレートするために比較的単純で実行可能な方法が抽出された可能性があります。
したがって、現在の表示電圧と明るさが基本的に線形であっても、このルールに従い、表示Gamma値を設定すると、より多くの暗い領域の明るさが表示できるようになり、人間の目に知覚された暗い領域の階調数は、明るい領域の階調数に等しくなります。
ただし、技術の進歩により、いつか各チャンネルが32ビットになり、各チャンネルで4294967296の階調が可能になります。階調数が非常に多いため、表示できる明るさは十分なので、Gammaが必要なくなります。保存された結果は対応する明るさとして直接表示できます。これは、この記事の冒頭で述べた前提です。階調の予算は限られており、明るさの範囲を確保するために適切な割り当てが必要です。
sRGB
CRTディスプレイと一致させ、変換せずにチャネルあたり8ビットのディスプレイに画像を表示できるようにするために、HPとMicrosoftは1996年にStandard Red Blue Green色空間(sRGB色空間)を設計しました。今のほとんどの画像ファイルはこの色空間にあります。
sRGB色空間は、Gamma0.45が配置されている空間に対応します。これは、物理空間の色に対してガンマ補正を実行するのと同じです。物理世界の色は、Gamma 0.45が配置されている空間に保存されます。ディスプレイGamma2.2で調整した後、実際のデバイスに表示される結果は、実空間の色と一致します。
まとめ
このセクションでは、Gamma Spaceの起源に関する2つの主流の見解を紹介し、対応する計算方法とその背後にある実際的な重要性を理解しました。次のセクションでは、それが私たちの仕事にどのように影響するか、そしてそれを適切に適用する方法について話します。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
Radial Blur
基本知識
Radial Blurは、中心から外側に放射状に広がるストライプ状のぼかしとして現れる一般的な視覚効果です。
レースゲームやアクションの特殊効果で、高速モーションの視覚効果やカメラを突然ズームインしたときの衝撃的な効果を強調するためによく使用されます。
Need for Speedにおけるラジアルブラー効果
Radial Blurの基本原理は他のぼかし効果と同じです。周囲のピクセルと元のピクセルの色の値が一緒になってピクセルの色に影響を与え、ぼかし効果を実現します。Radial Blurの効果は、中心から外側に放射状に広がる形状であるため、選択したサンプリングポイントは、中心点とピクセルポイントを結ぶ延長線上に配置する必要があります。
図に示すように、赤は中心点、青は現在処理中のピクセル、緑はサンプリングポイント、赤の矢印の方向は中心点から現在のピクセルまでの延長線の方向です。
ピクセルが中心点から離れるほど、ぼやけます。したがって、サンプリングポイント間の距離は大きくなります。他のぼかし効果と同様に、サンプルポイントが多いほどぼかし効果は良くなりますが、コストが増加します。
Unityの実装
上記の原理によると、UnityでBuild-inパイプラインを使用して、Radial Blur効果を実装します。
まず、画像の中心を中心点として設定し、サンプリングステップサイズを計算します。
float2 Vector = (float2(0.5, 0.5) - i.uv)*_MainTex_TexelSize.xy*_BlurRadius;
このように、ステップサイズ(Vector)は中心点からピクセルまでの距離に正の相関があり、中心点から遠いピクセルはよりぼやけます。変数_BlurRadiusを追加して、サンプリングステップサイズを調整し、ぼかしの強さを制御します。
サンプリング、色の重ね合わせ、平均値を求めることで計算を行い、または必要に応じて異なる重みを選択することもできます。変数_SampleCountを設定して、サンプリングポイントの数を制御します。サンプリングされるピクセルが多いほど、ぼかし効果はよくなりますが、コストも大きくなります。
fixed4 blur(v2f i):COLOR
{
float2 Vector = (float2(0.5, 0.5) - i.uv)*_MainTex_TexelSize.xy*_BlurRadius;
fixed4 resultColor = fixed4(0, 0, 0, 0);
for (int index = 0; index < _SampleCount; ++index)
{
resultColor += tex2D(_MainTex, i.uv);
i.uv += Vector;
}
return resultColor / _SampleCount;
}
効果は図のようになります。
動的に変数_BlurRadiusを調整し、効果は次のとおりです。
最適化
Radial Blurは、前述のBlur最適化と同じ方法で最適化されます。 DrawCallを追加する代わりに、ダウンサンプリングによって解像度を低下させ、それによって画像サンプリング操作が減少し、最適化の目的を達成します。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com