首先是main函数中的矩阵变换函数,这个部分在作业1已经实现过了,可以直接进行复制。
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
// TODO: Copy-paste your implementation from the previous assignment.
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
Matrix4f pro(4, 4);
//透视变换
pro << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear * zFar,
0, 0, 1, 0;
Matrix4f orth(4, 4);
float t = tanf(eye_fov / 2) * zNear * 2;
float width = aspect_ratio * t;
//平移矩阵
Matrix4f move(4, 4);
move << 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, -(zFar + zNear) / 2,
0, 0, 0, 1;
//缩放矩阵
orth <<
2 / width, 0, 0, 0,
0, 2 / t, 0, 0,
0, 0, 2 / (zFar - zNear), 0,
0, 0, 0, 1;
//注意乘积的顺序
projection = orth * move * pro;
return projection;
}
光栅化部分:
①检测当前点是否在所给三角形内
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
Vector3f ans1, ans2, ans3, vec1, vec2, vec3;
vec1 = _v[1] - _v[0];
ans1 = vec1.cross(Vector3f(x - _v[0][0], y - _v[0][1], 0));
vec2 = _v[2] - _v[1];
ans2 = vec2.cross(Vector3f(x - _v[1][0], y - _v[1][1], 0));
if (ans1.transpose() * ans2 < 0)
return false;
vec3 = _v[0] - _v[2];
ans3 = vec3.cross(Vector3f(x - _v[2][0], y - _v[2][1], 0));
if (ans1.transpose() * ans3 < 0)
return false;
return true;
}
思路:参照课程ppt,利用向量叉乘的性质对其进行判断。
若 “三角形的三个边对应的向量” 分别叉乘 “顶点到目标点所成向量” 所得向量均为同方向,则说明该点在所给三角形内部;反之,则说明点在三角形外部。
(本质就是“同侧 异侧”的问题,两向量交换位置,使用右手定则可以判断两次叉乘所得向量的方向相反,那么所得结果再相乘就会是负数)
②采样部分
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
//获取三角形的边界(外接矩形)
int x_min = min(v[0].x(), max(v[1].x(), v[2].x()));
int x_max = max(v[0].x(), max(v[1].x(), v[2].x()));
int y_min = min(v[0].y(), min(v[1].y(), v[2].y()));
int y_max = max(v[0].y(), min(v[1].y(), v[2].y()));
/*int width = x_max - x_min + 1;
int height = y_max - y_min + 1;*/
// 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 i = x_min; i <= x_max; ++i)
for (int j = y_min; j < y_max; ++j) {
//如果该点在三角形内,则进行进一步判断
if (insideTriangle(i+0.5, j+0.5, t.v)) {
auto [alpha, beta, gamma] = computeBarycentric2D(i+0.5, j+0.5, 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 cur_index = get_index(i, j);
//如果插值所得的该点深度小于缓存的深度,则进行替换(表示该点更近!)
if (z_interpolated < depth_buf[cur_index]) {
depth_buf[cur_index] = z_interpolated;
//这个point只是为了调用函数set_pixel而创建,只用到了x和y(用来获取该点在颜色储存数组中的对应位置并进一步更新),其z值实际上并没有任何作用,可以任意设值
Vector3f point;
point << i, j, z_interpolated;
/*frame_buf[cur_index] = t.getColor();*/
//设置对应的颜色
set_pixel(point, t.getColor());
}
}
}
}
几个坑点:
insideTriangle(x, y, t.v) 函数中,实际检测点的坐标应该是(x+0.5,y+0.5)
上课有提到坐标对应的是左下角点,检测应输入中心点坐标!
computeBarycentric2D(x, y, t.v) 也是如此!
ps:有些朋友可能不知道computeBarycentric2D(x, y, t.v) 有什莫用,其实就是为了获得该点的z值而进行的相关计算,所以也应该填入当前点对应的像素中心坐标!
附加题:
使用MASS:
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
//计算bounding box 边界(外接矩形)
int x_min = min(v[0].x(), max(v[1].x(), v[2].x()));
int x_max = max(v[0].x(), max(v[1].x(), v[2].x()));
int y_min = min(v[0].y(), min(v[1].y(), v[2].y()));
int y_max = max(v[0].y(), min(v[1].y(), v[2].y()));
/*int width = x_max - x_min + 1;
int height = y_max - y_min + 1;*/
//我们一个正方体继续划分成了四个小正方体
// pos数组记录的是相对于左下角点的坐标偏移量
float pos[][2] = { {0.25,0.25},{0.25,0.75}, {0.75,0.25}, {0.75, 0.75} };
for (int i = x_min; i <= x_max; ++i)
for (int j = y_min; j < y_max; ++j) {
float count = 0; //计数器 用于记录4个子采样点在三角形中的个数
float min_distance = FLT_MAX;//用于记录四个子采样点中最小的z值
for (int l = 0; l < 4; ++l) {
if (insideTriangle((float)i + pos[l][0],(float) j + pos[l][1], t.v)) {
count+=1;
auto [alpha, beta, gamma] = computeBarycentric2D(i + pos[l][0], j + pos[l][1], 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;
//更新最小的z值
min_distance = min(z_interpolated, min_distance);
}
}
//如果四个子采样点中至少有一个在三角形内
if (count > 0) {
int cur_index = get_index(i, j);
//且该点的最小z坐标比depth_buf中的还要小(更近)
//则进行更新
if (min_distance < depth_buf[cur_index]) {
depth_buf[cur_index] = min_distance;
Vector3f point;
point << i, j, 1;
/*frame_buf[cur_index] = t.getColor();*/
//这里我对set_pixel函数做了些更改,后文细说
set_pixel(point, t.getColor(),count);
}
}
}
}
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color,float count)
{
//old index: auto ind = point.y() + point.x() * width;
auto ind = (height-1-point.y())*width + point.x();
//count 其实就相当于起到一个 ”比例“的作用!
frame_buf[ind] = color *count /4.0;
}
注意点:
在MASS中,我们将一个小正方体切割成四个更小的正方体!
锚点坐标仍然是左下角的位置,使用了pos数组来储存四个点的相对位置偏移量!
count变量是用来记录当前四个子采样点中在三角形内的个数,后续set_pixel设置颜色的效果也会根据count的大小来调整相应的颜色比例。
注意注意注意!!!
insideTriangle(x,y,v.t) 需要进行调整,其接收变量类型由int设置为float!不然会自动取整没有效果!!!
set_pixel()函数也需要修改,如上图代码所示!
效果对比:
原:
使用MASS后:
虽然边界抗锯齿好多了,但是在颜色重叠出出现了”黑点“,有些小问题。