座標変換まとめてみた

DirectX11で板きれを表示するプログラムを作るぞ〜」と意気込んでみたものの、例によって大ハマリしてしまって何が悪いのか分からなくなったので、自分用に"そもそも3Dモデルを表示するまでの座標変換ってどうなってるのか"をまとめ。

ワールド変換までは分かるんだけど、ビュー変換、透視投影変換、ビューポートとくるとややこしいのでとりあえずビュー変換以降を整理してみる。
前提条件はこんな感じで。

  • 板きれを画面いっぱいに表示する
  • 板きれは三角形2つ(4頂点)で構成する
  • 画面サイズは1920x1080
  • カメラは板きれの真正面に置く(斜めにするとややこしいので)

まず、板きれは縦18.0、横32.0、中心位置は(17, 9, 0, 1)に設定。

+ 1.000000 + 0.000000 +0.000000 +1.000000
+ 1.000000 +18.000000 +0.000000 +1.000000
+33.000000 + 0.000000 +0.000000 +1.000000
+33.000000 +18.000000 +0.000000 +1.000000

板きれに合わせる形でカメラは、注視点(17, 9, 0, 1)、カメラ位置(17, 9, 13.5, 1)に設定。
モデルとカメラの距離は"モデルの高さの半分に対して1.5倍"に設定。1.5倍にした根拠は特になし。小数点誤差が出にくい数値なら他の数値でもOK。



この設定でD3DXMatrixLookAtLH()を呼ぶと変換行列はこんな感じ。

view matrix
+ 1.000000 +0.000000 + 0.000000 +0.000000
+ 0.000000 +1.000000 + 0.000000 +0.000000
+ 0.000000 +0.000000 + 1.000000 +0.000000
-16.999998 -8.999999 +13.500000 +1.000000

変換後の板きれはこっち。

view
-15.999999 -8.999999 +13.500000 +1.000000
-15.999999 +8.999999 +13.500000 +1.000000
+15.999999 -8.999999 +13.500000 +1.000000
+15.999999 +8.999999 +13.500000 +1.000000

ビュー変換は"カメラを中心とした座標系"に変換ってことで、変換行列はカメラ位置と逆向きに移動させる成分が入っているっぽい。(第4行目の(-17, -9, +13.5)の部分。小数点誤差がすでに出ているけど、とりあえず無視(^^;)
今回は板きれもカメラもワールド座標のX-Y平面に向いているので回転する必要はなしってことでビュー変換行列にも回転成分はなし(左上の3x3部分行列が単位行列になっている)。たぶん、回転が必要な場合は3x3部分行列が回転行列になるはず。このあたりの話はDirectXヘルプのD3DXMatrixLookAtLH()の項目に変換行列の計算式が書いてあるので、そっちを解析すれば分かるのかもしれない。が、個人的にイミフだったのでスルー…orz
一応変換後の板きれの座標値を見ると、X軸が-16〜+16、Y軸が-9〜+9ってことで、ちょうど板きれのど真ん中にカメラの中心が向いてて、Z軸はちょうど板きれとカメラの距離になってて思った通りになってる。



次の透視投影変換で死にそうになった…
D3DXMatrixPerspectiveFovLH()に視野角67.38°、アスペクト比16/9、near planeを1、far planeを65にした場合の変換行列がこれ。

projection matrix
+0.843750 +0.000000 +0.000000 +0.000000
+0.000000 +1.500000 +0.000000 +0.000000
+0.000000 +0.000000 +1.015625 +1.000000
+0.000000 +0.000000 -1.015625 +0.000000

変換後の板きれはこれ。

projection
-13.499999 -13.499998 +12.695312 +13.500000
-13.499999 +13.499998 +12.695312 +13.500000
+13.499999 -13.499998 +12.695312 +13.500000
+13.499999 +13.499998 +12.695312 +13.500000

nearとfarは適当に決めただけで、カメラと板きれの距離13.5が視錐台の範囲に収まっていればいくつでも大丈夫なはず。アスペクト比は固定だから気にしないとして、問題は視野角。これを調整してちょうど画面全体に板きれが表示されるようにする。

ここでちょうど良い視野角を計算する前に変換行列がどういう意味を持っているのか整理してみる。
DirectXヘルプによると変換行列はこんな感じ

xScale     0          0               0
0        yScale       0               0
0          0       zf/(zf-zn)         1
0          0       -zn*zf/(zf-zn)     0
ただし、
yScale = cot(fovY/2)
xScale = yScale / aspect ratio

ってことで、この変換行列を(x, y, z, w)と掛け算してみると(x*xScale, y*yScale, 省略, z*1)になる。この変換で初めてW値が1以外の値になってここからは同次座標*1に移行する。Wの値は結局zなので、これ以降の板きれモデルの座標値は全部z(ビュー空間でのZ座標値)で割った値が同次座標じゃない我々が本当に知りたい値になる。ので、XとYもScale/z倍の値になる。
で、XとYの差はアスペクト比の調整だけなので、yScaleが何なのかを考えてみる。分かりづらいので図を起こしてみた。図は視野角の1/2を「θhalf」、カメラとモデルの距離(ビュー空間でのZ値)を「Zdis」、距離Zdisにおいてカメラが写しだせる一番上までの高さを「Hhalf」として描いた。


この図を見ながら考える。まず、Y値はビュー座標のyScale/z==cot(fovY/2)/z==cot(θhalf)/Zdis倍になる。で、このcot()/zが何かというとtan(θ)の式を変形すると1/Hhalfってことになる。つまりY値は1/Hhalf倍の値になる。どういうことかと言うと、ビュー座標でY値がちょうどHhalfのときにHhalf*1/Hhalf==1.0になる。カメラは上半分だけじゃなくて下側も映し出すのでY値は-1.0〜+1.0の範囲に縮小されることになる。縮小率は視野角で決まる値なので、モデルの全頂点は同じ倍率で縮小される(形は崩れない)。アスペクト比がかかっているだけで、X値も同じく-1.0〜+1.0の範囲に縮小される。もちろんアスペクト比を間違えていなければ(^^;
今回の板きれはあらかじめアスペクト比を考慮して16:9の板きれにしていたので、ちゃんと透視投影変換後の座標値が1:1になってる。
あと、Z値がややこしくて良く分からない。Zf*(z-Zn)/(Zf-Zn)倍になるけど、なんだこれ…Excelでいろんなzをいれて曲線を描いてみたところ、z==Znのときに0.0、z==Zfのときに1.0になって、中間は非線形な感じで徐々に1.0に近付いていくっぽい感じに。なんで非線形なのか良く分からないけど、ビュー座標のz値を0.0〜1.0にマッピングしているのは分かったので良しとする(^^;
このあたりはどこかの書籍に書いてあったような気もするけど、今すぐ見つけられないのでスルー…
参考までにZのグラフをぺたり。



ということで、DirectXヘルプの「Direct3D 9」−「Programming Guide」−「Getting Started」−「Transforms」−「Projection Transform」に書いてある通り、カメラから見た視錐台の領域がちょうど(-1.0〜1.0, -1.0〜1.0, 0.0〜1.0)におさまるような変換をかける行列ってことが分かった。(ただし、X、Yについては形が維持されるがZは非線形と思われる変換がかかる)
ちなみに同次座標のところで、zで割るって話があったので、XとYはカメラからの距離に応じて縮むようにできている。うまくできてるもんだねぇ。

で、話を視野角に戻すと、さっきの図から考えて画面全体に写るってことはY値がちょうどHhalf値と等しいときなので、カメラと板きれの距離がZdisのときに板きれの高さの半分がHhalfとなるようなθhalfを求めればよいってことになる。今回の場合、ビュー変換後のYはHhalf=9.0で、ZはZdis=13.5なのでθhalf=arctan(Hhalf/Zdis)=arctan(9.0/13.5)で求まる。(計算式はtanの式を変形しただけ)
視野角自体はカメラの上下をあわせるので、θhalfの2倍ってことでarctan(9/13.5)*2==67.38°になる。


最後がビューポート変換。変換行列がこれ。

Viewport matrix
+960.000000 +  0.000000 +0.000000 +0.000000
+  0.000000 -540.000000 +0.000000 +0.000000
+  0.000000 +  0.000000 +1.000000 +0.000000
+960.000000 +540.000000 +0.000000 +1.000000

変換後の板きれがこっち。

Viewport
+    0.000977 +14579.999023 +12.695312 +13.500000
+    0.000977 +    0.000977 +12.695312 +13.500000
+25920.000000 +14579.999023 +12.695312 +13.500000
+25920.000000 +    0.000977 +12.695312 +13.500000

同次座標になってるので分かりづらいけど、ちゃんと1920x1080になってる。(微妙に誤差があるけど)
DirectXヘルプの「Direct3D 9」−「Programming Guide」−「Getting Started」−「Viewports and Clipping」によると変換行列はこんな算出らしい。

Width/2      0            0         0
0            -Height/2    0         0
0            0            MaxZ-MinZ 0
Left+Width/2 Height/2+Top MinZ      1


通常はMaxZ=1.0、MinZ=0.0固定っぽいので、右下の2x2部分行列は単位行列になる。なんか、強制的に一番前or一番後ろに書き込みたいときだけMaxZ=MinZ=0.0とかMaxZ=MinZ=1.0とか設定するらしい。
変換行列をみるとX値は横幅の1/2倍してから左端位置から横幅の半分だけずれた位置に移動ってことで、-1.0の場合は-1.0*960+0+960ってことでちょうど0.0になるし、+1.0の場合は+1.0*960+0+960ってことで1920になるのか。透視投影変換は中心が0.0だからビューポート変換で画面の中心位置までずらして、さらに画面サイズに合わせて拡大してる感じか。
これでレンダーターゲット(通常はバックバッファ)のどこにPixelを打てばよいのかが決まるわけか。なんとなくわかった気になってきた。

*1:本当はw=1でもwが入っていれば同次座標だけど、なんて言って区別すれば良いのか知らないので…