Unity を検索

テクスチャデータへの効率的なアクセス

2023年5月25日 カテゴリ: Engine & platform | 15 分 で読めます
Accessing texture data efficiently | Cover (hero) image
Accessing texture data efficiently | Cover (hero) image
シェア

Is this article helpful for you?

Thank you for your feedback!

Unity プロジェクトにおいて、基礎となるテクスチャピクセルデータにアクセスするさまざまな方法の利点とトレードオフについてご紹介します。

Unity でピクセルデータを扱う

ピクセルデータはテクスチャ内の個々のピクセルの色を記述します。Unity では、C# スクリプトでピクセルデータからの読み込みやピクセルデータへの書き込みを可能にするメソッドを提供しています。

テクスチャの複製や更新(例えば、プレイヤーのプロフィール写真にディテールを追加する)、あるいはワールドマップを表すテクスチャを読み取ってオブジェクトを配置する場所を決定するような場合に、テクスチャのデータを特定の方法で使用するなどの目的でこれらのメソッドを使用します。

ピクセルデータから読み込んだり、ピクセルデータに書き込んだりするコードの書き方には、いくつかの方法があります。どの方法を選択するかは、そのデータで何をするつもりかと、プロジェクトのパフォーマンス要件によって決まります。

このブログと付属のサンプルプロジェクトでは、利用可能な API と一般的なパフォーマンスの落とし穴についてご紹介することを目的としています。両方を理解することで、パフォーマンスの良いソリューションを作成したり、パフォーマンスのボトルネックが発生したときに対処する際に役立つでしょう。

ピクセルデータの CPU・GPU コピーについて

ほとんどの種類のテクスチャについて、Unity はピクセルデータのコピーを 2 つ保存します。1 つはレンダリングに必要な GPU メモリに、もう 1 つは CPU メモリに保存します。このコピーはオプションであり、CPU 上でピクセルデータからの読み出し、書き込み、操作を行うことができます。 ピクセルデータのコピーが CPU メモリに保存されているテクスチャを読み取り可能なテクスチャと呼びます。ただし、 RenderTexture は GPU メモリにしか存在しないので注意が必要です。

CPU と GPU の相違点

メモリ

ほとんどのハードウェアで、CPU が使用できるメモリと GPU が使用できるメモリは異なります。デバイスによっては部分的にメモリを共有する形式もありますが、このブログでは、CPU がマザーボードに接続された RAM にのみ直接アクセスし、GPU は独自のビデオ RAM(VRAM)に依存するという従来の PC 構成を想定します。これらの異なる環境間でデータを転送する場合、PCI バスを経由する必要があり、同じ種類のメモリ内でデータを転送するよりも速度が遅くなります。このようなコストが生じるため、1 フレームあたりの転送データ量を抑えるようにしたほうがよいでしょう。

Flowchart visualizing the relationship between CPU and GPU memory, and a cross section of the API
CPU メモリと GPU メモリの関係、および API が関わる部分の図解

処理

シェーダーでテクスチャをサンプリングすることは、GPU のピクセルデータ操作の中で最も一般的です。このデータを変更するには、テクスチャ間でコピーしたり、シェーダーを使ってテクスチャにレンダリングを行います。これらの操作はすべて GPU によって高速化させることができます。

場合によっては、CPU 上でテクスチャデータを操作する方が、データへのアクセス方法の自由度が高く、好ましいかもしれません。CPU のピクセルデータ操作は、データの CPU コピーに対してのみ作用するため、読み取り可能なテクスチャが必要です。更新されたピクセルデータをシェーダーでサンプリングしたい場合は、まず Apply を呼び出して CPU から GPU にコピーする必要があります。関係するテクスチャや操作の複雑さによっては、CPU 操作にこだわった方が高速で簡単な場合もあります(例えば、複数の 2D テクスチャを Texture2DArray アセットにコピーする場合など)。

Unity API は、テクスチャデータにアクセスしたり、処理したりするためのメソッドをいくつか提供しています。一部の演算は GPU コピーと CPU コピーの両方が存在する場合、両方に作用します。そのため、テクスチャが読み取れるかどうかによって、これらのメソッドのパフォーマンスは変化します。同じ結果を得るために異なるメソッドを使用することができますが、それぞれのメソッドには性能と使いやすさの特徴があります。

以下の質問に答えて、最適なソリューションを決めてください:

  • GPU は CPU よりも速く計算を行うことができるか
    • テクスチャキャッシュにどの程度の負荷がかかっているのか(例えば、ミップマップを使わずに高解像度のテクスチャを多数サンプリングすると、GPU の動作が遅くなる可能性がある。)
    • その処理には、ランダム書き込みのテクスチャが必要なのか、それとも色または深度アタッチメントに出力できるのか(テクスチャ上のランダムなピクセルに書き込むには、頻繁にキャッシュをフラッシュする必要があり、GPU の処理速度が低下する。)
  • プロジェクトで GPU がすでにボトルネックになっていないか。GPU が CPU より高速に処理を実行できたとしても GPU はフレーム時間予算を超えることなく、より多くの処理を実行する余裕があるか
    • GPU と CPU のメインスレッドの両方がフレーム時間の限界に近い場合、おそらく処理の遅い部分は CPU のワークスレッドで実行できる
  • 計算や処理のために、GPU にアップロードしたり、GPU からダウンロードしたりするデータはどのくらい必要か
    • シェーダーや C# のジョブで、必要な帯域幅を減らすためにデータをより小さな形式にパックすることは可能か
    • RenderTexture をダウンサンプリングして解像度の低いバージョンにし、代わりにダウンロードすることは可能か
  • プロセスを何回かに分けて実行することは可能か(一度に多くのデータを処理する必要がある場合、GPU のメモリが不足するリスクがある。)
  • どの程度の速さで結果が求められるか。計算やデータ転送を非同期で行い、後で処理することは可能か(1 フレームで多くの作業を行うと、GPU が各フレームの実際のグラフィックスをレンダリングする時間が足りなくなる危険性がある。)

テクスチャを読み取り可能または不可にする

デフォルトでは、プロジェクトにインポートしたテクスチャアセットは読み取りできませんが、スクリプトから作成したテクスチャーは読み取り可能です。

読み取り可能なテクスチャは、CPU RAM にピクセルデータのコピーを持つ必要があるため、読み取り不可能なテクスチャに比べて 2 倍のメモリを使用します。テクスチャを読み取り可能にするのは必要なときだけで、CPU 上でのデータ処理が終わったら読み取り不可にしたほうが良いでしょう。

プロジェクト内のテクスチャアセットが読み取り可能かどうかを確認して編集するには、Texture Import SettingsRead/Write Enabled オプション、または TextureImporter.isReadable API を使用します。

テクスチャを読み取り不可にするには、makeNoLongerReadable パラメーターを「true」に設定して Apply メソッドを呼び出します(例えば Texture2D.Apply や Cubemap.Apply などのように)。読み取り不可のテクスチャは、再度読み取り可能にすることはできません。

すべてのテクスチャは、編集モードと再生モードでエディターから読み取ることができます。Apply を呼び出してテクスチャを読み取り不可にすると、isReadable の値が更新され、CPU データにアクセスできなくなります。しかし、Unity の一部のプロセスは、CPU の内部データが有効であることを確認するため、テクスチャが読み取れるものとして機能します。

GitHub の Texture Access API examples

Example of a texture generated on the CPU each frame
CPU 上で毎フレーム生成されるテクスチャのサンプル

テクスチャデータへのアクセス方法の違いにより、特に CPU ではパフォーマンスが大きく異なります(低解像度ではそれほどでもないのですが)。GitHub 上の Texture Access API examples リポジトリには、テクスチャデータへのアクセスや操作を可能にするさまざまな API のパフォーマンスの違いを示すサンプルが多数含まれています。UI にはメインスレッドの CPU 時間のみが表示されます。中には、BurstJob System といった DOTS の機能を使い、パフォーマンスを最大限に発揮するケースもあります。

以下に、GitHub リポジトリに含まれるサンプルを示します:

  • SimpleCopy:1 つのテクスチャから別のテクスチャにすべてのピクセルをコピーする
  • PlasmaTexture:1 フレームごとに CPU で更新されるプラズマテクスチャ
  • TransferGPUTexture:GPU 上のすべてのピクセルをテクスチャから RenderTexture に転送する(異なるサイズや形式にコピーする)

次に挙げるのは、GitHub にあるサンプルから得られたパフォーマンス測定値です。これらの数値は、この後に続く推奨事項の根拠として使用されます。測定値は、3.7 GHz の 8 コアの Xeon® W-2145 CPU と RTX 2080 を搭載したシステムでのプレイヤービルドによるものです。

SimpleCopy example

テクスチャサイズが 2,048 の SimpleCopy.UpdateTestCase の CPU 実行時間の中央値です。

Graphics メソッドは、単に作業を RenderThread にプッシュするだけであり、メインスレッド上ではほぼ瞬時に完了します。その後、GPU によって実行されることに注意してください。その結果は、次のフレームがレンダリングされるときに準備されることになります。

結果

  • 1,326 ms – foreach(mip) for(x in width) for(y in height) SetPixel(x, y, GetPixel(x, y, mip), mip)
  • 32.14 ms – foreach(mip) SetPixels(source.GetPixels(mip), mip)
  • 6.96 ms – foreach(mip) SetPixels32(source.GetPixels32(mip), mip)
  • 6.74 ms – LoadRawTextureData(source.GetRawTextureData())
  • 3.54 ms – Graphics.CopyTexture(readableSource, readableTarget)
  • 2.87 ms – foreach(mip) SetPixelData(mip, GetPixelData(mip))
  • 2.87 ms – LoadRawTextureData(source.GetRawTextureData())
  • 0.00 ms – Graphics.ConvertTexture(source, target)
  • 0.00 ms – Graphics.CopyTexture(nonReadableSource, target)

PlasmaTexture example

テクスチャサイズが 512 の PlasmaTexture.UpdateTestCase の CPU 時間の中央値です。

SetPixels32 は SetPixels よりも予想外に遅いことがわかると思います。これは、プラズマピクセル計算で得られた float ベースの Color 構造体の計算結果を、バイトベースの Color32 構造体に変換する必要があるためです。SetPixels32NoConversion はこの変換をスキップし、Color32 出力配列にデフォルト値を割り当てるだけなので、結果的に SetPixels よりもパフォーマンスが良くなります。SetPixels のパフォーマンスと Unity で実行される基本的な色変換を上回るためには、ピクセル計算方法自体を Color32 の値を直接出力するように修正することが必要です。SetPixelData を使ったシンプルな実装は、SetPixels や SetPixels32 を使った慎重なアプローチよりも良い結果をもたらすことがほぼ保証されています。

結果

  • 126.95 ms – SetPixel
  • 113.16 ms – SetPixels32
  • 88.96 ms – SetPixels
  • 86.30 ms – SetPixels32NoConversion
  • 16.91 ms – SetPixelDataBurst
  • 4.27 ms – SetPixelDataBurstParallel

TransferGPUTexture example

以下は、テクスチャーサイズが 8,196 の TransferGPUTexture.UpdateTestCase の Editor GPU 時間です:

  • Blit – 1.584 ms
  • CopyTexture – 0.882 ms

ピクセルデータに関する API の推奨事項

ピクセルデータには様々な方法でアクセスすることができます。ただし、すべての方法がすべての形式、テクスチャタイプ、ユースケースをサポートしているわけではなく、実行に時間がかかるものもあります。ここでは、推奨される方法について説明し、次のセクションでは注意して使用する方法について説明します。

CopyTexture

CopyTexture は、GPU データをあるテクスチャから別のテクスチャに転送する最速の方法です。形式の変換は行いません。領域の幅と高さに加え、ソースとターゲットの位置を指定することで、データを部分的にコピーすることができます。両方のテクスチャが読み取り可能な場合、コピー操作は CPU データに対しても行われるため、この方法の総コストは、ソーステクスチャからの GetPixelData の結果を SetPixelData を使用して CPU のみでコピーする場合のコストに近くなります。

Blit

Blit はシェーダーを使用して GPU データを RenderTexture に転送する高速で強力な方法です。実際には、ターゲットの RenderTexture にレンダリングするために、グラフィックスパイプラインの API の状態を設定する必要があります。CopyTexture と比較して、解像度に依存しないセットアップコストが少なくて済みます。このメソッドで使用されるデフォルトの Blit シェーダーは、入力テクスチャを受け取り、ターゲット RenderTexture にレンダリングします。カスタムマテリアルやシェーダーを用意することで、複雑なテクスチャ間のレンダリング処理を定義することができます。

GetPixelData と SetPixelData

GetPixelDataSetPixelData は(GetRawTextureData とともに)、CPU データだけを扱う場合に使用する最速の方法です。どちらの方法もデータを再解釈するためのテンプレートパラメーターとして構造体型を提供する必要があります。メソッド自体は、正しいサイズを導き出すためにこの構造体を必要とするだけなので、テクスチャの形式を表すカスタム構造体を定義したくない場合は byte を使用すればよいでしょう。

個々のピクセルにアクセスする場合は、使いやすいように、いくつかのユーティリティメソッドを持つカスタム構造体を定義することをお勧めします。例えば、R5G5B5A1 形式を表す構造体は、ushort データメンバーといくつかの get/set メソッドで構成され、個々のチャネルをバイトとしてアクセスすることができます。

public struct FormatR5G5B5A1 { public ushort data; const ushort redOffset = 11; const ushort greenOffset = 6; const ushort blueOffset = 1; const ushort alphaOffset = 0; const ushort redMask = 31 << redOffset; const ushort greenMask = 31 << greenOffset; const ushort blueMask = 31 << blueOffset; const ushort alphaMask = 1; public byte red { get { return (byte)((data & redMask) >> redOffset); } } public byte green { get { return (byte)((data & greenMask) >> greenOffset); } } public byte blue { get { return (byte)((data & blueMask) >> blueOffset); } } public byte alpha { get { return (byte)((data & alphaMask) >> alphaOffset); } } }

上記のコードは、R5G5B5A5A1 形式のピクセルを表すオブジェクトの実装例で、対応するプロパティのセッターは簡潔にするために省略されています。

SetPixelData は、データの完全なミップレベルをターゲットテクスチャにコピーするために使用できます。GetPixelData は、Unity の CPU 内部のテクスチャデータのうち実際にミップレベルが 1 を指す NativeArray を返します。これによってコピー操作を必要とせず、そのデータを直接読み書きすることができます。GetPixelData が返す NativeArray は、GetPixelData を呼び出したユーザーコードが MonoBehaviour.Update が返るなどして Unity に制御を戻すまで有効であることが保証されるだけです。フレーム間で GetPixelData の結果を保存する代わりに、このデータにアクセスしたいフレームごとに GetPixelData から正しい NativeArray を取得する必要があります。

Apply

Apply メソッドは CPU データが GPU にアップロードされると呼び出し元に処理を戻してきます。アップロード後に CPU データのメモリを解放するため、makeNoLongerReadable パラメーターは可能な限り「true」に設定する必要があります。

RequestIntoNativeArray と RequestIntoNativeSlice

RequestIntoNativeArray メソッドと RequestIntoNativeSlice メソッドは、指定された Texture の GPU データを、ユーザーが提供する NativeArray(のスライス)に非同期でダウンロードします。

メソッドを呼び出すと、要求されたデータのダウンロードが完了したかどうかを示すことができるリクエストハンドルを返します。サポートされる形式はほんの一握りに限られているので、形式がサポートされているかをチェックするには、FormatUsage.ReadPixels と一緒に SystemInfo.IsFormatSupported を使用します。AsyncGPUReadback クラスにも Request メソッドがあり、NativeArray を割り当ててくれます。この操作を繰り返す必要がある場合は、代わりに再利用する NativeArray を割り当てた方がパフォーマンスが向上するでしょう。

使用にあたり注意が必要なメソッド

パフォーマンスに重大な影響を与える可能性があるため、注意して使用する必要があるメソッドがいくつかあります。それらを詳しく見てみましょう。

基礎的な変換を伴うピクセルアクセサー

これらのメソッドは、さまざまな複雑さのピクセル形式の変換を実行します。Pixels32 のバリアントがこの中で最もパフォーマンスが高いですが、テクスチャの基本形式が Color32 構造体と完全に一致しない場合は、形式の変換を行うことができます。以下のメソッドを使用する場合、ピクセル数が増加するにつれて、程度の差はあれパフォーマンスへの影響が大幅に増加することを念頭に置いておくと良いでしょう:

注意が必要な高速データアクセサー

GetRawTextureDataLoadRawTextureData は Texture2D 専用のメソッドで、すべてのミップレベルの生のピクセルデータを含む配列を次々に処理します。レイアウトは大きい方から小さい方へと進み、ミップごとに「高さ」のピクセル値と「幅」のピクセル値があります。これらの関数は CPU のデータアクセスを素早く行うことができます。GetRawTextureData には、テンプレート化されていないバリアントがデータのコピーを返すという「からくり」があります。これは少し遅く、Unity が管理する下層にあるバッファを直接操作することはできません。GetPixelData にはこのような特徴はなく、ユーザーコードが Unity に制御を戻すまで有効な、下層にあるバッファを指す NativeArray のみを返すことができます。

ConvertTexture

ConvertTexture は、あるテクスチャから別のテクスチャに GPU データを転送する際、転送元と転送先のテクスチャのサイズや形式が同じではない時に使える方法です。この変換プロセスは、この状況下では効率的と言えますが決して簡単なプロセスではありません。内部プロセスは以下のとおりです:

  1. 転送先テクスチャに一致する一時的な RenderTexture を割り当てる
  2. 転送元テクスチャから一時的な RenderTexture に Blit を実行する
  3. Blit 結果を一時的な RenderTexture から転送先のテクスチャにコピーする

このメソッドがあなたのユースケースに適しているかどうかを判断するために、以下の質問に答えてください:

  • この変換を行う必要があるか
    • インポート時に、転送元テクスチャが転送先のプラットフォーム用に希望のサイズ/形式で作成されているようにできるか
    • 同じ形式を使用するようにプロセスを変更し、あるプロセスの結果を別のプロセスの入力として直接使用することはできるか
  • 代わりに RenderTexture を作成し、転送先として使用することはできるか。そうすることで、転送先の RenderTexture への変換処理を 1 回の Blit 処理に減らすことができる

ReadPixels

ReadPixels メソッドは、アクティブな RenderTexture(RenderTexture.active)から Texture2D の CPU データに GPU データを同期的にダウンロードします。これによって、レンダリング操作からの出力を保存または処理することができます。サポートされる形式はほんの一握りに限られているので、形式がサポートされているかをチェックするには、FormatUsage.ReadPixels と一緒に SystemInfo.IsFormatSupported を使用します。

GPU からデータをダウンロードするのは時間がかかります。開始する前に、ReadPixels は GPU が前の作業をすべて完了するのを待つ必要があります。このメソッドは、要求されたデータが利用可能になるまで処理を返さないためパフォーマンスが低下するので避けた方がいいでしょう。また、GPU データを RenderTexture に入れる必要があり、RenderTexture は現在アクティブなものとして設定する必要があるためユーザビリティも懸念されます。先に説明した AsyncGPUReadback メソッドを使用すると、ユーザビリティとパフォーマンスの両方が向上します。

画像ファイル形式間の変換方法

ImageConversion クラスは、Texture2D といくつかの画像ファイル形式を変換するメソッドを持っています。LoadImage は JPG、PNG、EXR(Unity 2023.1 以降)のデータを Texture2D にロードし、GPU にアップロードしてくれます。ロードされたピクセルデータは、Texture2D の元の形式に応じて、その場で圧縮することができます。他のメソッドで、Texture2D やピクセルデータの配列を JPG、PNG、TGA、EXR データの配列に変換することができます。

これらのメソッドは特に高速というわけではないですが、プロジェクトが一般的な画像ファイル形式でピクセルデータを渡す必要がある場合に役立ちます。一般的なユースケースとしては、ユーザーのアバターをディスクからロードし、ネットワーク経由で他のプレイヤーと共有することなどが挙げられます。

重要なポイントとより高度なリソース

グラフィックスの最適化、関連するトピック、Unity でのベストプラクティスについてもっと学ぶために利用できるリソースがたくさんあります。ドキュメントのグラフィックスのパフォーマンスとプロファイリングのセクションは良い出発点です。

また、「Ultimate guide to profiling Unity games、「Optimize your mobile game performance」、Optimize your console and PC game performance」など、上級ユーザー向けの技術的な e ブックもご覧いただけます。

Unity how-to Hub には、さらに多くの高度なベストプラクティスが掲載されています。

覚えておくべきポイントのまとめは以下の通りです:

  • テクスチャを操作する場合において最初のステップは、どの操作を GPU 上で実行すれば最適なパフォーマンスが得られるかを評価することである。既存の CPU/GPU ワークロードと入出力データのサイズは、考慮すべき重要な要素である。
  • GetRawTextureData のような低レベルの関数を使用して、必要に応じて特定の変換パスを実装すれば、(しばしば冗長な)コピーや変換を実行する便利なメソッドよりもパフォーマンスが向上する。
  • 大規模なリードバックやピクセル計算など、より複雑な演算は非同期または並列で実行される場合にのみ CPU 上で実行可能である。Burst とジョブシステムを組み合わせることで、C# は GPU 上でしか実行できないような特定の処理を実行できるようになる。
  • 頻繁にプロファイリングを行う。予期せぬ不要な変換や、他のプロセス待ちによる停滞など、開発中に遭遇する落とし穴は数多くある。パフォーマンスの問題が表面化するのは、ゲームのスケールが大きくなり、コードの特定の部分の使用頻度が高くなってからである。この記事で紹介したサンプルプロジェクトは、一見小さなテクスチャ解像度の増加が特定の API のパフォーマンスの問題を引き起こす可能性があることを示している。

テクスチャデータに関するフィードバックは、Scripting または General Graphics フォーラムで共有してください。現在進行中の Tech from the Trenches シリーズの一環として、他の Unity 開発者による新しい技術ブログをぜひご覧ください。

2023年5月25日 カテゴリ: Engine & platform | 15 分 で読めます

Is this article helpful for you?

Thank you for your feedback!

関連する投稿