1、光栅化
光栅化是把图元画在屏幕上的一个过程,也就是把图元的顶点数据转换为片段的过程。该过程主要包含两部分工作,第一部分决定窗口内的哪些栅格区域被图元占有,第二部分计算出来每一个顶点的属性值。光栅化都是在视窗变换之后进行的。
2、点的光栅化
当要绘制的图元类型为点时,在点大小为 1 的情况下,一个点就是一个像素,但是当顶点大小不是 1 的时候,那么一个点就包含一块 n*n 大小的正方形区域。
假设一个顶点的坐标为 (200, 200),并且 point_size 为 10,那么这个点应该在屏幕上怎么绘制呢?
这个时候我们以给定的顶点为中心点,分别向上下左右四个方向进行增长,向下面和左面增长 (point_size - 1) / 2 个单元格,因为当前点算在下面和左面里面,上面和右面增长 point_size/2 个单元格,就比如下面这样
在之前着色器一文中,我们有提到片段着色器有一个内置的输入变量 gl_PointCoord,只有当图元类型为点时才有意义,它表示的是一个点精灵坐标,取值范围是 [0.0, 1.0],左上角为 (0, 0),右下角为 (1, 1)
接下来我们就通过代码简单实现点的光栅化,因为点的属性值都是一样的,并不会涉及到对点属性的插值
公共结构体
struct Attribute {
float attri[4];
};
struct Point {
int x;
int y;
float z;
float w;
float point_coord[2];
int attribute_size;
Attribute attributes[8];
};
假设第一个属性值就是点的颜色 ,我们可以将光栅化结果输出到文件中
void DoRasterPoint(const Point& point, int point_size) {
std::ofstream fout("point_output.txt", std::ios::binary | std::ios::out);
Point top_left = point;
top_left.x = top_left.x - (point_size - 1) / 2;
top_left.y = top_left.y + point_size / 2;
top_left.point_coord[0] = 0;
top_left.point_coord[1] = 0;
for (int y = 0; y < point_size; ++y) {
float point_coord_y = point_size == 1 ? 0 : y * 1.0 / (point_size - 1);
for (int x = 0; x < point_size; ++x) {
float point_coord_x = point_size == 1 ? 0 : x * 1.0 / (point_size - 1);
Point result = top_left;
result.y = result.y - y;
result.x = result.x + x;
result.point_coord[0] = point_coord_x;
result.point_coord[1] = point_coord_y;
std::cout << result.x << " " << result.y << " " << result.point_coord[0] << " "
<< result.point_coord[1] << std::endl;
fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
<< (int)(result.attributes[0].attri[1] * 255) << " "
<< (int)(result.attributes[0].attri[2] * 255) << " "
<< (int)(result.attributes[0].attri[3] * 255) << std::endl;
}
}
fout.close();
}
3、线的光栅化
正常情况下,我们常见的线段,可以分为以下几种
水平线段
竖直线段
水平增长大于竖直增长长度(正负斜率),这里称为沿 x 轴增长
竖直增长大于水平增长长度(正负斜率),这里称为沿 y 轴增长
当然线段也是有宽度的,我们可以通过 glLineWidth 来设置我们的线宽,不同斜率方向的线段加上宽度表现如下
一般情况下,当 x 轴增长大于 y 轴增长的情况下,线宽是竖直增长的,当 y 轴增长大于 x 轴增长的情况下,线宽是水平增长的
在进行线段光栅化时,我们根据不同方向增长来进行光栅化
首先,判断增长方向
void DoRasterLine(std::vector<Point>& points, int linewidth) {
int diff_x = fabs(points[1].x - points[0].x);
int diff_y = fabs(points[1].y - points[0].y);
bool incre_x = true;
if (diff_y > diff_x) {
incre_x = false;
}
}
在数学中,我们一般使用 y = kx + b 来表示直线方程,根据两个点坐标,我们可以计算出来 k 和 b,然后可以遍历线段上的每一个点,得到每一个点的坐标
void DoRasterLine(std::vector<Point>& points, int linewidth) {
// ......
float line_k = 0;
float line_b = 0;
if ((points[1].x != points[0].x)) {
line_k = (points[1].y - points[0].y) / (points[1].x - points[0].x);
line_b = points[0].y - line_k * points[0].x;
}
}
接下来我们可以按照不同的增长方向查找所有在线上面的点进行光栅化插值
沿 x 轴增长时,我们遍历两点之间的所有 x 坐标,然后计算 k 和 b 计算出来 y 坐标,然后再通过插值计算出来属性值,代码如下
if (points[0].x > points[1].x) {
std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_x; ++i) {
int x = points[0].x + i;
int y = line_k * x + line_b;
float weight = i * 1.0 / (diff_x - 1);
Point result = Lerp(points, weight);
result.x = x;
result.y = y;
for (int j = 0; j < linewidth; ++j) {
std::cout << result.x << " " << result.y << std::endl;
result.y = result.y + 1;
}
}
沿 x 轴增长时,我们遍历两点之间的所有 y 坐标,然后计算 k 和 b 计算出来 x 坐标
if (points[0].y > points[1].y) {
std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_y; ++i) {
int y = points[0].y + i;
int x = points[0].x;
if (points[1].x != points[0].x) {
x = (y - line_b) * 1.0 / line_k;
}
float weight = i * 1.0 / (diff_y - 1);
Point result = Lerp(points, weight);
result.x = x;
result.y = y;
for (int j = 0; j < linewidth; ++j) {
std::cout << result.x << " " << result.y << std::endl;
result.x = result.x + 1;
}
}
全部代码如下
Point Lerp(const std::vector<Point>& points, float weight) {
Point result;
result.x = points[0].x * (1 - weight) + points[0].x * weight;
result.y = points[0].y * (1 - weight) + points[0].y * weight;
result.z = points[0].z * (1 - weight) + points[0].z * weight;
result.w = points[0].w * (1 - weight) + points[0].w * weight;
result.attribute_size = points[0].attribute_size;
for (int i = 0; i < points[0].attribute_size; ++i) {
result.attributes[i].attri[0] = points[0].attributes[i].attri[0] * (1 - weight) +
points[1].attributes[i].attri[0] * weight;
result.attributes[i].attri[1] = points[0].attributes[i].attri[1] * (1 - weight) +
points[1].attributes[i].attri[1] * weight;
result.attributes[i].attri[2] = points[0].attributes[i].attri[2] * (1 - weight) +
points[1].attributes[i].attri[2] * weight;
result.attributes[i].attri[3] = points[0].attributes[i].attri[3] * (1 - weight) +
points[1].attributes[i].attri[3] * weight;
}
return result;
}
void DoRasterLine(std::vector<Point>& points, int linewidth) {
std::ofstream fout("line_output.txt", std::ios::binary | std::ios::out);
int diff_x = fabs(points[1].x - points[0].x);
int diff_y = fabs(points[1].y - points[0].y);
bool incre_x = true;
if (diff_y > diff_x) {
incre_x = false;
}
float line_k = 0;
float line_b = 0;
if (points[1].x != points[0].x) {
line_k = (points[1].y - points[0].y) * 1.0 / (points[1].x - points[0].x);
line_b = points[0].y - line_k * points[0].x;
}
if (incre_x) {
if (points[0].x > points[1].x) {
std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_x; ++i) {
int x = points[0].x + i;
int y = line_k * x + line_b;
float weight = i * 1.0 / (diff_x - 1);
Point result = Lerp(points, weight);
result.x = x;
result.y = y;
for (int j = 0; j < linewidth; ++j) {
fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
<< (int)(result.attributes[0].attri[1] * 255) << " "
<< (int)(result.attributes[0].attri[2] * 255) << " "
<< (int)(result.attributes[0].attri[3] * 255) << std::endl;
result.y = result.y + 1;
}
}
}
else {
if (points[0].y > points[1].y) {
std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_y; ++i) {
int y = points[0].y + i;
int x = points[0].x;
if (points[1].x != points[0].x) {
x = (y - line_b) * 1.0 / line_k;
}
float weight = i * 1.0 / (diff_y - 1);
Point result = Lerp(points, weight);
result.x = x;
result.y = y;
for (int j = 0; j < linewidth; ++j) {
fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
<< (int)(result.attributes[0].attri[1] * 255) << " "
<< (int)(result.attributes[0].attri[2] * 255) << " "
<< (int)(result.attributes[0].attri[3] * 255) << std::endl;
result.x = result.x + 1;
}
}
}
fout.close();
}
4、三角形的光栅化
相比于点和线的光栅化来讲,三角形的光栅化就会显得比较复杂了,因为点、线可以很好的计算出图元包含的点,但是三角形的判断就没有那么简单了。对于三角形的光栅化来说,我们也可以分为两步,第一步计算出三角形内部的点,第二部计算出内部点的属性值,这里在计算属性值的时候我们使用重心插值。
4.1 判断一个点是否在三角形内部
如图所示,为了判断一个点是否在三角形(这里三角形为逆时针)内部,首先先将三角形按照如图所示的方向连接起来,会有 3 个向量 AB,BC,CA,然后再以每个顶点为基准,获得向量 AP,BP,CP,如果 P 在三角形内部,那么由叉乘的性质
如果 P 在三角形内部,会有 AB * AP >= 0,BC * BP >= 0,CA * CP >= 0,=0,刚好是在某一条边上面,反之 P 就在三角形的外部,如果三角形为顺时针,那么判断条件则相反。
4.2 三角形的 Bounding Box(边界盒)
我们可以通过三角形的三个顶点,计算出来一个三角形的边界盒
如上图所示,三角形一定是在边界盒之内的,那么我们只要遍历边界盒内的每一个点,来判断这些点是不是在三角形之内就可以了,从左下角遍历一直到右上角
int VecCross(Point p1, Point p2, Point p3) {
int cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
return cross;
}
void DoRasterTriangle(std::vector<Point>& points) {
int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
for (int y = min_y; y < max_y; ++y) {
for (int x = min_x; x < max_x; ++x) {
Point p;
p.x = x;
p.y = y;
int cross_ab = VecCross(points[0], points[1], p) * coef;
int cross_bc = VecCross(points[1], points[2], p) * coef;
int cross_ca = VecCross(points[2], points[0], p) * coef;
if (cross_ab >= 0 && cross_bc >= 0 && cross_ca >= 0) {
std::cout << "inside: " << p.x << " " << p.y << std::endl;
}
}
}
}
4.3 相邻点叉乘值关系
假设当前点坐标 p 为 (x, y),右边点 right 坐标为 (x+1, y),上面点坐标为 top(x, y+1)
那么三个点的叉乘值分别为
// p 点 (x, y)
int cross_ab = (points[1].x - points[0].x) * (y - points[0].y) - (x - points[0].x) * (points[1].y - points[0].y);
// right 点 (x+1, y)
int right_cross_ab = (points[1].x - points[0].x) * (y - points[0].y) - ((x+1) - points[0].x) * (points[1].y - points[0].y);
// top 点 (x, y+1)
int top_cross_ab = (points[1].x - points[0].x) * ((y+1) - points[0].y) - (x - points[0].x) * (points[1].y - points[0].y);
那么 right_cross_ab - cross_ab 就等于
int cross_ab_diff_x = right_cross_ab - cross_ab;
cross_ab_diff_x = -((x + 1) - points[0].x) * (points[1].y - points[0].y) + (x - points[0].x) * (points[1].y - points[0].y);
cross_ab_diff_x = (points[1].y - points[0].y) * (x - points[0].x - (x + 1) - points[0].x);
cross_ab_diff_x = points[1].y - points[0].y;
同样计算 top_cross_ab - cross_ab 就等于
int cross_ab_diff_y = top_cross_ab - cross_ab;
cross_ab_diff_y = points[1].x - points[0].x;
那么相邻点的叉乘值就可以通过前一个点的叉乘值加上一个插值得到,而不用每次都去计算叉乘值了。那么我们只需要计算出来 (min_x, min_y) 这个点的叉乘值,后面点的叉乘值都通过加法计算得到,那么我们可以把代码修改为
void DoRasterTriangle(std::vector<Point>& points) {
int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
int cross[3];
int cross_diff_x[3];
int cross_diff_y[3];
Point p;
p.x = min_x;
p.y = min_y;
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
cross_diff_x[i] = points[(i + 1) % 3].x - points[i % 3].x;
cross_diff_y[i] = points[(i + 1) % 3].y - points[i % 3].x;
}
for (int y = min_y; y < max_y; ++y) {
int tmp_cross[3];
for (int i = 0; i < 3; ++i) {
tmp_cross[i] = cross[i];
}
for (int x = min_x; x < max_x; ++x) {
bool inside = true;
for (int i = 0; i < 3; ++i) {
if (tmp_cross[i] * coef < 0) {
inside = false;
break;
}
}
if (inside) {
std::cout << "inside: " << x << " " << y << std::endl;
}
for (int i = 0; i < 3; ++i) {
tmp_cross[i] += cross_diff_y[i];
}
}
for (int i = 0; i < 3; ++i) {
cross[i] += cross_diff_x[i];
}
}
}
4.4 分块判断
我们可以看到在边界框内,三角形内的像素和不在三角形内的像素都是一块一块的,这样我们以块的方式来遍历三角形可以减少三角形之外像素遍历的次数
如上图所示,我们按照一个块一个块的进行遍历,检测每个块的四个角是否在三角形中,那么每个块就会有以下三种情况
1. 完全在三角形内
2. 部分在三角形内
3. 部分在三角形外
对于 1 来说,块中的每个像素都在三角形内,可以直接计算属性插值进行渲染,对于 3 来说,可以直接将块丢弃,对于 2 来说,我们还需要使用刚在的逐像素的方式来判断块中的每个像素是否在三角形内。此方法对于占屏幕面积比较大的三角形优化效果明显,对于小的三角形和斜长的细三角形,可能效果就不好了。此外,如果分块大了会进一步降低小三角形的绘制效率,如果分块小了又不能有明显优化。因此分块的大小需要仔细的权衡。
这里还有一个问题就是,我们是否需要将边界盒内所有的块都访问一遍呢?肯定是不是的,我们可以看到从左往后遍历三角形的块时,刚开始的块不在三角形内部,中间的块在三角形内部,一旦从内部再到外部,那么我们就没有必要再去判断这一行后面的块了。
具体代码如下
void DoRasterTriangleBlock(std::vector<Point>& points) {
std::ofstream fout("triangle_output.txt", std::ios::binary | std::ios::out);
int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
int cross[3];
int cross_diff_x[3];
int cross_diff_y[3];
Point p;
p.x = min_x;
p.y = min_y;
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
cross_diff_x[i] = points[i % 3].y - points[(i + 1) % 3].y;
cross_diff_y[i] = points[(i + 1) % 3].x - points[i % 3].x;
}
std::vector<int> block_size{4, 4};
for (int y = min_y; y < max_y; y += block_size[1]) {
// 计算 4 个点的叉乘值
int tmp_cross[4][3];
for (int i = 0; i < 3; ++i) {
tmp_cross[0][i] = cross[i];
tmp_cross[1][i] = tmp_cross[0][i] + (block_size[0] - 1) * cross_diff_x[i];
tmp_cross[2][i] = tmp_cross[0][i] + (block_size[1] - 1) * cross_diff_y[i];
tmp_cross[3][i] = tmp_cross[2][i] + (block_size[0] - 1) * cross_diff_x[i];
}
bool inside_block = false;
for (int x = min_x; x < max_x; x += block_size[0]) {
int inside_count = 0;
for (int i = 0; i < 4; ++i) {
bool inside = true;
for (int j = 0; j < 3; ++j) {
if (tmp_cross[i][j] * coef < 0) {
inside = false;
break;
}
}
if (inside) {
inside_block = true;
inside_count++;
}
}
if (inside_count == 4) {
for (int i = 0; i < block_size[1]; ++i) {
for (int j = 0; j < block_size[0]; ++j) {
fout << x + j << " " << y + i << " 76 89 56 255" << std::endl;
}
}
}
else if (inside_count > 0) {
int block_cross[3];
for (int i = 0; i < block_size[1]; ++i) {
for (int k = 0; k < 3; ++k) {
block_cross[k] = tmp_cross[0][k] + i * cross_diff_y[k];
}
for (int j = 0; j < block_size[0]; ++j) {
bool inside = true;
for (int k = 0; k < 3; ++k) {
if (block_cross[k] * coef < 0) {
inside = false;
break;
}
}
if (inside) {
fout << x + j << " " << y + i << " 76 89 56 255" << std::endl;
}
for (int k = 0; k < 3; ++k) {
block_cross[k] += cross_diff_x[k];
}
}
}
}
else {
if (inside_block) {
break;
}
}
for (int i = 0; i < 3; ++i) {
tmp_cross[0][i] += block_size[0] * cross_diff_x[i];
tmp_cross[1][i] += block_size[0] * cross_diff_x[i];
tmp_cross[2][i] += block_size[0] * cross_diff_x[i];
tmp_cross[3][i] += block_size[0] * cross_diff_x[i];
}
}
for (int i = 0; i < 3; ++i) {
cross[i] += cross_diff_y[i] * block_size[1];
}
}
fout.close();
}
4.5 Half Space Rasterization
尽管基于块的方法显著地有助于更快地遍历边界框的空白区域,但遍历的空块的数量仍然是显著的和多余的。下面的例子很好地说明了这个问题
在上图中,橙色表示遍历的空块,虽然这些空块很少,但是还是会涉及到大量不必要的判断和计算,影响我们的效率
下面介绍一种基于块的算法,其目的是尽量减少遍历空块的数量,从而减少不必要的计算。下图解释了遍历的本质。
其基本思想是,如果遍历从三角形内的一个公共点开始,并向两个方向移动,则可以有效地解决空块最小化问题。
比如在上图中我们可以把三角形分为上三角形和下三角形,上三角形我们分别从 Top starting block 开始,然后分别向左右遍历,分别找到左右块的边界 left_bound 和 right_bound,并且下一行块的开始 x 坐标为 (left_bound + right_bound) / 2;然后下一行按照之前的方式计算遍历。下三角形从 Bottom starting block 开始,也分别向左右遍历
上三角形和下三角形,我们可以以三角形中间点坐标划分,如下图所示
通过上面的方式可以大大减少空块的遍历,关于 Half Space Rasterization 具体的算法文档可以参考这个https://acta.uni-obuda.hu/Mileff_Nehez_Dudra_63.pdf
下面是代码实现
#include <iostream>
#include <vector>
#include <fstream>
struct Bound {
int min_x;
int min_y;
int max_x;
int max_y;
};
struct Point {
int x{};
int y{};
Point(int x = 0, int y = 0) {
this->x = x;
this->y = y;
}
};
class Raster {
public:
Raster() = default;
~Raster() = default;
int VecCross(Point p1, Point p2, Point p3) {
int cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
return cross;
}
int Align(int val, int align, bool down = true) {
int result = val - (val & (align - 1));
if (!down) {
result = result + (val & (align - 1) ? align : 0);
}
return result;
}
bool Inside(float cross[3], int coef) {
for (int j = 0; j < 3; ++j) {
if (cross[j] * coef < 0) {
return false;
}
}
return true;
}
void OutputBlock(const Point& point) {
for (int j = 0; j < block_size_[1]; ++j) {
for (int i = 0; i < block_size_[0]; ++i) {
fout_ << point.x + i << " " << point.y + j << " 96 127 89 255" << std::endl;
}
}
}
void CheckBlock(const Point& point, std::vector<float>& cross) {
float tmp_cross[3];
for (int j = 0; j < block_size_[1]; ++j) {
for (int k = 0; k < 3; ++k) {
tmp_cross[k] = cross[k] + cross_diff_y_[k] * j;
}
for (int i = 0; i < block_size_[0]; ++i) {
if (Inside(tmp_cross, coef_)) {
fout_ << point.x + i << " " << point.y + j << " 96 127 89 255" << std::endl;
}
for (int k = 0; k < 3; ++k) {
tmp_cross[k] += cross_diff_x_[k];
}
}
}
}
void DoRasterTriangleHalf(std::vector<Point>& points) {
fout_.open("triangle_output.txt", std::ios::binary | std::ios::out);
bound_.min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
bound_.min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
bound_.max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
bound_.max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
coef_ = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
for (int i = 0; i < 3; ++i) {
cross_diff_x_[i] = points[i % 3].y - points[(i + 1) % 3].y;
cross_diff_y_[i] = points[(i + 1) % 3].x - points[i % 3].x;
}
int max_index, mid_index, min_index;
max_index = points[0].y > points[1].y ? 0 : 1;
max_index = points[max_index].y > points[2].y ? max_index : 2;
min_index = points[0].y < points[1].y ? 0 : 1;
min_index = points[min_index].y < points[2].y ? min_index : 2;
mid_index = 3 - min_index - max_index;
if (points[mid_index].y == points[min_index].y) {
Point point;
point.x = Align(points[max_index].x, block_size_[0]);
point.y = Align(points[max_index].y, block_size_[1]);
std::vector<float> cross(3);
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(points[i], points[(i + 1) % 3], point);
}
DoTopRaster(bound_, cross, point);
}
else if (points[mid_index].y == points[max_index].y) {
Point point;
point.x = Align(points[min_index].x, block_size_[0]);
point.y = Align(points[min_index].y, block_size_[1]);
std::vector<float> cross(3);
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(points[i], points[(i + 1) % 3], point);
}
DoBottomRaster(bound_, cross, point);
}
else {
int mid_y = Align(points[mid_index].y, block_size_[1]);
Bound bottom_bound = bound_;
Bound top_bound = bound_;
bottom_bound.max_y = mid_y;
top_bound.min_y = mid_y;
Point top_point;
top_point.x = Align(points[max_index].x, block_size_[0]);
top_point.y = Align(points[max_index].y, block_size_[1]);
std::vector<float> top_cross(3);
for (int i = 0; i < 3; ++i) {
top_cross[i] = VecCross(points[i], points[(i + 1) % 3], top_point);
}
DoTopRaster(top_bound, top_cross, top_point);
Point bottom_point;
bottom_point.x = Align(points[min_index].x, block_size_[0]);
bottom_point.y = Align(points[min_index].y, block_size_[1]);
std::vector<float> bottom_cross(3);
for (int i = 0; i < 3; ++i) {
bottom_cross[i] = VecCross(points[i], points[(i + 1) % 3], bottom_point);
}
DoBottomRaster(bottom_bound, bottom_cross, bottom_point);
}
fout_.close();
}
void DoTopRaster(const Bound& bound, std::vector<float>& cross, const Point& point) {
if (point.y < bound.min_y) {
return;
}
float cross_bound[4][3];
for (int i = 0; i < 3; ++i) {
cross_bound[0][i] = cross[i];
cross_bound[1][i] = cross_bound[0][i] + cross_diff_x_[i] * (block_size_[0] - 1);
cross_bound[2][i] = cross_bound[0][i] + cross_diff_y_[i] * (block_size_[1] - 1);
cross_bound[3][i] = cross_bound[2][i] + cross_diff_x_[i] * (block_size_[0] - 1);
}
int inside_count = 0;
std::vector<bool> inside_flag(4, false);
for (int i = 0; i < 4; ++i) {
inside_flag[i] = Inside(cross_bound[i], coef_);
if (inside_flag[i]) {
inside_count++;
}
}
if (inside_count == 4) {
OutputBlock(point);
}
else if (inside_count > 0) {
CheckBlock(point, cross);
}
Point point_left = point;
Point point_right = point;
if (inside_flag[1] || inside_flag[3]) {
float right_bound[4][3];
for (int i = 0; i < 12; ++i) {
right_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
}
for (int x = point.x + block_size_[0]; x <= bound.max_x; x += block_size_[0]) {
for (int i = 0; i < 3; ++i) {
right_bound[0][i] += block_size_[0] * cross_diff_x_[i];
right_bound[1][i] += block_size_[0] * cross_diff_x_[i];
right_bound[2][i] += block_size_[0] * cross_diff_x_[i];
right_bound[3][i] += block_size_[0] * cross_diff_x_[i];
}
inside_count = 0;
std::vector<bool> right_flag(4, false);
for (int i = 0; i < 4; ++i) {
right_flag[i] = Inside(right_bound[i], coef_);
if (right_flag[i]) {
inside_count++;
}
}
Point p(x, point.y);
if (inside_count == 4) {
OutputBlock(p);
}
else if (inside_count > 0) {
std::vector<float> right_cross{right_bound[0][0], right_bound[0][1], right_bound[0][2]};
CheckBlock(p, right_cross);
}
if (!right_flag[1] && !right_flag[3]) {
point_right.x = x;
break;
}
}
}
if (inside_flag[0] || inside_flag[2]) {
float left_bound[4][3];
for (int i = 0; i < 12; ++i) {
left_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
}
for (int x = point.x - block_size_[0]; x > (bound.min_x - block_size_[0]); x -= block_size_[0]) {
for (int i = 0; i < 3; ++i) {
left_bound[0][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[1][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[2][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[3][i] -= block_size_[0] * cross_diff_x_[i];
}
inside_count = 0;
std::vector<bool> left_flag(4, false);
for (int i = 0; i < 4; ++i) {
left_flag[i] = Inside(left_bound[i], coef_);
if (left_flag[i]) {
inside_count++;
}
}
Point p(x, point.y);
if (inside_count == 4) {
OutputBlock(p);
}
else if (inside_count > 0) {
std::vector<float> right_cross{left_bound[0][0], left_bound[0][1], left_bound[0][2]};
CheckBlock(p, right_cross);
}
if (!left_flag[0] && !left_flag[2]) {
point_left.x = x;
break;
}
}
}
Point result;
result.x = point_left.x + ((point_right.x - point_left.x) >> 1);
result.x = Align(result.x, block_size_[0]);
result.y = point.y - block_size_[1];
for (int i = 0; i < 3; ++i) {
cross[i] = cross[i] - cross_diff_y_[i] * block_size_[1] + (result.x - point.x) * cross_diff_x_[i];
}
DoTopRaster(bound, cross, result);
}
void DoBottomRaster(const Bound& bound, std::vector<float>& cross, const Point& point) {
if (point.y > bound.max_y) {
return;
}
float cross_bound[4][3];
for (int i = 0; i < 3; ++i) {
cross_bound[0][i] = cross[i];
cross_bound[1][i] = cross_bound[0][i] + cross_diff_x_[i] * (block_size_[0] - 1);
cross_bound[2][i] = cross_bound[0][i] + cross_diff_y_[i] * (block_size_[1] - 1);
cross_bound[3][i] = cross_bound[2][i] + cross_diff_x_[i] * (block_size_[0] - 1);
}
int inside_count = 0;
std::vector<bool> inside_flag(4, false);
for (int i = 0; i < 4; ++i) {
inside_flag[i] = Inside(cross_bound[i], coef_);
if (inside_flag[i]) {
inside_count++;
}
}
if (inside_count == 4) {
OutputBlock(point);
}
else if (inside_count > 0) {
CheckBlock(point, cross);
}
Point point_left = point;
Point point_right = point;
if (inside_flag[1] || inside_flag[3]) {
float right_bound[4][3];
for (int i = 0; i < 12; ++i) {
right_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
}
for (int x = point.x + block_size_[0]; x <= bound.max_x; x += block_size_[0]) {
for (int i = 0; i < 3; ++i) {
right_bound[0][i] += block_size_[0] * cross_diff_x_[i];
right_bound[1][i] += block_size_[0] * cross_diff_x_[i];
right_bound[2][i] += block_size_[0] * cross_diff_x_[i];
right_bound[3][i] += block_size_[0] * cross_diff_x_[i];
}
inside_count = 0;
std::vector<bool> right_flag(4, false);
for (int i = 0; i < 4; ++i) {
right_flag[i] = Inside(right_bound[i], coef_);
if (right_flag[i]) {
inside_count++;
}
}
Point p(x, point.y);
if (inside_count == 4) {
OutputBlock(p);
}
else if (inside_count > 0) {
std::vector<float> right_cross{right_bound[0][0], right_bound[0][1], right_bound[0][2]};
CheckBlock(p, right_cross);
}
if (!right_flag[1] && !right_flag[3]) {
point_right.x = x;
break;
}
}
}
if (inside_flag[0] || inside_flag[2]) {
float left_bound[4][3];
for (int i = 0; i < 12; ++i) {
left_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
}
for (int x = point.x - block_size_[0]; x > (bound.min_x - block_size_[0]); x -= block_size_[0]) {
for (int i = 0; i < 3; ++i) {
left_bound[0][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[1][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[2][i] -= block_size_[0] * cross_diff_x_[i];
left_bound[3][i] -= block_size_[0] * cross_diff_x_[i];
}
inside_count = 0;
std::vector<bool> left_flag(4, false);
for (int i = 0; i < 4; ++i) {
left_flag[i] = Inside(left_bound[i], coef_);
if (left_flag[i]) {
inside_count++;
}
}
Point p(x, point.y);
if (inside_count == 4) {
OutputBlock(p);
}
else if (inside_count > 0) {
std::vector<float> right_cross{left_bound[0][0], left_bound[0][1], left_bound[0][2]};
CheckBlock(p, right_cross);
}
if (!left_flag[0] && !left_flag[2]) {
point_left.x = x;
break;
}
}
}
Point result;
result.x = point_left.x + ((point_right.x - point_left.x) >> 1);
result.x = Align(result.x, block_size_[0]);
result.y = point.y + block_size_[1];
for (int i = 0; i < 3; ++i) {
cross[i] = cross[i] + cross_diff_y_[i] * block_size_[1] + (result.x - point.x) * cross_diff_x_[i];
}
DoBottomRaster(bound, cross, result);
}
private:
Bound bound_{};
int coef_{};
int cross_diff_x_[3]{};
int cross_diff_y_[3]{};
std::vector<int> block_size_{4, 4};
std::ofstream fout_{};
};
int main(void) {
std::vector<Point> points(3);
points[0].x = 200;
points[0].y = 200;
points[1].x = 600;
points[1].y = 200;
points[2].x = 400;
points[2].y = 400;
Raster raster;
raster.DoRasterTriangleHalf(points);
return 0;
}
4.6 重心插值
在上面我们已经找到了三角形内部所有的像素点,接下来我们就该进行插值计算每一个像素点的属性了,这里我们使用重心插值。
计算重心坐标
假设在三角形 ABC 内有一点 P,那么 P 的重心坐标 (i, j, k) 计算方式为
i = Area(ABP) / Area(ABC)
j = Area(BCP) / Area(ABC)
k = Area(CAP) / Area(ABC)
并且 i + j + k = 1;
在之前二维向量的叉乘中,我们提到叉乘可以计算平行四边形的面积,那么三角形的面积就是平行四边形面积的一半,而且在判断点是否在三角形内部时,都计算过对应叉乘的值,那么计算重心坐标的代码就可以变为下面这样
float cross[3];
float cross_diff_x[3];
float cross_diff_y[3];
float barycentric_coord[3];
float barycentric_diff_x[3];
float barycentric_diff_y[3];
Position p(min_x, min_y, 0, 1.0);
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(triangle.points[i].pos, triangle.points[(i + 1) % 3].pos, p);
cross_diff_x[i] = triangle.points[(i + 1) % 3].pos.x - triangle.points[i].pos.x;
cross_diff_y[i] = triangle.points[i].pos.y - triangle.points[(i + 1) % 3].pos.y;
barycentric_coord[i] = cross[i] / area;
barycentric_diff_x[i] = cross_diff_x[i] / area;
barycentric_diff_y[i] = cross_diff_y[i] / area;
}
int cross[3];
int cross_diff_x[3];
int cross_diff_y[3];
float barycentric_coord[3];
float barycentric_diff_x[3];
float barycentric_diff_y[3];
Point p;
p.x = min_x;
p.y = min_y;
int cross_abc = VecCross(points[0], points[1], points[2])
for (int i = 0; i < 3; ++i) {
cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
cross_diff_x[i] = points[i % 3].y - points[(i + 1) % 3].y;
cross_diff_y[i] = points[(i + 1) % 3].x - points[i % 3].x;
barycentric_coord[i] = cross[i] * 1.0 / cross_abc ;
barycentric_diff_x[i] = cross_diff_x[i] * 1.0 / cross_abc ;
barycentric_diff_y[i] = cross_diff_y[i] * 1.0 / cross_abc ;
}
后面相邻点的重心坐标也都可以通过差值计算得到
根据重心因子计算属性值
Point Lerp(const Point& p, const std::vector<Point>& triangle, float bary[3]) {
Point result = p;
result.z = bary[0] * triangle[2].z + bary[1] * triangle[0].z + bary[2] * triangle[1].z;
for (int i = 0; i < result.attribute_size; ++i) {
for (int j = 0; j < 4; ++j) {
result.attributes[i].attri[j] = bary[0] * triangle[2].attributes[i].attri[j] +
bary[1] * triangle[0].attributes[i].attri[j] +
bary[2] * triangle[1].attributes[i].attri[j];
}
}
return result;
}
4.7 透视校正
说到透视校正,就要提到我们在之前文章中说到过的透视变换和透视除法,透视变换会给我们呈现出一种近大远小的效果,为了呈现这种效果会改变w值,那么在透视除法中除 w 之后,坐标就从线性变为非线性的了,而且这里只对坐标做了处理,并没有对属性做处理,所以对属性进行透视校正,更为详细的文章可以参考图形学 - 关于透视矫正插值那些事 - 知乎
下面列出代码实现(仅供参考)
Point Lerp(const Point& p, const std::vector<Point>& points, float barycentric[3]) {
Point result = p;
std::vector<float> bary{barycentric[1], barycentric[2], barycentric[0]};
for (int i = 0; i < 3; ++i) {
bary[i] = (bary[i] * points[i].w);
// bary[i] = (bary[i] * points[i].point_position.w) / (points[i].point_position.z);
}
float z = 1.0f / (bary[0] + bary[1] + bary[2]);
for (int i = 0; i < 3; ++i) {
bary[i] *= z;
}
int attri_count = p.attribute_size;
for (int i = 0; i < attri_count; ++i) {
for (int j = 0; j < 4; ++j) {
result.attributes[i].attri[j] =
bary[0] * points[0].attributes[i].attri[j] +
bary[1] * points[1].attributes[i].attri[j] +
bary[2] * points[2].attributes[i].attri[j];
}
}
result.z = barycentric[1] * points[0].z +
barycentric[2] * points[1].z +
barycentric[0] * points[2].z;
return result;
}
关于三角形的光栅化就说到这里了