iFlash3Dさんの記事拙訳第三弾。
iFlash3D "Flash 3D, Molehill and The Perspective Divide"
透視投影のプロジェクション変換について。
いわゆる遠近法。この変換をするだけで俄然3Dっぽくなる。そんな話。
そもそもプロジェクション変換とはなにか?
まず3D世界のオブジェクトは、各頂点ベクトルに4*4の変換行列を掛けてていくことで2Dの座標に変換される。
この変換作業にはそれぞれ変換の意味を持った段階があり、一連の流れをジオメトリ(ビューイング)パイプラインという。
具体的には以下の変換を順番にしていく。
1.のワールド座標変換は、Flashではおなじみのローカル座標→グローバル座標の変換だ。各オブジェクトは座標を持ち、頂点はオブジェクトに決められた原点からの位置を示している。この頂点座標にオブジェクトの座標変換行列をかけるとグローバル座標になるという仕組みだ。オブジェクトが入れ子になっている場合も、さかのぼって親のローカル座標行列を掛けていくことでワールド座標になる。
2.のビュー座標変換では、1.で変換された原点からみたワールド座標をカメラから見た相対座標に変換する。一般的にはカメラオブジェクトを作り、普通のオブジェクトと同じように配置し、ビュー座標変換時にカメラの座標行列の逆行列を掛けることでビュー座標にするようだ。
ここまでは3D空間の話なのだが、ディスプレイに移す時には2次元のスクリーンに投影しなくはならない。3.のプロジェクション変換で3次元の座標を2次元に落とし込む。カメラからそのまま平行に切り出す平行投影や、遠近法を考えて投影する透視投影などがある。今回はこのお話。
プロジェクション変換で得られる座標系は範囲が-1~1に限られてしまう。そこで.4.のスクリーン座標変換でスクリーンの大きさにあわせる。この変換は単純に拡大(と平行移動)で実現できる。
これらジオメトリパイプラインの変換行列を順番に(左から)掛けていくことで各オブジェクトの変換行列が得られる。最終的に得られた変換行列を渡すことで、オブジェクトの各頂点をスクリーン座標に変換してくれるのがvertexシェーダのお仕事だ。
では記事を見ていこう。
---
パースペクティブ、パースペクティブ...3Dを扱っているといつもパースペクティブという言葉が出てくる。
パースペクティブとはなんだろう?現実世界では我々は遠近法でものを見る。これは一般的なものを見る方法だ。
遠近法は遠くのものは近くのものよりも小さく見えるということだ。
遠近法は直線の道路の真ん中に座ると(車には気をつけよう)、道路の境界線が集まって見えることだ。
これがパースペクティブだ。3Dにはパースペクティブがぜひ必要だ。さもなければ世界はリアルに見えないだろう。3Dに見えないのだ。
加えて複雑なことに、我々が3Dでやっていることは3D世界を頂点や面などで定義したオブジェクトでつくり、そのシーンをモニターのような2Dのデバイスで見たいということだ。
問題は3D世界をモニターの2D面に投影したいのだ。
これが透視投影呼ばれるもので、簡単に言うと遠近法の法則に従って3Dシーンを2Dスクリーンに投影することを意味する。
あまり難しくはない。しかしまず我々が直感的になにをしたいのか見てみよう。目の前のモニターが本物のモニターじゃないと想像してみよう。モニターの向こうにガラスの窓があり、実際の3Dシーンを覗いているとする。
あなたはモニターのこちら側にいて、頭をモニターのほうへ固定して観察しているとする。
蝶々がモニターの中央にとまっているのを見つけた。蝶々は結構大きく、真ん中に見えるだろう。そして蝶々はモニターに垂直にまっすぐな起動で飛び去ってしまった。蝶々は飛んでいくにつれどんどん小さくなり、しかしスクリーンの真ん中のままなのが見えるだろう。
同じようにモニターのガラスの向こう側にいて、しかし今度はモニターの端にとまっている。またガラス面に垂直に飛びさった。今回も蝶々は小さくなっていくのが見えるだろう。しかし遠くに飛んでいくのにつれてモニターの真ん中に近づいていくのも見みえるはずだ。
これが遠近法だ。遠くのものは小さくなり、遠くに行くほど投影面の真ん中に寄っていく。
スクリーン上の蝶々の投影座標をxPとyPとしよう。
そして3Dのワールド座標をxW、yW、zWとする。これは蝶々が実際にスクリーンの向こうの3Dシーンのどこに位置するかをあらわしている。
ワールド座標の座標系にはx軸は右方向、y軸は上方向、そして正のzがスクリーンの奥を指すものを使う。原点は私たちの目があるところにしよう。そう、スクリーンのガラスはz軸に垂直な平面となる。あるz座標をzNearと呼ぶことにする。
私たちが欲しいのはワールド座標xW、yWをzWで割った、投影座標xP、yPだ。
xP = K1 * xW / zW
yP = K2 * yW / zW
K1とK2はガラス窓(スクリーン)のアスペクト比や、どれだけ広角で見えるかに影響するカメラの視野などの幾何的要因から求まる。
この変換でどうなるだろう。スクリーンの端に落ちる(xP, yP)として変換された(xW, yW)のペアは目(zW)からの距離の増加に応じて真ん中に追いやられる。スクリーンの中央に近い(xP, yP)として変換された(xW, yW)のペアは目(zW)からの距離にほとんど影響せず、スクリーンの中央の近くのままだ。
これが私たちの望んだ結果だ。
このzでの除算は透視除算(perspective divide)として知られる。
行列演算による透視投影は一見してトリッキーだ。これは行列変換が線形変換、つまり変換されたベクトル成分が入力ベクトルの単純な線形結合であるためである。線形変換では移動、回転、拡大縮小、歪曲させることしかできない。透視除算のように成分が他の成分で割られるような操作はできないのだ。
ここで、我々は普通三次元座標をwは常に1となるような(x, y, z, w)の四次元ベクトルで表現していることを思い出そう。透視除算の解決方法は、4番目の座標wを独創的に使い、変換されたベクトルのw座標にzW座標を保持しておくことだ。
変換されたベクトルの他の成分はxPとyPに先にzWが掛けられたものになる。
つまり以下のような変換が欲しいわけだ。
xW → xP' = xP * zW = K1 * xW
yW → yP' = yP * zW = K2 * yW
zW → zP' = K3 * (zW - zNear)
変換されたベクトルが変換するワールドベクトルの線形結合なので、これなら確かに線形行列変換で可能だ。
このあと本当の変換されたxPとyPは、この変換されたx、y、z成分をwで割ることで得られる。
つまりこうだ。
xP = K1 * xW / zW yP = K2 * yW / zW zP = K3 * (zW - zNear) / zW
これがまさに欲しかったものだ。
ところで、Molehillではvertexシェーダで視点を以下のような特殊な空間に変換する、行列を使うことになっている。
(x, y, z, w) = (xP', yP', zP', zW)
xP'、yP'、zP'、zWは先に定義したもので、K1、K2、K3は3D世界の全ての可視点のxPとyPが-1~1の範囲に収まり、zPは0~1に落とし込まれるよう選ばれたものだ。つまりスクリーンの右端にあったオブジェクトは投影されるとxP=1となり、左端のものはxP=-1になる。
この(xP', yP', zP', zW)の四次元空間はクリップスペースと呼ばれ、割った後のxPとyPが-1~1、zPが0~1の(xP, yP, zP)の座標を正規化デバイス座標(NDC)と呼ぶ。
MolehillでGPUはクリップスペースの形でシェーダの出力を受け取り、透視除算を内部的に実行する。
スクリーンガラスにあるオブジェクトはzW=zNearにあったものはzP=0になり、zW=zFarと定義した距離にあったものはNDC空間ではzP=1に変換される。
zNearとzFarはいわゆるクリップ面を定義する。zNearより近い位置にあるオブジェクトはクリップされ(描画されず)、zFarより遠い位置にあるオブジェクトも同様だ。また、-1~1の範囲を外れたxPやyPを持つオブジェクトもクリップされる。
簡単にするためにオブジェクトを点として話したが、実際には広がっているオブジェクトは部分的にクリップされ、ある部分は視界内にあるが、他の部分は視界外に出るということもある。
幸運なことにAdobeがMatrix3Dクラスを拡張したものを作ってくれている。
ほぼ公式のクラスで、こちらからダウンロード可能だ。
このPerspectiveMatrix3Dクラスは簡単に透視投影変換行列を作れるメソッドが実装されている。
今回、xが右に、yが上に、zがスクリーンの奥になるワールド座標系を使ってきた。これを左手座標系という。なのでLHとあるメソッドを使う。
透視投影行列を作る。
var aspect:Number = 4/3; var zNear:Number = 0.1; var zFar:Number = 1000; var fov:Number = 45*Math.PI/180; var projectionTransform:PerspectiveMatrix3D = new PerspectiveMatrix3D(); projectionTransform.perspectiveFieldOfViewLH(fov, aspect, zNear, zFar);
以前の記事で作ったサンプルコードをこの行列を使って少しだけ拡張してみよう。
オブジェクトを移動、回転するのに使っていた行列変換にただappend(左からかける)する。遠近法になっているのがわかるよう、異なる回転も加えた。
ar m:Matrix3D = new Matrix3D(); m.appendRotation(getTimer()/30, Vector3D.Y_AXIS); m.appendRotation(getTimer()/10, Vector3D.X_AXIS); m.appendTranslation(0, 0, 2); m.append(projectionTransform);
---
実行した結果とソースコードは元記事にリンクがある。遠近法になっているのがわかるはずだ。
理論的には少しめんどくさい透視投影変換だが、PerspectiveMatrix3Dクラスを使えばアスペクト比とzNear/zFarと視野角を指定するだけでプロジェクション変換行列を作ってくれる。
アスペクト比はwidth/length、視野角はラジアン指定なのに注意。
Context3D.setProgramConstantsFromMatrixの第四引数はtrueにする。これによって行列は転置して渡される。なぜかわからないけどGPUで実行される演算の性質から、転置しなくてはいけないようだ。注意。
Author:9ballsyn
ActionScriptについて
最近はMolehill