0%

【Games 101】HomeWork 3:Pipeline and Shading(渲染小奶牛)

HomeWork 3:Pipeline and Shading(渲染小奶牛)

这次作业是渲染小奶牛,有 法线,Binne-Pong,纹理贴图,凹凸贴图,位移贴图;

这个作业好多呀,感觉挺难的…

得参考这几篇文章:

【Games 101】Lec 07:渲染1(光照 阴影 和 图形管线)

【Games 101】Lec 08:渲染2(阴影 渲染管线 和 纹理映射)

【Games 101】Lec 09:渲染3(纹理映射(续))

法线

首先实现一个法线的:

main.cpp

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
// 获取投影矩阵(直接搬前面的就行)
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();

// 透视投影压缩矩阵
Eigen::Matrix4f M_persp;
M_persp << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear*zFar,
0, 0, 1, 0;

// 求一下正交投影矩阵要用到的参数
float alpha = 0.5 * (eye_fov / 180.0f * MY_PI); // 视野角度的一半
float yTop = -zNear * tan(alpha); // 因为这里的z给的是负数,所以加负号之后再转换
float yBottom = -yTop;
float xRight = yTop * aspect_ratio; // aspect_ratio 是 xy 的比例
float xLeft = -xRight;

Eigen::Matrix4f M_trans;
M_trans << 1, 0, 0, -(xLeft + xRight) / 2,
0, 1, 0, -(yTop + yBottom) / 2,
0, 0, 1, -(zNear + zFar) / 2,
0, 0, 0, 1;

Eigen::Matrix4f M_ortho;
M_ortho << 2 / (xRight - xLeft), 0, 0, 0,
0, 2 / (yTop - yBottom), 0, 0,
0, 0, 2 / (zNear - zFar), 0,
0, 0, 0, 1;

// 这个就是 正交投影矩阵
M_ortho = M_ortho * M_trans;

// 透视投影矩阵 = 正交投影矩阵 * 透视压缩矩阵(注意顺序哦)
projection = M_ortho * M_persp * projection;

return projection;
}

rasterizer.cpp

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
/*
三角形光栅化
view_pos 是三角形三个顶点的试图坐标,视图坐标是指相机空间(或观察空间)中的坐标,即相对于相机的坐标。
以支持在屏幕空间内进行插值和渲染
*/
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
auto v = t.toVector4();

int min_x = INT_MAX;
int max_x = INT_MIN;
int min_y = INT_MAX;
int max_y = INT_MIN;

// 求一下三角形的包围盒
for (auto point : v) //获取包围盒边界
{
min_x = min((float)min_x, point[0]);
max_x = max((float)max_x, point[0]);
min_y = min((float)min_y, point[1]);
max_y = max((float)max_y, point[1]);
}

// 遍历包围盒里面的点
for (int y = min_y; y <= max_y; y++)
{
for (int x = min_x; x <= max_x; x++)
{
if (insideTriangle((float)x+0.5, (float)y+0.5, t.v))
{
// 求一下三角形重心坐标
auto[alpha, beta, gamma] = computeBarycentric2D(x+0.5, y+0.5, 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;

// 判断当前z值是否小于原来z表此位置的z值
if (z_interpolated < depth_buf[get_index(x, y)])
{
// 颜色插值
auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1.0f);

// 法线插值(需要归一化 .normalized())
auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1.0f).normalized();

// 纹理插值
auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1.0f);

// 视图坐标插值
auto interpolated_shadingcoords = interpolate(alpha,beta,gamma,view_pos[0],view_pos[1],view_pos[2],1.0f);

// 用一个结构体存一下
fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;

// 传给片段着色器,获取最终颜色
auto pixel_color = fragment_shader(payload);

// 更新深度缓存,并设置像素的颜色
depth_buf[get_index(x,y)] = z_interpolated;
set_pixel(Vector2i(x,y),pixel_color);
}
}
}
}
}

这样就能按照 pdf 里面的操作运行啦,结果如下:

Binne-Pong

main.cpp

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
/*
Blinn-Phong 片段渲染器
Blinn-Phong 模型 = 环境反射 + 漫反射模型 + 高光反射/镜面反射
总结:边看笔记的公式边写就行(主要是得搞清楚代码里面的每个变量到底是啥)
*/
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
// Ambient 环境反射的系数
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
// Diffuse 漫反射系数
Eigen::Vector3f kd = payload.color;
// Specular 高光反射
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};

// 环境反射的 Ia(也是就环境光的强度)
Eigen::Vector3f amb_light_intensity{10, 10, 10};

// 视点
Eigen::Vector3f eye_pos{0, 0, 10};

// 指数 p 的一个作用:控制高光的衰减大小(可以在笔记中搜索这句话)
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};

// 定义 环境反射,漫反射,高光反射
Eigen::Vector3f ambient={0,0,0};
Eigen::Vector3f diffuse={0,0,0};
Eigen::Vector3f specular={0,0,0};

// 对于每条光线,计算 漫反射 和 高光反射(环境光始终不变,最后加上去就行)
for (auto& light : lights)
{
/*
计算漫反射
- light.intensity:是光强,也就是公式中的 I
- r_squared:物体与光源距离的平方(也就是公式中的 r^2)
- light.position - point:表示的是光线到表面的向量(也就是公式中的 I,可以看一下 "漫反射模型")
- 从"漫反射模型"笔记里面可以看到,normal.dot(((light.position - point).normalized())) 这一项就是在求一个角度
- cwiseProduct:是Eigen库中的一个函数,用于对两个向量进行逐元素乘法。
*/
float r_squared=(light.position - point).squaredNorm();
diffuse = kd.cwiseProduct(light.intensity / r_squared) * MAX(0, normal.dot(((light.position - point).normalized())));

/*
计算高光反射
- h:入射与反射光的半程向量
- light.position - point:表示的是 光线 到表面的向量
- eye_pos - point:表示的是 眼睛 到表面的向量
- 计算高光的时候,一定要注意p次幂
*/
// 套公式
auto h = ((light.position - point).normalized() + (eye_pos - point).normalized()).normalized();
specular = ks.cwiseProduct(light.intensity / r_squared) * std::pow(std::max(0.0f, normal.dot(h)), p);

// 环境反射(放外面只算一次的话,渲染出来看起来就比较暗一点)
ambient = ka.cwiseProduct(amb_light_intensity);

// 最后加和一下
result_color += (ambient + diffuse + specular);
}

return result_color * 255.f;
}

结果:

纹理贴图(texture)

main.cpp

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
/*
纹理片段渲染器
在实现 Blinn-Phong的基础上,将纹理颜色视为公式中的 kd,实现 Texture Shading Fragment Shader.
- 使用uv坐标调用纹理get_color函数。
- uv坐标是rasterize_triangle函数中插值得到的。
- 用对应点纹理的颜色替换漫反射kd系数。
*/
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
auto Limit_Number = [](float &number){
number = max(number, (float)0.0);
number = min(number, (float)1.0);
};

Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
// 纹理坐标 u v(应该在 [0,1] 之间,限制一下,避免越界)
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
Limit_Number(u);
Limit_Number(v);

// 获取对应纹理坐标的颜色
return_color = payload.texture->getColor(u, v);
}
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);
// 将对应点的纹理颜色替换成漫反射kd系数
Eigen::Vector3f kd = texture_color / 255.f;
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 = texture_color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;

// 下面也是一样的 Blinn-Phong
Eigen::Vector3f result_color = {0, 0, 0};
Eigen::Vector3f ambient={0,0,0};
Eigen::Vector3f diffuse={0,0,0};
Eigen::Vector3f specular={0,0,0};

// 对于每条光线,计算 漫反射 和 高光反射(环境光始终不变,最后加上去就行)
for (auto& light : lights)
{
// 漫反射
float r_squared=(light.position - payload.view_pos).squaredNorm();
diffuse = kd.cwiseProduct(light.intensity / r_squared) * MAX(0, normal.dot(((light.position - point).normalized())));

// 高光反射
auto h = ((light.position - point).normalized() + (eye_pos - point).normalized()).normalized();
specular = ks.cwiseProduct(light.intensity / r_squared) * std::pow(std::max(0.0f, normal.dot(h)), p);

// 环境反射(放外面只算一次的话,渲染出来看起来就比较暗一点)
ambient = ka.cwiseProduct(amb_light_intensity);

// 最后加和一下
result_color += (ambient + diffuse + specular);
}

return result_color * 255.f;
}

结果:

凹凸贴图(Bump Mapping)

main.cpp

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/*
Bump Mapping 片段渲染器(凹凸贴图)
这个好难,建议回去看看视频(P10 前半部分)
再结合TBN矩阵的求法和应用。
TBN 矩阵就是世界空间到切线空间之间互相转变的桥梁:
- 世界空间 = TBN矩阵的逆 x 切线空间
- 切线空间 = TBN矩阵 x 世界空间

照着TODO里面写

-- 2024.02.07 [fjbq]
这里需要特别的注意这个 [切线空间/法线贴图的理解] 和 [TBN 矩阵的运用] !!!
我发现之前理解的有点问题,不太深刻,后面在复刻 tinyrenderer 的时候发现这个问题!
具体看这里,这里讲的比较清楚:https://blog.csdn.net/game_jqd/article/details/74858146
然后改了一下上面 世界空间 和 切线空间 的转换公式

然后看下面代码 TBN 矩阵的注释!!!
可以看到,一般的TBN矩阵是先行再列的,也就是:
TBN << t.x(), t.y(), t.z(),
b.x(), b.y(), b.z(),
n.x(), n.y(), n.z();
但是我们发现代码里面竟然是:
TBN << t.x(), b.x(), n.x(),
t.y(), b.y(), n.y(),
t.z(), b.z(), n.z();
也就是说它转置了一下!
同时,TBN矩阵是一个正交矩阵,它的逆等于它的转置;
那么也就是说下面代码的TBN矩阵其实是将原来的TBN矩阵求逆了!!!

所以这下就没问题了:
首先法线贴图里面存的都是切线空间中的法线,
而我们计算光照用到的法线需要统一到世界空间中来,
所以需要转换一下:
世界空间 = TBN矩阵的逆 x 切线空间
-- 2024.02.07 [fjbq]
*/
Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
auto Limit_Number = [](float &number){
number = max(number, (float)0.0);
number = min(number, (float)1.0);
};

/*
这里 .norm 求范数;
这里巨坑...
这里 u v 会大于1...
导致 getColor(u, v) 的时候会数组越界...所以需要限制一下
调了一下午了...
*/
auto func_h = [&payload, Limit_Number](float u, float v) -> auto {
Limit_Number(u);
Limit_Number(v);
return payload.texture->getColor(u, v).norm();
};

Eigen::Vector3f result_color = {0, 0, 0};

// 注意这里 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;

float kh = 0.2, kn = 0.1;

// n是原来模型空间的法线
Eigen::Vector3f n = normal;
float x = n.x(), y = n.y(), z = n.z();

// 计算得到法线空间的基向量t和b(法线空间中,n与模型空间一致)
Eigen::Vector3f t(x*y / sqrt(x*x + z*z), sqrt(x*x + z*z), z*y / sqrt(x*x + z*z));
Eigen::Vector3f b = n.cross(t);

// 这个是 TBN矩阵 的逆!!!: 将纹理坐标对应到模型空间中
Eigen::Matrix3f TBN;
TBN << t.x(), b.x(), n.x(),
t.y(), b.y(), n.y(),
t.z(), b.z(), n.z();

// 纹理坐标和大小
float u = payload.tex_coords.x(), v = payload.tex_coords.y();
float w = payload.texture->width, h = payload.texture->height;

float dU = kh * kn * (func_h(u+1/w, v) - func_h(u,v));
float dV = kh * kn * (func_h(u, v+1/h) - func_h(u,v));

// 获得切线空间中法线的坐标
Eigen::Vector3f ln(-dU, -dV, 1);

// 将它转换到模型空间中
// 世界空间 = TBN矩阵的逆 x 切线空间
normal = (TBN * ln).normalized();
result_color = normal;

return result_color * 255.f;
}

结果:

位移贴图(Displacement Mapping)

main.cpp

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
Displacement Mapping 片段渲染器(位移贴图)
*/
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
auto Limit_Number = [](float &number){
number = max(number, (float)0.0);
number = min(number, (float)1.0);
};

/*
这里 .norm 求范数;
这里巨坑...
这里 u v 会大于1...
导致 getColor(u, v) 的时候会数组越界...所以需要限制一下
调了一下午了...
*/
auto func_h = [&payload, Limit_Number](float u, float v) -> auto {
Limit_Number(u);
Limit_Number(v);
return payload.texture->getColor(u, v).norm();
};

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;

float kh = 0.2, kn = 0.1;

// n是原来模型空间的法线
Eigen::Vector3f n = normal;
float x = n.x(), y = n.y(), z = n.z();

// 计算得到法线空间的基向量t和b(法线空间中,n与模型空间一致)
Eigen::Vector3f t(x*y / sqrt(x*x + z*z), sqrt(x*x + z*z), z*y / sqrt(x*x + z*z));
Eigen::Vector3f b = n.cross(t);

Eigen::Matrix3f TBN; //TBN矩阵: 将纹理坐标对应到模型空间中
TBN << t.x(), b.x(), n.x(),
t.y(), b.y(), n.y(),
t.z(), b.z(), n.z();

// 纹理坐标和大小
float u = payload.tex_coords.x(), v = payload.tex_coords.y();
float w = payload.texture->width, h = payload.texture->height;

float dU = kh * kn * (func_h(u+1/w, v) - func_h(u,v));
float dV = kh * kn * (func_h(u, v+1/h) - func_h(u,v));

// 获得切线空间中法线的坐标
Eigen::Vector3f ln(-dU, -dV, 1.0f);
ln.normalize();

// 将它转换到模型空间中
normal = (TBN * ln).normalized();

// 改变目标点的位置,将目标点拔高
point = point + kn * n * (payload.texture->getColor(u,v).norm());

Eigen::Vector3f result_color = {0, 0, 0};

Eigen::Vector3f ambient={0,0,0};
Eigen::Vector3f diffuse={0,0,0};
Eigen::Vector3f specular={0,0,0};
for (auto& light : lights)
{
// 漫反射
float r_squared = (light.position - point).squaredNorm();
diffuse = kd.cwiseProduct(light.intensity / r_squared) * MAX(0, normal.dot(((light.position - point).normalized())));

// 高光反射
auto h = ((light.position - point).normalized() + (eye_pos - point).normalized()).normalized();
specular = ks.cwiseProduct(light.intensity / r_squared) * std::pow(std::max(0.0f, normal.dot(h)), p);

// 环境反射
ambient = ka.cwiseProduct(amb_light_intensity);

// 将三种光相加
result_color += ambient + diffuse + specular;
}

return result_color * 255.f;
}

结果:

欢迎关注我的其它发布渠道