GAMES101-作业3

这一系列博客主要是为了记录自己对GAMES101课程的作业完成情况.

作业3: 主要是写各个shader, 这里面也包括对代码框架的解读


作业描述

在本次作业中, 会进一步模型现代图形技术. 本次在代码框架中添加了Object Loader, Vertex Shader, Fragment Shader, 并且支持了纹理映射.

本次作业的主要任务其实是完成几个fragment shader: phong_fragment_shader, texture_fragment_shader, bump_fragment_shader, displacement_fragment_shader

由于本次代码的框架已经很切合现代的图形技术, 对于rasterizer这个过程的理解很有帮助, 故这里从main函数开始解读, 顺便看一看代码的框架.

main()

main()函数中:

  1. 初始化了一些参数, eye_pos, file_name, obj_path等等
  2. 进行了Object Loader(注: Object Loader 这个过程我这里不详细阅读代码(感觉其实一个很系统的工程, obj也是一个约定好的文件格式))
  3. 进行了Triangle Mesh 三角形网格的初始化

接下来, 则进行了光栅化器(Rasterizer)的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
rst::rasterizer r(700, 700);
r.set_texture(Texture(obj_path + texture_path)); // 为texture Mapping 提供一个二维材质

r.set_vertex_shader(vertex_shader); // 设置 vertex_shader
r.set_fragment_shader(active_shader); // 设置 fragment_shader

// 这俩 shader 的定义如下, 用的方法是回调函数的方式
// std::function<Eigen::Vector3f(fragment_shader_payload)> fragment_shader;
// std::function<Eigen::Vector3f(fragment_shader_payload)> vertex_shader;

r.clear(rst::Buffers::Color | rst::Buffers::Depth); // 清空buffer

// 这里初始化 model-view-projection矩阵
r.set_model(get_model_matrix(angle));
r.set_view(get_view_matrix(eye_pos));
r.set_projection(get_projection_matrix(45.0, 1, 0.1, 50));
// 修改函数 get_projection_matrix() in main.cpp: 将你自己在之前的实验中实现的投影矩阵填到此处.
// 这里三个矩阵的具体实现方法忽略, 前面几次作业已经完成

// 在所有初始化工作完成后, 接下来正式进入 rasterizer 的处理流程
r.draw(TriangleList);

// 调用 OpenCV画图

上述代码的最后一句调用draw()函数来正式进入渲染管线, 接下来将进行介绍


draw(): pipeline

我们可以看到, draw()函数整体是一个循环, 因为我们使用的是Triangle Mesh, 因此这里将对所有Triangle分别进行处理.

对于一个for循环, 其实主要包含三部分(按顺序):

  1. 对每个点进行model-view-proj变换(Vertex Processing)
  2. 对变换后的每个点生成新的Triangle, 其实就是求变换后的三角形(Triangle Processing)(不包含内插的过程, 也即求重心坐标的过程)
  3. Rasterization(光栅化)

注:

  • 展示代码中忽略for循环
  • 关于四个变换的问题, 请参考作业1的笔记
  • 关于法线如何进行model-view的变换这个问题, 请参考这篇文章
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Eigen::Matrix4f mvp = projection * view * model; // 这里设置 model-view-projection矩阵
// 关于 四个变换 的问题, 请参考作业1的笔记

// 注: 本代码不考虑for循环, 也即忽略掉for循环
// 这个代码有很多新的变量, 为了理解整个过程, 我决定倒着看
/*
* class Triangle{
* public:
* Vector4f v[3];
* Vector3f color[3];
* Vector2f tex_coords[3];
* Vector3f normal[3];
* }
*/
// 通过对 Triangle 的定义我们可以发现, 要获得一个变换后的 `Triangle`, 需要又以上的变量
// 但其中有一个需要特别注意: normal 的设置, 具体原因在后面讲
// 对照着这个看, 我们理解此函数中的代码

// 1. Vertex Processing
Triangle newtri = *t;

// model-view-projection变换
Eigen::Vector4f v[] = {
mvp * t->v[0],
mvp * t->v[1],
mvp * t->v[2]
};
//Homogeneous division
for (auto& vec : v) {
vec.x()/=vec.w();
vec.y()/=vec.w();
vec.z()/=vec.w();
}
// 第四次变换, 变换到成像平面
//Viewport transformation
for (auto & vert : v)
{
vert.x() = 0.5*width*(vert.x()+1.0);
vert.y() = 0.5*height*(vert.y()+1.0);
vert.z() = vert.z() * f1 + f2;
}

// 2. 法线 以及 一个比较特殊的 viewspace_pos
// 这部分我后面单独讲下我自己的理解
std::array<Eigen::Vector4f, 3> mm {
(view * model * t->v[0]),
(view * model * t->v[1]),
(view * model * t->v[2])
};

std::array<Eigen::Vector3f, 3> viewspace_pos;

std::transform(mm.begin(), mm.end(), viewspace_pos.begin(), [](auto& v) {
return v.template head<3>();
});

Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();
Eigen::Vector4f n[] = {
inv_trans * to_vec4(t->normal[0], 0.0f),
inv_trans * to_vec4(t->normal[1], 0.0f),
inv_trans * to_vec4(t->normal[2], 0.0f)
};

// 3. 变换后的 Triangle
for (int i = 0; i < 3; ++i)
{
//screen space coordinates
newtri.setVertex(i, v[i]);
}
for (int i = 0; i < 3; ++i)
{
//view space normal
newtri.setNormal(i, n[i].head<3>());
}
newtri.setColor(0, 148,121.0,92.0);
newtri.setColor(1, 148,121.0,92.0);
newtri.setColor(2, 148,121.0,92.0);

// 4. Rasterize 光栅化三角形
rasterize_triangle(newtir, viewspace_pos);

关于我个人对中间部分的viewspace_pos 以及 newNormal 的处理的理解(仅针对这个过程, 希望有专业的解读可以留言):

如上图, 我认为, 之所以对 normalviewspace_pos只使用 model-view的变换的原因是: 保证入射方向和出射方向的不变性, 以及法线方向的不变性

因为Camera的位置就是定义在 view坐标系下的, 而且经过model-view后的空间可以称为Camera Space, 在这个空间保留一个交点的坐标, 以及法线方向, 对于后续的Rander很有帮助

至于为什么不全部移到Pixel Space中去, 我认为原因可能是: 经过投影变换后的空间, 不能保证法线还是原来的法线, 甚至入射角和出射角都会发生变换(尤其是透视投影这种, 不仅仅是简单的平移和旋转).

线代中的线性变换对线性空间的影响会导致原来的坐标系不再垂直, 也即会改变整个入射角和出射角


Rasterizer

这个函数与作业2基本相同, 这里贴上相关代码, 具体求重心坐标的过程不在赘述

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
32
33
34
35
36
37
auto v = t.toVector4();

// bounding box
int widthMax = static_cast<int>(std::ceil(std::max(t.v[0][0], std::max(t.v[1][0], t.v[2][0]))));
int widthMin = static_cast<int>(std::floor(std::min(t.v[0][0], std::min(t.v[1][0], t.v[2][0]))));
int heightMax = static_cast<int>(std::ceil(std::max(t.v[0][1], std::max(t.v[1][1], t.v[2][1]))));
int heightMin = static_cast<int>(std::floor(std::min(t.v[0][1], std::min(t.v[1][1], t.v[2][1]))));

// 计算是否再三角形中
for(int x = widthMin; x <= widthMax; ++x){
for(int y = heightMin; y <= heightMax; ++y)
if(insideTriangle(x+0.5, y+0.5, t.v)){
// 插值
auto[alpha, beta, gamma] = computeBarycentric2D(x+0.5, y+0.5, t.v);
float Z = 1.0 /(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w()); // w 是向量的第四个维度
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();

// 也包括了 z-buffer 的一些细节
zp *= Z;
if(zp < depth_buf[get_index(x, y)]){
Eigen::Vector3f interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1.f);
Eigen::Vector3f interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1.f);
Eigen::Vector2f interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1.f);
Eigen::Vector3f interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1.f);

// 初始化 fragment_shader_payload, 以方便传给 fragment_shader
fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;

depth_buf[get_index(x, y)] = zp;

// 传递给 fragment_shader
auto pixel_color = fragment_shader(payload);
set_pixel(Vector2i(x, y), pixel_color);
}
}
}
其中调用fragment_shader进行下一步工作,


fragment_shader()

这个fragment_shader 也就是本作业的要求, 接下来各个进行分析.

phong_fragment_shader

这个函数感觉就是照着公式写, 所以这里直接粘贴代码了:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};

float p = 150;

Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;

Eigen::Vector3f result_color = {0, 0, 0};
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
// 首先, 求 三个方向向量
Eigen::Vector3f lightVec = light.position - point;
Eigen::Vector3f normalVec = normal;
Eigen::Vector3f viewVec = eye_pos - point;

float r = lightVec.dot(lightVec);

// diffues
Eigen::Vector3f diffuseReflect = kd.cwiseProduct(light.intensity / r) * std::max(0.0f, normalVec.dot(lightVec.normalized()));

// specular
Eigen::Vector3f harfVec = lightVec + viewVec;
harfVec.normalize();
Eigen::Vector3f specularReflect = ks.cwiseProduct(light.intensity / r) * std::pow(std::max(0.0f, normalVec.dot(harfVec)), p);

// ambient
Eigen::Vector3f ambientReflect = amb_light_intensity.cwiseProduct(ka);

result_color += (diffuseReflect + specularReflect + ambientReflect);
}

return result_color * 255.f;
}

texture_fragment_shader

emm, 就是将kd的内容变换为我们从材质文件中提取到的内容, 因此这里只粘贴前半部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
// TODO: Get the texture value at the texture coordinates of the current fragment
return_color = payload.texture->getColor(payload.tex_coords.x(), payload.tex_coords.y());
}
Eigen::Vector3f texture_color;
texture_color << return_color.x(), return_color.y(), return_color.z();

Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = texture_color / 255.f;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

// 后面的和上面的差不多, 照着写下来就行
}

displacement && bump

怎么说呢, 感觉注释给出了代码, 也就是照着注释实现一下, 这里就不在粘贴了.