1. Pixel Depth Offset(PDO) は悪か?
UE4のMaterialの機能で、PixelのDepthを画面奥方向に移動できるPixel Depth Offset(PDO)という機能があります。この機能は様々な箇所で活用されています。例えば。。
↓地面の凹凸をポリゴンではなくテクスチャだけで出す"Parallax Occlusion Mapping"(通称POM) (Contents Exampleより)
↓板ポリゴンで表現された髪などの生え際で、ポリゴンが刺さっている様に見えなくさせる(Contents Exampleより)
などなどです。
ただ、このパラメータは他と比べて少し特殊です。ピクセルの色を決めるのではなくピクセルのデプスをいじくります。なので「PDOを使うのはめちゃくちゃ重たいんじゃないのか。。。」などの質問をよく受けます。本記事では、PDOが内部でなにをしているか、また処理負荷としてどうなのかというのをざっくりと見ていきたいと思います。Pixel Depth Offsetを使ってかっこいいエフェクトをつくりたい!という方が本記事を読んでも、1pixelも絵はうまくなりません。ただ、挙動について知りたい人が読んだらすっきりするかもしれません。
ということで、最初に時間がない人に向けて、本記事のまとめを記載いたします。
忙しい人のための本記事のまとめ
- Pixel Depth Offset(PDO)を使うと、Prepass/ShadowPass/BasePassで処理負荷が増える可能性はたしかにあります。
- ですので、不必要にまたは過剰にPDOを使うことは避けるべきです。
- しかし多くのプラットフォームで"Conservative Depth"という機能が働き、PDOの負荷をかなり軽減してくれます。
- このConservative Depthを使う条件として、PDOはDepthの奥方向にしかいけないという制約を持っています。
- この"Conservative Depth"はAndroid端末を除いて現状(UE4.24)ではほとんど多くのデバイスで対応しています
- Parallax Occlusion Mapping(POM)などの処理負荷が非常に重たいのは、単純にPOMの内部計算が非常に重たいということで、PDOを使うといつもPOMほどの負荷がでるわけではありません。
上記項目を理解するため、以下ではひとつひとつ説明していこうと思います。まず、GPU内部のDepth Testについて2つほど説明したいと思います。1つ目はPreZ/PostZ、2つ目はタイル単位でのDepth Testです。その後PDOの問題とUE4が行っている対策方法について説明します。
2. PreZとPostZのおさらい(CEDEC2016: レンダリングフロー総おさらいより)
PreZとPostZのおさらいからです。こちらはCEDEC2016(はるか昔)で発表した資料がありますので、そちらから抜粋いたします。詳しくは講演資料をご参考頂ければ幸いです。
PreZとPostZは、GPU内部の機能であり、私達が明示的に設定するものではなくGPUがシェーダに応じてどちらかを使います。(明示的に設定できるコンソールなどもありますがここでは省きます。)
PreZ = Pixel Shaderの計算よりも前(Pre)でデプステストを行う
せっかくピクセルをの色を計算しても、そのピクセルがデプステストで負けて消えてしまうなら意味がありません。この様な無駄な処理を省くために、GPUは可能ならばピクセルシェーダの前にデプステストを行います。これをPreZと呼びます。高速なので、GPUはなるべくPreZで処理しようとします。
PostZ = Pixel Shaderの計算よりも後(Post)でデプステストを行う
一方。PostZはPixel Shaderの後でデプステストを行います。デプステストで負けるピクセルも計算するので負荷はあがってしまいますが、PostZでないと正しく動かないシェーダが来た場合GPUは自動でPostZでデプステストをします。
ではPostZでないと正しく動かないシェーダはどういうのかというと、「ピクセルシェーダを計算しないと最終的なデプスがわからない」シェーダです。これは先程の「Pixel Depth Offsetを使う=Shader内部でDepthを書き換える」がドンピシャでヒットしています。ですのでPDOを使うとPostZとなり、本来デプステストで消える見えないピクセルも計算負荷が生じる可能性があります。
3. ラスタライザ内部でのタイル単位のDepth Test
ポリゴンをピクセルに変換する処理を担当しているものをラスタライザと呼びます。このラスタライザ内部で、実は画面内部の数ピクセルを一つに集めたタイル単位でのデプステストも行っています。(1タイルの1辺が何ピクセルかはプラットフォームにより変わる可能性もあるので割愛します)
このタイル構成やテストの詳細は省きますが、ここで大切なのは「このタイル単位でデプステストに負けると決定しているタイルは、ピクセルを計算するまでもなく破棄できるので非常に高速」という点です。(この図を見てなんでPixelに分解しているのに4Pixelずつ同じ色にしてるのと疑問に思った方は一緒に飲みに行きましょう。)
タイル単位で破棄できれば楽ですが、タイルの中の1部ピクセルが生き残る可能性がある場合、ピクセルに分解します。
しかしPDOを使うと最終的なシェーダが走らないとDepthがわからないため、タイル単位でのデプステストを行わずに全てPixelに分解して計算する必要があります。
こう聞くと「やばい!PDOめちゃくちゃ重たいじゃん!!」と思うかもなのですが。。。UE4はConservative Depthという機能をもとにこの処理負荷の軽減をはかっています。重要なのは上の画像の**「1Pixelでもデプステストに勝つ可能性があれば。。。」**という部分です。
4. Conservative Depth OutputによってタイルのDepth Testは維持する!!
実は、「シェーダ内でDepth操作するけど、それは奥方向(もしくは前方向)一方向にしか行かないよ~」と明示的にシェーダ内部に記述することが可能です。この機能をConservative Depth Outputと呼び、UE4はDefaultでPDOは奥にしか行けないと記述しています。これを記述すると何が良いのでしょうか??下図の様に、もうそのピクセルは奥にしか行かないとわかるので、もう下図の様な青オブジェクトの一部ピクセルが生き残る可能性というのはありません。なので、タイル単位のデプステストによってこの青色オブジェクトを棄却することが可能となります。
この機能により、タイルレートでの早期Depth Testが行われ、タイル単位であれば無駄なピクセルシェーダを走らせずに棄却することができます。もちろん、タイルのデプステストを通過しPixelに分解されたら、PDOがある限りPostZの処理となり、シェーダは確実に走ってしまうのですが。。。
具体的にどのプラットフォームで対応されているかというと、シェーダ内部でSUPPORTS_CONSERVATIVE_DEPTH_WRITES
が1としてdefineされているプラットフォームです。4.24では、以下のように定義されていました。Win/Mac/iOS/また各種コンソールでは対応しているのがわかるかと思います。
#define SUPPORTS_CONSERVATIVE_DEPTH_WRITES ((COMPILER_HLSL && FEATURE_LEVEL >= FEATURE_LEVEL_SM5) || (PS4_PROFILE) || (COMPILER_METAL && FEATURE_LEVEL >= FEATURE_LEVEL_SM5) || SWITCH_PROFILE || SWITCH_PROFILE_FORWARD)
4-2(Advanced): Conservative Depthで指示するならPostZもPreZにできるのでは??
上図を見て、Conservative Depthで奥行きにしか行けないって指示できるなら、シェーダ単位でもPostZじゃなくてPreZできるんじゃない??と思った方がいるかもしれません。僕も最初はそう思っていました。しかしこちらはデフォルトではPreZは機能せずPostZとなります。なぜかというと、DepthTestだけじゃなく、最終的に計算した結果を書き込むDepthWrite処理もしなければならず、書き込むDepthはシェーダを計算しないとわからないためです。PreZでDepthTestして。。生き残ったらPostZでDepthWriteすれば良いじゃない。。。と更に思う方もいるかもしれません。。。そしてそれを行えるプラットフォームもあるかもしれません。しかしそれでパフォーマンスが向上するかは注意が必要です。Depthへのアクセスにも残念ながらコストがかかりタダではありません。PreZとPostZで二回デプスバッファへのアクセスをしているので、逆にそこがネックとなり重たくなる可能性もあります。
まとめ: P D O!! P D O!!
簡単にPDOの仕組みとConservative Depthによるタイルレートデプステストの説明をさせていただきました。Pixel Depth Offsetが奥にしか行けないこと、これで許してもらえると幸いです。