Zバッファ法(デプスバッファ法)


概要

Zバッファ法はピクセル単位で保存している深度情報を利用した隠面消去の方法で、
カメラに写っているポリゴンを正しい前後関係で描画するために使用されています。
このピクセル単位で深度情報を保存しているバッファのことをZバッファデプスバッファ深度バッファと呼んでいます。
ちなみに隠面消去を簡単に説明するとカメラで見えない部分を描画しないようにする手法です。

サンプル

サンプルはここからダウンロードでき、環境については以下の内容となっています。

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9(June 2010)

3Dの描画順問題

なぜZバッファ法が生まれたかというと3Dで描画を行う場合、
描画によるオブジェクトの前後関係が非常に複雑になる問題があるからです。

gmpg_0110	

上の絵の位置関係のポリゴンを描画する場合、メッシュAとBの描画の順番を
入れ替えると描画結果に変化が起こります。
	
gmpg_0111	

こちらが求めている描画結果は遠くのメッシュから始まり近くのメッシュを
描画して欲しいので「メッシュB => A」が求めてる結果で
「メッシュA => B」の場合は順番が前後しているので間違った結果となります。
このような場合、カメラから離れているメッシュの座標でソートすれば
対応可能と考えますが、実際に実装すると非常に難易度が高いです。
理由は「メッシュの各ポリゴンの頂点でカメラとの距離が異なる」からです。

理由の説明

ポリゴンの各頂点とカメラからの距離は等間隔ではありません。

gmpg_0229

上の絵のように各頂点の距離はカメラに近いモノもあれば遠いモノもありますが
これは特別なケースではなく、むしろ3Dモデルのメッシュ全ての頂点が
このような状態になっていると考えてください。
これらの頂点全てをカメラに遠い方からソートしなれば求める結果になりません。
また、ソートを実装しても新しい問題が発生します。
それは別のメッシュの頂点が描画されるという問題です。

gmpg_0230

上の絵は頂点ABCのポリゴンと頂点DEFのポリゴンが描画されています。
各ポリゴンの頂点の距離がまじりあっていることが分かります。
これらをまとめてソートする場合、メッシュ単位の描画処理を変更する必要があるので
大変な作業コストになります。
更にこの問題が解決して複数のメッシュの頂点の距離を正しくソートできたとしても
新しい問題が発生します。
それはポリゴンの面と面の重なり部分の前後判定です。

gmpg_0231

上の絵のように面と面とが交差したら面の一部が描画対象になり、
一部は非描画の対象となります。
この判定を各頂点の座標のみで行うのは非常に難易度の高い問題です。
仮に判定ができたとしても計算に見合ったコストになる保証がありません。
この問題を比較的低コストで解決する方法として考えられたのがZバッファ法です。

Zバッファ法詳細

Zバッファ法はメッシュがレンダーバッファに反映される直前に実行されます。
正確に書くとレンダリングパイプラインのレンダーバックエンドの処理として実行します。

gmpg_0232

上の絵で書いてあるようにZバッファのサイズはレンダーバッファと同じサイズを用意します。
このZバッファには0.0から1.0の値を保存できるようになっており、
0.0を最小値1.0を最大値として扱います。
次の項目からZバッファの流れを説明します。

①.Zバッファ情報のクリア

まず、Zバッファの情報はレンダーバッファ(バックバッファ)と同様に
基本的には毎フレーム初期化します。
これは前回のフレームの深度情報が今回のフレームで影響が出ないようするためです。
Zバッファの各要素の初期値には最大値である1.0を設定することが多いです。

gmpg_0233

②.ピクセルへの描画可否チェック

次はポリゴンをレンダーバッファに描画できるかの可否チェックを行います。
ここがZバッファ法のメイン処理となる部分です。
バッファへの描き込みはピクセル単位で行いますが、Zバッファ法を使用している場合、
書き込む予定のピクセル一つ一つに対して深度値が設定されています。

gmpg_0234

Zバッファ法はピクセルの深度値と同じ位置にあるZバッファの比較を行います。
この比較をZテストデプステストと呼びます。
Zテストは基本的に「ピクセルの深度値 <= Zバッファの値」で比較しますが
この比較が成立している場合(ピクセルの値がZバッファの値以下)のみ
ピクセルの内容をレンダーバッファに反映する許可がでます。
そして、この時のピクセルの深度値をZバッファの深度値にします。
もし、比較が不成立だった場合は当然ながらピクセルへの描画は行われず、
Zバッファの深度値の更新もありません。

gmpg_0235

③.スクリーンに描画されるポリゴンに対して②を繰り返す

スクリーンに描画されるポリゴンのピクセル描画全てに対して②の処理が実行されます。

gmpg_0236

その結果、正確な前後判定が行われたレンダーバッファが作成され、画面に描画されます。

gmpg_0237

Zバッファ法のメリットとデメリット

Zバッファ法を使用した際のメリットはやはり、描画順番を意識せずに描画処理を行えることです。
上の「3Dポリゴンの描画順問題」の項目で説明しましたが、3Dゲームでポリゴンを
前後関係を間違えずに正しく描画しようとすると大変な作業コストが発生します。
その問題をZバッファ法を使用することで解決できるので、基本的には使用すべきです。

デメリットはZバッファを用意しなくてはいけないので、メモリの使用量が増えてしまうことですが
メリットの恩恵を考えると対したことではありません。

Zバッファの使い分け

Zバッファ法の使用は任意ですので、必要かどうかの判断は開発側でします。
3Dモデルの描画には必要だと思いますが、プレイヤーの体力や、スコアなどの
UIには必ずしも必須というわけではないので、プログラム上でZバッファ法の
使用有無の設定を簡単に行えるようにした方がいいと思います。

アルファブレンドの相性問題

Zバッファ法はアルファブレンドと相性がよくありません。
何も対策を立てずに描画を行ってしまうと意図した結果とは違う内容になってしまいます。

問題

アルファブレンドによる色の算出には「レンダーバッファのピクセルのRGB値」
「新規ピクセルのRGB値」「新規ピクセルのアルファ値」が必要です。
この計算で使用される「レンダーバッファのピクセルのRGB値」が
Zバッファを使用した際に問題となる情報です。

Zバッファではカメラの奥から順番に描き込む必要はないので、
アルファ値を持ったピクセルを描き込む時点のレンダーバッファのピクセルに
計算するために必要なピクセルが描きこまれていない可能性があります。
ここからは問題の検証を行ってみたいと思います。

検証の前提データ
ポリゴンの種類 Z値 α値
赤三角形 0.5 1.0f
緑三角形 0.2 0.5f
上の検証データから赤が透過無しの三角形で、緑が半透明の三角形 位置関係として赤の方が奥、緑が手前にあることが分かります。 まずは正常な描画結果を確認しておきます。 gmpg_0115 きちんと緑の三角形が透過されており、下に赤の三角形が見えています。 次はZバッファを使用した状態で「赤三角形 => 緑三角形」の順番で描画してみます。 gmpg_0116 「赤 => 緑」の場合の結果は正常な描画結果と同じなので問題ありません。 では、次に「緑三角形 => 赤三角形」の順番で描画してみます。 gmpg_0117 「緑 => 赤」の場合、三角形の前後判定はきちんとされているため、 赤が奥、緑が手前に描画されていますが、肝心の透過が行われていません。 これがアルファブレンドの相性問題です。

原因

この問題は透過ポリゴンを描画する際のレンダーバッファのピクセルに
本来存在するはずの色情報がないことが原因です。
一般的に使用されるアルファブレンドの透過計算は以下の式が使われます。

/*
	SRC => レンダーバッファに書き込むピクセルの色
	DEST => レンダーバッファのピクセルの色
*/
SRC = (SRCの赤色 * SRCのα値, SRCの緑色 * SRCのα値, SRCの青色 * SRCのα値)
DEST = (DESTの赤色 * (1 - SRCのα値), DESTの緑色 * (1 - SRCのα値), DESTの青色 * (1 - SRCのα値))
合成色 = SRC + DEST

この式の通り、透過を行った色を算出するにはレンダーバッファの
ピクセルの色情報が必要不可欠です。
では、なぜレンダーバッファのピクセルが正常に反映されていないのかを
正常に透過が行われている場合と透過が失敗した場合の
レンダーバッファとZバッファ違いで確認をしたいと思います。

透過が行われる

透過が行われる場合は、まず赤色の三角形が描画されます。

gmpg_0238

次に緑の三角形が描画されます。

gmpg_0239

この時にレンダーバッファの赤色のピクセルと緑の三角形のピクセルで
重複している箇所が発生し、その箇所で透過計算が行われます。

gmpg_0240

重複しているピクセルには既に赤の色情報が保存されているので
透過計算に必要な情報は全てそろっており、問題なく色合成を行うことができます。

透過が行われない

次に透過が行われない場合ですが、こちらは最初に緑色の三角形が描画されます。

gmpg_0241

次に赤色の三角形が描画されます。

gmpg_0242

既に描画されている緑色の三角形の深度値がZバッファに反映されているため、
赤色の三角形の一部のピクセルはZテストをクリアできず、
結果として赤の三角形が更新できたピクセルは緑の三角形の範囲外
(絵でいうとZバッファの赤の0.5という数値になっている場所)だけです。
これでは透過計算をしたくても、そもそもピクセルの更新をしないので
計算を行う以前の問題です。
そして、これが透過が行われない原因です。

対策

対策は透過ポリゴンと不透過ポリゴンが描画処理で混在している場合に
不透過ポリゴンを先に描画し、その後透過ポリゴンを描画をします。
これである程度改善することができますが、求めている結果になる保証はありません。

また、透過ポリゴン側の描画は描画順番を気にする必要があります。
ポリゴン単位は難しいのでオブジェクト座標によるソートをかけるだけでも
やっておくとかなり効果的です。

DirectX9での使用方法

Zバッファ法はDirectX9で簡単使用することができます。
※この記事のサンプルもDirectX9を使用したZバッファ法のサンプルになっています。

実装の流れ

Zバッファ法を使用する流れは以下の通りです。
準備
初期設定
描画
バッファのクリア
Zバッファ法の有効化

①.初期設定

DirectX9のデバイス設定時のD3DPRESENT_PARAMETERS構造体で
Zバッファ法の使用の有無を決めます。
設定するパラメータは「EnableAutoDepthStencil」と「AutoDepthStencilFormat」です。

パラメータ名 説明
EnableAutoDepthStencil 深度バッファの使用の有無
true:使用 false:未使用
AutoDepthStencilFormat 深度バッファのフォーマットを指定
例:D3DFMT_D24S8
AutoDepthStencilFormatですが、このパラメータはZバッファとステンシルバッファの メモリ領域の分割設定を行っています。 例えばD3DFMT_D24S8はZバッファが24ビット、ステンシルバッファが8ビットで管理されます。

②.バッファのクリア

Zバッファはレンダーバッファと同様に毎フレームリセットする必要があります。
これは前回のバッファ情報を次のフレームに持ち越さないようにするためです。
クリア処理自体はIDirect3DDevice9の「Clear関数」で行います。
Clear関数の第三引数でクリアするバッファの指定をしていますが、
ここに「D3DCLEAR_ZBUFFER 」を追加します。
ただ、レンダーバッファのクリアも同時に行う必要があるので、ビット演算の論理和を使用して
「D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER」とします。

directx_0067
// バッファのクリア
g_D3DDevice->Clear(
	0, 
	nullptr,
	D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
	D3DCOLOR_XRGB(0, 0, 0), 
	1.0f,
	0);

③.Zバッファ法の有効化

Zバッファの使用の有無はIDirect3DDevice9の「SetRenderState関数」で行います。

directx_0068

Zバッファの有効化設定は第一引数に「D3DRS_ZENABLE」を指定し、
第二引数を「true」にしたら有効、「false」にしたら無効の設定になります。

// Zバッファの有効化設定
g_D3DDevice->SetRenderState(D3DRS_ZENABLE, true);

ここまでの設定でZバッファ法が有効化されます。
この状態で描画したらレンダーバックエンドでZテストが行われます。

Zテストの比較方法

Zテストの比較は「描き込みピクセルの深度値 < Zバッファの深度値」以外にも
いくつかの比較方法が用意されています。
「描き込みピクセルの深度値 < Zバッファの深度値」はデフォルトの設定です。

比較方法の変更

深度の比較方法の変更はSetRenderStateで変更可能です。
Zテストの比較方法の変更は第一引数に「D3DRS_ZFUNC」を指定し、
第二引数に比較設定の定数を指定します。

比較定数
定数 説明
D3DCMP_NEVER 必ず失敗する
D3DCMP_LESS 新規深度値 < Zバッファ深度値
D3DCMP_LESSEQUAL 新規深度値 <= Zバッファ深度値
D3DCMP_EQUAL 新規深度値 == Zバッファ深度値
D3DCMP_NOTEQUAL 新規深度値 != Zバッファ深度値
D3DCMP_GREATER 新規深度値 > Zバッファ深度値
D3DCMP_GREATEREQUAL 新規深度値 >= Zバッファ深度値
D3DCMP_ALWAYS 必ず成功する
// 新規深度値とZバッファの深度値が同じ値ならテスト成功にする g_D3DDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_EQUAL);

検証

以下の条件の赤三角形と緑三角形を比較方法変更して
描画結果がどのようになるかの検証をします。

前提条件
番号 条件
描画の順番は赤三角形 => 緑三角形の順番で行う
赤三角形と緑三角形のカメラまでの距離は同じ
まずはD3DRS_ZFUNCをD3DCMP_LESSEQUAL(新規 <= 現状)にしてみます。 どちらもZ値が同じなので緑三角描画時に行われるZテストは成功し、 赤三角が奥、緑三角が前になるように描画されるはずです。 gmpg_0165 上の実行結果の通り、きちんと検証と同じ内容になりました。 次はD3DRS_ZFUNCをD3DCMP_LESS(新規 < 現状)にしてみます。 今回の設定では赤三角と重複している緑三角のピクセル部分の テストは失敗するはずなので、緑三角が奥、赤三角が前に描画されるはずです。 gmpg_0166 こちらも検証通りの結果になりました。 このようにZテストの比較方法を変更することで、 テスト結果を変更することが可能となるので 変更できることを覚えておいてください。