そろそろShaderをやるパート68 ZWrite、ZTest、RenderQueueを理解する


そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

ZWrite

デプスバッファへの書き込みについて設定できます。
デプスバッファとはカメラからオブジェクトまでの距離を格納する記憶領域です。
描画するピクセルと1対1で用意されています。

Onにすると書き込みが有効になり、Offにすると書き込みを無効にします。
デフォルトではOnになっています。

この距離を深度値やZ値といい、Unityの場合0〜1で取得できます。
距離が遠くなるほどに値は大きくなります。

また、深度値がまだ書き込まれていないピクセルにおいては、初期値が書き込まれているそうです。基本的には1を設定することが多いとのことです。(恐らくUnityにおいても初期値は1)

【参考リンク】
【Unityシェーダ入門】デプスバッファの内容を表示する
ゲーム制作者になるための3Dグラフィックス技術 改訂3版
Zバッファ法(デプスバッファ法)

RenderQueue

RenderQueueを変更することで描画順を変更できます。
ここでいう"描画順"とは"レンダリングする順番"のことを指します。
実際に"表示される順番"を直接的に指しているわけではないので注意が必要です。

RenderQueueの値が小さければ小さいほど、
基本的には先にレンダリングされることになります。
UnityのInspector上では数字で表示されますが、Shader内では既定の文字列で指定が可能です。

タグ名 内部インデックス
Background 1000
Geometry 2000
AlphaTest 2450
Transparent 3000
Overlay 4000

"Queue" = "Geometry""Queue" = "Geometry+1"、のようにShader内で定義します。

【参考リンク】:Unityのレンダリング順について

RenderQueueの値が一致している場合、基本的にはカメラから近い順に描画されるようです。
(半透明は奥から順に描画)

ただ、例外もあるので厳密に描画順を制御したいなら
RenderQueueの値で管理した方が良さそうです。

【参考リンク】:【Unity】不透明キューの描画順が手前からじゃない件

ZTest

描画済みのピクセルの深度値と描画予定のピクセルの深度値を比較します。
ZTest以後の設定値に応じて合否(描画するかしないか)を判定します。

同じピクセルに対して、"描画済み"と"描画予定"という言葉が分かりにくいですが、
描画のフローをイメージすると理解しやすくなります。

以下の図のような空間を例に見ていきます。
赤いオブジェクト、青いオブジェクト、カメラの順に配置されています。
描画される順番は赤いオブジェクト、青いオブジェクトと仮定します。

赤いオブジェクトが描画済みとなり、深度値が書き込まれた場面が左の図です。
右の図は同じピクセルに青いオブジェクトを描画しようとしている場面です。
(深度値に関してはわかりやすいように0~1に変換された後の値を例にしています)

あとはこれらの深度値を比較します。
この"深度値の比較によってピクセルを上書きするか定めるフロー"をZTestといいます。
以下の図は"描画済みのピクセルの深度値 ≧ 描画予定のピクセルの深度値 なら合格"
という条件のZTestが行われた結果です。
ZTestに合格したピクセルが上書きされました。

ZTestの設定値は以下の通りです。

設定値 ZTestが合格する条件
Less 描画済みのピクセルの深度値 > 描画予定のピクセルの深度値
LEqual 描画済みのピクセルの深度値 ≧ 描画予定のピクセルの深度値
Equal 描画済みのピクセルの深度値 = 描画予定のピクセルの深度値
GEqual 描画済みのピクセルの深度値 ≦ 描画予定のピクセルの深度値
Greater 描画済みのピクセルの深度値 < 描画予定のピクセルの深度値
NotEqual 描画済みのピクセルの深度値 ≠ 描画予定のピクセルの深度値
Always 深度値に関係なく合格

デフォルトではLEqualとなっています。

ここで注意したいのが、ZWriteをOffにしたからといって、
ZTestの合格結果が一概には決定しないということです。

ZWriteをOffにしてもあくまで"デプスバッファに値を書き込まない"というだけであって、
描画予定のピクセルの深度値自体は前後関係の比較に使われています。

検証

ZWrite、ZTest、RenderQueueを使って検証をしてみます。

Shaderサンプル

以下のShaderを適用し、設定値を変えた複数のMaterialで検証します。

Shader "Custom/RenderingStudy"
{
    Properties
    {
        _Color("MainColor",Color) = (0,0,0,0)
        [KeywordEnum(OFF,ON)] _ZWrite("ZWrite",Int) = 0
        [Enum(UnityEngine.Rendering.CompareFunction)]_ZTest("ZTest", Float) = 4
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry-1"
        }
        
        Pass
        {
            ZWrite [_ZWrite]
            ZTest [_ZTest]

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            half4 _Color;

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                return half4(_Color);
            }
            ENDCG
        }
    }
}

ZWrite OnとRenderQueue2000、2001の組み合わせ

まずはZWriteをOnにし、RenderQueueを2000にした黒いQuad、
2001にした赤いQuadを並べてみます。ZTestはデフォルトのLEqualとなっています。

カメラから見て遠い方に"RenderQueueが2001の赤いQuad"を配置しました。

描画順はRenderQueueを2000にした黒いQuad、2001にした赤いQuadの順です。

黒いQuadが描画される際にピクセルの深度値が更新され、
赤いQuadが描画されるタイミングで"描画予定のピクセルの深度値"との比較が行われます。

"描画済みのピクセルの深度値 ≧ 描画予定のピクセルの深度値"の条件でZTestが行われ、
不合格となったピクセルは黒いQuadを描画した時点のままとなります。

ZWrite OffとRenderQueue2000、2001の組み合わせ

次にZWriteをOffにし、RenderQueueを2000にした黒いQuad、
2001にした赤いQuadを並べてみます。ZTestはデフォルトのLEqualとなっています。

こちらも先ほどと同様に、カメラから見て遠い方に
"RenderQueueが2001の赤いQuad"を配置しました。

描画順も先ほど同様にRenderQueueを2000にした黒いQuad、2001にした赤いQuadの順です。
しかし黒いQuadの描画時、ZWriteがオフのため深度値の書き込みは行われておらず、
初期値のままとなっています。

"描画済みのピクセルの深度値(初期値) ≧ 描画予定のピクセルの深度値"の条件でZTestが行われ、
合格となったピクセルは上書きとなり、赤色で塗りつぶされます。

ZTest LEqualでRenderQueue2000、ZTest EqualでRenderQueue2001の組み合わせ

続いてはZTestについてです。
"ZTest LEqualでRenderQueueが2000の黒いCube"と
"ZTest EqualのRenderQueueが2001の赤いCube"を重ねて並べてみます。
ともにZWrite Onです。

双方アクティブな際には赤いCubeが表示され、
黒いCubeのみを非アクティブにした場合は何も描画されなくなりました。

それぞれの解説は以下の通りです。

  • 双方アクティブにした場合、赤いCubeが表示された
    • 黒いCubeが描画された際にピクセルに書き込まれた深度値と"赤いCubeを描画予定となっているピクセルの深度値"が一致していたため、重なっている部分のZTestの結果は合格となり、赤いCubeが表示された。
  • 黒いCubeのみを非アクティブにした場合、何も描画されなくなった
    • 赤いCubeを描画する際に描画予定のピクセルにはまだ深度値の書き込みが行われておらず、初期値のままである。そのため、"描画済みのピクセルのZ値(初期値) = 描画予定のピクセルのZ値"とならず、ZTestの結果は不合格、何も描画されなかった。

理屈はわかりましたが、ZTest Equalの使い道はいまいちわかりませんでした。

ZTest LEqualでRenderQueue2000、ZTest GreaterでRenderQueue2001の組み合わせ

最後は、ZTestがデフォルト値でRenderQueueが2000の黒いCubeとZTest GreaterにしたRenderQueueが2001の赤いCubeを並べてみます。双方ともにZWrite Onです。

黒いCubeの手前に赤いCubeがある時は描画結果に変化はなく、
黒いCubeの奥に赤いCubeが回り込んだ際に描画結果が変わりました。

RenderQueueの関係で描画順は黒いCubeの方が先となります。
"黒いCubeが描画された際に書き込まれた深度値"との比較が、
赤いCubeを描画する際に行われます。

黒いCubeより手前に赤いCubeがあるときは、
"赤いCubeを描画予定となっているピクセルの深度値"の方が小さくなり、
"描画済みのピクセルの深度値 < 描画予定のピクセルの深度値"という条件である、
ZTest Greaterの結果は不合格となります。

黒いCubeの奥に赤いCubeが回り込んだ際には、
"赤いCubeを描画予定となっているピクセルの深度値"の方が大きくなり、
ZTest Greaterの結果は合格となります。

そのため、黒いCubeの奥に赤いCubeが回り込んだ際にのみ、
黒いCubeと重なっているピクセルは上書きされ、赤いCubeが描画されます。

参考リンク

RenderQueue
【Unity】【シェーダ】カメラから見た深度を描画する
ShaderLab :Culling と Depth Testing
その17 Zバッファとアルファブレンドの嫌な関係

ご助言頂いた方、ありがとうございました!