TL

深度バッファと隠面消去

三角形をどんな順で描いても手前のものだけが残る仕組みを、Zバッファの原理から深度精度の偏りや z-fighting、透過の落とし穴まで一気に理解できます。

応用グラフィックス深度バッファ隠面消去GPUレンダリングz-fighting最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.Zバッファは画素ごとに深度値を保持し、後から来たフラグメントの深度が既存値より手前のときだけ色と深度を更新する。これで描画順に依らない隠面消去が成立する。
  • 2.投影後の深度は eye 空間の z に対し 1/z 的な非線形写像になり、精度が near 側へ極端に偏る。near/far 比が大きいと同一面が近似で割れて z-fighting が起きる。
  • 3.early-Z と Hi-Z はシェーダ実行前に隠れ画素を捨てて無駄な計算を省くが、discard・アルファテスト・シェーダでの深度書き込みは early-Z を無効化しうる。透過は深度書き込みを止めて奥から描く。

隠面消去を「画素ごとの深度比較」に落とす

3D シーンを 2D 画面へ描くとき、手前の物体が奥の物体を隠す 隠面消去(hidden surface removal) をどう実装するかが問題になります。物体単位で前後を厳密に並べ替えるのは、面同士が互いに前後で入り組む(循環オクルージョン)と破綻します。そこで現代の GPU は問題を 画素単位 に分解します。これが Zバッファ法(深度バッファ法) です。

カラーバッファと同じ解像度で、各画素に「その画素に今描かれている最前面の深度」を記録する 深度バッファ(Zバッファ) を用意します。ラスタライズで生成された各フラグメントは自分の深度を持ち、それを同じ画素の格納値と比べ、より手前なら描く・奥なら捨てる。これを全三角形について繰り返すだけで、どんな描画順でも正しい可視面が残ります。物体の大域的なソートが不要になるのが本質です。

深度テストの流れ

パイプライン上、深度テストはフラグメントシェーダの周辺で行われます。標準的な流れは次の通りです。

各フラグメント(画素候補)ごとに:

  1. 補間で深度 z_f を求める(頂点の深度を perspective-correct 補間)
  2. depthBuffer[x,y] の格納値 z_b と比較(比較関数は設定可能: LESS など)
  3a. テスト成功 → フラグメントシェーダ結果を書き、
        depthWrite が有効なら depthBuffer[x,y] = z_f に更新
  3b. テスト失敗 → フラグメントを破棄(色も深度も書かない)

比較関数(depth compare function)は固定ではありません。カメラが -Z を向き手前ほど深度値が小さい規約では LESS(既存値より小さければ通す)が既定です。半透明の重ね合わせや特殊効果では LEQUALGREATER、常に通す ALWAYS、常に落とす NEVER などを使い分けます。

深度テストと深度書き込みは別スイッチ

「深度テストを行うか(depth test)」と「テスト通過時に深度を更新するか(depth write / depth mask)」は独立に設定できます。両方オンが通常の不透明描画。テストはオンだが書き込みオフ、という組み合わせが透過描画の要になります(後述)。テスト自体をオフにすると、そのフラグメントは無条件で描かれ深度も無視されます。

early-Z と Hi-Z ── 無駄なシェーダ実行を減らす

素朴なパイプラインでは、フラグメントシェーダを実行して色を計算した に深度テストします。しかし奥に隠れて捨てられる画素のシェーダ計算は完全に無駄です。そこで多くの GPU は、シェーダ実行の に深度テストを済ませる early depth test(early-Z) を行い、隠れると判った画素のシェーダ起動そのものを省きます。

さらに Hi-Z(hierarchical Z, coarse depth) は、深度バッファをタイル単位で粗く要約した階層を持ちます。各タイルの「最も手前/最も奥の深度」を保持しておき、これから描くプリミティブのタイルがタイル全体で既存深度より奥なら、そのタイル内の全画素を個別テストせず一括で棄却します。

最適化粒度効果効きやすくする条件
early-Z画素(フラグメント単位)隠れる画素のシェーダ実行を省く深度をシェーダで書き換えない/discard しない
Hi-Zタイル(画素ブロック単位)隠れるブロックをまとめて棄却不透明を手前→奥の順で描く(front-to-back)

これらが効くほど オーバードロー(同じ画素を何度も塗り直す無駄)が減ります。裏を返せば、不透明物体を 手前から奥(front-to-back) の順に描くと、早い段階で深度バッファが埋まり以降の隠れ画素を早期棄却できるため、透過と逆のソートが性能上は有利になります。

early-Z を壊す操作

フラグメントシェーダが discard(フラグメント破棄)を使う、アルファテストで通過可否を決める、あるいはシェーダ内でフラグメントの深度自体を書き換える(gl_FragDepth への代入など)と、GPU は「シェーダを実行し終えるまで最終的な深度や可視性が確定しない」ため、early-Z を無効化しシェーダ後テストへ退避します。木の葉のアルファテスト多用が重くなる一因です。API 側で「深度は保守的にしか変えない」宣言(conservative depth)を与えると early-Z を維持できる場合があります。

深度精度はなぜ near 側に偏るのか

深度値の精度は一様ではありません。原因は透視投影で深度が 1/z 的に非線形写像 される点にあります。透視投影行列は eye 空間の深度 z_e(カメラ前方を負とする規約で近い面ほど 0 に近い負値)を、クリップ空間で w 成分に対応づけます(標準的な行列では w_clip = -z_e)。続く 透視除算z_ndc = z_clip / w_clip)でこの 1/z_e の非線形性が生まれ、正規化デバイス座標(NDC)の深度 z_ndc が得られます。なお z_ndc 自体はスクリーン空間で線形に補間できるため、Zバッファのハードウェア補間は安価です(属性の遠近補正補間とは別物)。この関係はおおむね次の形になります。

z_ndc = A + B / z_e     (A, B は near/far から決まる定数)

  → z_e(実距離)に対して z_ndc は 1/z_e 的に変化する
  → near 付近では z_ndc がわずかな距離差で大きく動く(高精度)
  → far 付近では距離が大きく動いても z_ndc がほぼ動かない(低精度)

深度バッファは z_ndc を固定小数(例 24 bit 整数)や浮動小数で保存します。上の非線形性のため、利用可能なビットの大半が near 側の狭い範囲に費やされ、遠方の分解能はごくわずかになります。near 平面を極端に手前へ寄せる(near を 0 に近づける)ほど far 側が壊滅的に粗くなるので、near はできるだけ大きく取るのが実務の鉄則です。

z-fighting ── 同一深度の取り合い

z-fighting は、ほぼ同じ深度に位置する 2 面のどちらが手前かが画素ごとにブレて、ちらつき縞が出る現象です。原因は深度値の量子化です。連続的には別々の深度でも、有限ビットに丸めると隣接画素で大小関係が入れ替わり、フレームやカメラ位置でパターンが揺れます。前節の精度偏在のため、遠方ほど z-fighting が起きやすく なります。

対策は精度の使い方を改善することに尽きます。

  • near/far のレンジを詰める。特に near を必要最小限まで大きく取り、非線形写像の急峻な部分を無駄遣いしない。
  • reversed-Z を使う。深度を「near=1, far=0」と反転して 32bit 浮動小数の深度バッファに格納すると、浮動小数の指数分布が 1/z の偏りを打ち消す方向に働き、遠方の精度が桁違いに改善する。比較関数は GREATER 系に変える。
  • 同一平面上に重ねる面には depth bias(polygon offset) を与え、意図的に深度を一定量ずらして順序を確定させる(デカール、影の面など)。
試験・面接での定番の勘所

「遠くの地面と道路のテクスチャがちらつく」原因を問われたら、答えは深度バッファの精度不足(=1/z の非線形性で遠方の分解能が枯れる)です。改善策として (1) near を大きくする、(2) far を小さくする、(3) reversed-Z + 浮動小数深度、(4) 同一面には polygon offset、を挙げられると強い。「ビット数を増やす」だけでは非線形性の本質を外している点に注意します。

深度書き込みと透過 ── 順序が復活する場所

Zバッファは不透明物体の描画順を不要にしますが、半透明(アルファブレンド) では話が別です。ブレンドは「既に描かれている背景色」と「今のフラグメント色」をアルファで混合する演算で、混合結果は描く順序に依存 します(数学的に非可換)。奥のガラス→手前のガラスの順に重ねるのと逆順とでは、最終色が変わります。

さらに、半透明フラグメントで深度を書き込んでしまうと、その面より奥にある別の半透明面がテストで落とされ、透けて見えるべき背景が欠けます。そこで定石はこうです。

不透明パス:
  depthTest = ON, depthWrite = ON
  順序自由(性能のため front-to-back が有利)

透過パス(不透明パスの後に実行):
  depthTest  = ON      … 不透明物体には正しく隠される
  depthWrite = OFF     … 半透明どうしは深度で消し合わない
  描画順    = 奥→手前(back-to-front にソート)… ブレンドを正しい順で

つまり透過では 深度テストは残しつつ深度書き込みを止め、CPU 側で奥から手前へソート して描きます。これでも交差する半透明面や、1 つのメッシュ内での自己重なりは完全には解けません。順序に依存しない合成(OIT: order-independent transparency)は、深度別に色を貯めて後で合成するなど別枠の手法で近似します。

よくある透過バグ

半透明オブジェクトが「近くの半透明を突き抜けて見える」「回転で前後関係が破綻する」ときは、たいてい depthWrite を切り忘れているか、back-to-front ソートが漏れています。逆に「半透明の背後の不透明が消える」ときは depthTest まで切ってしまっているサインです。テストと書き込みのスイッチを分けて考えるのが切り分けの第一歩です。

まとめ

  • Zバッファ法 は隠面消去を画素ごとの深度比較に還元し、フラグメント深度が格納値より手前のときだけ色と深度を更新することで、描画順に依らない可視面決定を実現する。
  • 深度テストと深度書き込みは独立スイッチ。不透明は両方オン、透過はテストのみオンにするのが定石。
  • early-Z と Hi-Z はシェーダ前・タイル単位で隠れ画素を捨ててオーバードローを削るが、discard・アルファテスト・シェーダでの深度書き換えが early-Z を無効化しうる。front-to-back 描画が早期棄却を助ける。
  • 深度精度は 1/z の非線形写像で near 側に偏る。near を大きく取り、reversed-Z + 浮動小数深度や polygon offset で z-fighting を抑える。
  • 透過は順序が復活する。ブレンドが非可換なため深度書き込みを止め、奥→手前にソートして描く。より根本的には OIT で順序依存を回避する。

グラフィックス Article

深度バッファと隠面消去を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

グラフィックス

比較で見る軸

難易度: advanced / カテゴリ: グラフィックス / タグ数: 6

導入後に効く点

投影後の深度は eye 空間の z に対し 1/z 的な非線形写像になり、精度が near 側へ極端に偏る。near/far 比が大きいと同一面が近似で割れて z-fighting が起きる。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
グラフィックス
タグ数
6

判断チェックリスト

  • 自社の用途が「グラフィックス / 深度バッファ」に近いか確認する。
  • 強みである「Zバッファは画素ごとに深度値を保持し、後から来たフラグメントの深度が既存値より手前のときだけ色と深度を更新する。これで描画順に依らない隠面消去が成立する。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

グラフィックス深度バッファ隠面消去GPUレンダリンググラフィックス深度バッファ隠面消去