3D座標変換

自分用メモ。

3Dの座標変換はワールド変換→ビュー変換→射影変換→ビューポート変換の順でおこなう。
ワールド変換:オブジェクトデータの頂点群を、ワールド内の姿勢にあわせて回転し、ワールド内の位置に移動させる。
ビュー変換:カメラから見た風景になるよう、カメラの向き・位置にあわせて回転・移動させる。
射影変換:難しいのはここ。
やっていることはおおきくわけて3つ。
頂点座標を-1〜1(zは0〜1)になるようスケーリング、奥のほうが真中に寄るような計算、それからクリッピング用のデータ作成。
ビュー変換でxとyは画面の真中が原点(0,0)になるように移動されている。
頂点[x,y,z,1]をスケーリングさせるだけの計算なら、xをスクリーンの幅の半分で割り、yを高さの半分で割る。

[ 2 / width,          0, 0, 0
          0, 2 / height, 0, 0
          0,          0, 1, 0
          0,          0, 0, 1]

という行列をかけることになる。
クリッピングは計算結果の頂点の4つ目のデータ、w(この例では1固定)を使う。
上記の行列をかけると、xとyはそれぞれ-1〜1になっていて、wは1だから、-w <= x <= w、-w <= y <= wという条件で画面内に入っていると考える。
この時点ではzでのクリッピングは考えていない。
zが奥向きに増加することを前提にすると、なんらかの計算値をzで割れば、奥のほうが真中に寄る。
DirectXではzの手前クリッピング距離を指定できて、それをznと表し、zn/zをかけることで真中に寄せる。

[ (2 * zn) / (width * z) ,                       0, 0, 0
                        0, (2 * zn) / (height * z), 0, 0
                        0,                       0, 1, 0
                        0,                       0, 0, 1]

xとyをzで割っているが、[x,y,z,1]にかける行列は固定値じゃないと困るので、この中にzを入れるわけにはいかない。
zで割らずにwをzにしてもクリッピング処理は同じ結果になる。

[ (2 * zn) / width ,                 0, 0, 0
                  0, (2 * zn) / height, 0, 0
                  0,                 0, 1, 1
                  0,                 0, 0, 0]

この場合、行列をかければクリッピングはできるが、xとyが-1〜1に収まらないので、あとでそれぞれwで割る必要がある。
最後に、zをクリッピング範囲 0 <= z <= wに収めるような計算を3列目に追加する。
変換後のzもw(変換前のzの値)で割られるから、それを考慮に入れる。
すなわち、znとzが同じなら0になって、奥のクリッピング距離zfとzが同じならzになるような式ということになる。
これを計算式にすると(z - zn) * zf / (zf - zn)。
行列の3列目の上から3つめはzにかけられる値だが、3列目の一番下はzがかけられずに足される値になる。
式を変形して、(z * zf) / (zf - zn) + (-zn * zf) / (zf - zn)とすると、左側はzをかけずに上から3つめに、右側はそのまま一番下に入れればよいことがわかる。
そうすると行列は、

[ (2 * zn) / width ,                 0,                    0, 0
                  0, (2 * zn) / height,                    0, 0
                  0,                 0,       zf / (zf - zn), 1
                  0,                 0, -zn * zf / (zf - zn), 0]

となる。
これがDirectXの射影変換行列である。
で、最後のビューポート変換は、射影変換してそれぞれwで割った[x', y', z', 1]の値にビューポート変換行列をかけてスクリーン座標にする計算。

[     width / 2,              0,           0, 0
              0,     height / 2,           0, 0
              0,              0, maxz - minz, 0
  x + width / 2, y + height / 2,        minz, 1]

ここで使っているx/y/width/heightはビューポート矩形で、画面のどこにどのサイズでスケーリングするかを決める。
y座標は3D座標空間だと上に向かって増えるからスクリーン座標とは反対で、座標をそのように管理していた場合は2列目の上から2つ目は-height/2となる。
たとえば(0,0,640,480)のビューポート矩形なら、この計算で-1〜1の範囲を0〜640、0〜480にすることができる。
zの計算は0〜1のzを好きな範囲に移動することができる。z座標によらず全部手前、とか、そういう用途に使うらしい。別空間として計算したものを同じ画面に重ねて描画するとかかな。
zはレンダリングするときにGPUが使う。