2026年5月26日

逆Zバッファ

Zバッファー(深度バッファー)は、コンピューターグラフィックスにおいて各ピクセルでのオブジェクトの可視性を決定するために使用されます。
オブジェクトを描画するたびに、カメラからの距離もZバッファーに保存します。
次にオブジェクトを描画する際、Zバッファーに以前保存された値を確認できます。Zバッファーの値が描画しようとしているオブジェクトよりも近い場合、そのオブジェクトは遮蔽されていると判断し、ピクセルを描画しません。この技術は深度テストと呼ばれることもあります。

深度テストのアプローチはすべてのグラフィックスエンジンで広く普及しており、Evergineも同様です。しかし、これから見るように、単純なアプローチは精度の面で最良の結果をもたらしません。このリリースでは、特に遠くのオブジェクトの精度を大幅に向上させるリバースZバッファリングを導入しました。

従来のZバッファー

従来のZバッファリングでは、深度値を[0, 1]の範囲に正規化して格納します。0が最も近い値、1が最も遠い値です。

次の図は、ある立方体を見つめる3Dカメラを示しています。
緑の線は「ニア」平面(近クリップ面)を表し、ニア平面より手前のピクセルはクリップ(破棄)されます。
赤の線は「ファー」平面(遠クリップ面)を表し、ファー平面より奥のピクセルはクリップされます。

マゼンタの線は、描画しようとしているピクセルまでの距離(d)を表します。
正規化された深度バッファ値は、次の式で計算されます。

depthBuffer=(far)/(far-near)-(far・near)/(far-near)・1/d

dがnearと等しい場合、結果の値はゼロになります。
同様に、dがfarと等しい場合、結果の値は1になります。

次の図は、典型的な深度バッファがどのように見えるかを示しています。

精度の問題

従来の深度バッファリングで発生しうる精度の問題を示すために、次のデモを作成しました。

このシーンの球体は、実際には二重構造になっています。一方の球体がもう一方の内側にあります。
これらの層状の球体の配置を次の図に示します。2つの層は非常に近接して配置されています。

当然ながら、内側の赤い球体は見えないはずです。しかし、デモでご覧いただけるように、カメラからの距離が離れるにつれて、赤いアーティファクトが見え始めます。

この現象はエイリアシングとして知られています。数値表現の精度により、深度値が正規化されると同じ数値にマッピングされてしまい、どちらが手前かを判別できなくなります。

一つの選択肢として、深度バッファで値を表現するために使用するビット数を増やすことが考えられますが、ほとんどのハードウェアは32ビットを超えることができず、32ビットでは不十分です。

リバース深度

この精度の問題は、リバース深度バッファリングと呼ばれる技術を使用して対処できます。

この技術の背後にある考え方は非常にシンプルです。正規化深度の計算に使用される通常の式の代わりに、

depthBuffer=(far)/(far-near)-(far・near)/(far-near)・1/d

near平面とfar平面を入れ替えます。

depthBuffer=(near)/(near-far)-(near・far)/(near-far)・1/d

この式では、近平面が1に、遠平面が0にマッピングされます。そのため、深度バッファは反転して見えます。

この小さな変更とfloat深度フォーマットの組み合わせにより、私たちのデモはすべて緑色になります。

なぜReverse-Zは精度を向上させるのか?

Reverse-Zは非常にシンプルな技術です。近平面と遠平面を入れ替えるだけです。それにもかかわらず、結果は非常に良好です。
また、この技術は非常に堅牢であるように思われます。近平面と遠平面の選択に細かな調整が不要になりました。
実際には、遠平面を無限遠に設定するだけで、すべてが完全に機能します。無限の表示距離を扱えることは、巨大な世界を扱う必要があるアプリケーションにとって大きな意味を持ちます。例えば、新しいCesium Addonで行っているように、地球全体をレンダリングする場合などです。近平面も、ゼロにはできませんが、はるかに近づけることができます。

なぜReverse-Zが機能するのか疑問に思われる方のために、このセクションでは数学的および技術的な詳細を深く掘り下げます。

UNormとFloatの表現

投影された深度値は[0, 1]の範囲で格納されます。
0と1の間には無限に多くの実数が存在します。しかし、円周率のすべての桁を紙に書ききれないのと同様に、コンピュータはそれらすべてを正確に表現することはできません。そのため、コンピュータは異なる表現を用いて実数を近似します。例えば、円周率を最初の7桁で書くことで近似するようなものです。

深度バッファで深度値を表現するために使用されるフォーマットには、UNormとfloatの2種類があります。

UNormはunsigned normalizedの略です。符号なし整数を使用して、[0, 1]の範囲の実数を表現します。
表現される値は、[0, 1]の範囲で均一に分布しています。

次の図は、UNormで表現される値を示しています。ご覧のように、表現される値の間隔は一定です。

Floatは浮動小数点表現を意味します。
Float表現は(-∞, +∞)の範囲をカバーします。
単精度floatは32ビットを使用し、符号部、指数部、仮数部の3つの部分に分割されます。

(出典: https://en.wikipedia.org/wiki/Single-precision_floating-point_format#/media/File:Float_example.svg)

この記事では浮動小数点表記の詳細をすべて網羅することはできませんので、興味のある読者のために2つのリンクを残しておきます:https://floating-point-gui.de、https://en.wikipedia.org/wiki/Single-precision_floating-point_format。
私たちの問題の文脈において、floatについて知っておくべき重要なことは、投影された深度値がマッピングされる範囲である[0, 1]において、表現される値がどのように分布しているかということです。

前の図で見られるように、サンプルは均一に分布していません。ゼロに近い値ほど、より多くの精度が割り当てられています。これは科学技術計算アプリケーションにとって興味深い特性ですが、私たちもこの特異性を活用します。

さて、深度投影の式に戻りましょう。

depthBuffer=(far)/(far-near)-(far・near)/(far-near)・1/d

この式をプロットすると、次のような曲線が得られます。

X軸は線形深度です。Y軸は[0, 1]の範囲に投影された深度を示します。
この関数がいかに急速に増大するかに注目してください。
Y軸では、UNorm表現を使用しているため、サンプルが均一に分布していることがわかります。
X軸には一連の小さな目盛りがあります。これらは、投影前のY軸における均一な目盛りに対応しています。ほとんどの目盛りが近平面付近に集中し、遠平面に近づくにつれて目盛りの密度が非常に低くなっていることに注目してください。

しかも、これは近平面と遠平面を非常に有利に選択した場合の結果です。near = 0.1、far = 100 の場合に何が起こるかを見てみましょう。

ここでの問題は、20から100までの深度値がすべて1にマッピングされることです。そのため、投影後には、45が46よりも小さいかどうかを判別できなくなります。

ここで浮動小数点表記を使用しても役に立ちません。実際には、表現される線形深度値が近平面側にさらに集中するため、状況はさらに悪化します。

ここまでで、サンプルの不均一な分布(近平面に近い値を優先する)に寄与する2つの要素があることを確認しました。1) 投影式の曲線 2) 浮動小数点表記

Reverse-Zのアイデアは、投影式を反転させることです。これにより、サンプルの分布が逆になり、遠平面に近い距離が優先されます。浮動小数点表記は逆の効果を持つため、これを補償し、結果としてよりバランスの取れたサンプル分布が得られます。次の図のX軸の目盛りの分布を見てください。

Reverse ZがEvergineユーザーに与える影響

新しいバージョンにアップデートすると、リバース深度が自動的に有効になります。

ほとんどのプロジェクトでは影響はありませんが、以下の2つのユースケースでは問題が発生する可能性があります。

深度バッファを使用するシェーダー

生の深度バッファを読み取るカスタムシェーダーを作成している場合は、それに応じてシェーダーを変更する必要があります。

リバース深度をフォワード深度に変換するのに役立つシェーダーパラメータをいくつか追加しました。

ReverseDepthは、従来のフォワード深度バッファリングでレンダリングしている場合は0、リバース深度を使用している場合は1です。ただし、今後は常に1になります。
ReverseDepthFactorは、以下の関数を使用してフォワード深度に変換するためのものです。

// This is a function that converts a reverse-depth value to forward-depth, if needed, without branching
inline float forwardDepth(float depth)
{
    return depth * ReverseDepthFactor + ReverseDepth;
}

カスタムレンダーレイヤー

プロジェクトにカスタムレンダーレイヤーがある場合は、深度テストの条件を反転させる必要があります。

新リリースのEvergineドキュメントには、このプロセスを自動化するために設計されたスクリプトを含む移行ガイドが記載されています。

著者
Alejandro Juan Pérez
Plain Concepts Research