📌 はじめに
入社してきた新入社員が期待と不安とやる気に満ちあふれているところで,まずは研修から始めるところが多いかと思います.
研修なんて受けたことがないので,きちんと基礎を叩き込んでくれるところで研修受けたい.そう思うこの頃です.
今回は3次元座標変換について,巷にはそういった情報は沢山あるのですが,自分なりの整理も含めて少し書くことにしました.新人向けに書いていますので,最初は基本的なところから入っていきます.
📌 基本的な座標変換
座標系(coordinate system)
3次元空間は実世界と同じであり,上下・左右・前後の3つの次元です.この上下・左右・前後はそれぞれ直交関係にあります.そして,例えば上下というのは,私から見た上下と,あなたから見た上下は必ずしも一致するとは限りません.つまり,上下・左右・前後というのはどこか基点があるということです.この場合,私やあなたが基点であり,向いている方向に依存しています.次に,例えば前方に5歩進むとしたら人によって進む距離に違いがあります.もし,前方に1メートル進む場合は全員同じ距離になりますが,今度は1メートル進むための歩数が変わってきます.このように,単位も重要になってきます.よって,基点と向きと単位を決めれば,その3次元空間での座標が特定できることになります.この基点を原点(origin)とし,向き(axis),単位(unit)を決めたものを座標系といいます.ここでは上下をY,左右をX,前後をZとします.
3次元空間では座標系を決めるときに2つの種類があります.左手座標系と右手座標系です.左手座標系ではZ方向が奥に向かって正であり,DirectXやUnrealEngine,Unityなどが左手座標系です.右手座標系はZ方向が手前に向かって正であり,OpenGLやWebGLなどが右手座標系です.座標系が同じだとしても,上下方向がYやZだったりするので,座標系と軸を確認するようにしましょう.
ここでは右手座標系で説明します.
ベクトル(vector)
座標系が決まれば,位置を表現することができます.3次元空間での位置はベクトルで表されます.まず,点pの位置をXYZの各軸で測った値をそれぞれx,y,zとすると
p=(x,y,z)で位置を表現できます.ベクトルは方向と大きさを表していて,例えば点qから点pに向かうベクトルvは
q=(0,30,0),p=(0,50,0),v=qp=(0,20,0)となります.これにはベクトルの始点,終点の位置,大きさ,方向がありますね.ここで,ベクトルvの始点を原点O=(0,0,0)とすれば,
OV=v=(0,20,0)となって,大きさと方向のみを表すことになります.このときのx,y,zのことをそれぞれX成分,Y成分,Z成分といい,この表記を成分表示といいます.
ベクトルの成分表示が出来たので,ベクトルから大きさと方向をそれぞれ取り出してみましょう.まず,大きさですがこれは始点から終点までの距離になります.始点を原点とすればピタゴラスの定理を使ってベクトルの大きさ∣v∣は
∣v∣=x2+y2+z2で得られます.3次元ベクトルが3つの値を持っていることに対して,ベクトルの大きさは1つの量を表す値です.この値をベクトルに対してスカラ(scalar)といいます.
次に方向は,大きさが1のベクトルで表します.このようなベクトルを単位ベクトル(unit vector)といいます.単位ベクトルはベクトルの各成分を大きさで割ることで求められます.
∣v∣v=(∣v∣x,∣v∣y,∣v∣z)これを正規化(normalize)といいます.つまり,ベクトルを正規化すればそれは方向を表すベクトルということになります.
※ベクトルや行列の演算について,これから出てきますが詳しいことはここでは解説しません.ベクトルや行列などの線形代数については他を参照したり,もしくは私が書いた「CGのための線形代数入門シリーズ」を参照してください.
基底(basis)
ベクトルの成分表示でXYZで測った値を表記することがわかりました.もちろん,このXYZというのはそのベクトルを測った座標系に基づいています.この座標系とベクトルの関係を見ていきましょう.
XYZというのは座標系における軸のことです.これまで,上下・左右・前後という曖昧な表現でしたが,方向を表すベクトル,つまり単位ベクトルを使うと,XYZの向きをベクトルで表すことが出来ます.一般的な座標系として,左右をX,上下をY,前後をZとしたとき,それぞれの向き(単位ベクトル)ex,ey,ezは
ex=(1,0,0),ey=(0,1,0),ez=(0,0,1)となります.これを使うとベクトルの成分表示は
p=pxpypz=px100+py010+pz001=pxex+pyey+pzezとなります.このex,ey,ezはお互い直交関係になっています.ベクトルの内積が0のとき,直交であることから
ex⋅ey=ey⋅ez=ez⋅ex=0の関係であることがわかります.これらの単位ベクトルによって座標系の向きを決めることができます.このXYZの向きベクトルの組を基底といい,それぞれのベクトルのことを基底ベクトルといいます.ベクトルの内積は片方が単位ベクトルのとき,もう片方のベクトルのその単位ベクトル方向の大きさを求められます.これを使って,ある座標系の基底ベクトルと,位置を表すベクトルから
pxpypz=p⋅ex=p⋅ey=p⋅ezと各成分が得られます.さらに行列を使って,ex=i,ey=j,ez=kとおくと
Mp′=ixjxkxiyjykyizjzkz=Mp=p⋅i+p⋅j+p⋅kとなります.この行列MはXYZの基底ベクトルで構成されていることから,基底を表していることが想像できると思います.行列とベクトルの内積の性質から,基底を表す行列Mに,任意のベクトルpをかけると(一次変換),行列Mで表す基底での成分に変換することができ,この値は一意に決まります.よって,この行列Mはベクトルpからp′の写像を表しています.
基底ベクトルというのは常に単位ベクトルというわけでもなく,また,3次元での基底は直交していなくても定義することができます.このような座標系は斜交座標系といったりします.ここでは,右手座標系ですので,基底ベクトルは直交関係であり,また,基底ベクトルが単位ベクトルであるとします.このような基底を正規直交基底といいます.
いよいよ座標変換に入っていきます.座標変換といっても色々種類がありますので,1つずつ見ていきましょう.まずは基底変換です.基底というのはすでに見てきたように,基底ベクトルの組であり,基底ベクトルは単位ベクトルで互いに直交になっています.座標系で出てきた「原点」「向き」「単位」のうち,「原点」はO=(0,0,0)のままでどの基底でも同じです.また,単位も基底ベクトルは常に単位ベクトルですので,単位も変わりません.結局,基底は向きのみを表しているといえます.そうすると基底が変わるということはどういうことでしょうか.原点が固定で,大きさも変わらないとなれば,それは向きが「回転」すると考えられます.つまり,基底変換は回転させる座標変換といえます.
ここで少し三角関数について復習しておきましょう.まずは三角比から.
sinθ=ca,cosθ=cb,tanθ=ba
単位円で三角関数を見てみると
x=cosθ,y=sinθ
となります.また,ピタゴラスの定理から
sinθ2+cosθ2=1が得られます.三角関数の逆関数にはそれぞれarcsin,arccos,arctanがあります.これらの関係は
θθθ=arcsin(sinθ)(−2π≦θ≦2π), =arccos(cosθ)(0≦θ≦π), =arctan(tanθ)(−2π<θ<2π)となります.次に加法定理です.次の図を見てください.

Δabcにおいて,斜辺の長さが1から
ac=sin(A+B),bc=cos(A+B)Δabdにおいて,
ad=sinB,bd=cosB次に
affc=adcosA=cosAsinB=de=bdsinA=sinAcosBac=af+fcの関係から
sin(A+B)cos(A+B)sin(A−B)cos(A−B)=sinAcosB+cosAsinB=cosAcosB−sinAsinB=sinAcosB−cosAsinB=cosAcosB+sinAsinBとなります.
それでは回転を見ていきましょう.次の図のように点pを点p′に回転する場合を考えます.ここではz=0とします.

すると点pの各成分は
x=rcosϕ,y=rsinϕとなります.回転後の点p′の各成分は加法定理を使って
x′y′=rcos(θ+ϕ)=rcosθcosϕ−rsinθsinϕ=rsin(θ+ϕ)=rsinθcosϕ+rcosθsinϕとなります.ここで
r=cosϕx,r=sinϕyを代入して,整理すると
x′y′z′=cosϕxcosθcosϕ−sinϕysinθsinϕ=xcosθ−ysinθ=cosϕxsinθcosϕ+sinϕycosθsinϕ=xsinθ+ycosθ=zとなって,行列で書くと
x′y′z′=cosθsinθ0−sinθcosθ0001xyzとなります.この行列の基底ベクトルに注目してみると,回転させたい方向とは逆の方向に回転したベクトルになっています.つまり,反対方向に回転させた基底を使って変換を行うと,回転した座標になるということになります.

この回転行列はZ軸を基準に回転を表していることになります.このZ軸回転の行列をRzとおくと
Rz=cosθsinθ0−sinθcosθ0001となります.同じようにX軸,Y軸の回転行列Rx,Ryは
RxRy=1000cosθsinθ0−sinθcosθ=cosθ0−sinθ010sinθ0cosθとなります.ここで各回転行列の行ベクトル,および列ベクトルは
sinθ2+cosθ2=1から,すべて単位ベクトルであり,列ベクトルにおいても互いに内積がゼロなので直交していることがわかります.
行列は掛け合わせて合成できるので,例えばXYZ回転の行列Rxyzは
Rxyz=RzRyRxとなります.ここでは取り上げませんが,任意の軸を基準に回転する行列の式も簡単に見つかると思います.これらの回転行列を組み合わせて基底変換の行列をつくることが出来ます.
剛体変換(rigid-body transformation)
基底変換では座標系のうち,向きしか変換できませんでした.ここで,原点を平行移動させる変換を考えます.このような大きさが変わらず,向きと位置が変わる変換を剛体変換といいます.剛体(rigid body)は力を加えても形状を変えない物体のことで物理シミュレーションでよく使います.
基底変換では向きしか変換できなかったので,これに加えて平行移動する変換をしなければなりません.基底変換ではある点pを点p′に変換する行列Mから
p′=Mpと表せました.ここで,この変換にtの平行移動を追加するには
p′=Mp+tとすればよいことになります.これは1次関数の形になっていますね.しかし,このままでは3行3列の行列で表すことができず,扱いづらいものになっています.そこで,4次元の同次座標系(homogeneous coordinate)を使います(同次座標系は斉次座標系とも呼ばれます).4次元の同次座標系を使うと4次元空間の断面と3次元空間の座標を同じ(写像)扱いにすることができます.4次元空間の位置はwが追加されたx,y,z,wで表し,3次元空間との関係は
xyz=x/wy/wz/wとなります.通常はw=1として
xyz=xyz1とします.ちなみにw=0とすると,無限遠点となり大きさが無限となります.ただし,無限遠点でも向きは存在しているので,w=0の場合は向きだけを表しているとも考えることができます.
4次元の同次座標系を使うことで4行4列の行列を扱えるようになりました.平行移動を行列で表記すると
T=100001000010txtytz1となります.基底変換の行列は回転を表す行列なので,回転行列をRとおくと,剛体変換は次のようになります.
p′=Mp+t=TRp=100001000010txtytz1R11R21R310R12R22R320R13R23R3300001pxpypz1=R11R21R310R12R22R320R13R23R330txtytz1pxpypz1基底変換の式
p′=Mpは一次変換でした.次に,剛体変換の式は基底変換に平行移動を追加した式
p′=Mp+tは1次関数と同じ形になっていましたね.もちろん,これは写像を表しています.この1次関数をベクトル空間で一般化し,一次変換と平行移動を合わせた変換をアフィン変換といいます.よって,剛体変換も基底変換もアフィン変換の特殊な形となります.アフィン変換では回転,平行移動の他に,拡大・縮小,鏡映,せん断があります.
先に鏡映とせん断についてです.鏡映(reflection)は基準となる軸に対して対称となる位置に移動する変換です.例えば,右手座標系と左手座標系を相互に変換する場合は次のような行列になります.
Mreflection=10001000−1せん断(shearing)はスキュー(skew)変換とも呼ばれ,歪ませる変換です.これは別軸の成分の値が他の軸の成分に影響させることで歪ませます.例えば,Y軸方向の値分,X軸方向にずらす場合は
Mskew=100110001となります.
次は拡大・縮小です.この行列Sは次のようになります.
S=sx000sy000sz拡大・縮小行列で変換してみると
p′=Sp=sx000sy000szpxpypz=sxpxsypyszpzとなります.この拡大・縮小と回転,平行移動の3つを組み合わせて座標変換行列をつくるのが基本的なやり方となります.式で表すと
p′=TRSp=100001000010txtytz1R11R21R310R12R22R320R13R23R3300001sx0000sy0000sz00001pxpypz1=sxR11sxR21sxR310syR12syR22syR320szR13szR23szR330txtytz1pxpypz1となります.また,TRSの変換行列において4行目のベクトルは固定値になっているため
TRS=sxR11sxR21sxR31syR12syR22syR32szR13szR23szR33txtytzと3行4列の部分だけあればよいことになります.これは変換行列のデータ量を小さくするときに利用します.
TRSの変換行列が得られたので,今度はTRSから拡大・縮小,回転,平行移動に分解することを考えてみましょう.まず,平行移動はtx,ty,tzをそのまま取り出すことができます.問題は拡大・縮小と回転です.これを抽出するためには回転行列の性質を利用します.
ここからは少しややこしくなりますが,1つ1つ整理しながら読み進んでもらえるといいかなと思います.
基底変換のところで回転行列について扱いました.θ回転させる場合,反対方向に回転,つまり−θ回転した基底を使えばθ回転することになるということでしたね.さらにいうと,回転した座標を回転した基底で変換すると,回転前と同じ座標になります.ここで注意なのが,回転した基底は回転行列とは異なるということです.基底を表す行列M,点をpとして,回転前の行列M1,回転前の点p1,回転後の行列M2,回転後の点p2とすれば
M1p1=M2p2の関係が成り立ちます.また,逆行列を使えば
p1=M1−1M2p2,p2=M2−1M1p1となります.θ回転する基底を作ると,それは−θ回転した基底ベクトルということでした.この行列の逆行列を考えてみると,θ回転した基底ベクトルになっているということになります.これはまさに回転後の基底です.ここまでをまとめると,
- 基底を表す行列の逆行列は,回転後の基底である
- 基底を表す行列は,回転後の基底における回転前の基底である
となります.次に回転行列は直交行列の性質を持っています.直交行列はその転置行列を自身にかけると単位行列となるものです.つまり,直交行列の転置行列は逆行列ということになります.
MTM=MMT=I,MM−1=M−1M=I,∴MT=M−1また,直交行列の行列式の性質も確認してみましょう.行列A,Bの行列式の関係は
∣AB∣=∣A∣∣B∣です.これにA=M,B=MTを代入すると
∣MMT∣=∣M∣∣MT∣行列式の性質として行列とその行列の転置行列の行列式は同じです.
∣M∣=∣MT∣よって
∣MMT∣=∣M∣∣MT∣=∣M∣∣M∣=∣M∣2となります.一方で
および,単位行列Iの行列式は∣I∣=1ということがわかっているので
∣MMT∣=∣I∣=1となり,
∣MMT∣=∣I∣=∣M∣2=1となります.よって,二乗して1となる値が直交行列の行列式となるので,1と−1ということになります.また,回転行列のように行ベクトル,列ベクトルがすべて単位ベクトルであり,互いに直交である場合は行列式の値が1になります.
これまで直交行列の性質をみてきました.回転行列は直交行列ですので,直交行列の性質から回転行列の逆行列は転置行列と等しくなります.このことを踏まえて
を考えてみると,逆行列は転置行列だったので,基底を表す行列の列ベクトルが回転後の基底ベクトルとなります.結局
- 基底を表す行列の列ベクトルは,回転後の基底ベクトルである
- 基底を表す行列は,回転後の基底における回転前の基底である
となります.これでTRSの座標変換行列から回転と拡大・縮小を抽出する準備ができました.もう一度変換行列を確認してみると
p′=TRSp=sxR11sxR21sxR310syR12syR22syR320szR13szR23szR330txtytz1pxpypz1ここで列ベクトルに注目し,それぞれvx,vy,vzとおくと
vx=sxR11sxR21sxR31,vy=syR12syR22syR32,vz=szR13szR23szR33となります.各ベクトルの大きさを求めると
∣vx∣∣vy∣∣vz∣=(sxR11)2+(sxR21)2+(sxR31)2=(syR12)2+(syR22)2+(syR32)2=(szR13)2+(szR23)2+(szR33)2となります.さらに二乗して平方根をはずして整理すると
∣vx∣2∣vy∣2∣vz∣2=sx2R112+sx2R212+sx2R312=sx2(R112+R212+R312)=sy2R122+sy2R222+sy2R322=sy2(R122+R222+R322)=sz2R132+sz2R232+sz2R332=sz2(R132+R232+R332)となります.ここで回転行列の列ベクトルは回転後の基底ベクトルであり,単位ベクトルなので,
R112+R212+R312=1∴R112+R212+R312=12=1R122+R222+R322=1∴R122+R222+R322=12=1R132+R232+R332=1∴R132+R232+R332=12=1となります.これを代入すると
∣vx∣2=sx2,∣vy∣2=sy2,∣vz∣2=sz2∴∣vx∣=sx,∣vy∣=sy,∣vz∣=szとなって,列ベクトルの大きさが拡大・縮小の値と一致することがわかります.sx,sy,szがわかれば,回転は
sx1vx,sy1vy,sz1vzで各成分が得られます.まとめると,TRSの合成行列をMとおくと
p′=TRSp=M11M21M31M41M12M22M32M42M13M23M33M43M14M24M34M44pxpypz1これからTRSを求めると
t=M14M24M34,s=M112+M212+M312M122+M222+M322M132+M232+M332,R=M11/sxM21/sxM31/sxM12/syM22/syM32/syM13/szM23/szM33/szとなります.
📌 レンダリングパイプライン
コンピュータグラフィックスでは,レンダリングするために入力データ(頂点データやテクスチャなど)から画面に表示するまでの工程があり,その工程順番を決めたものをレンダリングパイプラインといいます.座標変換はそのレンダリングパイプラインの工程で行われます.具体的にはモデル変換,ビュー変換,投影変換,ビューポート変換です.
座標変換ができれば座標系がいくつもあっても構わないのですが,一般的にローカル座標系(local coordinate system)とワールド座標系(world coordinate system)が使われます.頂点データはローカル座標系で作成し,それをワールド座標系に変換します.このワールド座標系への変換をモデル変換といいます.頂点データはローカル座標系で作成しますが,普通は親子階層を持っていて,親の座標系の下にさらに子の座標系,さらにその子の座標系・・・というようになっています.座標変換は一方向に伝搬するので,ワールド←親←子←孫・・・と座標変換できます.逆行列を使えば逆方向に伝搬することも出来ますね.モデル変換や親子での座標変換も基本的に平行移動・回転・拡大・縮小のTRSで座標変換されます.
レンダリングではカメラを導入して,カメラから見えるシーンを描画します.モデル変換によってワールド座標系にすべて変換されますので,ワールド空間上にカメラを配置します.そして,ワールド座標系の座標をカメラから見た座標系(カメラ座標系またはビュー座標系)に変換します.これをビュー変換といいます.ビュー変換には一般的に2種類あります.
LookAt方式
まず,LookAt方式です.これはカメラの位置(C)と対象の位置(P),カメラの傾きを表す方向(U)の3つで指定します.まず,カメラの位置が原点となりますので,平行移動が必要です.
M=100001000010−tx−ty−tz1次に,カメラから見て前後方向(Z軸)を決定します.それは対象の位置からカメラの位置に向かうベクトルです.
z=∣C−P∣C−P次に,カメラの上方向を表すベクトルと求めたZ方向との外積を求めます.これはX方向になります.
x=∣U×z∣U×z最後に,Z方向とX方向の外積を求めます.これはY方向になります.
y=∣z×x∣z×xこれで基底ベクトルが揃ったので,行列にすると
M=xxyxzx0xyyyzy0xzyzzz00001100001000010−tx−ty−tz1=xxyxzx0xyyyzy0xzyzzz0−x⋅t−y⋅t−z⋅t1となります.
位置と回転を指定する方式
カメラの位置と回転を表す行列をそれぞれT,Rとすれば,
p′=TRpはカメラ座標系からワールド座標系に変換する式です.ここで逆行列を使えば
p=R−1T−1p′=(RT)−1p′となって,ワールド座標系からカメラ座標系に変換する式となります.さらに,回転行列の逆行列は転置行列,平行移動の逆行列は符号を反転したもの
T=100001000010txtytz1,T−1=100001000010−tx−ty−tz1ですから,
R−1T−1=R11R12R130R21R22R230R31R32R3300001100001000010−tx−ty−tz1と書くことも出来ます.
投影変換はとても重要な変換であり,ここは環境に依存する部分でもあります.投影変換は射影変換とも言われます.投影変換には近くのものが大きく,遠いものが小さくなる透視投影変換(perspective projection)と,奥行きによって変換しない正投影変換(orthogonal projection)があります.
投影変換の手順としては,まず,カメラから見える範囲を決めます.具体的には前方クリッピング面(near clipping plane)と後方クリッピング面(far clipping plane)というのを導入し,その空間内に入っているものだけが投影されるようにします.この領域をビューボリューム(view volume)といい,透視投影変換では視錐台(view frustum),正投影変換では長方体の形になります.
次に,このビューボリュームの後方クリッピング面がz=1で,このときのビューボリュームのx,yが−1≤x≤1,−1≤y≤1となるように正規化します.このビューボリュームを正規化ビューボリュームといいます.
この時点でビューボリュームも正規化ビューボリュームもまだカメラ空間にありますが,正規化ビューボリュームを−1≤z≤1の空間に変換します.この空間をクリップ空間(clip space)といい,この空間でのビューボリュームは標準視体積(canonical view volume)といいます.このとき,Zは奥の方向が正となって左手座標系になります.このクリップ空間は4次元の同次座標系になっていて,x,y,z,wで表されます.投影変換はカメラ空間からクリップ空間に変換することをいいます.
※DirectXなどはZの範囲が[0,1]になっているので注意が必要です.
クリップ空間の座標でx,y,zをwで除算することを透視除算といい,x/w,y/w,z/wとなります.この透視除算した座標を正規化デバイス座標(normalized device coordinate)といいます.
透視投影変換
まずは透視投影変換から見ていきましょう.透視投影では近いほど大きく,遠いほど小さく見えます.次の図を見てください.

原点Oの位置から点Pを見たとき,点Pをz′=−1の平面に投影すると,三角形xozとx′oz′は相似の関係なので
x′:x=z′:z=−1:z,∴x′=−zxとなります.これはYについても同じです.よって
x′=x/−z,y′=y/−zとなります.次に投影面というのを考えます.これは先程の投影される平面のことで,z′=−1です.

また視野角を90∘とします.そうするとz′におけるXとYは[−1,1]になります.

ここで,最初の透視変換の式と,投影面を考慮して行列で表すと次のようになります.
Mp=10000100001−10000この行列で頂点pを座標変換すると
p′=Mp⋅p=Mpxyz1=xyz−zとなります.pw′でpxyz′の各成分を除算すれば
x/−zy/−z−1となります.次に正規化ビューボリュームの範囲[−1,1]に値を変換する必要があります.まずはZ値からですが,次のような1次関数を考えます.
pz′=az+bこの行列は次のようになります.
Mw=1000010000a−100b0Mwxyz1=xyza+b−zクリップ座標系は同次座標系ですので,wで各成分を割ることになります.透視投影ではこれを透視除算といい,透視除算した座標を正規化デバイス座標といいます.ここで
ですから,
pz′=az+b=−a⋅pw′+bという関係になります.また,正規化デバイス座標のzndcは
zndc=pw′pz′=pw′−a⋅pw′+b=−a+pw′b=−a+−zbとなります.ここで,前方クリッピング面と後方クリッピング面を導入します.

この前方クリッピング面(z=n)のときzndc=−1,後方クリッピング面(z=f)のときzndc=1となるように,aとbを求めます.
−a+−nb=−1,−a+−fb=1,∴b=−f(1+a)
−a+−nbaanan−afa=−1=−nb+1=−n−f(1+a)+1=−n−f−af−n=nf+af+n=f+af+n=a(n−f)=f+n=n−fn+f
b=−f(1+a)=−f−af=−f−f(n−fn+f)=−n−fnf−f2−n−fnf+f2=n−fnf+f2−nf−f2=n−f−2nfよって
∴a=n−fn+f,b=n−f−2nfまた,
pz′=az+b=n−fn+fz+n−f−2nf行列に書き直せば
Mw=1000010000n−fn+f−100n−f−2nf0Mwxyz1=xyzn−fn+f+n−f−2nf−zとなります.ここで,n=−1,f=−5としたときの図を以下に示します.

この図では横軸がz,縦軸がzndcです.z=−1のときzndc=−1,z=−5のときzndc=1になっているのがわかります.
次にl,r,t,bを[−1,1]に収まるように拡大・縮小します.z=−1のとき−1,1,z=−nのときl,rまたはt,bですから,相似の関係から
−11−(−1)/nr−l=r−l−2n,−11−(−1)/nt−b=t−b−2nとなります.これを行列表記すると
Ms=r−l−2n0000t−b−2n0000100001となります.あとは,これまでZ軸がクリッピング面の中心を通っていることを想定していました.これがずれている場合に対処します.そのためにせん断を使います.前方クリッピング面での中心位置分をZ軸の値によって変化させてずらします.この行列は次のようになります.
Msh=10000100−2r+ln1−2t+bn1100001Mshxyz1=x−2r+lnzy−2t+bnzz1最終的に透視投影変換は
Mp=MwMsMsh=1000010000n−fn+f−100n−f−2nf0r−l−2n0000t−b−2n000010000110000100−2r+ln1−2t+bn1100001=r−l−2n0000t−b−2n00r−lr+lt−bt+bn−fn+f−100n−f−2nf0となります.
この方法は汎用性が高いものの扱いづらいため,実際には画角とアスペクト比を使った方法がよく使われます.画角をfovy,アスペクト比をaspect(=幅/高さ)とすれば
Mp=aspectf0000f0000n−fn+f−100n−f−2nf0ここで
f=tan(2fovy)1=cot(2fovy)となっています.
正投影変換
透視投影変換のときは奥行きによって見え方が変わらないので,wによる透視除算がありません.正投影変換では,ビューボリュームの中心を原点に持ってきて,大きさを正規化するだけです.まずはビューボリュームの中心を原点にする平行移動を求めます.これは次のようになります.
Mt=100001000010−2r+l−2t+b−2f+n1各軸での中心を求めて移動しています.
次に正規化です.各軸がそれぞれ[−1,1]の範囲になるようにします.これは次のようになります.
Ms=r−l20000t−b20000f−n200001よって,正投影変換の行列は
Mo=MsMt=r−l20000t−b20000f−n200001100001000010−2r+l−2t+b−2f+n1=r−l20000t−b20000f−n20−r−lr+l−t−bt+b−f−nf+n1となります.次の図は正投影変換においてn=−1,f=−5のときのグラフです.
6-37468526dcc1.png)
OpenGLの投影関数
OpenGLには投影変換行列を設定してくれる関数があります.glFrustum
関数は前方クリッピング面の左右上下の座標l,r,t,bとZ軸の距離n,および遠方クリッピング面のZ軸の距離fを指定して,透視投影変換行列を設定します.gluPerspective
関数は視野角(fovy)とアスペクト比(aspect),前方・後方クリッピング面のZ軸の距離n,fを指定して,透視投影変換行列を設定します.glOrtho
関数はクリッピング面の左右上下の座標l,r,t,bと前方・後方クリッピング面のZ軸の距離n,fを指定して正投影行列を設定します.これらの関数のドキュメントには行列の各要素が記載されています.その行列と,これまで導出してきた行列と比較してみるといくつか符号が違っているところがあります.これは,OpenGLの関数ではn,fを正の値,導出した行列のn,fは負の値だからです.そのため,導出した行列をOpenGLの関数の行列と同じにするにはn,fの符号を反転すればよいことになります.よって,それぞれの行列は
MfrustumMperspectiveMortho=r−l−2(−n)0000t−b−2(−n)00r−lr+lt−bt+b(−n)−(−f)(−n)+(−f)−100(−n)−(−f)−2(−n)(−f)0=r−l2n0000t−b2n00r−lr+lt−bt+b−f−nf+n−100f−n−2nf0=aspectf0000f0000(−n)−(−f)(−n)+(−f)−100(−n)−(−f)−2(−n)(−f)0=aspectf0000f0000−f−nf+n−100f−n−2nf0=r−l20000t−b20000(−f)−(−n)20−r−lr+l−t−bt+b−(−f)−(−n)(−f)+(−n)1=r−l20000t−b20000n−f20−r−lr+l−t−bt+b−f−nf+n1投影変換によってクリップ空間に変換された座標は透視除算することで正規化デバイス座標に変換されます.
pndc=x/wy/wz/wこの正規化デバイス座標系はXYZともに−1から1の範囲になっています.これを画面に表示するためにビューポート変換をします.画面のことをウィンドウとかスクリーンというので,ビューポート変換後をウィンドウ座標系やスクリーン座標系ともいいます.また,ビューポート変換をスクリーン座標変換ということもあります.
ビューポート変換ではスクリーンに表示する領域と位置を指定します.ここで,スクリーンの幅をw,高さをh,始点をox,oyとおくとビューポート変換は
Mv=2w0000−2h00002f−n02w+ox2h+oy2f+n1,Mvpndc=x2w+2w+ox−y2h+2h+oyz2f−n+2f+n1となります.Yが反転しているのは正規化デバイス座標系ではY軸の上が正に対して,スクリーンでは左上が原点でありY軸は下が正だからです.Zは[n,f]の間になるようにしています.通常はn=0,f=1です.
一般的な頂点データのパイプライン
レンダリングパイプラインの主要な座標変換を一通り見てきました.一般的に頂点データがどのような座標変換が適用されて画面に表示されるかを図にしてみました.

実際は環境によって変わる場合があると思いますので,この辺り触れる機会があれば,その環境のドキュメントを確認してください.
📌 おわりに
簡単にまとめるつもりが,なんか思っていた以上に長くなってしまった気がしますね.また,説明がまだ上手く出来ていないところが多々あるので反省.意見や感想,間違いの指摘など受け付けていますので,なにかあればよろしくおねがいします.また,少しでも参考になれば幸いです.
📌 付録 A:Three.jsの深度の関数
Three.jsのシェーダコードには深度とZ値を変換する以下の関数があります.
- viewZToOrthographicDepth
- orthographicDepthToViewZ
- viewZToPerspectiveDepth
- perspectiveDepthToViewZ
例えば,深度の値からカメラ座標系でのZ値を求めれば,スクリーンの座標からカメラ座標系での座標(XYZ)を求めることができます.また,透視変換の場合は一般的に深度の値が線形になっていないため,線形に変換して使用することがあります.これらの関数のコードは次のようになっています.
最初に,perspectiveDepthToViewZから見ていきましょう.これを数式で表すと
viewZ=(f−n)×invClipZ−fnf(1)となります.まず,これらの関数のnとfは正の値を指定します.よって,透視投影の式は
pz′ab=az+b=n−fn+fz+f−n−2nf=n−fn+f=f−n−2nfです.また,正規化デバイス座標のzndcは次のようになります.
zndc=pw′pz′=−z1(n−fn+fz+f−n−2nf)(2)正規化デバイス座標では[−1,1]の範囲になりますが,一般的に深度バッファに格納される深度値depthは[0,1]の範囲にビューポート変換されます.よって
depth=2zndc+21,∴zndc=2⋅depth−1これを式(2)に代入すると
2⋅depth−1=−z1(n−fn+fz+f−n−2nf)となります.zについて解くと
2⋅depth−12⋅depthdepth(f−n)znf(f−n)znfnf(f−n)z(f−n)zz=−z1(n−fn+fz+f−n−2nf)=−n−fn+f+f−n−2nf(−z)=f−nf+n+(f−n)z2nf+1=(f−n)z(f+n)z+(f−n)z2nf+(f−n)z(f−n)z=(f−n)zfz+nz+2nf+fz−nz=(f−z)n2fz+2nf=(f−n)zfz+nf(3)=(f−n)zfz+(f−n)zfn=f−nf+(f−n)zfn=depth−f−nf=f−n(f−n)⋅depth−f−nf=f−n(f−n)⋅depth−f=(f−n)⋅depth−ff−n=(f−n)⋅depth−f(f−n)nf=(f−n)⋅depth−fnf(4)ここで式(1)と比べると
viewZ=(f−n)×invClipZ−fnf一致していますね.よって,invClipZにはdepthを渡せばいいことになります.そして,perspectiveDepthToViewZの逆関数viewZToPerspectiveDepthはdepthについて解けば式が得られます.式(3)から
depth=(f−n)zfz+nf=(f−n)z(n+z)fとなって,viewZToPerspectiveDepthの実装と同じ式になっていることがわかります.今度はorthographicDepthToViewZを見てみると
viewZ=linearClipZ∗(n−f)−n(5)となっています.こちらもnとfは正の値なので
pz′ab=az+b=n−f2z+n−fn+f=n−f2=n−fn+f正規化デバイス座標のzndcは
zndc=pw′pz′=1pz′=pz′(6)深度値depthは
depth=2zndc+21,∴zndc=2⋅depth−1これらの式から
2⋅depth−1n−f2z2zz=n−f2z+n−fn+f=2⋅depth−1−n−fn+f=n−f2⋅depth⋅(n−f)−n−fn−f−n−fn+f=n−f2⋅depth⋅(n−f)−n+f−n−f=n−f2⋅depth⋅(n−f)−2n=2⋅depth⋅(n−f)−2n=depth⋅(n−f)−n(7)となります.これは式(5)と一致します.また,orthographicDepthToViewZの逆関数viewZToOrthographicDepthは
depth⋅(n−f)−ndepth⋅(n−f)depth=z=z+n=n−fz+nとなります.
線形(リニア)深度
一般的に透視投影の深度は線形ではありません.これだと扱いづらいので,線形深度に変換して使用します.透視投影では深度が線形ではありませんが,正投影では線形深度になっています.

図:透視投影の深度

図:正投影の深度
Three.jsではperspectiveDepthToViewZとviewZToOrthographicDepthを使って次のように線形深度に変換することができます.
またperspectiveDepthをdepth,orthographicDepthをlinearDepthとおくと
linearDepth=n−fz+n,z=(f−n)⋅depth−fnflinearDepthについて解くと
linearDepth(n−f)⋅linearDepth−(f−n)⋅linearDepthlinearDepth=n−fz+n=z+n=(f−n)⋅depth−fnf+n=(f−n)⋅depth−fnf+(f−n)⋅depth−f{(f−n)⋅depth−f}⋅n=(f−n)⋅depth−fnf+n(f−n)⋅depth−nf=(f−n)⋅depth−fn(f−n)⋅depth=(f−n)⋅depth−fn(f−n)⋅depth=(f−n)⋅depth−f−n⋅depth=f⋅depth−n⋅depth−f−n⋅depth=f(depth−1)−n⋅depth−n⋅depthとなります.よって,getLinearDepth関数は次のように書きかえることができます.
📌 参考文献
Discussion