作业2在作业1的基础上,增加了遮挡情况,且需要填充三角形
总览
在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是
那么的有趣。所以这一次我们继续推进一步——在屏幕上画出一个实心三角形,
换言之,栅格化一个三角形。上一次作业中,在视口变化之后,我们调用了函数rasterize_wireframe(const Triangle& t)
。但这一次,你需要自己填写并调用函数 rasterize_triangle(const Triangle& t)
。
该函数的内部工作流程如下:
- 创建三角形的 2 维 bounding box。
- 遍历此 bounding box 内的所有像素(使用其整数索引)。然后,使用像素中
心的屏幕空间坐标来检查中心点是否在三角形内。 - 如果在内部,则将其位置处的插值深度值 (interpolated depth value) 与深度
缓冲区 (depth buffer) 中的相应值进行比较。 - 如果当前点更靠近相机,请设置像素颜色并更新深度缓冲区 (depth buffer)。
你需要修改的函数如下:
• rasterize_triangle()
: 执行三角形栅格化算法
• static bool insideTriangle()
: 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。
因为我们只知道三角形三个顶点处的深度值,所以对于三角形内部的像素,我们需要用插值的方法得到其深度值。我们已经为你处理好了这一部分,因为有关这方面的内容尚未在课程中涉及。插值的深度值被储存在变量 z_interpolated
中。
请注意我们是如何初始化 depth buffer 和注意 z values 的符号。为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。
在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。最后,我们提供了两个 hard-coded 三角形来测试你的实现,如果程序实现正确,你将看到如下所示的输出图像:
开始编写
在你自己的计算机或虚拟机上下载并使用我们更新的框架代码。你会注意到,在 main.cpp 下的get_projection_matrix()
函数是空的。请复制粘贴你在第一次作业中的实现来填充该函数。
作业代码
get_projection_matrix
函数,与作业一一样,具体讲解可以看我作业一的文章
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();
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Compute l, r, b, t
float t = tan((eye_fov*MY_PI/180)/2) * fabs(zNear);
float r = aspect_ratio * t;
float l = -r;
float b = -t;
// std::cout<<t<<" "<<r<<" "<<" "<<l<<" "<<b<<std::endl;
// Orthographic projection
// Translate to origin
Eigen::Matrix4f translate = Eigen::Matrix4f::Identity();
translate(0,3) = -(r+l)/2;
translate(1,3) = -(t+b)/2;
translate(2,3) = -(zNear+zFar)/2;
// Sclae to [-1,1]^3
Eigen::Matrix4f scale = Eigen::Matrix4f::Identity();
scale(0,0) = 2/(r-l);
scale(1,1) = 2/(t-b);
scale(2,2) = 2/(zNear-zFar);
// get Orthographic projection
Eigen::Matrix4f ortho = scale * translate;
// std::cout<<"Orthographic:"<<std::endl<<ortho<<std::endl;
// Perspective projection
// get Matrix_persp2ortho
Eigen::Matrix4f persp2ortho = Eigen::Matrix4f::Zero();
persp2ortho(0,0) = zNear;
persp2ortho(1,1) = zNear;
persp2ortho(2,2) = zNear+zFar;
persp2ortho(2,3) = -zNear*zFar;
persp2ortho(3,2) = 1;
// std::cout<<persp2ortho<<std::endl;
// get Perspective projection
projection = ortho * persp2ortho;
// std::cout<<"Perspective:"<<std::endl<<projection<<std::endl;
// Then return it.
return projection;
}
main
函数部分,需要注意的是,和作业一一样需要把 zNear
和 zFar
由正数改为负数,因为课程中的透视投影矩阵是按 zNear
和 zFar
推导的,如果不改为负数,会发现三角形颠倒
r.set_projection(get_projection_matrix(45, 1, -0.1, -50));
下面进入 rasterizer.cpp
insideTriangle
函数判断当前像素是否在三角形内部,具体原理如下
static bool insideTriangle(int x, int 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]
float x_mid = x + 0.5;
float y_mid = y + 0.5;
Eigen::Vector2f CA(_v[0].x()-_v[2].x(), _v[0].y()-_v[2].y());
Eigen::Vector2f AP(x_mid -_v[0].x(), y_mid -_v[0].y());
Eigen::Vector2f AB(_v[1].x()-_v[0].x(), _v[1].y()-_v[0].y());
Eigen::Vector2f BP(x_mid-_v[1].x(), y_mid-_v[1].y());
Eigen::Vector2f BC(_v[2].x()-_v[1].x(), _v[2].y()-_v[1].y());
Eigen::Vector2f CP(x_mid-_v[2].x(), y_mid-_v[2].y());
// cross product have equal sign
if ((CA.x()*AP.y()-CA.y()*AP.x()>=0) == (AB.x()*BP.y()-AB.y()*BP.x()>=0) && (AB.x()*BP.y()-AB.y()*BP.x()>=0) == (BC.x()*CP.y()-BC.y()*CP.x()>=0)) {
return true;
} else {
return false;
}
}
rasterize_triangle
找出三角形的 bounding box,然后遍历 bounding box 里的每一个像素,判断其是否在三角形内,如果在三角形内部,再判断其深度是否能更新 Z_Buffer
,如果能更新,就把该点涂上该三角形的颜色。需要注意,这里需要注意,要把 depth_buf
初始为0,然后深度越大离相机越近(具体原因不太懂,有时间深入研究一下)。
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
// TODO : Find out the bounding box of current triangle.
int boundingbox_x_left = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
int boundingbox_x_right = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
int boundingbox_y_left = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
int boundingbox_y_right = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
// iterate through the pixel and find if the current pixel is inside the triangle
for (auto x=boundingbox_x_left; x<=boundingbox_x_right; ++x)
for (auto y=boundingbox_y_left; y<=boundingbox_y_right; ++y) {
std::cout<<x<<" "<<y<<std::endl;
if (insideTriangle(x, y, t.v)) {
// 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();
// std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
z_interpolated *= w_reciprocal;
// std::cout<<"z: "<<z_interpolated<<std::endl;
// z_interpolated = std::fabs(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.
if (z_interpolated > depth_buf[get_index(x,y)]) {
depth_buf[get_index(x,y)] = z_interpolated;
set_pixel(Eigen::Vector3f(x, y, z_interpolated), t.getColor());
}
}
}
}
这里有用到重心坐标插值获得采样点深度,这个部分的代码如下:
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;
computeBarycentric2D
函数就是获得该像素点对应的重心坐标
(
α
,
β
,
γ
)
(\alpha,\beta,\gamma)
(α,β,γ),其具体代码如下
static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
return {c1,c2,c3};
}
其具体计算就是下面公式推导出来的
之后就是
α
,
β
,
γ
\alpha,\beta,\gamma
α,β,γ 分别乘上对应深度在求和,就是该点的插值深度。
至于为什么这里是 z 值越大越靠前,是因为其在 draw
中,有一处变换了 z
值,这里 f1
为 (50 - 0.1)/2.0
,f2
为 (50 + 0.1)/2.0
,而 z
因为透视投影变换,已经变为 [-1,1]
,因为 z
之前是负的,投影变换之后还是负数,而越小,其经过下面变换之后,变为正数就越大。所以越近,其变换后 z 值越大。
vert.z() = vert.z() * f1 + f2;
其结果如下:
可以看到放大后,边缘有锯齿,这就是出现走样问题
进阶代码
使用 2x2 MSAA 缓解走样问题,即对每个像素进行 2x2 超采样,看一个像素的四个部分,有几个部分在三角形内,就把该点像素颜色乘上对应占比。(其实就是进行一次模糊操作)
因为存在遮挡问题,如果不处理,会出现下方异常黑线的问题
所以对于每个超像素,也要考虑其深度信息,rasterizer.hpp
添加以下变量
std::vector<Eigen::Vector3f> msaa_frame_buf;
std::vector<float> msaa_depth_buf;
其大小为之前 buffer
的四倍。
rasterize_triangle
函数,依旧遍历三角形的 bounding box,但对于每个像素,要进行 2x2 超采样,并且要考虑每个超采样像素的深度,最后像素的颜色等于每个部分按四分之一相加。
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
// TODO : Find out the bounding box of current triangle.
int boundingbox_x_left = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
int boundingbox_x_right = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
int boundingbox_y_left = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
int boundingbox_y_right = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
int n = 2;
int m = 2;
// iterate through the pixel and find if the current pixel is inside the triangle
for (auto x=boundingbox_x_left; x<=boundingbox_x_right; ++x)
for (auto y=boundingbox_y_left; y<=boundingbox_y_right; ++y) {
// std::cout<<x<<" "<<y<<std::endl;
// if (insideTriangle(x, y, t.v)) {
// // 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();
// // std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
// z_interpolated *= w_reciprocal;
// // std::cout<<"z: "<<z_interpolated<<std::endl;
// // z_interpolated = std::fabs(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.
// if (z_interpolated < depth_buf[get_index(x,y)]) {
// depth_buf[get_index(x,y)] = z_interpolated;
// set_pixel(Eigen::Vector3f(x, y, z_interpolated), t.getColor());
// }
// }
int blockinTriangle = 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();
// std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
z_interpolated *= w_reciprocal;
if ((blockinTriangle=MSAA(x, y, t.v, n, m, z_interpolated, t.getColor())) > 0) {
// std::cout<<"blockinTriangle: "<<blockinTriangle<<std::endl;
// std::cout<<"z: "<<z_interpolated<<std::endl;
// z_interpolated = std::fabs(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.
int idx = get_index(x, y);
set_pixel(Eigen::Vector3f(x, y, z_interpolated), (msaa_frame_buf[idx*4]+msaa_frame_buf[idx*4+1]+msaa_frame_buf[idx*4+2]+msaa_frame_buf[idx*4+3])/4.0);
}
}
}
MSAA
函数如下,四个超采样像素的起点分别是,(x, y),(x, y+0.5),(x+0.5, y),(x+0.5, y+0.5)。只有深度大于 msaa_depth_buf
才考虑是否在三角形内部。最后返回满足的超采样像素个数
int rst::rasterizer::MSAA(int x, int y, const Vector3f* _v, int n, int m, float z, Eigen::Vector3f color)
{
float size_x = 1.0/n; // the size_x of every super sample pixel
float size_y = 1.0/m;
int blocksinTriangle = 0;
for(int i=0; i<n; ++i)
for(int j=0; j<m; ++j) {
if (z>msaa_depth_buf[get_index(x,y)*4 + i*2 + j] && insideTriangle(x+i*size_x, y+j*size_y, _v, size_x, size_y)) {
msaa_depth_buf[get_index(x,y)*4 + i*2 +j] = z;
msaa_frame_buf[get_index(x,y)*4 + i*2 +j] = color;
blocksinTriangle ++;
}
// std::cout<<blocksinTriangle<<std::endl;
// std::cout<<"true"<<std::endl;
// } else {
// std::cout<<"false"<<std::endl;
// }
}
return blocksinTriangle;
}
insideTriangle
函数也要更改,因为中点不是 (x+0.5, y+0.5),而是(x+0.25, y+0.25)
static bool insideTriangle(float x, float y, const Vector3f* _v, float size_x, float size_y)
{
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
float x_mid = x + size_x/2.0;
float y_mid = y + size_y/2.0;
Eigen::Vector2f CA(_v[0].x()-_v[2].x(), _v[0].y()-_v[2].y());
Eigen::Vector2f AP(x_mid-_v[0].x(), y_mid-_v[0].y());
Eigen::Vector2f AB(_v[1].x()-_v[0].x(), _v[1].y()-_v[0].y());
Eigen::Vector2f BP(x_mid-_v[1].x(), y_mid-_v[1].y());
Eigen::Vector2f BC(_v[2].x()-_v[1].x(), _v[2].y()-_v[1].y());
Eigen::Vector2f CP(x_mid-_v[2].x(), y_mid-_v[2].y());
// cross product have equal sign
if ((CA.x()*AP.y()-CA.y()*AP.x()>=0) == (AB.x()*BP.y()-AB.y()*BP.x()>=0) && (AB.x()*BP.y()-AB.y()*BP.x()>=0) == (BC.x()*CP.y()-BC.y()*CP.x()>=0)) {
return true;
} else {
return false;
}
}
可以看到走样问题被缓解