こんにちは。情熱開発部プログラム課所属の廣江です。
少し前に「Clair Obscur: Expedition 33」をプレイしていたのですが、ボリュームレンダリングやってそうだなー。。自分でもやってみたい!!
ということで、色々と調べていました。
その中でどうやら2Dテクスチャ配列を使用している場合があるという情報がありました。
基本的にボリュームレンダリングでは3Dテクスチャを使用するものと考えていたのですが、確かにデータ構造は似ているので2Dテクスチャ配列も使えそうですね。
2Dテクスチャ配列と3Dテクスチャでどういう違いがあるのでしょうか?
今回はボリュームレンダリングで使用する観点から、2Dテクスチャ配列と3Dテクスチャの違いについて検証を行ってみようと思います!
※Unity 6000.0.40f1を使用
2Dテクスチャ配列と3Dテクスチャ
まずは2Dテクスチャ配列と3Dテクスチャのスペックについて比較してみます。
(テクスチャ構造の詳細については、Direct3D 11 のテクスチャの概要をご参照ください)

ボリュームレンダリングをやる上で、Z軸が補間されるかは、見た目の品質的に重要そうですね。
品質的に3Dテクスチャの方がよさそうですが、何か2Dテクスチャ配列を採用するメリットはあるんでしょうか?
ということでChatGPT先生に聞いてみました!
ChatGPT先生によると、「3Dテクスチャはキャッシュのヒット率が低くなる傾向にある」とのことでした。
このあたり色々と調べてみたのですが、明確に3Dテクスチャのキャッシュについて説明されているものが見つかりませんでした。
うーん、これは検証して確認してみるしかないですね!
検証の準備
検証のために、テクスチャとボリュームレンダリング用のシェーダを用意します。
テクスチャ準備
まずはそれぞれのテクスチャを用意します。
UnityではテクスチャのInspectorでTexture Shapeを変更して、タイル状に並んでいるテクスチャをTexture2DArrayまたは、Texture3Dにすることができます。
ColumnsとRowsは縦横のタイル数を指定します。
これだけでテクスチャの準備は完了!Unity便利ですね~

テクスチャ参照用のシェーダを作成
次は検証用のシェーダを作成します。
今回はBox内でレイマーチングを行い、2Dテクスチャ配列または、3Dテクスチャをサンプリングして描画するシェーダを作成します。
こちらはBoxのオブジェクト空間でレイマーチングを行うシェーダです。
テクスチャサンプリングのパフォーマンスも検証したいため、無駄な処理は省きシンプルな実装にしています。
float4 RaymarchingVolumetricFog(RaymarchingData raymarchingData)
{
const int c_MaxSteps = 256;
const float c_StepSize = max(raymarchingData.rayMaxDistance / c_MaxSteps, 0.001);
const float c_StepWeight = 1.0 / (float) c_MaxSteps;
const float3 c_BoxSize = float3(2, 2, 2); // 仮でBoxサイズは固定
const float3 c_HalfBoxSize = c_BoxSize / 2;
float sum = 0;
for (int i = 0; i < c_MaxSteps; ++i)
{
float t = i * c_StepSize;
float3 rayPosOS = raymarchingData.rayStartOS + raymarchingData.rayDirOS * t;
float3 samplePos = saturate((rayPosOS + c_HalfBoxSize) / c_BoxSize);
float density = SampleDensity(samplePos);
density = saturate(density - (1 - _DensityStrength));
sum += density * c_StepWeight;
if (raymarchingData.rayMaxDistance <= t)
break;
}
return float4(_FogColor.rgb, sum);
}
以下はテクスチャをサンプリングする関数です。
引数のsamplePosはオブジェクト空間で正規化した座標です。
また、ShaderKeywordで使用するテクスチャを切り替え出来るようにしておきます。
float SampleDensity(float3 samplePos)
{
#if defined(USE_3D_VOLUME)
float3 uvw = float3(samplePos.x * _TillingX, samplePos.z * _TillingY, samplePos.y);
float density = SAMPLE_TEXTURE3D(_DensityTexture3D, sampler_DensityTexture3D, uvw).r;
return density;
#else
const float c_TextureSlices = 16;
int sliceIndex = clamp(float(samplePos.y * c_TextureSlices), 0, c_TextureSlices);
float density = SAMPLE_TEXTURE2D_ARRAY(_DensityTexture, sampler_DensityTexture, float2(samplePos.x * _TillingX, samplePos.z * _TillingY), sliceIndex).r;
return density;
#endif
}
一応RaymarchingDataの中身も貼っておきます。
struct RaymarchingData
{
float3 rayDirOS; // オブジェクト空間のレイベクトル
float3 rayStartOS; // オブジェクト空間のレイ開始座標
float rayMaxDistance; // レイの最大距離
};
検証
それでは見た目とパフォーマンスについて検証を行っていきたいと思います。
事前に調査した内容的には、見た目の品質は3Dテクスチャの方が良く、パフォーマンスは2Dテクスチャ配列の方が良くなるはずです。
果たして結果は…!!
見た目の比較
以下の画像は2Dテクスチャ配列と3Dテクスチャの比較です。
テクスチャ座標のXY平面から見た場合、そこまで差は出なかったのですが、XZ平面から見た場合は2Dテクスチャ配列だとピクセル感が目立つ結果となりました。
これは2Dテクスチャ配列がZ軸に対して補間を行わないためです。
3Dテクスチャはどの方向から描画しても品質が崩れるようなことはありませんでした。




パフォーマンスの比較
パフォーマンスの比較はGPU時間を画面に表示して確認します。
GPU時間の計測にはFrameTimingManagerを使用します。
弊社のブログにFrameTimeingManagerについて紹介しているものがあるので、興味がある方はこちらご参照ください!
【Unity】UnityでのリアルタイムGPU負荷測定について
ざっくりとしたものですが、2Dテクスチャ配列よりも3Dテクスチャの方がGPU時間が0.3~2.0ms増という結果になりました。
ということで、この検証結果を見る限り、ChatGPT先生の言っていることは正しそうですね!
まとめ
今回はボリュームレンダリングで2Dテクスチャ配列と3Dテクスチャの比較を行ってみました。
見た目、パフォーマンスのどちらも概ね予想通りの結果となりました!
ローエンド端末ではできる限り2Dテクスチャ配列を使用した方がよさそうです。
また、ハイエンド端末でも特定の方向からのみボリュームを見る場合、品質にそこまで差は出なかったため、2Dテクスチャ配列を使用してもいいかもしれません。
参考文献
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。