Abstract
相比作業1僅進行線框的繪制,作業2要求通過光柵化技術對三角形內部的顏色進行填充,並通過z-buffer技術實現遮擋效果,實現MSAA多重采樣抗鋸齒當然更好。
Reference:
閆教授已為我們於main函數中將需繪制的兩個三角形的頂點坐標、索引、以及頂點顏色值定義好了。
注意到rasterizer的draw函數中將rasterize_wireframe函數換成了rasterize_triangle,傳入的實參依然是構造好的Triangle實例:
void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
...
rasterize_triangle(t);
}
如何對三角形內部點進行光柵化呢?在閆教授的提示下,我們采用一個bounding box來將三角形框住
對於每個三角形,我們對其頂點的x與y值進行遍歷,並找出x_min、x_max、y_min以及y_max。
由於頂點坐標是浮點型,為了不丟失信息,對於"min"值,bounding box的邊界進行向下取整;對於"max"值,bounding box的邊界進行向上取整:
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
// x_l = x_min ; x_r = x_max ; y_b = y_min ; y_t = y_max
int x_l = std::floor(std::min(v[0][0], std::min(v[1][0], v[2][0])));
int x_r = std::ceil(std::max(v[0][0], std::max(v[1][0], v[2][0])));
int y_b = std::floor(std::min(v[0][1], std::min(v[1][1], v[2][1])));
int y_t = std::ceil(std::max(v[0][1], std::max(v[1][1], v[2][1])));
...
接下來用兩層循環來遍歷bounding box的所有點。我們需要判斷當前點是否被包含在三角形內部,如果包含,則對它進行着色:
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
...
for(int x = x_l ; x <= x_r ; x++)
for(int y = y_b ; y <= y_t ; y++) {
if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
...
}
}
由於(x,y)表示當前點像素的左下角坐標,我們需要判斷的是當前點的中心是否在三角形內:
如何實現insideTriangle呢:
insideTriangle
static bool insideTriangle(double x, double y, const Vector3f* _v)
{
Vector2f point(x, y);
Vector2f AB = _v[1].head(2) - _v[0].head(2);
Vector2f BC = _v[2].head(2) - _v[1].head(2);
Vector2f CA = _v[0].head(2) - _v[2].head(2);
Vector2f AP = point - _v[0].head(2);
Vector2f BP = point - _v[1].head(2);
Vector2f CP = point - _v[2].head(2);
return AB[0] * AP[1] - AB[1] * AP[0] > 0
&& BC[0] * BP[1] - BC[1] * BP[0] > 0
&& CA[0] * CP[1] - CA[1] * CP[0] > 0;
}
向函數傳入需判斷的點,以及三角形的頂點數組。我們算出三角形三條邊的向量值(逆時針),並算出需判斷點與三角形三頂點分別連線的向量值。
向量兩兩對應做叉積,只要叉積結果符號均相同,則說明判斷點在三角形內部。
只要點在三角形內部,我們馬上對當前點的深度進行插值計算:
if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
auto[alpha, beta, gamma] = computeBarycentric2D((float)x + 0.5f, (float)y + 0.5f, t.v);
float w_reciprocal = 1.0f/(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;
}
插值算法閆教授為我們封裝進一個名叫computeBarycentric2D的函數了。它接受一個三角形內部點的x、y值,以及該三角形的頂點坐標數組作為參數:
computeBarycentric2D
static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
float xp = x, yp = y;
float xa = v[0].x(), ya = v[0].y();
float xb = v[1].x(), yb = v[1].y();
float xc = v[2].x(), yc = v[2].y();
float gamma = ((xb - xa) * (yp - ya) - (xp - xa) * (yb - ya)) /
((xb - xa) * (yc - ya) - (xc - xa) * (yb - ya));
float beta = (xp - xa - gamma * (xc - xa)) / (xb - xa);
float alpha = 1.0f - beta - gamma;
return {alpha,beta,gamma};
}
此處的插值算法涉及到"重心坐標"的計算,你會發現我在這里放置的實現與框架的實現不一樣,我實現了一個稍微快一點的版本:淺談重心坐標
回到之前的代碼:
if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
auto[alpha, beta, gamma] = computeBarycentric2D((float)x + 0.5f, (float)y + 0.5f, t.v);
float w_reciprocal = 1.0f/(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;
}
回憶作業1解析手記對draw函數的分析過程,我們僅是對兩個三角形的三個頂點進行了投影變換,之后才在這三個頂點內部進行插值着色。雖然最后的光柵化步驟僅在2D平面內進行,理論上只需三個頂點的(x,y)值便能完成插值計算,但是為了實現z-buffer,我們沒有將原頂點的z值丟棄,而是將它們一齊進行了投影變換,並被用來插值三角形內部點的深度值,但這會帶來一個問題。
根據閆教授的B站課程Lecture 09-17:38位置,重心坐標並不具備變換一致性,也即,經過變換后的三個頂點的重心坐標和變換前的對應原頂點的重心坐標並不相同,這會導致,變換后的頂點插值出來的內部點的z深度會和變換前的頂點的深度插值結果不一致。而我們容易知道,用來進行遮擋判斷的z值應該是變換前的原值,所以在光柵化這一步驟,我們不應計算變換后的插值z,而是通過當前插值點算出其變換前的z值。
關於作業2 MSAA網上實現的版本,是不太可靠的,具體可看我在淺談MSAA抗鋸齒中的討論。