第二课 三角形光栅化和背部剔除

Reference

Lesson 2: Triangle rasterization and back face culling · ssloy/tinyrenderer Wiki — 第 2 课:三角形光栅化和背面剔除 ·ssloy/tinyrenderer 维基 (github.com)
https://www.cnblogs.com/TenosDoIt/p/4024413.html

扫线法绘制面

用扫线法绘制面,就类似于我们画画一样,一条一条线的画,直到将三角形画满

在上一节课中我们已近写了画线的方法了,需要得到两个点,我们从下往上画,每次都得到三角形两个边上的对应顶点。

就像下面的图形一样:

图1 扫线法

但是有的同学已经发现问题了,这样做的话我们不能一次全部扫完,需要将三角形分为两段

高度差最大的边 (V0V2) 找到,并且在高度在中间的点 (V1) 上做一条水平线,得到水平线与高度差最大的边的交点 (Mid),并且以这个交点与高度在中间的点 (V1) 为划分线将三角形划分开。

图2 扫线法分割三角形

求解的话,用最简单的两点式求解就好了。

1
2
3
int xy_learp(int x0, int y0, float t, int x) {
return y0 + t * (x - x0);
}

t就是斜率,并且根据如果求解y翻转输入参数x0,y0,并且斜率是原来的倒数,例如:

1
2
float t_01 = (float(v1.x - v0.x) / (v1.y - v0.y));
int left_x = xy_learp(v0.y, v0.x, t_01, y);

下面是源码时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//扫线法
void triangle_scanning(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color) {
//升序排列顶点
if (v0.y > v1.y) std::swap(v0, v1);
if (v0.y > v2.y) std::swap(v0, v2);
if (v1.y > v2.y) std::swap(v1, v2);
/*line(v0, v1, image, *green);
line(v1, v2, image, *green);
line(v2, v0, image, *red);*/
//分割成为两部分
float t_02 = (float(v2.x - v0.x) / (v2.y - v0.y));
int mid_x = xy_learp(v0.y, v0.x, t_02, v1.y);
Vec2i v_mid = Vec2i(mid_x, v1.y);
//line(v_mid, v1, image, *green);
//开始扫线 下半部分
float t_01 = (float(v1.x - v0.x) / (v1.y - v0.y));
for (int y = v0.y; y <= v1.y; y++)
{
int left_x = xy_learp(v0.y, v0.x, t_01, y);
int right_x = xy_learp(v0.y, v0.x, t_02, y);
line(left_x, y, right_x, y, image, color);
}
//上部分
float t_21 = (float(v1.x - v2.x) / (v1.y - v2.y));
for (int y = v1.y; y <= v2.y; y++)
{
int left_x = xy_learp(v2.y, v2.x, t_02, y);
int right_x = xy_learp(v2.y, v2.x, t_21, y);
line(left_x, y, right_x, y, image, color);
}
}

很完美

包围盒遍历法

这个方法我们先要得到包围这个三角形的矩形,然后遍历这个矩形的像素,判断某个像素点在不在三角形内。

点是否在三角形内

这个求解暂时不知道求对没有,暂且先记下来吧,现在知道了u v w是下面两个叉乘结果

补充:

于是我们得到最关键的函数:

1
2
3
4
5
6
7
8
9
10
11
//返回 u v w
Vec3f barycentric(Vec2i* pts, Vec2i P) {
Vec2i AB = pts[2] - pts[0];
Vec2i AC = pts[1] - pts[0];
Vec2i PA = pts[0] - P;
Vec3f uvw =
Vec3f(AB.x, AC.x, PA.x) ^
Vec3f(AB.y, AC.y, PA.y);
if (std::abs(uvw.z) < 1) return Vec3f(-1, 1, 1);
return Vec3f(1.f - (uvw.x + uvw.y) / uvw.z, uvw.y / uvw.z, uvw.x / uvw.z);
}

我们找出三角形的包围盒,并且开始遍历,如果像素点在三角形内就上色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//遍历法
void triangle_foreach(Vec2i* pts, TGAImage& image, TGAColor color) {
//包围盒
Vec2i left_down(image.get_width() - 1, image.get_height() - 1);
Vec2i right_up(0, 0);
for (int i = 0; i < 3; i++)
{
left_down.x = std::min(left_down.x, pts[i].x);
left_down.y = std::min(left_down.y, pts[i].y);

right_up.x = std::max(right_up.x, pts[i].x);
right_up.y = std::max(right_up.y, pts[i].y);
}
//遍历包围盒
for (int x = left_down.x; x <= right_up.x; x++)
{
for (int y = left_down.y; y <= right_up.y; y++)
{
Vec2i point(x, y);
Vec3f uvw = barycentric(pts, point);
if (uvw.x < 0 || uvw.y < 0 || uvw.z < 0) continue;
image.set(x, y, color);
}
}
}

加载模型

现在我们遍历模型的每一个三角形,并且得到每个面的法线(用叉乘得到),并且用光源方向法线方向点乘(兰伯特),得到光强,与颜色相乘得到最终颜色,我们去掉为负数的点乘结果,为负数说明光源方向法线方向大于90度是来自背面的光

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//加载模型面
void load_modele_triangle(Model* model, TGAImage* image) {
//灯光方向
Vec3f light_dir(0, 0, 1);

int height = image->get_height();
int width = image->get_width();
for (int i = 0; i < model->nfaces(); i++)//遍历三角形
{
std::vector<int> face = model->face(i);//得到一个面

Vec2i triangle[3];
Vec3f world_pos[3];
for (int j = 0; j < 3; j++) {
Vec3f v = model->vert(face[j]);
triangle[j] = Vec2i(((model->vert(face[j])).x + 1) * width / 2, ((model->vert(face[j])).y + 1) * height / 2);
world_pos[j] = v;
}
//TGAColor color(std::rand() % 255, std::rand() % 255, std::rand() % 255, 255);
//得到法线方向
Vec3f normal = (world_pos[0] - world_pos[2]) ^ (world_pos[1] - world_pos[2]);
normal.normalize();
//与光的方向点成
float intensity = light_dir * normal;
if (intensity > 0) {//去除背部
TGAColor color(255 * intensity, 255 * intensity, 255 * intensity, 255 * intensity);
triangle_foreach(triangle, *image, color);
}
}
}

完美。