前言
在上一篇中,我們以正交投影的方式學習了如何繪制三角形,但在生活中我們眼睛看到的現象用正交投影無法解釋,比如如果是正交投影,我們看到的鐵道兩個鐵軌在視角上并不會交于一點,也就是兩個鐵軌的間距并不會隨著離我們的位置越遠,而變小,實際上這正是透視投影,因此,對于渲染攝像機理解為我們的眼睛,用透視投影來表示;而正交投影則主要用于主要用于二維渲染以及建筑工程軟體

如下右邊是正交投影,左邊是透視投影

2d變換
? 開始之前,需要提醒一下,以下向量表示都是列向量
縮放 沿主軸縮放

反射 2維中圍繞直線翻轉物件

錯切(剪切) 不均勻地拉伸坐標空間,思路是將一個坐標的倍數添加到另一個坐標上

旋轉 二維中圍繞一點旋轉

平移 前面這幾種變化都是線性變換,因為它們是原點不會改變的變換,對于平移來說,他的原點改變了,因此無法用線性變換(矩陣)來表示平移


可以看到這樣對于平移我們需要在線性變換的基礎上,在后面跟上個加法表示位移,但這樣表示并不方便,懶是人類進步的階梯!因此我們需要想辦法將其用一個矩陣來表示,方法是引入齊次坐標w,也就是說多加一個坐標
注解:我們如何理解齊次坐標的含義呢?想象一下,我們有一臺攝影機將畫面投射到一個螢屏上,我們移動攝像機,離螢屏越近,w越小,畫面肯定會被壓縮,這樣這些畫面上的物體的坐標是不是縮小了,離螢屏越遠,w放大,畫面被放大,畫面上的物體的坐標肯定是放大了,
如何理解point添加的是1,vector是0呢?因為向量只描述方向和大小,與位置毫無關聯,所以對于向量來說,平移是毫無意義的,因此為0,又或者點減點表示一個向量,點加上點表示中點


變為齊次坐標后來表示平移的矩陣

仿射變換 線性變化 + 平移,縮放旋轉平移組合在一起,值得注意的是,矩陣乘法并沒有交換律,因此需要按照順序,從右到左

3d變換
齊次坐標 同樣是多加一個數

縮放

平移

旋轉 圍繞一個主軸旋轉,根據左右坐標系,分別伸出左手或右手,大拇指方向指向旋轉軸的正方向,四指彎曲方向為旋轉方向

仿射變換

值得注意的是,旋轉矩陣、反射矩陣都有正交變換,且矩陣乘以其逆矩陣是單位矩陣,正交矩陣乘以轉置矩陣是單位矩陣,因此我們若要求這些矩陣的逆矩陣,只需求其轉置矩陣,這有什么用呢?逆矩陣運算量大,而轉置十分簡單,避免了昂貴的計算成本
坐標空間與空間變換
? 我們已經學會如何用矩陣對頂點進行變換,現在我們將他應用在渲染中,所謂渲染的藝術,就是將一個3d場景轉變為2d圖形,這一變化如何實作呢?接下來我們將進一步探討各個坐標空間
? 首先我們來看渲染需要經過哪些坐標系統:
- 區域空間
- 世界空間
- 觀察空間
- 裁剪空間
- 螢屏空間
從區域空間轉變到世界空間被稱為Model矩陣,;從世界空間轉變到觀察空間稱為View矩陣;從觀察空間轉變到裁剪空間稱為projection矩陣;這就是我們常說的MVP矩陣
區域空間、世界空間 試想一下,許多人做一個大專案,我們并不可能直接在整個專案中直接進行創作,否則很有可能一不小心破防場景中的其他物體,而是將其拆分成一個個子專案,待大功告成后,才會將它們合并在一個整體的大專案中,因此我們定義,區域坐標系是一種以目標物體的中心為原點,且坐標軸與該物體對齊的簡單便用的坐標系,只要在區域空間中定義3D模型的各頂點,我們就可以將其變換至全域場景(世界空間)中;而世界空間則是一個宏觀的特殊坐標系,其代表了我們關心的最大坐標系,我們合并的一個整體的專案就在世界空間中
區域空間的優點:
- 易于使用,物體的中心通常位于區域空間的原點,且關于主軸對稱
- 物體可以橫跨多個場景且重復使用
- 我們很可能需要在一個場景中繪制同一個但位置、方向、大小不同的物體,如果每次創建物體實體時都要復制其頂點和索引,那將消耗大量資源,因此,通常的做法是存盤一個幾何體,按照世界矩陣來指定物體在世界空間中的位置、方向、大小
從區域空間變換到世界空間需要經歷平移縮放旋轉三個步驟
觀察空間 相當于給人拍照,擺好姿勢,攝像機調好位置和朝向,攝像機也就是觀察空間,而調整攝影機的程序,就是觀察矩陣,觀察空間又被稱作視圖空間、視覺空間
從我們描述的觀察空間可得出,觀察空間需要定義三個向量:
- 相機位置(Position) $$\vec{e}$$
- 觀察方向(Look-at / gaze direction) $$\vec{g}$$
- 世界空間中向上方向的向量(Up direction)$$\vec{t}$$
我們規定變換后的相機在原點,看向-z方向,t為y正半軸

View矩陣 從世界空間變化到觀察空間,它們大小是相同,我們只需變換位置和朝向,我們需要(平移)將相機位置移回世界坐標原點,(旋轉)觀察方向朝向-Z軸,向上方向為y軸正半軸,最后平移回去,這時會發現將相機坐標軸旋轉到世界坐標軸很復雜,但是從世界坐標軸旋轉到相機坐標軸很簡單,因此我們只需要求逆,又因為旋轉是正交矩陣,因此我們只需要求其轉置即可


但其實從相機坐標軸旋轉到世界坐標軸一步也可以完成,在這里我們需要先了解坐標變換:不同坐標系下的坐標轉換
我們先來看向量坐標變換
現有坐標系A,在這個坐標系中有一向量v(x,y),現在我們想要把向量v轉換到坐標系B中,我們設轉換后的向量為v' = (x', y'),如下圖中可得知,v = ux + vy,其中u和v分別是x軸和y軸的單位向量;我們再用坐標系B來表示v' = u\(b\)x + v\(b\)y,因此如果給定v = (x,y),也知道u向量和v向量在坐標系b中的坐標u\(b\) = (u\(x\),u\(y\)),v\(b\) = (v\(x\),v\(y\)),我們就可以求出v'了.推理到三維道理一樣,v = (x,y,z),v' = u\(b\)x + v\(b\)y + w\(b\)z,u、v、w為坐標系A中x、y、z軸正方向上的單位向量

點的坐標變換
點的坐標變換與向量稍有不同,這是因為位置是點的一個重要屬性,如下圖所示,我可以求得在坐標系A中p\(A\) = ux + uy + Q\(A\),其中u和v分別是x軸和y軸的單位向量,Q\(A\)為坐標系A的原點,而我們在坐標系中表示則是p\(B\) = Q\(B\) + u\(b\)x + v\(b\)y,其中QB是坐標系A的原點在坐標系B的坐標,u\(b\)是u在坐標系B中的坐標,v\(b\)是v在坐標系B中的坐標

因此從區域空間到世界空間得變換矩陣如下,這個矩陣對之前區域到世界同樣適用

而從世界空間到觀察空間如下,也就是求逆

裁剪空間 對著人拍照,按下快門,將三維的場景變成了2維圖片,同時攝影機螢屏外的部分不要,這一投影程序用到的就是透視投影矩陣,攝影機螢屏也就是裁剪空間,為什么叫裁剪空間呢?因為我們期望所有的坐標都能落在一個特定的范圍內,且任何在這個范圍之外的點都應該被裁剪,被裁剪的坐標就會被忽略,不納入計算,這部分最終也看不到,所以剩下的坐標就將變為螢屏上可見的片段,為了將頂點坐標從觀察空間變換到裁剪空間,我們需要定義一個投影矩陣,它指定了一個范圍內的坐標,如[-100, 100],接著投影矩陣便會將這個范圍內的坐標變換為標準化設備坐標[-1.0, 1.0],這被稱為視口變化(因為將所有坐標都指定在[-1.0,1.0]內不是很直觀,所以我們指定自己的坐標集并將它變換回標準化設備坐標系),因此投影就是將特定范圍內的坐標轉化到標準化設備坐標系的程序;我們前面交過齊次坐標的含義,因此可以得出透視是通過改變w的分量實作的.那么是怎樣實作的呢?一個物體離相機越遠,也就是z越大,那么向量會變小,沒錯是通過z來影響w的
? 正交投影的程序很簡單,坐標的相對位置都不會改變,我們只需要將物體變換到[-1.0,1.0]的范圍即可,程序就是平移至原點,縮放,最后平移回去,不過創建一個正射投影矩陣需要指定可見平截頭體的寬、高和長度



? 由于這里渲染器主要用透視投影,因此我們重點講解透視投影,下圖中左邊是透視投影,右邊是正交投影

? 開頭我們講過,透視投影類似人眼,看到的方式是近大遠小,隨后我們,除此以外它還包含一個組成要素攝像機可觀察到的空間體積,這個空間體積的范圍我們用一個四棱錐截取的平截頭體來表示,也就是上圖中的Frustum,我們將頂點到觀察點的連線稱為頂點的投影線,透視投影的步驟是將遠平面拉成和近平面一樣的大小,組成一個方體,再進行正交投影

? 在觀察空間中,近平面n是已知的,遠平面f是未知的,觀察點為原點,z = -n為投影平面,我們約定經過透視投影后近平面的所有點(x,y)不會改變,遠平面上的點的z值不變,根據相似三角形的原理可以求得x'和y'


我們需要求得一個矩陣完成以下變換,因為z是未知的,所以是Unknown,我們可以看到w為z,這印證了我們之前說的z的值在影響w的

即

轉換成矩陣形式如下,但我們并不知道第三行

如何求得?很簡單,現在我們先考慮近平面,近平面上的任何一個點進行透視投影后必定不會改變,用n取代z,結果如下

又因為之前求出的結果前兩項是nx和ny,因此我們可以確定unknow這一行前兩個位置肯定都是0,然后我們將后面兩個位置設為A和B,結果如下

不難推出

在用遠平面f取代z,推出如下

求解

最后只需要再進行正交投影即可求得結果
最終矩陣如下

視口變換 將[-1,1]的立方體變換到[0,width]和[0,height],因為這是2d平面,因此我們只需要計算x和y.因為標準立方體中心在原點,而螢屏原點在左下角,所以還需要經過一個平移使得原點坐標對齊

實作
//觀測矩陣
mat4 MatrixLookAtLH( vec3 eyePosition, vec3 lookAt, vec3 up )
{
mat4 view = mat4::identity();
vec3 z = unit_vector( eyePosition - lookAt ); //因為是對物體進行旋轉,所以是方向是到攝像機
vec3 x = unit_vector( cross( up, z ) );
vec3 y = unit_vector( cross( z, x ) );
mat4 translation = mat4::identity();
translation[0][3] = -eyePosition.x();
translation[1][3] = -eyePosition.y();
translation[2][3] = -eyePosition.z();
mat4 rotate = mat4::identity();
rotate[0][0] = x[0];
rotate[0][1] = x[1];
rotate[0][2] = x[2];
rotate[1][0] = y[0];
rotate[1][1] = y[1];
rotate[1][2] = y[2];
rotate[2][0] = z[0];
rotate[2][1] = z[1];
rotate[2][2] = z[2];
view = rotate * translation * view;
return view;
}
//正交矩陣
mat4 MatrixOrtho( float top, float bottom, float left, float right, float Nearz, float Farz )
{
mat4 ortho = mat4::identity();
ortho[0][0] = 2 / ( right - left );
ortho[1][1] = 2 / ( top - bottom );
ortho[2][2] = 2 / ( Farz - Nearz );
ortho[0][3] = -( right + left ) / ( right - left );
ortho[1][3] = -( top + bottom ) / ( top - bottom );
ortho[2][3] = -( Nearz + Farz ) / ( Nearz - Farz );
return ortho;
}
//透視投影
mat4 MatrixPerspectiveFovLH( float FovAngleY, float Aspect, float Nearz, float Farz )
{
mat4 m = mat4::identity();
FovAngleY = FovAngleY / 180.0 * PI;
m[0][0] = Nearz / 2 * ( Aspect * tan(FovAngleY) * Nearz ) ;
m[1][1] = Nearz / 2 * ( tan(FovAngleY) * Nearz );
m[2][2] = ( Nearz + Farz ) / ( Nearz - Farz );
m[2][3] = -2 * Nearz * Farz / ( Farz - Nearz );
m[3][2] = -1;
m[3][3] = 0;
return m;
}
//視口變換
mat4 viewPort( const int width, const int height )
{
mat4 m = mat4::identity();
m[0][0] = width / 2;
m[0][3] = width / 2;
m[1][1] = height / 2;
m[1][3] = height / 2;
return m;
}
//繞z軸旋轉
mat4 mat4_rotate_z(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[0][0] = c;
m[0][1] = -s;
m[1][0] = s;
m[1][1] = c;
return m;
}
//繞y軸旋轉
mat4 mat4_rotate_y(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[0][0] = c;
m[0][2] = s;
m[2][0] = -s;
m[2][2] = c;
return m;
}
//繞x軸旋轉
mat4 mat4_rotate_x(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[1][1] = c;
m[1][2] = -s;
m[2][1] = s;
m[2][2] = c;
return m;
}
//縮放
mat4 mat4_scale(float sx, float sy, float sz)
{
mat4 m = mat4::identity();
m[0][0] = sx;
m[1][1] = sy;
m[2][2] = sz;
return m;
}
//平移
mat4 mat4_translate(float tx, float ty, float tz)
{
mat4 m = mat4::identity();
m[0][3] = tx;
m[1][3] = ty;
m[2][3] = tz;
return m;
}
reference
? Fundamentals of Computer Graphics 5th
? GAMES101
?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/533541.html
標籤:其他
上一篇:阿里云 ACK 接入觀測云
下一篇:Nginx優化與防盜鏈
