深度値と第四成分

深度値を理解する

depth and w

 今回で入門編は終わりです。深度値について簡単に説明します。これもカリングと同じで、基本的には3D描画のための機能です。手前と奥の概念を導入します。つまりポリゴン描画の際に深度を付与することで、めちゃくちゃな順で描画してもきちんと深度の順に整列するようにします。その仕組みは単純で、描画の際にピクセルに「色」を付けます。あとから来たポリゴンは、その「色」を自身の「色」と比較して手前か奥か判断し、手前の場合だけ描いて、「色」も更新します。簡単ですね!早速見て行きましょう。
 (こういう「手前か奥か」って書き方をしただけでQiitaは駄目出ししてくるんです...自然な書き方なのに...)

コード全文

作品8のリンク

描画順に基づく通常描画

 まず今回のプログラムですが、pg0が赤い三角形で左下、右下、左上です。pg1が青い三角形で左下、右上、左上です。描画順は見ての通り、赤で、青です。なのでデフォルトでこれを実行すると、まあリンク先を見ればわかるんですが、

default draw

青が上に来ます。当然ですね。普通に上書きされます...とはいえ、今まではこんな風にドローコールを重ねることをしてこなかったので驚く人は驚くかもしれません(そうだと思います...記憶が確かなら)。そうです、デフォルトではドローコールを重ねた場合、その場所にそのまま上書きされます。そういう仕組みです。ブレンドが有効だとしても、計算したうえで、上書きされます。
 それを変更するのが深度テストです。

深度テスト

 深度テストとは、後から描画した場合にピクセルごとにそこに描画するかどうかを何らかの基準に基づいて判断し、通過した場合だけ描画する仕組みです。どういう仕組みかというと、バッファというかまあ仮のキャンバスがあって、そこに描画のたびに数字を書き込むんですが、その状態でさらなる書き込みを行う場合に、ピクセルごとにすでに書き込んである数字と比較して、基準をクリアしたら描き込むわけです。
 今の場合だと、赤い三角形の描画において、

  gl_Position = vec4(p, -0.5, 1.0);

となっています。この-0.5は0.5倍して0.5を足すことで深度値になります。つまり0.25です。で、青の場合、

  gl_Position = vec4(p, 0.5, 1.0);

なので0.75ですね。0.25より大きいことに注意してください。これを元に判定します。なお、デフォルトでは深度テストは有効でないので、

gl.enable(gl.DEPTH_TEST);

enable()で有効にする必要があります(configで切り替えます)。ここにチェックを入れると、先ほどの結果がこうなります:

default draw

 これを図示すると、こう:

default draw

 つまり、0.75より0.25の方が小さいので、小さい方が残るわけです。ところでこの基準というのは関数による判定です。デフォルトではより小さい方がクリアする仕組みになっており、depthFuncといいます。gl.LESSがデフォルトです。LESSなので、「小なり」の場合に通過するわけです。

 これを指定します。指定せずともデフォルトではLESSが指定されています。自分はp5も使っているLEQUALが好きなので、これをデフォルトにしています:

gl.depthFunc(config.depthFunc);

それで青い方が下に来るわけですね。
 なお、DEPTH_TESTの有効化は、深度テストの実行、及び深度バッファへの書き込みの有無の両方です。これを有効にしないと深度バッファにはなにも書き込まれません。用意はされています。それはレンダリングコンテキストを用意した時に作られているんですが、使用されないだけです。

深度値のグラデーション

 さらに追加します。深度値を左から右に向かって大きくなるようにしてみましょう。-1~1なので、深度値としては0~1ですね。つまり右ほど奥というわけです。

  gl_Position = vec4(p, p.x, 1.0);

 これを最後に描画すると冒頭の画像になります:

default draw

 赤は0.25なので0.25以上のところは0.25である赤が勝っています。つまり右側。そして青ですが、0.75なので0.75以上である右端は青が勝っていますね。きちんとLEQUALに従っています。色々変えてみてください。イメージとしては、「描き込む側」LEQUAL「すでにある数」という感じです。ソースで、デストです。ソース、デストの順だと思ってください。ステンシルやブレンドも同じ考え方です。

深度マスク

 深度テストに通過した場合のみ、深度バッファは更新されるんですが、深度テストに通過させるだけで、色バッファのみ更新し、深度バッファへの書き込みはしたくない場合があります。それが深度マスクです。これはデフォルトではtrueになっています。これをfalseにすると、深度テストのみ行われ、通過すれば色バッファに書き込まれますが、深度バッファにはなにも書き込まれません(通過しなければそもそも何も起きません)。
 それで、これを赤い三角形の描画の後でfalseにすると、青い三角形の描画は深度テストに基づいて実行されますが、深度バッファは据え置きとなります。それゆえ、disableDepthMaskをチェックして、つまり全部チェックしてLEQUALで描画すると、こうなります:

default draw

 青い三角形の分の深度更新がなされないので、そこは緑が勝ちます。
 深度マスクの注意ですが、これがtrueであっても深度テストが有効でなければ深度バッファにはなにも書き込まれません。すべては深度テストが有効であればこそです。有効でなければ、すべての描画は上書きで実行され、深度バッファも据え置きです。ただし更新されないだけで内容はそのままです。
 gl.clear()では今まで色バッファだけ更新していましたが、深度バッファもこれによりクリアすることができます(敢えてクリアせずに描画し続ける場合もあります...すべては描画の方針次第です)。その場合、フラグにgl.DEPTH_BUFFER_BITを指定します:

gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT); // 深度値もクリアするにはこうする

 3D描画で毎フレーム更新するならこれは当然の処置です。単に色バッファをクリアするだけだと、深度バッファの内容が残ってしまい、以降の描画に無視できない影響が出ます。
 なお、クリアするときのデフォルトの値は「1」です。つまり-1~1の1です。一番奥です。あくまでLESSやLEQUALの場合ですが。「小さい方が手前で大きい方が奥」と覚えてください。そんな感じですから、この2つ以外のdepthFuncはほぼ使う機会はないです。きわめて特殊な場合のみですね...

クリッピング

 さっきQiitaの方ちらっと見たら扱ってたので触れましょう。クリッピングですね。
 深度値は-1~1が0~1に変換されて扱われるんですが、そこから外れるとどうなるかという話です。赤い三角形のコードで、

  gl_Position = vec4(p, -1.1, 1.0);

とし、青い三角形のコードで、

  gl_Position = vec4(p, 1.1, 1.0);

とするとどうなるでしょうか。LEQUALなので-1.1の赤い三角形は書き込まれると思うでしょう。結果は:

default draw

 ご覧の通りです。なにも書き込まれません。なんと深度テストを有効にしていてもしていなくても書き込まれないので、本当に「枠の外」という扱いになります。実は深度値が0~1に入らない場合は容赦なく描画は失敗します。もちろん深度バッファへの書き込みも無しです。クランプも無しです。ガン無視です。これをクリッピングと言います。まあクランプなんてしたらめちゃくちゃなことになってしまうので仕方ないですね。3D描画では指定された範囲内で描画が実行されるよう注意する必要があります。とはいえほとんどの場合、カメラは適切に設定されるので問題はないと思います。

ちょっとだけ第四成分

 第四成分についてもちょっとだけ触れます。
 前回触れたように、gl_Positionの第四成分であるwは、x,y,zをこれで割り、正規化デバイス座標にする仕事をしています。はっきり言って3D描画を扱わない限り「なんでやねん」みたいな仕様です。つまりどういうことかというと、wを2.0とかにすると、単純に描画結果が「半分」になります。

default draw

 これはこの記事のコードで上2つだけ有効にしたコードで、赤と青の三角形の描画におけるgl_Positionの第四成分を2.0にした「だけ」の描画結果です。すべてが半分になるので、位置も深度値(ただし変換前)も半分です。ほんとにこれだけだと、何のありがたみも感じられません。なので、この仕様の本質を知りたい人は、3D描画までお待ちいただけると幸いです。

 今回はこれで終わりです。入門は以上となります。

今回登場した関数

depthFunc
depthMask