games101作业二 ---- 三角形光栅化 超采样抗锯齿 黑边处理
(本文是在学习计算机图形学时根据课程作业进行整理的笔记,有错误请指出,如果是同课程,请勿复制粘贴,谢谢!)
以下是这次作业中主要撰写的几个函数,及其简要功能描述,本次作业完整代码在github;链接,可能会有一些细节问题需要在项目中进行微小的设置。
static bool insideTriangle(float x, float y, const Vector3f* _v)
通过向量叉乘 判断点是否在三角形内部
void rst::rasterizer::rasterize_triangle(const Triangle& t)
三角形光栅化,超采样super_sample, 用于将一个三角形图形采用超采样的方法 投影到屏幕上的像素并进行渲染优化。最终通过记录每一个点的超采样子像素值进行优化黑边问题。
int rst::rasterizer::get_super_index(int x, int y)
超采样时候,将记录域扩大四倍,重新定义像素点到深度和颜色缓冲区的索引
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
扩大深度和颜色的空间
Vector3f
: 常见的数据类型,通常用于表示三维空间中的向量或点。
Vector3f
中的 “Vector” 意味着它是一个矢量,可以表示方向和大小。Eigen::Vector2f
: 是Eigen库中的数据类型,它表示一个二维的浮点数向量。Eigen库是一个C++模板库,用于进行线性代数运算,特别是矩阵和向量运算.
Eigen
是库的命名空间。Vector2f
表示这是一个二维的浮点数向量。AP = P - A
: 计算从点 A
到点 P
的向量。它从点 P
的坐标中减去点 A
的坐标来获得这个向量。eq1 = AB[0] * AP[1] - AB[1] * AP[0]
: 是计算在三角形光栅化中用于判断一个点是否在三角形内部的关键步骤。 补充(ABXAP=x1y2-x2y1)std::tie
: 创建一个 std::tuple{}
接受后面重心插值的结果insideTriangle()
判断点是否在三角形内部
insideTriangle()
是一个用于确定给定点(x,y)是否位于三角形内部的常见数学算法的实现。它使用重心坐标的概念来进行判断。以下是代码如何工作的详细步骤:
- 首先,定义了三个二维向量,
A
、B
和C
,它们表示三角形的顶点。这些顶点从_v
数组中提取而来。- 计算了三个向量,
AP
、BP
和CP
,它们表示点P
到三角形的各个顶点A
、B
和C
的向量。- 计算了三个值,
eq1
、eq2
和eq3
,它们本质上是向量AP
、BP
和CP
分别与三角形的边AB
、BC
和CA
的叉乘。- 最后,检查
eq1
、eq2
和eq3
的符号。如果它们三个都是正数或者都是负数,那意味着点P
位于三角形内部,函数返回true
。否则,它返回false
。本质上是检查点是否在三角形的每条边的同一侧,这是在三角形内的点所具有的特性。如果三个检查都通过,它会认为点在三角形内部。
计算的时候都是指定三角形的顶点是按逆时针顺序的,右手系法则。
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]
const Eigen::Vector2f P(x, y);
const Eigen::Vector2f A = _v[0].head(2), B = _v[1].head(2), C = _v[2].head(2);
const Eigen::Vector2f AP = P - A;
const Eigen::Vector2f BP = P - B;
const Eigen::Vector2f CP = P - C;
const Eigen::Vector2f AB = B - A;
const Eigen::Vector2f BC = C - B;
const Eigen::Vector2f CA = A - C;
float eq1 = AB[0] * AP[1] - AB[1] * AP[0]; // ABXAP
float eq2 = BC[0] * BP[1] - BC[1] * BP[0]; // BCXBP
float eq3 = CA[0] * CP[1] - CA[1] * CP[0]; // CAXCP
if (eq1 > 0 && eq2 > 0 && eq3 > 0) // 有无在三角形内
return true;
else if (eq1 < 0 && eq2 < 0 && eq3 < 0)
return true;
else
return false;
}
rasterize_triangle(const Triangle& t)
光栅化,超采样super_sample用于将一个三角形图元投影到 屏幕上的像素并进行渲染
超采样改进函数:
rasterize_triangle(const Triangle& t)
: 用于三角形光栅化(Rasterization
)的函数,通常用于计算机图形学中将三维图形渲染到二维屏幕上的过程。具体计算步骤如下:
- Bounding Box计算:首先,代码计算了包围三角形的边界框(Bounding Box)。这是通过找到三个顶点的x和y坐标的最小和最大值来完成的。然后,这些坐标被取整,以便将Bounding Box转换为整数坐标,以便在之后的循环中使用。
- 超采样(Super Sampling):代码定义了一个超采样步骤的集合,用于抗锯齿处理。在每个像素内部,它会采样四个子像素,并检查它们是否在三角形内。如果是的话,就增加一个计数器,以确定超采样过程中有多少个子像素落在了三角形内,最终颜色取四个的平均值。
- 遍历Bounding Box:然后,代码通过两个嵌套的循环遍历了Bounding Box 内的每个像素。对于每个像素,它检查是否有子像素落在三角形内,如果有,就增加计数器。这部分的目标是确定在像素内的子像素数量,以实现超采样抗锯齿。
- 深度测试:如果有子像素在三角形内,代码将执行深度测试。它使用三角形的顶点数据(v)和像素位置(x,y)来计算插值的深度值(z_interpolated)。然后,它与深度缓冲区(depth_buf)中的当前深度值进行比较。如果新的深度值更小(表示该像素离观察者更近),则将该像素的颜色设置为三角形的颜色(使用getColor函数)的加权平均,同时更新深度缓冲区中的深度值。
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle
//1. 找到Bounding Box,也就是最大最小的x,y坐标然后再进行向上向下取整。并将bounding box扩大一点,得到得结果是整数值,方便迭代遍历。
float xmin = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
float xmax = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
float ymin = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
float ymax = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
xmin = (int)std::floor(xmin);
xmax = (int)std::ceil(xmax);
ymin = (int)std::floor(ymin);
ymax = (int)std::ceil(ymax);
// 超采样 super_sample_step 将一个正方形内再细分四个点,计算点的个数,着色的时候取均值t.getColor()*count/4
std::vector<Eigen::Vector2f> super_sample_step
{
{0.25,0.25},
{0.75,0.25},
{0.25,0.75},
{0.75,0.75},
};
//2. 在Bounding box 内遍历所有元素,判断是否在三角形内部,并且进行超采样判断有几个点落在三角形内
for (int x = xmin; x <= xmax; x++)
{
for (int y = ymin; y <= ymax; y++)
{
int count = 0;
float minDepth = FLT_MAX;
// 如果超采样有数据,整体的颜色就取均值
for (int i = 0; i < 4; i++)
{
if (insideTriangle(x + super_sample_step[i][0], y + super_sample_step[i][1], t.v))
{
count++;
}
}
//像素的坐标值只是比整数编号值大0.5。
if (count>0)
{
// 求深度值 z_interpolated
float alpha, beta, gamma;
std::tie(alpha, beta, gamma) = computeBarycentric2D(x, y, t.v);
// std::tie : 创建一个std::tuple{}接受后面重心插值的结果
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
// z_interpolated :
// w_reciprocal :
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;
//若当前位置深度比depth_buf)更小,则更新颜色值 。
if (z_interpolated < depth_buf[get_index(x, y)])
{
set_pixel(Vector3f(x, y, z_interpolated), t.getColor()*count/4);
depth_buf[get_index(x, y)] = z_interpolated;
}
}
}
}
}
原因分析:
解决办法:
改进总结:
超采样测试:在每个像素内部,代码进一步遍历了四个超采样子像素。对于每个子像素,它调用
insideTriangle
函数来检查是否在三角形内部。如果在三角形内部,它执行以下操作:
- 计算重心坐标(alpha、beta、gamma)以及插值深度值(z_interpolated),这部分代码似乎使用了未定义的变量
v
,应该使用t.v
。- 计算
z_interpolated
的深度值。- 更新超采样深度缓冲区
super_depth_buf
和超采样帧缓冲区super_frame_buf
的对应子像素位置的深度值和颜色。判断是否需要着色:接下来,代码检查一个名为
judge
的标志,如果其中任何一个子像素通过深度测试,则将judge
设置为1。这个标志用于确定是否需要在原始像素位置上进行着色。着色:如果
judge
被设置为1,表示至少一个子像素通过了深度测试,那么代码会计算原始像素位置上的颜色。它将四个超采样子像素的颜色取平均值,并使用set_pixel
函数将平均颜色设置为原始像素位置的颜色。
// 扩大深度和颜色的空间
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
super_frame_buf.resize(w * h * 4);
super_depth_buf.resize(w * h * 4);
}
// 重新定义像素点到深度和颜色缓冲区的索引
int rst::rasterizer::get_super_index(int x, int y)
{
// 扩大四倍的域都进行记录
return (height * 2 - 1 - y) * width * 2 + x;
}
std::vector super_sample_step
{
{0.25,0.25},
{0.75,0.25},
{0.25,0.75},
{0.75,0.75},
};
// 遍历变量进行深度检测并赋予颜色color值
for (int x = xmin; x <= xmax; x++)
{
for (int y = ymin; y <= ymax; y++)
{
int judge = 0;
//具体思路就是记录四倍的数据量,把超采样的数据都记下来
for (int i = 0; i < 4; i++)
{
// 如果在在三角形内部进行深度判断
if (insideTriangle(x + super_sample_step[i][0], y + super_sample_step[i][1], t.v))
{
float alpha, beta, gamma;
std::tie(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;
// 再次进行 坐标变换
// 在x,y轴上坐标处理方式不一样,在四个块状正方形 从0到3 x轴会变化四次,y轴变化两次因此如下处理。
if (super_depth_buf[get_super_index(x*2 + i % 2, y*2 + i / 2)] > z_interpolated)
{
judge = 1;
super_depth_buf[get_super_index(x*2 + i % 2, y*2 + i / 2)] = z_interpolated;
super_frame_buf[get_super_index(x*2 + i % 2, y*2 + i / 2)] = t.getColor();
}
}
}
if (judge)
//若像素的四个样本中有一个通过了深度测试,就需要对该像素进行着色。
{
Vector3f point = { (float)x,(float)y,0 };
Vector3f color = (super_frame_buf[get_super_index(x*2 , y*2)]+ super_frame_buf[get_super_index(x*2+1, y*2)]+ super_frame_buf[get_super_index(x*2, y*2+1)]+ super_frame_buf[get_super_index(x*2+1, y*2+1)])/4;
set_pixel(point, color);
}
}
}