目录
题目总览
在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是那么的有趣。所以这一次我们继续推进一步——在屏幕上画出一个实心三角形,换言之,栅格化一个三角形。上一次作业中,在视口变化之后,我们调用了函数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 进行了反转,保证都是正数,并且越大表示离视点越远。
在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。
任务一:编写rasterize_triangle()函数
由总览中我们可以知道,这个函数的作用是执行三角形栅格化算法。
那么栅格化的流程是什么,这个需要清楚。
函数所给参数及代码介绍
我们可以看到函数给出的参数是:const Triangle& t
第一行所给出的代码是:auto v = t.toVector4();
那么参数和代码究竟是什么意思呢?
首先const只是用来定义常量的,不用管,Triangle是指这是一个自定义的Triangle类型的变量。
那代码是啥意思?先在IDE下按住Ctrl键并单击“toVector4()
”去看看函数是怎么定义的。
如图是函数的定义,可见,toVector4()
返回的是一个array(也就是数组)类型,array<Vector4f,3>表示的意思是,一个数组中,可以放置三个Vector4f类型的数据。也就等效于这么定义了一个数组:Vector4f array[3](当然定义数组中的的“array”只是一个变量名而已)。
这个代码中的auto是自动识别返回类型的,其实这个代码也等效于我们这么写:Vector4f v[3] = t.toVector4();
因为这是个数组,那么我们通过v[0]、v[1]、v[2]就可以取出三角形的三个顶点,而二维数组诸如v[0][0]、v[0][1]、v[0][2]这一些就是分别取出Vector4f中的前三个数。
光栅化的流程
光栅化(即栅格化,我还是更习惯称为光栅化)其实就是遍历每一个像素点,然后判断这个点的中心是否在图形的内部(这个“判断”就会用到总览中的“insideTriangle()
”方法):
- 如果在图形的内部,就给它涂上颜色
- 如果不在图形的内部,我们就什么都不做
遍历也很简单,只用两个for循环
for (int x = 0; x < max_x; x++)
{
for (int y =0; y < max_y; y++)
{
//...判断及着色代码
}
}
光栅化的加速
光栅化的方法我们已经知道了,但是思考一个问题:假如这个三角形非常小,我们对每一个像素都进行遍历,就会很浪费时间,这样就会使得光栅化较慢,那么我们就想办法解决这个问题,从而实现对光栅化的一个加速,那么解决这个问题的方法是什么呢?闫老师在课上提出了两种方法,如下:
第一种方法做起来没那么麻烦,索性我们这里就用第一种。方法正如图中右侧写的那样:只要取三角形ABC三个点的最小的x、最大的x、最小的y、最大的y就可以了。
我们求出包围盒的代码如下:
// TODO : Find out the bounding box of current triangle. 找出当前三角形的包围盒(创建bounding box)
float min_x = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
float min_y = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
float max_x = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
float max_y = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
此时,遍历的代码也可更改如下:
for (int x = min_x; x < max_x; x++)
{
for (int y = min_y; y < max_y; y++)
{
//...判断及着色代码
}
}
判断与着色部分
我们知道,从题目总览中可以知道,通过insideTriangle()
函数可以对该pixel是否在三角形内进行判断,这个函数我们可以控制它返回一个bool类型的值:
- 如果返回值为true,则该pixel在三角形内,则我们对其进行Z-Buffer更新和着色
- 如果返回值为false,我们就不进行处理。
写判断语句时候,我们直接在括号内使用insideTriangle()
函数,先看看insideTriangle需要我们传些什么值(见编写insideTriangle()函数前边)。
我们就可以写出如下的判断语句:
if (insideTriangle(x + 0.5, y + 0.5, triangle))// 判断这个点是否在三角形内部
{
//计算当前插值深度值,与depth buffer中的响应值进行比较
//若当前值大于深度缓冲区的值则更新
//若当前值小于深度缓冲区的值则什么都不做
}
这里再多说一句,为什么用x+0.5,y+0.5作为传给insideTriangle的参数?这里我们要知道我们定义的pixel是怎样的(如下图),这里也提出了,像素中心点实际上是(x+0.5,y+0.5),而前边我们提及的光栅化的流程,要判断的也是判断这个pixel的中心点是否在三角形内部。
按照上边代码给出的步骤,我们先要计算当前插值与深度值,代码如下(这个是代码框架给出来的,并不需要我们自己写,这里也就不提及什么原理了):
float alpha, beta, gamma;
std::tie(alpha, beta, gamma) = computeBarycentric2D(x, y, t.v);
//auto[alpha, beta, gamma] = std::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;
然后我们将得到的当前深度值与深度缓冲区中的相应值比较,并更新:
// 将当前的深度值与深度缓冲区中的相应值进行比较
if (z_interpolated < depth_buf[get_index(x, y)])
{
// 更新深度缓冲区中的响应值
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.
// 对该点进行着色
set_pixel(Vector3f(x, y, 1), t.getColor());
}
这里再说一句,get_index(x,y)
是通过当前点的x、y值计算出在depth_buf对应的下标。
代码汇总
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
// auto类型?auto可以在声明变量时根据变量初始值的类型自动为此变量选择匹配的类型。
// 这里转换出来的类型:array<Vector4f, 3>,数组大小为3,含有3个Vector4f。
auto v = t.toVector4();
Vector3f triangle[3];
for (int i = 0; i < 3; i++)
{
triangle[i] = Vector3f(v[i].x(), v[i].y(), v[i].z());
}
// TODO : Find out the bounding box of current triangle. 找出当前三角形的包围盒(创建bounding box)
float min_x = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
float min_y = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
float max_x = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
float max_y = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
// iterate through the pixel and find if the current pixel is inside the triangle 遍历bounding box内所有像素(整数索引)
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, triangle))// 判断这个点是否在三角形内部
{
// 如果在内部,则将其位置处的插值深度值与深度缓冲区(depth buffer) 中的相应值进行比较。
// 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;
// 计算当前位置处的插值深度值(给出的代码)
float alpha, beta, gamma;
std::tie(alpha, beta, gamma) = computeBarycentric2D(x, y, t.v);
//auto[alpha, beta, gamma] = std::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;
// 将当前的深度值与深度缓冲区中的相应值进行比较
if (z_interpolated < depth_buf[get_index(x, y)])
{
// 更新深度缓冲区中的响应值
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.
// 对该点进行着色
set_pixel(Vector3f(x, y, 1), t.getColor());
}
}
}
}
}
任务二:编写insideTriangle()函数
函数所给参数及代码介绍
函数所给的参数为:
- int x
- int y
- const Vector3f* _v
x,y传进来的就是需要判断像素的位置坐标。而最后一个v就是指向Vector3f的一个指针。对于传进来的最后一个参数,我们要将其转换为一个Vector3f的数组。
我们知道, 在rasterize_triangle()中,只能够通过t.toVector4f()
得到一个Vector4f array[3]这样的类型,那么我们还需要自己将这个auto v转换成适当的类型,如下转换:
Vector3f triangle[3];
for (int i = 0; i < 3; i++)
{
triangle[i] = Vector3f(v[i][0], v[i][1], v[i][2]);
}
在开头声明了auto v = t.toVector4f()后,我们就可以用上面这段代码得到一个Vector3f array[3]类型的数组了。这个数组便可直接传给insideTriangle()。
怎么判断点是否在三角形内部?
放上我在闫令琪老师课上做的笔记
在这里,我将流程再次归纳如下:
- 已知三角形三个顶点A、B、C,以及一个待判断的点P;
- 分别求出向量AB,BC,CA。再分别求出AP,BP,CP;
- 将AB与AP叉乘,BC与BP叉乘,CA与CP叉乘,得到三个结果;
- 若得到的三个结果都是同号,则说明该点在三角形内部;若不是同号,则该点不再三角形内部。
根据这个流程,我们逐步地做。
从第二步开始,求出向量AB、BC、CA、AP、BP、CP:
//分别求出向量AB、BC、CA:point - point = vector
Vector3f AB = _v[0].head(3) - _v[1].head(3);
Vector3f BC = _v[1].head(3) - _v[2].head(3);
Vector3f CA = _v[2].head(3) - _v[0].head(3);
//定义出这个点P
Vector3f P = Vector3f(x, y, 1);
//设这个点为P点,则分别求出AP,BP,CP(这是向量,所以最后一位为0)
Vector3f AP = _v[0].head(3) - P;
Vector3f BP = _v[1].head(3) - P;
Vector3f CP = _v[2].head(3) - P;
第三步第四步结合:将AB与AP叉乘,BC与BP叉乘,CA与CP叉乘,得到三个结果,若三个结果同号,则在三角形内,返回true,否则返回false:
//计算AB×AP,BC×BP,CA×CP,如果皆为同号,则说明在三角形内,如果出现异号,说明不在三角形内
if (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)
return true;
else if (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)
return true;
else
return false;
代码汇总
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]
// 在这里,假设_v[0]代表三角形点A,_v[1]代表三角形点B,_v[2]代表三角形点C
//分别求出向量AB、BC、CA:point - point = vector
Vector3f AB = _v[0].head(3) - _v[1].head(3);
Vector3f BC = _v[1].head(3) - _v[2].head(3);
Vector3f CA = _v[2].head(3) - _v[0].head(3);
//定义出这个点P
Vector3f P = Vector3f(x, y, 1);
//设这个点为P点,则分别求出AP,BP,CP(这是向量,所以最后一位为0)
Vector3f AP = _v[0].head(3) - P;
Vector3f BP = _v[1].head(3) - P;
Vector3f CP = _v[2].head(3) - P;
//计算AB×AP,BC×BP,CA×CP,如果皆为同号,则说明在三角形内,如果出现异号,说明不在三角形内
if (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)
return true;
else if (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)
return true;
else
return false;
}
题目(非提高题)运行结果
提高:使用MSAA进行光栅化
MSAA是一种消除Jaggies的方法(说消除可能不太准确,只是减轻这个Jaggies的度),出现锯齿情况我们也称为走样。我们要做的,就是抗锯齿、反走样。
MSAA做法如下(以下是我听闫令琪老师课做的笔记):
总结地说,MSAA就是将原本的一个采样点细分为了四个采样点。判断这四个采样点,有多少个在三角形内部。
假如一个点,我们要给它填充的颜色是红色,我们简单表示为RGB(红色)。
使用MSAA细分为4个采样点,对采样点判断,我们发现,这个点细分为采样点后,只有三个点在三角形内,有一个不在,我们只用将它填充的颜色变为(3/4)×RGB(红色)
即可。
上面普通的方法是对x+0.5,y+0.5进行判断,我们这里则分别对4个点进行判断:
- x+0.25,y+0.25
- x+0.25,y+0.75
- x+0.75,y+0.25
- x+0.75,y+0.75
算出有多少个点在三角形内部后,再将颜色着至x,y就好了。
只需更新rasterize_triangle()函数的代码
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
// auto类型?auto可以在声明变量时根据变量初始值的类型自动为此变量选择匹配的类型。
// 这里转换出来的类型:array<Vector4f, 3>,数组大小为3,含有3个Vector4f。
auto v = t.toVector4();
Vector3f triangle[3];
for (int i = 0; i < 3; i++)
{
triangle[i] = Vector3f(v[i].x(), v[i].y(), v[i].z());
}
// TODO : Find out the bounding box of current triangle. 找出当前三角形的包围盒(创建bounding box)
float min_x = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
float min_y = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
float max_x = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
float max_y = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
// iterate through the pixel and find if the current pixel is inside the triangle 遍历bounding box内所有像素(整数索引)
//预设一些值,等下x和y分别加上这些向量中的值就好了,只是为了通过for循环方便计算,其实不用也可以
Vector2f addPoint[4];
addPoint[0] = Vector2f(0.25, 0.25);
addPoint[1] = Vector2f(0.75, 0.25);
addPoint[2] = Vector2f(0.25, 0.75);
addPoint[3] = Vector2f(0.75, 0.75);
for (int x = min_x; x < max_x; x++)
{
for (int y = min_y; y < max_y; y++)
{
int pointCount = 0;
//遍历四个点,如果点在三角形内部,则pointCount+1
for (int i = 0; i < 4; i++)
if (insideTriangle(x + addPoint[i][0], y + addPoint[i][1], triangle))
pointCount++;
if (pointCount != 0)
{
float alpha, beta, gamma;
std::tie(alpha, beta, gamma) = computeBarycentric2D(x, y, t.v);
//auto[alpha, beta, gamma] = std::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;
// 将当前的深度值与深度缓冲区中的相应值进行比较
if (z_interpolated < depth_buf[get_index(x, y)])
{
// 更新深度缓冲区中的相应值
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.
// 对该点进行着色,这里乘上一个系数(pointCount/4.0),可以控制颜色深/浅
set_pixel(Vector3f(x, y, 1), t.getColor() * (pointCount / 4.0));
}
}
}
}
}
主要的变化还是在两个for循环前5行、两个for循环内部的前4行 以及最后着色代码乘上了一个系数
使用MSAA(提高题)后的结果
结果对比
没用MSAA前明显的锯齿化:
用了MSAA后:
效果显然好了不少。
补充:main函数中的get_projection_matrix()函数
直接粘贴上次作业的代码:
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 ortho = Eigen::Matrix4f::Identity();
Eigen::Matrix4f scale = Eigen::Matrix4f::Identity();
Eigen::Matrix4f transformation = Eigen::Matrix4f::Identity();
float t = tanf(eye_fov / 2) * abs(zNear);
float b = t;
t = -t;
float r = aspect_ratio * t;
float l = -r;
scale << 2 / (r - l), 0, 0, 0,
0, 2 / (t - b), 0, 0,
0, 0, 2 / (zNear - zFar), 0,
0, 0, 0, 1;
transformation << 1, 0, 0, -((r + l) / 2),
0, 1, 0, -((t + b) / 2),
0, 0, 1, -((zNear + zFar) / 2),
0, 0, 0, 1;
ortho = scale * transformation;
Eigen::Matrix4f persp2ortho = Eigen::Matrix4f::Identity();
persp2ortho << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -(zNear * zFar),
0, 0, 1, 0;
projection = ortho * persp2ortho;
return projection;
}
如果自己写时候得到的三角形是倒过来的,只需要将top和bottom的值换一下就好了,我这里的代码是换过了的。
主要是因为opencv是左手系的,见:作业2:结果上下颠倒
完结撒花(~ ̄▽ ̄)~