19. 投影とラスタライズの詳細
第2部へようこそ。
第1部(第1章〜第18章)では、CGエンジニア検定エキスパートのシラバス全体を、初心者にも分かる深さで一通り見て来ました。
第2部(第19章〜第29章)はシラバスの範囲を超えます。
第1部では概略しか描けなかった話題を、きちんと最後まで導きます。此の章の投影とラスタライズの詳細に始まり、アンチエイリアシングと透明表現、メッシュ処理と符号付き距離場、物理ベースシェーディング、モンテカルロレンダリング、リアルタイムの影と大域照明、高度なアニメーションと物理、HDRと画像処理、ボリュームレンダリング、そして最後に現代のGPUとニューラルレンダリングです。
第2部と第1部の関係はどうなっているのでしょうか?
第2部の各章は第1部で紹介した何かから出発し、其れを紹介した節にリンクで戻り、同じ用語と記法を使います。
第1部で「第2部で扱います」と書いた約束は、其の話題を受け持つ章で果たします。
導出は増えますが、スタイルは同じです。先ず直観、次に式、そして実際の数値で確かめます。
検定に合格する為に第2部は必要ありません。第30章の模擬試験が問うのはシラバスの範囲です。
但し、検定の答えの多くは、其れが何故正しいのかを知ると当たり前になります。第2部は其の為にあります。
話題がシラバスの項目に繋がる所では、「検定のポイント」で其れがどの項目かを示します。
此の章では、第1部でブラックボックスとして扱ったパイプラインの2つの箱、投影行列とラスタライザーを開けます。
第6章で「詳しい導出は第2部で行います」と約束し、第5章と第10章では奥行きの精度、リバースZ、デカール用の深度オフセットを後回しにしました。
今回の予定は次の通りです:
- 任意の視錐台と任意の深度の規約(OpenGL、Vulkan/Direct3D、リバース)に対する透視投影行列の導出。yの反転、無限遠のファー平面、正射影も含みます。
- 逆投影:ピクセルと其の深度からカメラ空間の位置を取り戻す方法。
- 同次座標のクリップ空間でのクリッピング:其れが無いと何が起きるか、ニア平面でのクリッピングの計算例、ガードバンド。
- 深度精度:精度が何処に行くか、固定小数点と浮動小数点、リバースZ、対数深度、デプスバイアス。
- ラスタライザー:エッジ関数、トップレフトルール、固定小数点へのスナップ、タイル単位の走査、パースペクティブコレクト補間の導出、2×2クアッドでの微分、アーリーZと階層Z。
前提は線型代数の第12章(投影)、第10章(逆行列)、そして第6章の6.9節(エッジ関数)です。其れ以外は全て此処で組み立てます。
19.1 どの規約にも使える透視投影行列
行列がしなければならない事
線型代数の第12章では、対称な視錐台に対してperspectiveGlとperspectiveVkを導きました。
此処では全ての変種を一度に扱える形でやり直します。
カメラは−z方向を向いているので、カメラの前にある点の距離は正です。
行列には行ごとに1つ、合わせて4つの仕事があります:
- 一番下の行は距離をにコピーし、GPUの透視除算が其れで割る様にします:。
- xとyの行は、其の除算の後に視錐台の端で丁度±1になる値を作ります。
- zの行は、ニア平面とファー平面で決められた値になる深度を作ります。
- そして全ては行列なので、について線型でなければなりません。
xとyの行:非対称な視錐台
視錐台を、距離にあるニア平面と、其の平面から切り取る長方形で表します。xはからまで、yはからまでです。
点はニア平面上のに投影されます(相似な三角形、第5章の5.9節)。
を−1に、を+1にしたいので、第4章の1次元のウィンドウ・ビューポート変換を使います:
係数は透視除算なので、行列は括弧の中だけを作れば良いのです:
対称な視錐台では、なので、zの項は消えます。
縦の視野角をとするとなのでとなり、アスペクト比を使うとxの成分はです。正にperspectiveGlの成分です。
非対称な視錐台は珍しい物ではありません。VRヘッドセットは片目ごとに其れで描き、複数のモニターを並べた壁は1つの大きな視錐台の一部を各モニターに割り当て、テンポラルアンチエイリアシングは毎フレーム視錐台をピクセルの何分の一かずらします(第20章)。
NDCで像をだけ動かすには、にを足します。つまり0行2列にを入れます。
幅800ピクセルの画面で半ピクセルはです。
zの行:深度は1/zに従うしか無い
zの行はにしかなれません。xやyに依存してはいけませんし(同じ距離の点は全て同じ深度になる)、zと1の線型結合以外の物は入れられません。
除算の後は:
深度バッファの非線型性の話は此れで全部です。
とをどう選んでも、保存される深度はのアフィン関数です。
此れは誰かが決めた設計ではなく、投影が「4×4行列を1つ掛けてから割る」事である以上、避けられない結果です。
そして良い面もあります。19.5節で見る様に、、従って深度は三角形の上で画面に対して線型なので、ラスタライザーは深度を画面空間の普通の重みで補間出来るのです。
全ての深度の規約に使える1つの式
ニア平面の深度を、ファー平面の深度をにしたいとします。
条件が2つ、未知数が2つです:
分母を払うととで、2つ目から1つ目を引くとです。
を1つ目の式に戻します:
此れで、どの規約も代入1回で求まります:
| 規約 | 、 | ||
|---|---|---|---|
| OpenGL | −1、1 | ||
| Vulkan、Direct3D、Metal | 0、1 | ||
| リバースZ | 1、0 |
最初の2行はperspectiveGlとperspectiveVkの成分です(線型代数の第12章ではVulkanの物をとと書いていますが、同じ値です)。
3行目のリバースZはニア平面を1に、ファー平面を0にします。何故そんな事をしたいのかは19.4節で説明します。
計算例。 第6章6.2節の頂点はカメラ座標にあるのでで、、です:
- OpenGL:、、、で、深度バッファにはが入ります。
- Vulkan:、、深度はで、同じ値です。
- リバース:、、、深度はです。
一般に、リバースの深度は通常の0〜1の深度を1から引いた物と丁度等しくなります。同じ平面に、逆から番号を振っただけです。
yの反転
3つのAPIは、yがどちら向きかでも意見が分かれます。
OpenGL、Direct3D、MetalはNDCのyが上向き、Vulkanは下向きです。
Vulkanでは、yの行の符号を反転するか(perspectiveVkのが其れです)、yの行は其の儘にしてビューポートの高さを負にするかのどちらかで、両方はしません(線型代数の第12章の12.7節)。
反転は像を鏡に映すので、時計回りと反時計回りが入れ替わります。背面除去に使う表面の向きの設定が其れと合っているか確かめて下さい。
無限遠のファー平面
にするとどうなるでしょうか?
分子と分母をで割り、とします:
| 規約 | 距離での深度 | ||
|---|---|---|---|
| OpenGL | −1 | ||
| Vulkan、Direct3D | −1 | ||
| リバースZ | 0 |
何も壊れません。ファー平面が無限遠に移るだけで、無限遠の点(カメラ空間で同次座標の成分が0の方向)は丁度深度1に、リバースZなら0に来ます。
リバースの無限遠版は此の章で一番美しい行列です。zの行が定数なので、保存される深度は丁度です。
距離4の頂点でならです。
無限遠の投影は第11章のZフェイル法のシャドウボリュームを頑丈にし(無限遠の蓋が切り取られない)、広い屋外のシーンにも向いています。
同じ枠組みで見る正射影
正射影の一番下の行はなのでで、何も割りません。
従ってzの行は距離に対して線型で、同じ2つの条件から次が得られます:
OpenGLでは、で、orthographicGlの成分です。
深度が線型なら全ての距離が同じ精度を持つので、歩幅が一様な深度フォーマット(固定小数点)が一番合っています。反転しても言う程の得はありません。
其の為、太陽用の正射影のシャドウマップ(第11章)は19.4節の距離の問題に悩まされません。
19.2 逆投影
何故逆向きに辿るのか?
多くのエフェクトは深度バッファから出発し、ピクセルが3次元の何処にあるかを知る必要があります。
ディファードシェーディングは、法線や色は持つが位置は持たないGバッファでライティングをし(第17章)、画面空間のアンビエントオクルージョンは深度バッファの中を進み(第24章)、フォグや投影するデカールは各ピクセルの下の位置を必要とします。
深度から復元すれば、1ピクセルあたり12バイトの代わりに数命令で済みます。
行列を元に戻す
透視投影行列は次の形をしています:
(掛けて確かめられます。の3行目との最後の2列の積はとです。)
はである限り存在し、上の全ての透視投影行列で其れは成り立ちます(は因数を含みます)。
ピクセルから戻るには、先ずビューポート変換を元に戻してNDCを求め、次にを掛け、最後に結果ので割ります。が返すのは同次座標の点だからです。
計算例。 第6章で、頂点は800×600のウィンドウ(行は上から数える)のピクセルに、深度0.825で描かれました(OpenGL、、、90°、アスペクト比4/3)。
ビューポート変換を元に戻します:
、、、なので、の最後の行はです:
で割るとで、出発したカメラ座標に戻りました。
3番目の成分のに注目して下さい。は除算の前には常にを出し、で割る事で其れがになります。
近道:線型な深度とレイの積
書き下すと、同じ計算は3つの式だけです。
先ず深度から距離を求めます(をについて解きます):
此の数値ではです。
次にNDCの位置からxとyを求めます。なので:
幾何学的には、はzが−1になる様に伸縮したピクセルのレイの方向で、位置は其のレイに距離を掛けた物です。
エンジンは其のレイをピクセルごとに1回(又は全画面三角形の頂点ごとに計算してラスタライザーに補間させて)求めておき、後は距離だけを使う事がよくあります。
リバースの無限遠投影では距離が一番簡単です。深度がなのでです。
コード
linalg.hhと同じ書き方で書いたリバースの行列とリバースの無限遠の行列(ヘッダーには含まれません)を第6章の頂点に適用し、続けてヘッダーのinverseで復元します:
#include <cstdio>
#include "linalg.hh"
// Reversed Z, depth 0..1 (Vulkan/Direct3D style, y down): near -> 1, far -> 0.
inline Mat4 perspectiveVkReversed(float fovY, float aspect, float n, float f) {
float t = 1.0f / std::tan(fovY * 0.5f);
Mat4 m;
m.at(0, 0) = t / aspect;
m.at(1, 1) = -t;
m.at(2, 2) = n / (f - n);
m.at(2, 3) = (f * n) / (f - n);
m.at(3, 2) = -1.0f;
return m;
}
// Reversed Z with the far plane at infinity: the stored depth is exactly n / distance.
inline Mat4 perspectiveVkInfiniteReversed(float fovY, float aspect, float n) {
float t = 1.0f / std::tan(fovY * 0.5f);
Mat4 m;
m.at(0, 0) = t / aspect;
m.at(1, 1) = -t;
m.at(2, 3) = n;
m.at(3, 2) = -1.0f;
return m;
}
void show(const char *name, const Mat4 &p, Vec4 v) {
Vec4 c = p * v;
printf("%-18s clip (%g, %g, %g, %g) z/w %g\n", name, c.x, c.y, c.z, c.w, c.z / c.w);
}
int main() {
const float pi = 3.14159265f;
Vec4 cam{2, 1, -4, 1}; // the vertex of chapter 6 in camera space
show("OpenGL", perspectiveGl(pi / 2, 4.0f / 3.0f, 1, 11), cam);
show("Vulkan", perspectiveVk(pi / 2, 4.0f / 3.0f, 1, 11), cam);
show("reversed", perspectiveVkReversed(pi / 2, 4.0f / 3.0f, 1, 11), cam);
show("infinite reversed", perspectiveVkInfiniteReversed(pi / 2, 4.0f / 3.0f, 1), cam);
// back from pixel (550, 225) of an 800x600 window with depth 0.825 (OpenGL depth range 0..1)
float px = 550, py = 225, depth = 0.825f;
Vec4 ndc{2 * px / 800 - 1, 1 - 2 * py / 600, 2 * depth - 1, 1};
Vec4 v = inverse(perspectiveGl(pi / 2, 4.0f / 3.0f, 1, 11)) * ndc;
printf("P^-1 * ndc = (%g, %g, %g, %g)\n", v.x, v.y, v.z, v.w);
printf("camera = (%g, %g, %g)\n", v.x / v.w, v.y / v.w, v.z / v.w);
return 0;
}
OpenGL clip (1.5, 1, 2.6, 4) z/w 0.65
Vulkan clip (1.5, -1, 3.3, 4) z/w 0.825
reversed clip (1.5, -1, 0.7, 4) z/w 0.175
infinite reversed clip (1.5, -1, 1, 4) z/w 0.25
P^-1 * ndc = (0.5, 0.25, -1, 0.25)
camera = (2, 1, -4)
シェーダーで書くのは近道の方です。
リバースの無限遠Zを使うVulkanのレンダラーで、画面全体で0〜1になるテクスチャ座標uv(VulkanのNDCと同じくyは下向き)と、行列の対角成分の2つscale を使うと:
// depth: reversed infinite Z, so depth = n / distance
float3 viewPosFromDepth(float2 uv, float depth, float n, float2 scale) {
float d = n / depth; // distance in front of the camera
float2 ndc = uv * 2.0 - 1.0; // Vulkan NDC, y down
return float3(ndc.x * d / scale.x, -ndc.y * d / scale.y, -d);
}// depth: reversed infinite Z, so depth = n / distance
vec3 viewPosFromDepth(vec2 uv, float depth, float n, vec2 scale) {
float d = n / depth; // distance in front of the camera
vec2 ndc = uv * 2.0 - 1.0; // Vulkan NDC, y down
return vec3(ndc.x * d / scale.x, -ndc.y * d / scale.y, -d);
}// depth: reversed infinite Z, so depth = n / distance
float3 viewPosFromDepth(float2 uv, float depth, float n, float2 scale) {
float d = n / depth; // distance in front of the camera
float2 ndc = uv * 2.0 - 1.0; // Vulkan NDC, y down
return float3(ndc.x * d / scale.x, -ndc.y * d / scale.y, -d);
}
yのマイナス記号はVulkanの行列のを元に戻します。
何も描かれていない背景はクリアした深度0を持ち、距離が無限大になってしまうので、実際のコードでは先に其れを判定します。
19.3 同次座標のクリップ空間でのクリッピング
クリッピングが無いと何が起きるか
第6章の6.3節では、カメラの後ろまで伸びる線分をクリッピングし、先に割ると遠い方の端が間違った所に行く事を見ました。
此処では其の理由をもう少し詳しく見てから、三角形全体をクリッピングします。
原因はによる除算です。
カメラの前の点はです。視点を通る平面上の点はで、0で割る事になります。カメラの後ろの点はで、除算でxとyの符号が反転し、画面の中心を挟んだ反対側に映ります。
前の点から後ろの点への線分はを通り、其処で投影は無限遠に飛んで行きます。除算の後、2つの端は本当に通る無限遠ではなく、線分が覆っていない画面の部分を通って「逆回り」に繋がれてしまいます。
投影された3つの角しか知らないラスタライザーは、間違った領域を塗ります。
除算の前にクリッピングすれば、ニアの距離よりが小さい点は全て取り除かれるので、除算がに出会う事はありません。
ニア平面でのクリッピングの計算例
第6章のOpenGLの行列をもう一度使います(90°、アスペクト比4/3、、:、、、)。
高さの床の三角形がカメラの下まで伸びています:、、で、Cは視点の1単位後ろです。
各頂点はテクスチャ座標も持ちます:、、。
クリップ座標は:
OpenGLのニア平面はなので、判定に使う符号付きの値はです:
Cは外側、AとBは内側です。
サザーランド・ホッジマン法(6.3節)で辺A→B→C→Aを順に辿ります:
- A→B:Aは内側なのでAを出力。Bも内側なので交差はありません。
- B→C:Bを出力。Bは内側でCは外側なので、辺はで交差します:を出力。
- C→A:Cは外側なので何も出力しません。辺はCからで交差します:を出力。
三角形は四角形になり、GPUは其れを2つの三角形として描きます。
新しい2つの頂点はなので、NDCの深度は−1、つまり丁度ニア平面上にあります。
属性は同じで補間します:、。
クリップ空間での線型補間は属性に対して正しいのでしょうか?
正しいです。クリップ座標はカメラ座標の線型な関数(行列を1回掛けるだけ)なので、クリップ空間の線分上のパラメータの点は、カメラ空間の線分上の同じの点です。そして3次元の三角形の上で線型に変わる属性は、其の点で其の値を取ります。
確かめると、カメラ空間でで、ニア平面の上にあります。
線型補間が正しくなくなるのは除算の後だけで、其れが19.5節の話題です。
割ってから、素朴な方法と比べます:
クリッピング後の四角形のNDCの角は、、、です。足元まで伸びる床として当然、画面の下に向かって広がっています。
先にCを割るとで、中心の上に来ます。ラスタライザーは空に向かって尖った三角形を描いてしまいます。
クリッパー自体は、を内側の判定に使い、全ての属性を同じで補間する、第6章のサザーランド・ホッジマン法のループ其の物です。
頂点が1つ外にあれば三角形は四角形(三角形2つ)になり、2つ外にあれば三角形の儘です。
Vulkan及びDirect3Dの規約ではニア平面はなので、符号付きの値は単にです。リバースZではニア平面がなのでです。
ガードバンド、深度クランプ、クリッピング無しのラスタライズ
クリッピングは新しい頂点を作り、新しい頂点には時間が掛かるので、GPUは出来るだけ避けます。
4つの側面に対しては、殆どクリッピングしません。ラスタライザーがビューポートの外のピクセルを訪れないだけです(19.5節のバウンディングボックスとタイルなら其れはタダです)。
限界はラスタライザーの数値の形式です。位置は固定小数点に変換されるので(19.5節)、頂点は画面のずっと外にあっても構いませんが、どれだけでも遠くて良い訳ではありません。
ラスタライザーがまだ表せる領域がガードバンドで、ビューポートよりずっと大きく、其れからはみ出す稀な三角形だけが幾何学的にクリッピングされます。
ニア平面は此の方法では扱えません。其処での問題は大きな座標ではなくだからです。
ニア平面とファー平面には、もう1つの選択肢があります。OpenGLとVulkanには深度クランプ(GL_DEPTH_CLAMP、VulkanのdepthClampEnable)があり、ニアとファーのクリッピングを止めて、代わりに深度を範囲内にクランプします。
シャドウマップは、光源とシャドウマップのニア平面の間にある遮蔽物を失わない為に其れを使います。遮蔽物はニア平面に「押し潰され」、其れでも影を落とします。
(19.5節のエッジ関数を同次座標のから直接組み立てれば、クリッピングを全く省く事も出来ます。1997年にOlanoとGreerが述べた方法で、結果の画像は同じです。)
19.4 深度精度
精度は何処に行くか
19.1節で、保存される深度がのアフィン関数である事を証明しました。
0〜1の範囲では:
2つ目の式、つまり逆関数は、各深度の値が何処にあるかを教えてくれます。
、(第6章の設定)では、保存される深度が0.1、0.2、…、0.9になる平面は次の距離にあります:
深度の値の半分は距離までに使い切られ、最後の10%の値で5.5から11までの全てを賄わなければなりません:
保存される値の1段は、どれだけの距離に当たるのでしょうか?
微分するとなので、1段は次の距離の幅になります:
第10章の10.3節の式です。1段は距離の2乗で大きくなり、に反比例します。
第10章が暗黙に仮定していたのは、がどこでも同じという事です。
其れが成り立つ深度フォーマットもあれば、成り立たない物もあります。
固定小数点と浮動小数点
深度バッファには2つの系統があります:
- 固定小数点(UNORM)フォーマットは0からまでの整数を保存し、を意味します。1段はどこでも同じで、16ビットなら、24ビットならです(24ビットは普通8ビットのステンシル値と一緒に詰めます)。
- 浮動小数点フォーマット(32ビットfloat、Vulkanの
D32_SFLOAT)は符号、8ビットの指数、23ビットの仮数を保存します。1段は値に依存し、との間ではです。従って0.5と1の間では1段はで24ビット固定小数点と同じですが、0.001と0.002の間では約で、0に向かって更に小さくなり続けます。
要するに、floatは相対的な精度が一定です。1段は常に値其の物のから倍の間です。
では2つの非線型性を組み合わせてみましょう。
通常の対応では、遠くはが1に近い所で、1の近くではfloatは24ビット固定小数点より良くありません。
floatの余分な精度は全ての近く、つまりニア平面の直ぐ前にあり、其処ではの対応が既に必要以上の精度を与えています。
通常のZで32ビットfloatのバッファを使うのは、8ビットの無駄です。
リバースZ
今度は対応を反転させます。ニア平面を1に、ファー平面を0にします。
によって深度の1段が粗くなる遠くでは、保存される値が0に近く、其処ではfloatの1段がとても小さくなります。
2つの効果が打ち消し合います。其の度合いは、となるリバースの無限遠投影で正確に分かります:
距離の1段は距離の一定の割合、約から倍で、全ての距離でそうなり、もうにすら依存しません。
(ファー平面が有限なら、同じ導出でとなり、の近くでは更に少し良くなります。)
此れは距離其の物をfloatで持った時と同じ種類の精度で、望み得る最善です。
比較の計算例。 第10章の設定、 m、 mで、隣り合う保存値の間の実際の距離です(保存値と次に表せる値を取り、両方を距離に戻して計算しました):
| 距離 | 16ビットUNORM | 24ビットUNORM | float32、通常 | float32、リバース |
|---|---|---|---|---|
| 1 m | 0.15 mm | 0.6 µm | 0.6 µm | 0.07 µm |
| 10 m | 15 mm | 0.06 mm | 0.06 mm | 0.9 µm |
| 100 m | 1.55 m | 6.0 mm | 6.0 mm | 5.8 µm |
| 500 m | 38 m | 15 cm | 15 cm | 18 µm |
1つ手で確かめましょう。
100 mで24ビット固定小数点なら: mで、第10章の6 mmです。
リバース:はとの間にあるので、floatの1段はです。傾きで割ると mです。
100 mで24ビットより約1000倍細かく、其の差は距離と共に広がります。
グラフは範囲全体を示します(リバースの曲線のノコギリの歯はfloatの指数の段です):
誤差の原因は保存形式だけではありません(頂点シェーダーのfloatの計算も丸め、通常の対応ではがほぼ等しい2つの数の割り算になります)。其処でもリバースZは役に立ち、無限遠版ではが正確です。
そしてリバースのfloat深度では、ニア平面の位置はもう殆ど問題にならないので、其れを使うエンジンはニア平面を数センチメートルにする余裕があります。
設定の仕方
リバースZには、4つの事が揃っている必要があります:
- 浮動小数点の深度フォーマット(Vulkanの
D32_SFLOAT、Direct3DのDXGI_FORMAT_D32_FLOAT、OpenGLのGL_DEPTH_COMPONENT32F)。 - 19.1節のリバースの投影行列(有限でも無限遠でも)。
- 深度バッファを1ではなく0でクリアする事。
- 深度テストを「より小さい」ではなく「より大きい」(又は「以上」)に反転する事。
更に0〜1の深度範囲も必要です。
Vulkan、Direct3D、Metalは標準で其の範囲です。
OpenGLの標準の範囲は−1〜1で、其処で反転しても得はありません。遠くの値はfloatが粗いの近くに来て、保存値への変換が其れに1を足すので、小さな差はバッファに届く前に消えてしまいます。
OpenGL 4.5からは、glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE)でOpenGLを0〜1の範囲に切り替えられます。
深度を読む他の全ての物も反転が必要です:シャドウマップの比較、階層Z(19.5節)、ポリゴンオフセット(後述)、19.2節の「背景かどうか」の判定。
深度テストはOpenGLの第15章で自分で扱います。
対数深度について簡単に
相対精度を一定にするもう1つの方法は、距離の対数を保存する事です。例えば:
するとで、一様な1段は距離の1段になり、やはり距離に比例します。
24ビット固定小数点、、ではなので、、100 mで0.055 mmです。
100 mでは通常の24ビット深度より約100倍良いのですが、リバースのfloatより約10倍悪く、代償もあります。対数は射影的な行列からは出て来ないので、深度はもう画面に対して線型ではありません。
頂点ごとに書けば大きな三角形の上で間違って補間され、フラグメントごとに書けば(gl_FragDepth、SV_Depth)19.5節のアーリーZが無効になります。
リバースのfloat深度が使えるなら、普通はそちらの方が良い選択です。
デプスバイアスとポリゴンオフセット
完璧な精度があっても、同じ平面上にある2つの面は区別出来ません。
デカール(弾痕、路面標示、ポスター)は壁の丁度上に2枚目のポリゴンとして描かれます。2つの三角形の深度は別々の頂点から別々の丸めで補間されるので、各ピクセルで勝者がでたらめに決まり、どんな距離でもZファイティングが起きます。
第11章(11.4節)のシャドウアクネは、面と、シャドウマップ内の其の面自身の深度の間で起きる同じ問題です。
ハードウェアによる対策がデプスバイアス(OpenGLではポリゴンオフセット)で、ポリゴンの全てのフラグメントの深度に値を足します:
必要なら最大値でクランプします。
はウィンドウ空間でのポリゴンの深度の傾き(1ピクセルで深度がどれだけ変わるか。仕様は2乗和の平方根で定義し、此のmaxを近似として認めています)で、は深度フォーマットで区別出来る最小の差です。Nビットの固定小数点なら(又は其の2倍まで)、floatのフォーマットならで、はポリゴンの深度の値の最大の指数です。
パラメータは、OpenGLではglPolygonOffset(factor, units)、VulkanではdepthBiasSlopeFactor、depthBiasConstantFactor、depthBiasClamp、Direct3DではSlopeScaledDepthBias、DepthBias、DepthBiasClampです。
何故傾きの項があるのでしょうか?
同一平面上の2つの三角形の丸めの差は、1ピクセルで深度がどれだけ速く変わるかに比例するからです。
計算例。 後の19.5節のミップマップの例と同じ床を、1 m上から約9.8 mの距離で見ます。、、24ビットのバッファです。
画面上で1行上に行くと床の点は mから mに移るので、保存される深度は次だけ変わります:
定数項だけのは、其れより5,600倍小さいです。此の2つの距離の間での傾き(1メートルあたり)を使うと、其の1単位はデカールを僅か0.06 mmしか動かさず、の何分の一かの丸めの差に勝つには足りません。
傾きの係数1はデカールを、距離にして約0.31 m動かします。此の床の1ピクセルが跨ぐ深度の幅です。
其れでちらつきは止まりますが、デカールの後ろ0.31 m以内(視線に沿って)に立っている物体はデカールに上書きされ得ます。其の為にクランプがあり、エンジンが代わりにデカールを画面空間で、19.2節の復元した位置を使って投影する事が多いのも其の為です。
符号に注意して下さい。
通常のZでは近い程小さいので、デカールには負のオフセットが要ります(glPolygonOffset(-1, -1)は定番です)。一方シャドウマップには、保存する深度を遠ざける正のオフセットが要ります。リバースZではどちらの符号も反転します。
19.5 ラスタライズの詳細
エッジ関数は線型関数
第6章の6.9節では、有向辺のエッジ関数を紹介しました。辺の左側で正です(yは上向き)。
展開すると、ピクセルの位置の普通の線型関数です:
其の零点の集合は辺を通る直線で、は其の直線の、三角形の内側を向く法線です。
第6章の三角形、、では:
ピクセルの中心では、、で、第6章の値です。
3つの関数を足すと、xとyの項は消えてが残ります。此れは、つまり面積の2倍です。
3つの重心座標の和が常に1になるのは其の為です。
線型性がラスタライズを安くしています。
1ピクセル右に進むとを足し、1ピクセル上ならを足し、ピクセル右ならを足します。
行に沿って、はで42から始まり、36、30、24、18、…と1回の足し算ずつ進みます。
1988年にJuan Pinedaが並列ハードウェア向けに提案したのが正に此の形です。各ピクセルの判定は互いに独立なので、との異なる倍数を足せば、多くのピクセルを同時に判定出来ます。
共有する辺でのトップレフトルール
第6章ではトップレフトルールを述べ、1つの三角形に適用しました。
本当の役目は三角形同士の間にあるので、肝心な所で試しましょう:からまでの6 × 6の正方形を対角線で(右下)と(左上)に分けます。どちらも反時計回りです。
角はわざとピクセルの中心に置いてあります。各辺に7つ、対角線に7つの中心が乗る、最悪のケースです。
メッシュでは全ての辺の反対側は隣の三角形の物なので、正しい規則なら正方形に丁度面積分の36ピクセルを与える筈です。
辺を分類します(yは上向き、反時計回り:左辺は下向きに進み、上辺は水平で左向きに進みます):
- :P→Qは右向き(下辺、該当せず)、Q→Rは上向き(右辺、該当せず)、R→Pは下向き(左辺、該当)。
- :P→Rは上向き(右辺、該当せず)、R→Sは水平で左向き(上辺、該当)、S→Pは下向き(左辺、該当)。
共有する対角線はの左辺での右辺なので、其の7つの中心はの物になり、両方の物には決してなりません。
正方形の左側と上側は自分の物、右側と下側は隣に任せます。
角さえ正しく決まります。S(左上の角)はの自分の物である2辺の上にあるので描かれ、P、Q、Rはどれも自分の物でない辺に少なくとも1つ乗っているので、隣の正方形に任されます。
数(下のプログラムと図のスクリプトによる):
- 全て「≥ 0」:各三角形が28の中心を取り、36ピクセルの正方形に56フラグメント。対角線の7ピクセルは2回描かれます(半透明では間違いで、仕事も無駄)。
- 全て「> 0」:10 + 10 = 20フラグメント。対角線の内部の5つの中心は誰の物でもなく(隙間)、メッシュの中では正方形の辺上の24の中心も隙間になります。
- トップレフト:15 + 21 = 36で、全てのピクセルが丁度1回。
Vulkanの仕様が求めるのは、2つのポリゴンが共有する辺の上のサンプルが其の丁度一方に覆われる事だけで、OpenGLも同じ事を言っています。Direct3Dは其れを実現する方法としてトップレフトルールを定めています。
もう1つ細かい点として、Direct3Dは此の規則をyが下向きの自分の画面空間で定義しており、其処では「上」はyが一番小さい事を意味します。
此処や第6章の様にyが上向きなら、同じ規則は上の通りになります。
サブピクセル精度と固定小数点へのスナップ
トップレフトルールは、かどうかを正確に判定出来る事と、共有する辺の2つの三角形が丁度反対の値を見る事に依存しています。
浮動小数点ではどちらも保証されません。はからを引き、はを引くので、積の丸め方が違います。
32ビットfloatでの実例です:からへの辺と、ピクセルの中心。
の正確な値は約です(中心は辺の僅かに右にあり、隣だけが描くべきです)。ところがfloat32ではは、はになります。
両方の三角形が正の値を見て、両方が其のピクセルを描きます。
其の為、ラスタライザーは此処でfloatを使いません。
ビューポート変換の後、各頂点の位置は固定小数点の格子にスナップされ、エッジ関数は正確な整数で評価されます。
Direct3Dは16.8固定小数点(小数部8ビット、位置は1/256ピクセル刻みで最も近い値に丸める)を要求し、Vulkanはデバイスの小数部のビット数をsubPixelPrecisionBits(4以上)で報告します。
計算例。 pxの頂点は、つまり pxになります。動いたのは pxで、1/256への丸めの最大誤差 pxより小さいです。
整数は大きくなります(エッジ関数は座標の差を2つ掛けるので、4096ピクセルと小数部8ビットなら積はまで)。ガードバンドが無限に出来ない理由の1つです。
スナップには目に見える副作用もあります。
小さな三角形は面積0に潰れたり(其の時は捨てられます)、回る向きが反転したりする事さえあります。細長い三角形は少しずれます。そして各頂点は1回だけスナップされるので、同じ頂点は常に同じ格子点に来ます。此れが共有する辺に隙間を作らない鍵です。
整数を使うと、トップレフトルールは定数1つになります。辺がトップレフトでなければ其のから1(格子の1単位)を引き、3辺全てで単純にを判定すれば良いのです。
スナップとインクリメンタルな評価を備えた其のラスタライザーで、正方形の例を描きます:
#include <cmath>
#include <cstdint>
#include <cstdio>
#include "linalg.hh"
const int SUB = 256; // 8 fractional bits: positions in 1/256 pixel
struct IPoint {
int64_t x, y;
};
IPoint snap(Vec2 p) { return {std::llround(p.x * SUB), std::llround(p.y * SUB)}; }
// Edge function as a linear function E(x, y) = A x + B y + C (y up, counter-clockwise inside)
struct Edge {
int64_t A, B, C;
};
Edge makeEdge(IPoint a, IPoint b) {
Edge e{a.y - b.y, b.x - a.x, a.x * b.y - a.y * b.x};
// top-left rule: a left edge goes down, a top edge is horizontal and goes left.
// Other edges must not own centers with E == 0, so we subtract 1 (one unit of SUB * SUB).
bool topLeft = b.y < a.y || (b.y == a.y && b.x < a.x);
if (!topLeft) e.C -= 1;
return e;
}
int rasterize(Vec2 v0, Vec2 v1, Vec2 v2, char mark, char grid[8][10]) {
IPoint a = snap(v0), b = snap(v1), c = snap(v2);
Edge e[3] = {makeEdge(b, c), makeEdge(c, a), makeEdge(a, b)};
int count = 0;
for (int y = 0; y < 8; ++y) {
// value at the first pixel center of the row, then add A per pixel (incremental evaluation)
int64_t px = SUB / 2, py = y * SUB + SUB / 2;
int64_t w[3];
for (int k = 0; k < 3; ++k) w[k] = e[k].A * px + e[k].B * py + e[k].C;
for (int x = 0; x < 10; ++x) {
if (w[0] >= 0 && w[1] >= 0 && w[2] >= 0) {
grid[y][x] = mark;
++count;
}
for (int k = 0; k < 3; ++k) w[k] += e[k].A * SUB;
}
}
return count;
}
int main() {
char grid[8][10];
for (auto &row : grid)
for (char &ch : row) ch = '.';
Vec2 p{1.5f, 0.5f}, q{7.5f, 0.5f}, r{7.5f, 6.5f}, s{1.5f, 6.5f};
int n1 = rasterize(p, q, r, '1', grid); // lower-right triangle
int n2 = rasterize(p, r, s, '2', grid); // upper-left triangle, shares the diagonal p-r
for (int y = 7; y >= 0; --y) printf("%.10s\n", grid[y]);
printf("triangle 1: %d, triangle 2: %d, total %d\n", n1, n2, n1 + n2);
return 0;
}
..........
.222222...
.222221...
.222211...
.222111...
.221111...
.211111...
..........
triangle 1: 15, triangle 2: 21, total 36
走査:バウンディングボックスとタイル
ラスタライザーはどのピクセルを判定するのでしょうか?
一番単純な答えは、第6章の様に三角形のバウンディングボックス内の全てのピクセルの中心です。
太った三角形なら其れで良いのですが、細長い三角形では大損です。三角形、、の面積はピクセルで、バウンディングボックスはなので、判定の10回に9回は無駄です。
其の為GPUは画面を、例えば4 × 4や8 × 8ピクセルのタイル単位で辿り、先ずタイル全体を各辺に対して判定します。
エッジ関数は線型なので、長方形の上での最大値と最小値は角の2つで取ります(どの2つかはとの符号だけで決まります)。
タイルごとに3つの場合があります:
- 自明な棄却:ある辺が4つの角全てで負。タイルのどのピクセルも内側にはあり得ないので飛ばします。
- 自明な受理:3辺全てが4つの角全てで負でない。全てのピクセルが内側なので、ピクセルごとの判定無しにタイルを塗ります。
- 部分:其れ以外。タイルの各ピクセルを今まで通り判定します。
計算例。 4 × 4のタイル(60個)に分けた40 × 24ピクセルの画面と、面積382ピクセルの三角形、、:
20個のタイルは角の判定1回ずつで棄却され、12個のタイル(192ピクセル)は1ピクセルも判定せずに塗られ、ピクセルごとの判定が必要なのは28個の部分タイルだけです。判定は回で、バウンディングボックスなら回です。
覆われたピクセルの中心は381個で、当然ながら面積382にとても近くなります。
実際のハードウェアは此れを階層的に行い(大きなタイル、次に小さなタイル)、タイルのピクセルを並列に判定します。
タイルベースのGPU(モバイルの殆ど、第17章)は更に一歩進んで、ラスタライズの前に全ての三角形を画面のタイルに振り分け、各タイルの色と深度を高速なオンチップメモリに置いた儘にします。
重心座標と属性の平面
エッジ関数を面積の2倍で割ると重心座標になります(6.9節)。エッジ関数はとについて線型なので、重心座標も、其れで補間する全ての属性も線型です。
三角形の上で線型に補間した値は平面で、其の2つの傾きはエッジ関数の係数から直接求まります。
計算例。 第6章の三角形に深度、、を与えます。
上の係数を48で割ると、、の傾きになります。xについては、、、yについては、、です。
従って:
ピクセルで確かめます。のから始めて、第6章の重みではです。
此の2つの傾きこそ、19.4節のポリゴンオフセットが使う物です。此の三角形ではです。
ハードウェアは三角形と属性ごとに1回此の様な平面を組み立て、後は足し算だけをします。
パースペクティブコレクト補間の導出
第6章と第11章では規則だけを述べました:とを画面上で線型に補間し、割る。
其の理由は次の通りです。
三角形のクリップ空間の頂点を、とします。
3次元の三角形上の点は、3次元の重心座標の重み(負でなく、)による組み合わせです。属性は3次元の三角形の上で線型に変わるので、此の重みこそが属性を正しく補間します:。そして行列は線型なので、此の重みはカメラ空間でもクリップ空間でも同じです。
点の画面上の位置は、クリップ空間の位置を其の、で割った物です:
一方ラスタライザーが測るのは画面上の重みで、投影された角を使ってです:
2つを比べるとで、yについても同じです。
重みの和はで、同じ画面上の点を再現します。そして(退化していない)三角形の重心座標は一意なので:
此処でを使うと、つまり:
は画面上で線型です。
そして属性は:
此れが規則です。画面上で線型な2つの量と、そしてピクセルごとに1回の割り算です。
深度の問題も此れで片付きます。保存される深度は(19.1節、)で、のアフィン関数なので、此れも画面上で線型で、補正無しに普通ので補間されます。
リバースの無限遠投影では、保存される深度は其の物、つまり透視補正が補間する量其の物です。
一方、ビュー空間の距離自体は画面上で線型ではありません。
(HeckbertとMoretonは1991年にテクスチャマッピングのハードウェアの為に同じ結果を導いています。)
計算例。 三角形の角のが、、で、テクスチャ座標が、、です。
あるピクセルの画面上の重みはです。
素朴な補間では、になります。
パースペクティブコレクトでは:
3次元の重みはです。画面を広く占める手前の角が、此の点の半分ではなく、本当はを持っているのです。
確認:で、同じです。
素朴な方法だとどう見えるでしょうか?
6 × 6の市松模様を貼った正方形の床のタイルを2つの三角形(赤い線が共有する対角線)で描き、角は手前が、奥がです:
左では、各三角形が画面に平らに置かれているかの様にテクスチャが貼られます。マス目の列は奥に行っても小さくならず、2つの三角形の歪み方が違うので、線が対角線で折れ曲がります。初代PlayStationで有名な揺れです。
右では列が奥に向かって縮み、テクスチャの全ての線が真っ直ぐな儘です。射影的な写像では直線は直線でなければならないからです。
GPUは全てのvarying変数を標準でパースペクティブコレクトに補間しますが、シェーディング言語では選ぶ事も出来ます。
noperspectiveは画面上で線型に補間し(ワイヤーフレーム描画の為の三角形の辺までの距離の様な、画面空間の量に便利です)、GLSLのflatやSlang/HLSLのnointerpolationは1つの頂点の値を其の儘渡します(マテリアルIDの様な整数はflatでなければなりません):
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0; // perspective-correct (the default)
noperspective float2 screenUV : TEXCOORD1; // linear on the screen
nointerpolation uint materialId : TEXCOORD2; // one vertex's value, not interpolated
};layout(location = 0) out vec2 uv; // perspective-correct (the default)
layout(location = 1) noperspective out vec2 screenUV; // linear on the screen
layout(location = 2) flat out uint materialId; // one vertex's value, not interpolatedstruct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0; // perspective-correct (the default)
noperspective float2 screenUV : TEXCOORD1; // linear on the screen
nointerpolation uint materialId : TEXCOORD2; // one vertex's value, not interpolated
};微分と2×2クアッド
第11章の11.7節では、テクスチャ座標の微分等からミップマップのレベルを選び、GPUは隣り合うピクセルを比べて其れを求めると述べました。
其の仕組みはこうです。
ラスタライザーはピクセルを1つだけシェーディングする事はありません。フラグメントを2 × 2のクアッドで出し、クアッドの4つのフラグメントシェーダーの起動は一緒に実行されます。
ddx(v)(Slang、HLSL)やdFdx(v)(GLSL)は、単にクアッドの右と左のピクセルのの差で、ddy/dFdyは2つの行の差です。
クアッドの一部しか三角形に覆われていない時も、覆われていないピクセルはヘルパー起動としてシェーダーを実行します。其の結果は捨てられますが、其の値が差の計算に使われます。
計算例。 床(カメラは其の1 m上)、800 × 600の画面(アスペクト比4/3)で縦の視野角90°、1メートルあたり100テクセルのテクスチャです。
(上から数えた)ピクセルの行はで、其の下の床の点は距離にあり、テクセル単位のテクスチャ座標はとです(19.2節を逆に辿ります)。
行330と331のピクセル400と401のクアッドでは:
| ピクセル | (m) | (テクセル) | (テクセル) |
|---|---|---|---|
| (400, 330) | 9.836 | 1.639 | 983.61 |
| (401, 330) | 9.836 | 4.918 | 983.61 |
| (400, 331) | 9.524 | 1.587 | 952.38 |
| (401, 331) | 9.524 | 4.762 | 952.38 |
従って、1ピクセルあたりテクセル、テクセルです。
第11章の式で、なので、殆ど丁度ミップレベル5です。
フットプリントは幅の約倍の長さなので、異方性フィルタリングなら代わりにレベルから約10個のサンプル(16倍の異方性なら可能)を取ります。
ポリゴンオフセットの例と同じ数値です。此処では1行のピクセルが床の mを覆います。
知っておくべき結果が2つあります。
小さな三角形は高く付きます:1ピクセルの三角形でも4つの起動のクアッドが必要で、其の内3つはヘルパーです(効率25%)。LOD(詳細度)が使われる理由の1つであり、GPU駆動のレンダラーの中に小さな三角形をソフトウェアでラスタライズする物がある理由でもあります(第29章)。
そして微分には4つの起動全てが必要なので、レベルを自動で選ぶテクスチャの参照は、クアッドの一部のピクセルだけが進む分岐の中に置くべきではありません。
アーリーZと階層Z
アーリーZは、結果が変わり得ない時に深度テストをシェーダーの前に行います。シェーダーが自分で深度を書かず、フラグメントを捨てず、他の副作用も無い場合(又はGLSLのlayout(early_fragment_tests)やSlang/HLSLの[earlydepthstencil]で、プログラムが問題無いと約束した場合)です。
19.4節のフラグメントごとの対数深度の様に、フラグメントシェーダーで深度を書くと実際に性能の代償があるのは其の為です。
階層Z(Greene、Kass、Miller、1993年)は、タイル全体を一度に棄却します。
GPUは深度バッファと並べて、タイルごとに其の中に保存されている一番遠い深度(通常のZでは最大値)を持っておきます。
タイルの中で三角形のピクセルをラスタライズする前に、タイル上での三角形の一番近い深度、つまりタイルの角での深度の平面の最小値を、其の値と比べます。
計算例。 8 × 8のタイルの一番遠い保存値が0.6で、ある三角形の深度の平面はタイルの角で0.70、0.71、0.72、0.73です。
タイル内で一番近い点の0.70が既にある全ての物より後ろなので、64個のフラグメントは全て深度テストに落ち、1回の比較で其の全てを棄却出来ます。
リバースZでは「一番遠い」は最小値になり、比較も反転します。
シーンを大まかに手前から奥へ描くか、デプスプリパス(第10章)を使う事で、此の2つの早期のテストが効果を発揮します。
検定のポイント
此の章はシラバスの範囲を超えますが、分野2(投影、ビューイングパイプライン、描画パイプライン)と分野4(隠面消去、マッピング)の幾つかの項目の理由を説明しています:
- 透視投影で保存される深度は距離に対して線型ではなく、に従うので、精度はニア平面の近くに集中します(第5章、第10章)。ニア平面を遠ざけるのが一番効きます。
- 投影後の深度は画面上では線型です。其の為、Zバッファ法は画面空間の(重心座標の)重みで深度を補間出来ます。
- クリッピングは同次座標のクリップ座標で、透視除算の前に行います。視点の後ろの点はだからです(第6章)。
- テクスチャ座標等の属性にはパースペクティブコレクト補間が必要です:とを補間し、割ります(第6章、第11章)。
- ミップマップのレベルは、テクスチャ座標の画面空間での微分から選びます(第11章)。
- シャドウアクネや同一平面上の面のZファイティングは、傾きに比例する部分を持つデプスバイアスで直します(第11章)。
19.6 まとめ
- 透視投影行列は距離をに入れます。xとyの行は視錐台のニア平面の長方形を±1に写し(非対称な視錐台ではzの項が加わる)、zの行はにしかなれないので、深度は常にのアフィン関数です。
- 1つの式がOpenGL(−1〜1)、Vulkan/Direct3D(0〜1)、リバースZ(1〜0)を全て扱います。とすると無限遠の投影になり、リバースの無限遠版は丁度を保存します。
- 逆投影は深度からカメラ空間の位置を取り戻します。を掛けてで割るか、距離にピクセルのレイを掛けます。
- クリッピングは除算の前にクリップ空間で行います。新しい頂点の属性はクリップ空間での線型補間で求め、其れは正確です。GPUはガードバンドで側面のクリッピングを避け、ニアとファーのクリッピングの代わりに深度をクランプする事も出来ます。
- 固定小数点の深度は1段が一様、floatの深度は1段の相対的な大きさが一様です。通常のZはfloatの精度を無駄にし、floatのバッファでリバースZを使うと、距離の1段はどこでも距離の約〜倍になります。
- 対数深度も相対精度を与えますが、画面上の線型性かアーリーZを壊します。デプスバイアスは同一平面上の面を分け、シャドウアクネを防ぎます。
- エッジ関数は線型でインクリメンタルに評価されます。トップレフトルールは共有する辺上の全てのピクセルを丁度1つの三角形に与えます。サブピクセルの格子にスナップした後の整数演算が其れを正確にします。
- タイルを使うと多くのピクセルを一度に自明に棄却・受理出来ます。重心座標によって全ての属性は三角形上の平面になります。
- は画面上で線型です。属性はをで割った物です。深度に補正は要りません。
- フラグメントはミップマップの為の微分を得る為に2 × 2のクアッドでシェーディングされます。アーリーZと階層Zは隠れたフラグメントをシェーディングの前に棄却します。
19.7 用語
| English | 日本語 |
|---|---|
| projection matrix | 投影行列 |
| off-center (asymmetric) frustum | 非対称な視錐台 |
| depth range, depth convention | 深度範囲 |
| reversed Z | リバースZ |
| infinite far plane | 無限遠のファー平面 |
| inverse projection | 逆投影 |
| depth reconstruction | 深度からの位置の復元 |
| clip coordinates | クリップ座標系 |
| homogeneous clipping | 同次座標でのクリッピング |
| near-plane clipping | ニア平面でのクリッピング |
| guard band | ガードバンド |
| depth clamping | 深度クランプ |
| depth precision | 深度精度 |
| fixed-point (UNORM) depth format | 固定小数点(UNORM)の深度フォーマット |
| floating-point depth format | 浮動小数点の深度フォーマット |
| logarithmic depth | 対数深度 |
| z-fighting | Zファイティング |
| depth bias, polygon offset | デプスバイアス、ポリゴンオフセット |
| slope-scaled depth bias | スロープスケールのデプスバイアス |
| decal | デカール |
| edge function | エッジ関数 |
| incremental evaluation | インクリメンタルな評価 |
| top-left rule | トップレフトルール |
| sub-pixel precision | サブピクセル精度 |
| fixed-point snapping | 固定小数点へのスナップ |
| bounding box traversal | バウンディングボックスの走査 |
| tiled traversal | タイル単位の走査 |
| trivial reject, trivial accept | 自明な棄却、自明な受理 |
| barycentric coordinates | 重心座標 |
| perspective-correct interpolation | パースペクティブコレクト補間 |
| helper invocation | ヘルパー起動(ヘルパーインボケーション) |
| 2×2 quad | 2×2クアッド |
| screen-space derivatives | スクリーン空間の微分 |
| early-Z | アーリーZ |
| hierarchical Z | 階層Z(Hi-Z) |
19.8 課題
- ★ 19.1節のzの行の一般式を使い、、のOpenGL、Vulkan、リバースの規約でのと、及び其々で距離2の点の0〜1の保存される深度を求めて下さい。(確認:リバースの値は1からVulkanの値を引いた物になり、OpenGLの値をで変換した物はVulkanの値と等しくなります。)
- ★ Vulkanのレンダラーが800 × 600のウィンドウ(yは下向き)で、90°、アスペクト比4/3、、の
perspectiveVkを使っています。ウィンドウの位置で深度バッファの値は0.9です。19.2節の近道でカメラ空間の位置を復元し、もう一度投影して確かめて下さい。 - ★★ カメラ座標が、、の三角形を、19.3節のOpenGLの行列(、)でニア平面に対してクリッピングして下さい。結果の頂点は幾つで、新しい頂点はカメラ空間とNDCの何処にありますか?
- ★★ m、 mで、通常のZの24ビットUNORMバッファとリバースZのfloat32バッファについて、50 mと400 mでの距離の1段を見積もって下さい。ニア平面を0.05 mに動かすと、其々はどれだけ変わりますか?
- ★★ 辺が、の頂点から、の頂点まで伸びています。画面上の中点でのはいくつで、画面上の距離のどの割合でになりますか?画面上の位置に対するのグラフを描いて下さい。
- ★★★ 4つの三角形がピクセルの中心である頂点を共有し、からまでの正方形の中で扇形を作っています(正方形の辺ごとに三角形1つ)。トップレフトルール(yは上向き、反時計回り)で、4つの内丁度1つが中心を覆う事を示し、其れがどれかを求め、正方形の全てのピクセルを数えて下さい。辺ごとに適用するトップレフトルールが、共有する頂点も正しく扱えるのは何故ですか?
選択問題
問1 ★ 次のうち、ビューイングパイプラインにおけるクリッピングについて正しい物はどれか。
| あ | 透視除算の後は視体積が単純な立方体になるので、クリッピングは透視除算の後に行う。 |
| い | カメラの後ろにある頂点はクリップ座標のwが正なので、クリップの判定を通過する。 |
| う | OpenGLの規約では、ニア平面の判定はである。 |
| え | ニア平面上に作られた新しい頂点の属性はクリップ空間での線型補間で求め、クリップ座標はカメラ座標の線型な関数なので此れは正しい。 |
| お | ガードバンドがあれば、GPUはニア平面に対してクリッピングする必要が全く無い。 |
問2 ★★ ニア平面0.1 m、ファー平面1000 mの透視投影を使う。次のうち、深度精度について正しい物はどれか。
| あ | 32ビット浮動小数点の深度バッファとリバースZ(ニアが1、ファーが0)では、保存される深度の1段に当たる距離はおおよそ距離に比例する。 |
| い | 通常の対応(ニアが0、ファーが1)では、32ビットfloatのバッファは遠くで24ビット固定小数点のバッファより約1000倍精度が高い。 |
| う | ニア平面を0.1 mから0.01 mへとカメラに近付けると、24ビット固定小数点のバッファは遠くでより精度が高くなる。 |
| え | リバースZは、OpenGLの標準の−1から1の深度範囲でも同じ様に効果がある。 |
| お | 24ビット固定小数点のバッファでは、1段に当たる距離はどの距離でも同じである。 |
問3 ★★ 三角形の辺が、でテクスチャ座標の頂点から、での頂点まで伸びている。画面上で此の辺の中点における、パースペクティブコレクトなの値はどれか。
| あ | 0.5 |
| い | 0.2 |
| う | 0.8 |
| え | 0.625 |
| お | 0.4 |
19.9 実践課題
リバースの無限遠投影を使い、1つの三角形にクリッピング、ラスタライズ、パースペクティブコレクト補間、深度からの復元を全て手で施します。
16 × 16の格子の方眼紙(1ピクセル1 cmが楽です)、定規、鉛筆、2色のペン、電卓を用意して下さい。
投影は、19.1節のリバースの無限遠の行列で、視野角90°、アスペクト比1、です。OpenGLのyが上向きのウィンドウで、深度範囲は0〜1にして使います(glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE)の設定です)。
行列の行は、、、なので、、です。
クリップの判定はとです。
ウィンドウは原点が左下の16 × 16ピクセルで、、、保存される深度はです。
三角形のカメラ空間の頂点は、、で、テクスチャ座標は、、です。
- 投影する。 A、B、Cのクリップ座標を求め、クリップの判定をして下さい。どの頂点がどの平面で落ちますか?又、其の頂点を其ので割る事がどの道出来ないのは何故ですか?
- クリッピングする。 ニア平面の符号付きの値はです。となるAC上の点とBC上の点を、クリップ座標とと共に求めて下さい。クリッピング後の多角形をA、B、、の順に書きます。
- ウィンドウへ。 4つの頂点を割ってビューポート変換し、其々のウィンドウ上の位置と保存される深度を書いて下さい。方眼紙に四角形を描き、に沿ってとに分けます。
- ラスタライズする。 中心がの中にあるピクセルを1色目、の中にあるピクセルを2色目のペンで塗って下さい。中心が辺に近い時はエッジ関数を使い、丁度辺の上にあればトップレフトルールを使います。ピクセルを数え、靴紐の公式で求めた四角形の面積と比べて下さい。
- 補間する。 中心がのピクセルについて、のエッジ関数と画面上の重みを求めて下さい。次にを素朴な方法とパースペクティブコレクトな方法で求め、補間したと補間した保存される深度も求めます。
- 復元する。 ピクセルのNDCの位置と保存される深度から、カメラ空間の位置を求めて下さい(19.2節:此処では距離はです)。3次元の重みを求め、其れからを計算します。
自分で確認する方法:
- 段階2:新しい2つの点はどちらもカメラのzがで、はAとCの丁度真ん中にあります。
- 段階3:4つのウィンドウ上の位置は全て格子内の整数です。との深度は1(ニア平面上にある)で、全ての深度はと等しくなります。
- 段階4:塗ったピクセルの数は面積(95ピクセル)と数ピクセルしか違わず、両方の色を持つピクセルはありません。
- 段階5:3つのの和は1です。此の投影では深度が其の物なので、補間した深度は補間したと丁度等しくなります。
- 段階6:復元した点は三角形ABCの平面の方程式を満たします。の和は1で、は段階5のパースペクティブコレクトなと等しく、素朴な方は違います。
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。