Depth textureをサポートしない端末でdepth textureを使う方法


イメージエフェクトをつけたい!Depth textureがサポートされていない?!

ゲームの画面を綺麗に見えるために、shaderを書いてイメージエフェクトをpost processで
レンダリングした画像に付けることが少なくないと思います。
その内、相当一部のエフェクトはピクセルのdepthをアクセスしないと実現出来ません。
例えば、global fogとか、depth of view(被写界深度)とか、SSAO(環境光の遮断計算)とか。
せっかくこういうshaderを書いて、Unity editor上でもちゃんと表示されているのに
端末に入れた途端、なんと、何も表示されていない!
という事象の原因の一つは一部GPUがShader Model 2.0系の古い端末がカメラのdepth textureをサポートしていないからです。

depth texture.png

私が作ったゲームはまさにこの事象と出会いました。
端末上でfragment shaderから_CameraDepthTextureにアクセスしようとしましたが
このテキスチャには値が一切入っていません。。。

ではカスタマイズのdepth textureを作りましょう!

幸い、サポートしていないことは自動生成出来ないだけで、全くアクセスする方法がない訳でもありません。
レンダリングの途中、post processする前(post processに入ったら2Dな画像になるから)
画面内のメッシュ毎にそのメッシュの頂点の世界座標をアクセスするのは可能です(むしろこれすら出来ないと、レンダリングも出来ません)。
しかし、メッシュ毎に各自のshaderがあり、全てのshaderを編集してdepthをどこかに保存するコードを書くのがバカし過ぎます。。。

では、どうすればいいでしょう?答えはUnityのshader置換機能(shader replacement)を使うことです。
shader置換というのは一時的、該当カメラがレンダリングする全てのメッシュを各自のshaderではなく、特定なshaderで描画することです。
影響されるのが該当カメラのレンダリングのみ、他のカメラはいつも通りメッシュ各自のshaderでレンダリングします。

具体的な手順をざっくりと言うと、以下のステップがあります:
1. シーンにdepthが必要となるpost processカメラと同じ設定のカメラ(depthカメラと言いましょう)を一個設置します。
2. レンダリングターゲットをRenderTextureと設定します。(ここで使うRenderTextureは実行時で生成するのも可能です)
3. depthカメラのSetReplacementShaderメソッドを使ってdepthを取るshaderを置換shaderとして設定します。
4. レンダリングしたら、上記のRenderTextureにdepthデータが入ります。
5. そのままShader.SetGlobalTexture("_CameraDepthTexture", m_DepthTexture /*depthデータを持っているRenderTexture*/ )でデフォルトのdepth textureを上書きすれば、depth textureをサポートしない端末でもdepth textureをサポートする端末と同じ変数でdepthデータをアクセス出来ます。

ここで使うshaderコードも極シンプルで、ただメッシュ各頂点のz値をレンダリングターゲットに書くだけです。
ここでコアの部分だけを見せます。

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

struct v2f 
{
    float4 pos : SV_POSITION;
    float4 pos2 : TEXCOORD0;
};

v2f vert(appdata_img v)
{
    v2f o;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    o.pos2 = o.pos;

    return o;
}

float4 frag(v2f input) : SV_Target
{
    float depth = input.pos2.z / input.pos2.w; // Perspective division.
    return depth.xxxx;
}

上記のコードを見て、”何故直接にSV_POSITIONを使わないの?わざわざTEXCOORD0を使うのが何のため?”という質問が出るかも知れません。
理由はまた同じく、GPUがSV_POSITIONをfragment shaderからアクセス出来ない端末が存在しているからです。とても残念なことです。

以上のshaderが出力したdepth textureを直接表示すると、以下の画像になります。

Screenshot_2015-07-15-16-25-56.png

上記shaderが出力したdepth textureをそのまま表示するサンプルshaderは以下です。

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

struct v2f 
{
    float4 pos : SV_POSITION;   
    float2 uv : TEXCOORD0;
};

v2f vert(appdata_img v)
{
    v2f o;
    o.uv = v.texcoord.xy;
    v.vertex.z = _ProjectionParams.y;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

    return o;
}

float4 frag(v2f input) : COLOR
{
    float raw_depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, input.uv);
    float linearized_depth = Linear01Depth(raw_depth);

    return float4(linearized_depth, linearized_depth, linearized_depth, 1);
}

Depth textureが出来ましたが、でもdepthの変化がスムーズに見えない!

目が鋭い人はもう気づいたかも知れませんが、depthの変化は結構段階的で、スムーズではありません。
原因を簡単に説明すると、カメラの透視効果によるdepth値の分布が不均一 + Shader Model 2.0のGPUがサポートするRenderTextureのフォマットの精度が足りないことです(1チャンネルが最大8bitしかありません)。
CustomVsReal1.png

この問題を解決するには上記二つの原因のどちらを潰すことです。
先ず”Shader Model 2.0のGPUに高精度のフォマットを追加する!”のようなハードウェアの仕様を変更することは流石に無理です。

なので、選択肢はdepth値の分布を均一化するしか残っていません。
実は上記のコードに既にdepth値を均一化する操作が入っています。Linear01Depthというメソッドはまさにこの操作を行うメソッドです。
残念ながら、他のdepth利用イメージエフェクトshaderと同じく、この均一化操作が発生するタイミングはdepthがRenderTextureに書き込んだ後
つまり、RenderTextureの精度が足りない場合で均一化する前にdepthの精度が既に失っています。
自然に、精度が低いdepthをもとに均一化しても精度が低いままです。

なので、Linear01Depthの実行をdepthデータがRenderTextureに書き込まれる前に移動すれば(CustomDepth shaderに移動)解決出来るはずです。
以下は変更したコード。

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

struct v2f 
{
    float4 pos : SV_POSITION;
    float4 pos2 : TEXCOORD0;
};

v2f vert(appdata_img v)
{
    v2f o;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    o.pos2 = o.pos;

    return o;
}

float4 frag(v2f input) : SV_Target
{
    float depth = input.pos2.z / input.pos2.w; // Perspective division.
    float linearized_depth = Linear01Depth(depth); // Depthを均一化
    return linearized_depth.xxxx;
}
DepthView
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag

struct v2f 
{
    float4 pos : SV_POSITION;   
    float2 uv : TEXCOORD0;
};

v2f vert(appdata_img v)
{
    v2f o;
    o.uv = v.texcoord.xy;
    v.vertex.z = _ProjectionParams.y;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

    return o;
}

float4 frag(v2f input) : COLOR
{
    float raw_depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, input.uv);
    float linearized_depth = raw_depth;  // 均一化は既にCustomDepthで行ったから

    return float4(linearized_depth, linearized_depth, linearized_depth, 1);
}

以下は上記変更後の実行結果です。
CustomVsReal2.png

どうでしょう?”本当のdepth texture”と似ているスムーズさになったんではありませんか?

最後に、何故”本当のdepth texture”が均一化せずにそのままdepthデータを渡すんでしょう?
何故”本当のdepth texture”の不均一なdepthデータをpost processで均一化しても精度が失わないんでしょう?
原因はdepth textureがサポートするGPUが高精度なRenderTextureフォマット(1チャンネル16bit ~ 32bitぐらい)をサポートしていて
それをdepth textureとして使っているからです。