games101-三角形光柵化與抗鋸齒

haihaichibaola發表於2024-08-16

三角形光柵化和簡單抗鋸齒

一個場景中的模型,投影到我們螢幕上的過程就像是拍照。先把物體擺放好(M),然後設定好相機(V),形成透視關係(P),最後按下快門(視口)。這一系列做好之後,再進行光柵化,影像就會呈現到我們的螢幕上

首先進行MVP變換和視口變換。MVP變換是下面三種變換的總稱

  • Model:模型變換

    模型變換是將物體從區域性空間變換到世界空間的一個矩陣,也就是將模型從區域性座標系轉換到世界座標系。在世界空間中,物體具有*移、縮放和旋轉三種操作,對應T(Translate)矩陣、R(Rotate)矩陣、和S(Scale)三個矩陣。將座標點和變換矩陣相乘就可以得到變換後的結果

    對於一個二維影像中的點,我們假設對它進行線性變換

    \[x^{'}=ax+by\\ y^{'}=cx+dy \]

    寫成矩陣形式為

    \[\left[ \begin{matrix} x^{'}\\ y^{'} \end{matrix} \right]=\left[ \begin{matrix} a & b\\ c & d \end{matrix} \right]\left[ \begin{matrix} x\\ y \end{matrix} \right] \]

    我們常用的線性變換都可以用這種形式,列如縮放、切變,旋轉等

    但是當我們需要*移的時候,就會發現一個問題

    假設我們進行下圖中的*移

\[x^{'}=x+t_{x}\\ y^{'}=y+t_{y} \]

此時可以發現,我們只能將矩陣寫成下面的形式

\[\left[ \begin{matrix} x^{'}\\ y^{'} \end{matrix} \right]=\left[ \begin{matrix} a & b\\ c & d \end{matrix} \right]\left[ \begin{matrix} x\\ y \end{matrix} \right]+\left[ \begin{matrix} t_{x}\\ t_{x} \end{matrix} \right] \]

因為無論如何,透過乘的方式得到的項一定是\(ax+by\)的形式(*移變換不屬於線性變換),而這種形式是不利於計算的。我們不希望*移(仿射變換)成為一種特例,所以我們就需要引入一個新的概念:齊次座標

對於二維的點和向量,我們增加一個維度,將它表示成如下的形式

\[2Dpoint=(x,y,1)^{T}\\ 2D vector=(x,y,1)^{T} \]

則對於之前所述的變換,就有

\[\left( \begin{matrix} x^{'}\\ y^{'}\\ w^{'} \end{matrix} \right) =\left( \begin{matrix} 1 & 0 & t_{x} \\ 0 & 1 & t_{y} \\ 0 & 0 & 1 \end{matrix} \right) \cdot \left( \begin{matrix} x\\ y\\ 1 \end{matrix} \right) =\left( \begin{matrix} x+t_{x}\\ y+t_{y}\\ 1 \end{matrix} \right) \]

(向量的第三維度的座標為0是為了保護向量的*移不變性)

補充:

vector + vector = vector

point - point = vector

point + vector = point

point + point = ?

前三個沒有什麼問題,第四個我們得到的是兩個點的中點

\[\left( \begin{matrix} x\\ y\\ w \end{matrix} \right) 表示成二維的點= \left( \begin{matrix} x/w\\ y/w\\ 1 \end{matrix} \right) \]

兩個點在上述的公式下相加,第三維度的1會變成2,要把二變回1,就要除2。故表示的是中點

好了,回到之前的問題,我們現在開始表示S、R、T矩陣

S和T與二維情況類似

\[Scale = \left[ \begin{matrix} s_{x} & 0 & 0 & 0 \\ 0 & s_{y} & 0 & 0 \\ 0 & 0 & s_{z} & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \qquad Translate = \left[ \begin{matrix} 1 & 0 & 0 & t_{x} \\ 0 & 1 & 0 & t_{y} \\ 0 & 0 & 1 & t_{z} \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

對於R,需要進行單獨的討論

我們先討論繞x軸旋轉的

\[y=sin(θ+α)=sinθcosα+cosθsinα\\ z=cos( θ + α )=cosθcosα−sinθsinα \]

將y和z分別乘cosθ和sinθ

\[ycosθ=sinθcosθcosα+cos^{2}θsinα\\ zsinθ=sinθcosθcosα−sin^{2}θsinα \]

兩個式子做差,可以得到

\[sinα=ycosθ−zsinθ \]

同理,y和z分別乘sinθ和cosθ,相加可以得到

\[cosα=ysinθ+zcosθ \]

根據剛才推導得出的式子,則有

\[R_{x}(θ)=\left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & cosθ & -sinθ & 0 \\ 0 & sinθ & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

對於其它的座標軸,使用相似的方法可以得到

\[R_{y}(θ)=\left[ \begin{matrix} cosθ & 0 & sinθ & 0 \\ 0 & 1 & 0 & 0 \\ -sinθ & 0 & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

\[R_{z}(θ)=\left[ \begin{matrix} cosθ & -sinθ & 0 & 0 \\ sinθ & cosθ & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

(旋轉過特定軸使用羅德里格斯旋轉公式,先不討論)

由於座標系的轉換,T矩陣、R矩陣、和S矩陣三個矩陣的順序會影響我們變換的結果。假設說我們先進行*移,再進行旋轉,我們可以發現,由於旋轉矩陣的軸心點為(0,0,0),而*移之後模型的原點與世界座標系的原點不再重合,於是我們會得到錯誤的結果

所以我們必須按照S、R、T的順序(其它的組合透過簡單證明可以發現也會存在上述的問題)來形成M矩陣

\[M_{model\rightarrow world}=T\cdot R\cdot S \]

則有以下程式碼

Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

    // TODO: Implement this function
    // Create the model matrix for rotating the triangle around the Z axis.
    // Then return it.
    float x = rotation_angle * MY_PI / 180;
    Eigen::Matrix4f r;
    r<<cos(x),-sin(x),0,0,
    sin(x),cos(x),0,0,
    0,0,1,0,
    0,0,0,1;
    model = r * model;


    return model;
}
  • View:檢視變換

    經過模型變換,物體已經變換到世界座標系下。這時候我們就要確定相機的位置,並進行檢視變換

    其實這裡的相機,我個人理解為就是確定一個觀測位置,我們後續所做的就是把在這個觀測位置看到的東西投影到我們的螢幕上。所以我們每確定一個觀測矩陣,就代表著我們確定了一個相機。這裡我們只使用一個相機

    為了便於計算,我們需要把相機移動到世界座標系的原點位置。在空間中,如果相機和模型進行相同的變換,那麼他們之間的位置關係不會發生變化。也就是說,讓我們的觀測位置和模型進行相同的變換,使得在將相機移動到原點的同時,觀察到的模型形象不會變化(類似兩個相同速度的並列物體相互觀測對方不會發生變化)

    此時再去對相機展開一些較為細緻的思考。舉個例子,我們自己就是一臺攝像機,前方有一個物體。我們可以前後左右移動,也可以看向任意的方向,也可以進行左右歪頭。所以,對於相機,有一個位置向量\(\vec{e}\)來確定位置。然後我們認為相機位於位置向量的頂端,在這裡,我們規定了向量\(\vec{g}\)為相機的朝向,表示了向量看向的方向;向量\(\vec{t}\)為相機的向上方向,表示了歪頭的情況(可愛捏);最後,因為是三維空間,我們使用兩個向量就可以表示出相機處的位置狀態,所以叉乘在得到下圖中綠色的向量,便於表示而已,沒有意義。

這樣,我們就可以細化檢視變換。首先,根據向量\(\vec{e}\)把相機移動到原點,然後根據\(\vec{t}\)\(\vec{g}\)以及那個綠色的向量,讓他們旋轉回到x、y、z軸,並且讓物體和相機進行完全相同的操作。這樣,相機移動到了原點,以原點作為觀測位置看到的形態和沒有變換前也是一樣的。

則我們可以寫出

\[M_{view}=R_{view}T_{view} \]

\(\vec{e}\)移動到原點並不難實現,只是簡單的*移

\[T_{view}=\left[ \begin{matrix} 1 & 0 & 0 & -x_{e} \\ 0 & 1 & 0 & -y_{e} \\ 0 & 0 & 1 & -z_{e} \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

但旋轉就不好處理了,所以我們用一些新的方法

假設一個二維的旋轉矩陣

\[R_{θ}= \left( \begin{matrix} cosθ & -sinθ\\ sinθ & cosθ \end{matrix} \right) \]

如果我們旋轉\(-θ\)角,旋轉矩陣就會變為

\[R_{θ}= \left( \begin{matrix} cosθ & sinθ\\ -sinθ & cosθ \end{matrix} \right) =R^{T}_{θ} \]

定義有旋轉\(-θ\)角,就是旋轉\(θ\)角得到的旋轉矩陣的逆

\[R_{-θ}=R_{θ}^{-1}(definition) \]

而剛才我們發現,旋轉矩陣的逆等於旋轉矩陣的轉置(正交矩陣)。所以,對於我們要將相機旋轉回座標軸的操作,我們可以先計算將x、y、z旋轉到相機位置的矩陣,再將這個矩陣轉置,就可以得到將相機旋轉回座標軸的旋轉矩陣了

\[R_{view}^{-1}=\left[ \begin{matrix} x_{\hat{g}\times\hat{t}} & x_{t} & x_{-g} & 0 \\ y_{\hat{g}\times\hat{t}} & y_{t} & y_{-g} & 0 \\ z_{\hat{g}\times\hat{t}} & z_{t} & z_{-g} & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \Longrightarrow R_{view}=\left[ \begin{matrix} x_{\hat{g}\times\hat{t}} & y_{\hat{g}\times\hat{t}} & z_{\hat{g}\times\hat{t}} & 0 \\ x_{t} & y_{t} & z_{t} & 0 \\ x_{-g} & y_{-g} & z_{-g} & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

則可以寫出以下程式碼

Eigen::Matrix4f get_rotation(Vector3f axis,float angle){

    float rotation_angle_radian = angle * MY_PI / 180;
    //identity matrx
    Eigen::Matrix3f I = Eigen::Matrix3f::Identity();
    Eigen::Matrix3f N;
    N<<0,-axis(2),axis(1),
    axis(2),0,-axis(0),
    -axis(1),axis(0),0;
    

}
  • Projection:投影變換

    在檢視和投影都完成之後,物體已經擺放到了我們想要的位置,並且已經轉換到攝像機視角下了。我們就要進行最後一步變換——投影變換,利用投影矩陣,將攝像機看到的畫面投影到指定大小的空間內

    投影分為正交投影和透視投影。正交投影就是直接把物體從原來的檢視空間轉換到座標系中心\([-1,1]^{3}\)的一個立方體中

我們將原來的檢視空間假設為上圖中的長方體,用遠*、上下、左右六個變數來表示它。首先將其*移到原點,然後直接縮放為原點的立方體即可

\[M_{ortho}=\left[ \begin{matrix} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] \left[ \begin{matrix} 1 & 0 & 0 & -\frac{r+l}{2} \\ 0 & 1 & 0 & -\frac{t+b}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

正交投影是一種*行投影,類似於使用一束*行光把我提的影像垂直地投射到地面上。透視投影則可以產生*大遠小的效果

假設可視區域類似於左圖,我們在左圖中規定*和遠兩個*面。假設我們把遠*面擠壓到**面的大小,那麼如果說在遠**面上各有一個完全一樣的物體,那麼這個處在遠*面的物體就會看起來小一些。所以我們可以嘗試將視錐中位於*遠*面中的所有*面都按照一定比例縮放,擠壓成**面的大小。由於距離**面*的*面受擠壓的程度小,距離遠的擠壓程度大,將縮放形成的右圖中的長方體進行正交投影,就可以形成類似*大遠小的效果

為了能夠完成這個擠壓的操作,我們在這裡做一些定義

  1. **面永遠不變
  2. 遠*面的z軸座標不會發生變化
  3. 遠*面的中心點永遠不變

在擠壓的過程中,我們可以透過相似三角形發現一些規律

上圖是我們在側面對視錐的觀察情況。由圖中關係可知,視錐中的任意一個*面都與**面存在上述的關係。y座標是這樣,x也同理

我們要求出座標的投影矩陣,但是我們目前並不清楚z的情況

先將投影矩陣大體情況寫出來。x和y的變化情況我們已經知道,所以我們可以寫出上圖中的變換矩陣

這時,我們使用我們剛才的定義。因為**面上所有的點都不會發生變化,在進行透視投影時可以寫出下面的矩陣

\[\left( \begin{matrix} x\\ y\\ n\\ 1 \end{matrix} \right) \Longrightarrow \left( \begin{matrix} x\\ y\\ n\\ 1 \end{matrix} \right) == \left( \begin{matrix} nx\\ ny\\ n^{2}\\ n \end{matrix} \right) \]

整個投影矩陣我們只有第三行是未知的,所以我們對第三行進行單獨討論

\[(0 \quad 0\quad A\quad B) \left( \begin{matrix} nx\\ ny\\ n^{2}\\ n \end{matrix} \right) = n^{2} \]

又因為遠*面中心點\((0,0,f)\)在投影變換過程中也不會發生變化在進行透視投影時可以寫出下面的矩陣

\[\left( \begin{matrix} 0\\ 0\\ f\\ 1 \end{matrix} \right) \Longrightarrow \left( \begin{matrix} 0\\ 0\\ f\\ 1 \end{matrix} \right) == \left( \begin{matrix} 0\\ 0\\ f^{2}\\ f \end{matrix} \right) \]

對矩陣的第三行進行單獨討論,也會得到*似於**面的結果。聯立可得

\[An+B=n^{2}\\ Af+B=f^{2} \]

則我們可以解得第三行中兩個位置變數的值

\[\left( \begin{matrix} n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ 0 & 0 & n+f & -nf\\ 0 & 0 & 1 & 0 \end{matrix} \right) \]

則可以寫出以下程式碼

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar)
{
    // Students will implement this function

    Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
    float x = eye_fov * MY_PI / 180;
    float t = tan(x /2) * abs(zNear);
    float r = aspect_ratio * t;
    float l = -r;
    float b = -t;
    Eigen::Matrix4f translation;
    translation<<1,0,0,-(r+l)/2.0f,
    0,1,0,-(t+b)/2.0f,
    0,0,1,-(zNear+zFar)/2.0f,
    0,0,0,1;

    Eigen::Matrix4f scale;
    scale<<2.0f/(r-l),0,0,0,
    0,2.0f/(t-b),0,0,
    0,0,2.0f/(zNear-zFar),0,
    0,0,0,1;

    projection = scale * translation;

    Eigen::Matrix4f M;
    M<<zNear,0,0,0,
    0,zNear,0,0,
    0,0,zNear+zFar,-zNear*zFar,
    0,0,1,0;

    // TODO: Implement this function
    // Create the projection matrix for the given parameters.
    // Then return it.
    projection = projection * M;

    return projection;
}

至此,我們已經完成了MVP變換,物體和相機都已經準備好

下一步,我們就要將在已經處於原點立方體中的所有物體顯示到螢幕上,也就是進行光柵化

先對一些要用到的內容進行定義

通常使用垂直可視角度和寬高比表示出我們的視錐大小

也有下面的關係

在圖形學中,我們認為螢幕是一個畫素的二維陣列,而這個二維陣列的大小就是解析度(resolution)。螢幕是一種典型的光柵成像裝置

我們在這裡將畫素作為一個長寬為一的小方塊(其實不是)來理解,一個畫素有固定的顏色,用r\g\b表示。我們用每一個畫素塊的中心點來表示這個畫素的座標

而經常提到的光柵化(raster),就是把我們上面已經處理完成的場景呈現在螢幕上,也就是把上文中的立方體呈現在螢幕上

我們先進行視口變換,處理x和y座標軸,直接將這個立方體的寬高拉伸為height和width即可(由於立方體的中心在原點,所以我們也要進行*移,讓影像在經過拉伸和*移之後變成類似於上圖中的模樣)

\[\left( \begin{matrix} \frac{width}{2} & 0 & 0 & \frac{width}{2}\\ 0 & \frac{height}{2} & 0 & \frac{height}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{matrix} \right) \]

然後,我們要將現在處於螢幕上的類似於照片的情景打散成為畫素。這裡我們使用不同的三角形去拼成畫面,再用螢幕中的畫素去顯示三角形,就能將圖片呈現出來,所以,在進行光柵化的時,顯示這些三角形成為了必不可少的一步,我們需要考慮我們要顯示的三角形和螢幕畫素點之間的位置關係

先介紹透過取樣的方法實現。透過畫素中心點進行取樣,判斷畫素中心點是否在三角形內。我們先討論二維的情況,直接遍歷螢幕上三角形所在區域,如果中心點在三角形內,這個畫素點就染色。

可以透過叉積的方法判斷。三角形的三個頂點以順時針的順序兩兩組成一個向量,並用每個向量的起始點與要進行判斷的點組成一個新的向量。使三個向量都與新的向量進行相乘,得到的結果如果同號,就證明點在三角形內部

這裡也可以用老師的判斷方法(注意!要用小數值傳遞,抗鋸齒拆分出來小數坑了我一晚上)

static bool insideTriangle(float x, float y, const Vector3f* _v){
    Vector3f v[3];
    for(int i=0;i<3;i++)
        v[i] = {_v[i].x(),_v[i].y(), 1.0};
    Vector3f f0,f1,f2;
    f0 = v[1].cross(v[0]);
    f1 = v[2].cross(v[1]);
    f2 = v[0].cross(v[2]);
    Vector3f p(x,y,1.);
    if((p.dot(f0)*f0.dot(v[2])>0) && (p.dot(f1)*f1.dot(v[0])>0) && (p.dot(f2)*f2.dot(v[1])>0))
        return true;
    return false;
}

將原來的二維*面升維,並將三角形的三個頂點和需要判斷的點轉換為起點為原點,指向頂點和待判斷點的四個向量。三角形的每兩個向量組成一個*面,和原來的三角形*面形成了一個三稜錐。此時可以想象,如果待判斷點在三角形內部,則原點指向這個點的向量也在三稜錐內部,並且這個向量和其它三個向量組成的三個*面的法向量的叉積都是同號的,反之則會由一個異號

此時可以得到類似於這樣的圖,有很嚴重的鋸齒。出現這個問題的原因是因為我們對於訊號的取樣率不夠,導致了走樣。我們要進行抗鋸齒(aliasing),也就是反走樣,來減少影像中鋸齒的產生。在這之前,我們先要對原因進行一些分析

上圖中有五個不同頻率的函式,我們對它們使用相同的方式進行取樣。可以發現取樣點可以大致呈顯出低頻率的函式的影像,但隨著函式頻率的增高取樣點恢復出來的函式影像與真實的函式影像的差距越來越大。透過這個例子我們可以知道,對於高頻率的函式應該使用較高的取樣率,低頻率的函式應該使用較低的取樣率

對上圖的藍色曲線進行取樣,取樣的結果是白色的取樣點。而對黑色曲線進行取樣,得到的也是相同的白色取樣點。也就是說,使用同樣的取樣方法對兩個函式進行取樣,得到的結果無法區分,這種情況就稱為走樣

濾波就是去掉一系列頻率,而傅立葉變換可以把函式變為不同頻率的段。我們先可以使用傅立葉變換,將影像由時域變換到頻域

上圖中右側就是左側影像在頻域上展開的結果(影像的中心為低頻率,四周為高頻率,使用亮度表示頻率出現的情況;分析訊號時會認為訊號是一個無限重複的週期性訊號,為了滿足這個,我們把圖片在水*方向上拼接無限多個,豎直方向也拼接無限多個;觀察右圖發現,出現較明顯的豎槓是因為圖片的拼接處產生了較強的變化。中心較亮說明這張圖片集中在低頻)

我們現在嘗試開始濾波。我們先去除低頻訊號(高通濾波)

可以發現,去除低頻訊號後進行逆傅立葉變換得到的影像只顯示出了一些邊界(變化大的區域)

再去除高頻訊號看看(低通濾波)

可以發現去除高頻訊號之後,影像呈現出了模糊的效果

從上文看出,濾波就是去除掉一些特定的頻率資訊。而從另外的角度分析,濾波(filtering)=*均(averaging)=卷積(convolution)

上圖就大致描述了卷積的過程,其實也就是加權*均。不過這個是在時域上,卷積具有對偶性

時域上的乘積等於頻域上的卷積,反之亦然

就如這張圖中所描述,我們可以取一個畫素周圍3*3的區域(上圖中的表格為卷積核)進行卷積,也可以將原圖和我們的卷積核都進行傅立葉變換並進行相乘,再進行逆傅立葉變換也得到了類似的效果

取樣其實就是在重複頻域或者頻域上的內容

我們將a與c (衝激函式)相乘,得到的是一系列取樣點。而前面得知時域上的相乘也對應了頻域上的卷積。我們將a與c傅立葉變換得到的c和d圖進行卷積操作,得到的就是f圖。可以發現,取樣是把原來的頻譜給複製貼上了很多

衝激函式在頻域的間隔是時域的倒數。取樣頻率低,衝激函式時域寬,對應的頻域就窄,會造成上圖中訊號重疊的現象,也就是說,訊號發生了走樣

增加取樣率會使重疊情況減緩,也可以將高頻位置的訊號直接去除。也就是說,我們可以提高螢幕的解析度,增加畫素點的密集程度,可以減緩鋸齒(走樣)的產生,但這在大部分形況下不現實;我們選擇使用濾波的方式(低通濾波)濾除高頻訊號。上文中可以看到,我們模糊的操作,類似於濾波濾除高頻訊號,類似於卷積

相比較於失真,丟失部分細節顯得更好一些

所以介紹了這麼多,我們得出結論,使用模糊的方式,來減少我們光柵化三角形時出現的走樣問題。最直接的理解方式,就是對每一個畫素進行*均

根據三角形覆蓋畫素小方塊的面積大小,決定畫素小方塊的亮度,類似於*均

這個原理*似於接下來要介紹的抗鋸齒方法:MSAA(Multisampling Antialiasing)

這個方法可以理解為將一個物理畫素劃分為多個小畫素,在取每個小畫素的中心為取樣點。假設我們設定MSAA$\times$4的話,那麼每個物理畫素就有了四個取樣點。三角形對這些取樣點覆蓋情況就可以*似為對每一個物理畫素的覆蓋情況(點越多越精確),就達到了模糊的效果

至此,我們已經可以完成一個三角形的簡單的光柵化和抗鋸齒

下面是程式碼

由於老師的框架中函式傳入的是三角形物件,所以先看三角形類的程式碼。

class Triangle{

public:
    Vector3f v[3]; /*the original coordinates of the triangle, v0, v1, v2 in counter clockwise order*/
    /*Per vertex values*/
    Vector3f color[3]; //color at each vertex;
    Vector2f tex_coords[3]; //texture u,v
    Vector3f normal[3]; //normal vector for each vertex

    //Texture *tex;
    Triangle();

    void setVertex(int ind, Vector3f ver); /*set i-th vertex coordinates */
    void setNormal(int ind, Vector3f n); /*set i-th vertex normal vector*/
    void setColor(int ind, float r, float g, float b); /*set i-th vertex color*/
    Vector3f getColor() const { return color[0]*255; } // Only one color per triangle.
    void setTexCoord(int ind, float s, float t); /*set i-th vertex texture coordinate*/
    std::array<Vector4f, 3> toVector4() const;
};
Triangle::Triangle() {
    v[0] << 0,0,0;
    v[1] << 0,0,0;
    v[2] << 0,0,0;

    color[0] << 0.0, 0.0, 0.0;
    color[1] << 0.0, 0.0, 0.0;
    color[2] << 0.0, 0.0, 0.0;

    tex_coords[0] << 0.0, 0.0;
    tex_coords[1] << 0.0, 0.0;
    tex_coords[2] << 0.0, 0.0;
}

void Triangle::setVertex(int ind, Vector3f ver){
    v[ind] = ver;
}
void Triangle::setNormal(int ind, Vector3f n){
    normal[ind] = n;
}
void Triangle::setColor(int ind, float r, float g, float b) {
    if((r<0.0) || (r>255.) ||
       (g<0.0) || (g>255.) ||
       (b<0.0) || (b>255.)) {
        fprintf(stderr, "ERROR! Invalid color values");
        fflush(stderr);
        exit(-1);
    }

    color[ind] = Vector3f((float)r/255.,(float)g/255.,(float)b/255.);
    return;
}
void Triangle::setTexCoord(int ind, float s, float t) {
    tex_coords[ind] = Vector2f(s,t);
}

std::array<Vector4f, 3> Triangle::toVector4() const
{
    std::array<Eigen::Vector4f, 3> res;
    std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) { return Eigen::Vector4f(vec.x(), vec.y(), vec.z(), 1.f); });
    return res;
}

類中封裝並在類外實現了toVector4()函式,我們直接呼叫就能獲取三角形的各個頂點座標

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    int min_x, max_x, min_y, max_y;
    min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
    max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
    min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
    max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
    //這裡找出三角形的所在區域,避免不必要的遍歷

    
    // TODO : Find out the bounding box of current triangle.
    //  iterate through the pixel and find if the current pixel is inside the triangle
   for (int x = min_x; x <= max_x; x++)
   {
       for (int y = min_y; y <= max_y; y++)
       {
           //判斷畫素中心點是否在連續三角形內,若在三角形內,就嘗試對該畫素進行著色,若深度測試透過,便著色
           if (insideTriangle(x + 0.5, y + 0.5, t.v))
           {
               // If so, use the following code to get the interpolated z value.
               auto tup = computeBarycentric2D((float)x + 0.5, (float)y + 0.5, t.v);
               float alpha;
               float beta;
               float gamma;
               std::tie(alpha, beta, gamma) = tup;
               float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
               float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
               z_interpolated *= w_reciprocal;
               //深度測試,透過便著色,並同時將深度存入快取
               //這裡有個細節之前沒注意,就是buf的取值要用get_index函式
               if (depth_buf[get_index(x, y)] > z_interpolated)
               {
                   // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                   //深度存入快取
                   depth_buf[get_index(x, y)] = z_interpolated;
                   Vector3f point = { (float)x,(float)y,z_interpolated};
                   Vector3f color = t.getColor();
                   //著色
                   set_pixel(point, color);
               }
           }
       }
   }

    // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}

insideTriangle函式在上文中已經提及。如果返回true,則證明這個點在三角形內,我們就進行深度測試(還未涉及,後面再講),透過就進行著色

這樣,我們就可以得到下面的影像

甚至不用放大就可以看到很多鋸齒

接下來要進行抗鋸齒。我們就使用剛才所提到的MSAA方法

int mass_w=2;
int mass_h=2;
const int max_count=mass_h*mass_w;
float pixel_step=1.0/mass_w;
float start_point=pixel_step/2.0;

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    float min_x=v[0].x();
    float max_x=v[0].x();
    float min_y=v[0].y();
    float max_y=v[0].y();
    for(int i=0;i<3;i++){
        min_x=std::min(min_x,v[i].x());
        max_x=std::max(max_x,v[i].x());
        min_y=std::min(min_y,v[i].y());
        max_y=std::max(max_y,v[i].y());
    }//找到邊界
   
    for(int x=min_x;x<max_x;x++){
        for(int y=min_y;y<max_y;y++){//per piexs
            float the_num=0.0f;
            for(float x_step=start_point;x_step<1;x_step+=pixel_step){
                for(float y_step=start_point;y_step<1;y_step+=pixel_step){
                    if(insideTriangle(x+x_step,y+y_step,t.v)){
                        the_num+=1.0f;//計算在三角形內部的畫素點的數量
                    }
                }
            }
            if(the_num>0){//有畫素分點在三角形內部
                auto[alpha,beta,gamma]=computeBarycentric2D(x,y,t.v);
                float w_reciprocal = 1.0/(alpha / v[0].w()+beta / v[1].w() + gamma / v[2].w());
                float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                z_interpolated*=w_reciprocal;
                int idx=get_index(x,y);
                if(z_interpolated<depth_buf[idx]){
                    Eigen::Vector3f p;
                    p<<x,y,z_interpolated;
                    if(the_num!=4)cout<<the_num<<endl;
                    set_pixel(p,t.getColor()*(the_num/max_count));
                    depth_buf[idx]=z_interpolated;
                }
            }
            
        }
    }
    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle
 
    // If so, use the following code to get the interpolated z value.
    //auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
    //float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
    //float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
    //z_interpolated *= w_reciprocal;
 
    // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}

得到下面的影像

可以看到鋸齒現象得到了明顯的改善,但是出現了我們不喜歡的黑邊

產生這個的原因是因為原本位於上面的畫素使用了抗鋸齒,著色比例下降模糊後直接忽略了重疊的現象。

所以我們需要維護子畫素的顏色,直接使用物理畫素進行判定是不合理的。一般情況下,將子畫素的顏色進行記錄後取*均值即可

    for(int x=0;x<height;x++){
        for(int y=0;y<width;y++){
            //當前的畫素的顏色,由子畫素決定
            Eigen::Vector3f color={0.0,0.0,0.0};
            for(float x_s=start_point;x_s<1.0f;x_s+=pixel_step){
                for(float y_s=start_point;y_s<1.0f;y_s+=pixel_step){
                    int idx=get_index_ssaa(x,y,x_s,y_s);
                    color+=frame_buf_ssaa[idx];
                }
            }
            Eigen::Vector3f p;
            p<<x,y,0.0f;//設定最*的距離0
            set_pixel(p,color/(max_count));//設定*均顏色
        }
    }

這時我們可以發現,成功去除了黑邊

有一個問題,兩個三角形的順序似乎搞反了

發現問題在於深度陣列初始化為無窮大。我們的視角假設是向-z的方向觀察的,而我們進行深度檢測時設定的值小的覆蓋,這導致我們的影像重疊順序反了,更改為無窮小和大的覆蓋即可

相關文章