Unity プロジェクトにおいて、基礎となるテクスチャピクセルデータにアクセスするさまざまな方法の利点とトレードオフについてご紹介します。
ピクセルデータはテクスチャ内の個々のピクセルの色を記述します。Unity では、C# スクリプトでピクセルデータからの読み込みやピクセルデータへの書き込みを可能にするメソッドを提供しています。
テクスチャの複製や更新(例えば、プレイヤーのプロフィール写真にディテールを追加する)、あるいはワールドマップを表すテクスチャを読み取ってオブジェクトを配置する場所を決定するような場合に、テクスチャのデータを特定の方法で使用するなどの目的でこれらのメソッドを使用します。
ピクセルデータから読み込んだり、ピクセルデータに書き込んだりするコードの書き方には、いくつかの方法があります。どの方法を選択するかは、そのデータで何をするつもりかと、プロジェクトのパフォーマンス要件によって決まります。
このブログと付属のサンプルプロジェクトでは、利用可能な API と一般的なパフォーマンスの落とし穴についてご紹介することを目的としています。両方を理解することで、パフォーマンスの良いソリューションを作成したり、パフォーマンスのボトルネックが発生したときに対処する際に役立つでしょう。
ほとんどの種類のテクスチャについて、Unity はピクセルデータのコピーを 2 つ保存します。1 つはレンダリングに必要な GPU メモリに、もう 1 つは CPU メモリに保存します。このコピーはオプションであり、CPU 上でピクセルデータからの読み出し、書き込み、操作を行うことができます。 ピクセルデータのコピーが CPU メモリに保存されているテクスチャを読み取り可能なテクスチャと呼びます。ただし、 RenderTexture は GPU メモリにしか存在しないので注意が必要です。
ほとんどのハードウェアで、CPU が使用できるメモリと GPU が使用できるメモリは異なります。デバイスによっては部分的にメモリを共有する形式もありますが、このブログでは、CPU がマザーボードに接続された RAM にのみ直接アクセスし、GPU は独自のビデオ RAM(VRAM)に依存するという従来の PC 構成を想定します。これらの異なる環境間でデータを転送する場合、PCI バスを経由する必要があり、同じ種類のメモリ内でデータを転送するよりも速度が遅くなります。このようなコストが生じるため、1 フレームあたりの転送データ量を抑えるようにしたほうがよいでしょう。
シェーダーでテクスチャをサンプリングすることは、GPU のピクセルデータ操作の中で最も一般的です。このデータを変更するには、テクスチャ間でコピーしたり、シェーダーを使ってテクスチャにレンダリングを行います。これらの操作はすべて GPU によって高速化させることができます。
場合によっては、CPU 上でテクスチャデータを操作する方が、データへのアクセス方法の自由度が高く、好ましいかもしれません。CPU のピクセルデータ操作は、データの CPU コピーに対してのみ作用するため、読み取り可能なテクスチャが必要です。更新されたピクセルデータをシェーダーでサンプリングしたい場合は、まず Apply を呼び出して CPU から GPU にコピーする必要があります。関係するテクスチャや操作の複雑さによっては、CPU 操作にこだわった方が高速で簡単な場合もあります(例えば、複数の 2D テクスチャを Texture2DArray アセットにコピーする場合など)。
Unity API は、テクスチャデータにアクセスしたり、処理したりするためのメソッドをいくつか提供しています。一部の演算は GPU コピーと CPU コピーの両方が存在する場合、両方に作用します。そのため、テクスチャが読み取れるかどうかによって、これらのメソッドのパフォーマンスは変化します。同じ結果を得るために異なるメソッドを使用することができますが、それぞれのメソッドには性能と使いやすさの特徴があります。
以下の質問に答えて、最適なソリューションを決めてください:
デフォルトでは、プロジェクトにインポートしたテクスチャアセットは読み取りできませんが、スクリプトから作成したテクスチャーは読み取り可能です。
読み取り可能なテクスチャは、CPU RAM にピクセルデータのコピーを持つ必要があるため、読み取り不可能なテクスチャに比べて 2 倍のメモリを使用します。テクスチャを読み取り可能にするのは必要なときだけで、CPU 上でのデータ処理が終わったら読み取り不可にしたほうが良いでしょう。
プロジェクト内のテクスチャアセットが読み取り可能かどうかを確認して編集するには、Texture Import Settings の Read/Write Enabled オプション、または TextureImporter.isReadable API を使用します。
テクスチャを読み取り不可にするには、makeNoLongerReadable パラメーターを「true」に設定して Apply メソッドを呼び出します(例えば Texture2D.Apply や Cubemap.Apply などのように)。読み取り不可のテクスチャは、再度読み取り可能にすることはできません。
すべてのテクスチャは、編集モードと再生モードでエディターから読み取ることができます。Apply を呼び出してテクスチャを読み取り不可にすると、isReadable の値が更新され、CPU データにアクセスできなくなります。しかし、Unity の一部のプロセスは、CPU の内部データが有効であることを確認するため、テクスチャが読み取れるものとして機能します。
テクスチャデータへのアクセス方法の違いにより、特に CPU ではパフォーマンスが大きく異なります(低解像度ではそれほどでもないのですが)。GitHub 上の Texture Access API examples リポジトリには、テクスチャデータへのアクセスや操作を可能にするさまざまな API のパフォーマンスの違いを示すサンプルが多数含まれています。UI にはメインスレッドの CPU 時間のみが表示されます。中には、Burst や Job System といった DOTS の機能を使い、パフォーマンスを最大限に発揮するケースもあります。
以下に、GitHub リポジトリに含まれるサンプルを示します:
次に挙げるのは、GitHub にあるサンプルから得られたパフォーマンス測定値です。これらの数値は、この後に続く推奨事項の根拠として使用されます。測定値は、3.7 GHz の 8 コアの Xeon® W-2145 CPU と RTX 2080 を搭載したシステムでのプレイヤービルドによるものです。
テクスチャサイズが 2,048 の SimpleCopy.UpdateTestCase の CPU 実行時間の中央値です。
Graphics メソッドは、単に作業を RenderThread にプッシュするだけであり、メインスレッド上ではほぼ瞬時に完了します。その後、GPU によって実行されることに注意してください。その結果は、次のフレームがレンダリングされるときに準備されることになります。
テクスチャサイズが 512 の PlasmaTexture.UpdateTestCase の CPU 時間の中央値です。
SetPixels32 は SetPixels よりも予想外に遅いことがわかると思います。これは、プラズマピクセル計算で得られた float ベースの Color 構造体の計算結果を、バイトベースの Color32 構造体に変換する必要があるためです。SetPixels32NoConversion はこの変換をスキップし、Color32 出力配列にデフォルト値を割り当てるだけなので、結果的に SetPixels よりもパフォーマンスが良くなります。SetPixels のパフォーマンスと Unity で実行される基本的な色変換を上回るためには、ピクセル計算方法自体を Color32 の値を直接出力するように修正することが必要です。SetPixelData を使ったシンプルな実装は、SetPixels や SetPixels32 を使った慎重なアプローチよりも良い結果をもたらすことがほぼ保証されています。
以下は、テクスチャーサイズが 8,196 の TransferGPUTexture.UpdateTestCase の Editor GPU 時間です:
ピクセルデータには様々な方法でアクセスすることができます。ただし、すべての方法がすべての形式、テクスチャタイプ、ユースケースをサポートしているわけではなく、実行に時間がかかるものもあります。ここでは、推奨される方法について説明し、次のセクションでは注意して使用する方法について説明します。
CopyTexture は、GPU データをあるテクスチャから別のテクスチャに転送する最速の方法です。形式の変換は行いません。領域の幅と高さに加え、ソースとターゲットの位置を指定することで、データを部分的にコピーすることができます。両方のテクスチャが読み取り可能な場合、コピー操作は CPU データに対しても行われるため、この方法の総コストは、ソーステクスチャからの GetPixelData の結果を SetPixelData を使用して CPU のみでコピーする場合のコストに近くなります。
Blit はシェーダーを使用して GPU データを RenderTexture に転送する高速で強力な方法です。実際には、ターゲットの RenderTexture にレンダリングするために、グラフィックスパイプラインの API の状態を設定する必要があります。CopyTexture と比較して、解像度に依存しないセットアップコストが少なくて済みます。このメソッドで使用されるデフォルトの Blit シェーダーは、入力テクスチャを受け取り、ターゲット RenderTexture にレンダリングします。カスタムマテリアルやシェーダーを用意することで、複雑なテクスチャ間のレンダリング処理を定義することができます。
GetPixelData と SetPixelData は(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 メソッドは CPU データが GPU にアップロードされると呼び出し元に処理を戻してきます。アップロード後に CPU データのメモリを解放するため、makeNoLongerReadable パラメーターは可能な限り「true」に設定する必要があります。
RequestIntoNativeArray メソッドと RequestIntoNativeSlice メソッドは、指定された Texture の GPU データを、ユーザーが提供する NativeArray(のスライス)に非同期でダウンロードします。
メソッドを呼び出すと、要求されたデータのダウンロードが完了したかどうかを示すことができるリクエストハンドルを返します。サポートされる形式はほんの一握りに限られているので、形式がサポートされているかをチェックするには、FormatUsage.ReadPixels と一緒に SystemInfo.IsFormatSupported を使用します。AsyncGPUReadback クラスにも Request メソッドがあり、NativeArray を割り当ててくれます。この操作を繰り返す必要がある場合は、代わりに再利用する NativeArray を割り当てた方がパフォーマンスが向上するでしょう。
パフォーマンスに重大な影響を与える可能性があるため、注意して使用する必要があるメソッドがいくつかあります。それらを詳しく見てみましょう。
これらのメソッドは、さまざまな複雑さのピクセル形式の変換を実行します。Pixels32 のバリアントがこの中で最もパフォーマンスが高いですが、テクスチャの基本形式が Color32 構造体と完全に一致しない場合は、形式の変換を行うことができます。以下のメソッドを使用する場合、ピクセル数が増加するにつれて、程度の差はあれパフォーマンスへの影響が大幅に増加することを念頭に置いておくと良いでしょう:
GetRawTextureData と LoadRawTextureData は Texture2D 専用のメソッドで、すべてのミップレベルの生のピクセルデータを含む配列を次々に処理します。レイアウトは大きい方から小さい方へと進み、ミップごとに「高さ」のピクセル値と「幅」のピクセル値があります。これらの関数は CPU のデータアクセスを素早く行うことができます。GetRawTextureData には、テンプレート化されていないバリアントがデータのコピーを返すという「からくり」があります。これは少し遅く、Unity が管理する下層にあるバッファを直接操作することはできません。GetPixelData にはこのような特徴はなく、ユーザーコードが Unity に制御を戻すまで有効な、下層にあるバッファを指す NativeArray のみを返すことができます。
ConvertTexture は、あるテクスチャから別のテクスチャに GPU データを転送する際、転送元と転送先のテクスチャのサイズや形式が同じではない時に使える方法です。この変換プロセスは、この状況下では効率的と言えますが決して簡単なプロセスではありません。内部プロセスは以下のとおりです:
このメソッドがあなたのユースケースに適しているかどうかを判断するために、以下の質問に答えてください:
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 には、さらに多くの高度なベストプラクティスが掲載されています。
覚えておくべきポイントのまとめは以下の通りです:
テクスチャデータに関するフィードバックは、Scripting または General Graphics フォーラムで共有してください。現在進行中の Tech from the Trenches シリーズの一環として、他の Unity 開発者による新しい技術ブログをぜひご覧ください。
Is this article helpful for you?
Thank you for your feedback!