0%

前言

准备学一下 Games 101, 这一篇记录一下学习笔记 ;

对应的应该还有 Games 101 作业 ( 希望能够坚持 ! )

导航

课程

【Games 101】Lec 01:计算机图形学概述

【Games 101】Lec 02:回顾线性代数

【Games 101】Lec 03:变换

【Games 101】Lec 04:变换(续)

【Games 101】Lec 05:光栅化1(三角形)

【Games 101】Lec 06:光栅化2(抗锯齿 和 深度缓冲)

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

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

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

【Games 101】Lec 10:几何 1 (入门)

【Games 101】Lec 11:几何 2 (曲线 和 曲面)

【Games 101】Lec 12:几何 3

【Games 101】Lec 13:光线追踪 1(Shadow Mapping,Whitted-Style Ray Tracing)

【Games 101】Lec 14:光线追踪 2(加速光线追踪 和 辐射度量学)

【Games 101】Lec 15:光线追踪 3(辐射度量学,光传输,全局光照)

【Games 101】Lec 16:光线追踪 4(蒙特卡罗积分,路径追踪)

【Games 101】Lec 17:材质和外观

【Games 101】Lec 18:渲染的前沿话题

【Games 101】Lec 19:照相机、镜头、光场

【Games 101】Lec 20:颜色和感知

【Games 101】Lec 21:动画

【Games 101】Lec 22:动画(续)

作业

ps:做起来真的感觉不简单啊!

【Games 101】HomeWork 0:虚拟机的使用

【Games 101】HomeWork 1:旋转与投影

【Games 101】HomeWork 2:Triangles and Z-buffering(光栅化 和 抗锯齿)

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

【Games 101】HomeWork 4:Bézier 曲线

【Games 101】HomeWork 5:光线与三角形相交

【Games 101】HomeWork 6:加速结构

【Games 101】HomeWork 7:路径追踪

【Games 101】HomeWork 8:质点弹簧系统

参考

可以参考这三个大佬的分析

Keanu - 知乎 (zhihu.com)

柚子没有贝壳味 - 知乎 (zhihu.com)

Magic__Conch-CSDN博客

Eigen 库:

Eigen: Main Page

Eigen库学习教程(全)_find_package(eigen3 3 required)-CSDN博客

快速入门矩阵运算——开源库Eigen - 知乎 (zhihu.com)

前言

可以参考菜鸟教程:
Swift 教程 | 菜鸟教程 (runoob.com)

1
2
3
4
5
6
7
8
print(1, 2, 3, 4, 5, separator:" . ", terminator:"-")

- print 函数默认换行(默认终结符是\n);
- separator:定义输出的内容之间的分隔符;
- terminator:定义输出的终结符;

var:定义变量
let:定义常量

字符串

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
// 字符串

// 重复十个a
var s0 = String(repeating: "a", count: 10)

// 多行字符串
var s1 = """
中国
人民
站起来了
"""
print(s1)

// 多行字符串(但是只输出一行)
var s2 = """
中国\
人民\
站起来了
"""
print(s2) // 中国人民站起来了

// 转义字符\
var s3 = "\"中国\""
print(s3) // 输出:“中国”

// 定界符(不用转义)
var s4 = #""中'国""#
print(s3) // 输出:"中'国"(第一对 "" 表示字符串的意思)

// 字符串链接
var name = "fj"
var age = 22
var hello = "bq"
var s5 = name + hello // s5 = fjbq
name.append(hello) // name = fjbq
name += hello // name = fjbqbq
var s7 = "\(name) 年龄是: \(age)岁" // 字符串转义

print("s5 : " + s5)
print("s7 : " + s7)

--------------------------------------------------------------

// 字符串属性
var name = "Fj Qq"
print(name.isEmpty)
print(name.count) // 返回字符串的长度
print(name.description) // 输出字符串的值
print(name.debugDescription) // 便于调试的值
print(name.hashValue) // 输出哈希值(每一个变量都有一个hash值)
print(name.uppercased()) // 大写
print(name.lowercased()) // 小写

print("proNum".hasPrefix("pro")) // 判断字符串是否有pro前缀
print("proNum".hasPrefix("Num")) // 判断字符串是否有Num后缀

// 字符串遍历

var ss = "fjbq中国人👀"

// 直接遍历值
for c in ss {
print(c)
}

// 遍历索引(索引不是012...)
for index in ss.indices {
print(ss[index])
}

// ss.startIndex 是 ss 的第一个索引
print(ss[ss.startIndex])

// ss.endIndex 是ss的最后一个索引(但是不是最后一个字符,是最后一个字符的下一个位置)
// ss.index() 表示对ss的索引进行操作:
// ss.index(before: ss.endIndex) 表示获取 ss.endIndex 索引的前一个索引
// ss.index(after: ss.endIndex) 表示获取 ss.endIndex 索引的后一个索引
// ss.index(ss.endIndex, offsetBy: -2) 表示获取 ss.endIndex 索引偏移 -2 的索引
print(ss[ss.index(before: ss.endIndex)])

整数、浮点数、双精度

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
整数浮点数双精度

var a = 4.0
print(type(of: a)) // double

var myAge:Int = 50 // 显示声明整数
var yourAge = Int(60.0) // double转换为int

var a = 0b10 // 2进制
var b = 0o7 // 8进制
var c = 0xf // 16进制

var d = 89_000_000 // 分割数字
print(d) // 89000000

var price = Int("3") ?? 0 // ?? 运算符:Int("3") 成功则为3,不成功则为 0(不加??会报错)
print(price)

// 指定转换的进制
var price1 = Int("ff", radix: 16) ?? 0
print(price1)

// 随机数
print(Int.random(in: 1...100)) // [1,100]
print(Int.random(in: 100..<110)) // [100,110)

var x = -21
x.negate() // -21 变成 21

x = -30
print(x.magnitude) // magnitude 绝对值属性

// quotientAndRemainder:求商和余数;dividingBy:除以
x = 100
let (q,r) = quotientAndRemainder(dividingBy: 9)

// 返回x的符号:-1,0,1
print(x.signum())

// 整数的常量
print(Int.zero)
print(Int.max)
print(Int.min)

-------------------------------------------------------------

双精度

var z1 = Double.random(in: 10.0...100.0)
print(z1.squareRoot()) // 平方根

// 近似值
x = 100.23
print(x.rounded()) // 默认四舍五入
print(x.rounded(.awayFromZero)) // 四舍五入到幅度大于或等于源值的最接近的允许值
print(x.rounded(.down)) // 四舍五入为小于或等于源值的最接近的允许值
print(x.rounded(.toNearestOrAwayFromZero)) // 四舍五入到最接近的允许值;如果两个值同样接近,则选择幅度较大的一个
print(x.rounded(.toNearestOrEven)) // 四舍五入到最接近的允许值;如果两个值同样接近,则选择偶数
print(x.rounded(.towardZero)) // 四舍五入到幅度小于或等于源值的最接近的允许值
print(x.rounded(.up)) // 四舍五入为大于或等于源值的最接近的允许值

y = -1.1
print(y.magnitude) // 绝对值
print(y.sign) // 符号:plus,minus

print(Double.pi) // 常量

------------------------------------------------------------------------------------

布尔值

var x = true
var y:Bool = false

x.toggle() // x 取反
print(x)

print(Bool.random()) // 随机布尔值

运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运算符

var (a, b) = (10,20)

// 空合运算符
var h:String? // 表示可能没有值
print(h ?? "") // 有值输出h,没值输出""

// 区间运算符
// 1...10 [1,10]
// 1..<10 [1,10)
// 下面两个只能在有区间限制的情况下使用
// ...10 [0,10]
// ..<10 [0,10)

// 其他的基本和C++一样

小练习:计算圆的周长和面积

1
2
3
4
5
6
7
8
9
10
11
12
小练习:计算圆的周长和面积

print("请输入圆的半径:", terminator: "")
var value = readLine() ?? "0.0" // 读入的是字符串,可能不读入

var radius:Double = 0.0
radius = Double(value) ?? 0.0 // 可能转化失败

let area = Double.pi * radius * radius
let perimeter = 2 * Double.pi * radius

print("半径为 \(radius) 的圆\n周长是 \(perimeter.rounded()) \n面积是 \(area.rounded())")

数组

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
数组

性质:
1. 有序
2. 类型统一
3. 内容可重复

--------------------------------------------------------------------------------

// 数组的声明和定义

var a = [1,2,3]
var b = ["a","b","c"]
var c = [true,false,true]
print(a,b,c)

var a1:Array<Int> = [1,2,3]
var a2:[Int] = [1,2]
print(a1,a2)

var b1 = Array<Int>()
var b2 = Array<Int>([1,2])
var b3 = Array(1...7)
print(b1,b2,b3)

var c1 = Array(repeating:"*", count:6)
print(c1)
print(c1.count) // 统计元素的个数
print(c1.isEmpty) // 判断是否为空

--------------------------------------------------------------------------------

// 下标访问数组

print(a[0]) // 输出:1
print(b[0...2]) // 输出:["a", "b", "c"]
print(b[...2]) // 输出:["a", "b", "c"]
print(b[1...]) // 输出:["b", "c", "d", "e"]

--------------------------------------------------------------------------------

// 数组的遍历

for item in b
{
print(item, terminator: "-")
}
print("")
// 输出:a-b-c-d-e-

for (i, value) in b.enumerated()
{
print(i, value, separator: ":")
}
/*
输出:
0:a
1:b
2:c
3:d
4:e
*/

print(b[b.count - 1]) // 输出数组的最后一个元素,数组为空会报错
print(b.first ?? "") // 输出数组的第一个元素,没加 ?? "" 会输出 Optional("a"),表示这个值是可选的(可能为空),当数组为空时输出nil
print(b.last ?? "") // 输出数组的最后一个元素
print(b.randomElement() ?? "") // 随机一个元素

--------------------------------------------------------------------------------

// 数组 追加元素

var v = ["坚定", "坚持", "坚强"]
v.append("努力") // 追加一个元素
print(v)

v.append(contentsOf: ["勇敢","乐观"])
print(v)

v += ["奋斗","幸运"]
print(v)

--------------------------------------------------------------------------------

// 数组 插入和替换
v.insert("fjbq", at: 0) // 将fjbq插入到第0的位置
v.insert(contentsOf:["fj","fj777"], at: 0) // 插入数组

var v1 = Array<Int>()
v1.insert(contentsOf: 1...3, at: 0) // 插入序列
print(v1)

print(v)
v.replaceSubrange(0...2, with:["1","2"]) // 将v数组的0-2的元素替换成后面的数组(个数可以不匹配)
print(v)

----------------------------------------------------------------------------------------

// 数组的删除和查找

var v2 = ["111","222","333","444","111"]
var v3 = Array<String>()
var v4 = [111,222,333,444,111]

// v2.remove(at: 1) // 删除指定下标的元素,下标越界会报错,空数组会报错
// v2.removeFirst() // 删除第一个元素,空数组会报错
// v2.removeLast() // 删除最后一个元素,空数组会报错
// v2.removeSubrange(1...2) // 删除一个子区间,下标越界会报错,空数组会报错
// v2.removeAll() // 全部删除
// print(v2)

var flag = v2.contains("111") // 查找数组v2里面是否包含"111",返回布尔值

/*
方法一:

v2.first(where: { }):找到第一个满足条件的元素
where: {$0 == "222"} 这是一个条件,也是一个闭包
$0 表示闭包中的隐式参数
*/
var value = v2.first(where: {$0 == "222"})
print(value ?? "") // 输出:222(加?? ""的原因是可能找不到,找不到为nil)

/*
方法二:

v2.firstIndex(where: { }):找到第一个满足条件的元素的下标
n 也是相当于是隐式参数

值得注意的是:
这里的结果不能直接用上面的 value 承接;
因为 swift 实际上是 强数据类型 的语言,不像 lua 是弱数据类型的语言
value 在上面已经被用作 String 了,下面再给他赋值为 Int 就不行!
*/
var value1 = v2.firstIndex(where: {n in
n == "333"
})
print(value1) // 输出:Optional(2)

/*
其他:
.first 找到第一个满足条件的元素
.firstIndex 找到第一个满足条件的元素的索引号
.last 最后一个元素
.lastIndex 最后一个元素索引
*/

----------------------------------------------------------------------------------------


HomeWork 7:路径追踪

首先按照 pdf 里面的操作,将相关函数迁移过来;

IntersectP 函数最后判断的时候,需要=,不然可能会有问题:

1
2
3
4
5
6
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) const
{
...
// 注意这里需要 = ,不然好像会出问题
return t_exit >= 0.0f && t_enter <= t_exit;
}

然后就是本次作业的内容,本次作业要实现的是路径追踪函数;

这个作业是真的有点难… 主要是知识点都忘了…

建议回去看一遍视频… 然后对照课程中的伪代码一步一步比对实现

castRay 这个函数实现的就是路径追踪

具体细节看代码里面的注释!

知识点参考的是这一节课的内容:

【Games 101】Lec 16:光线追踪 4(蒙特卡罗积分,路径追踪)

Scene.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
/*
这个作业是真的有点难... 主要是知识点都忘了...
建议回去看一遍视频... 然后对照课程中的伪代码一步一步比对实现
castRay 这个函数实现的就是路径追踪
具体细节看代码里面的注释!
*/
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
// 首先,获得光线第一个打到的点(下面叫做着色点吧)
Intersection p = intersect(ray);

// 没打到,直接返回
if(!p.happened) return Vector3f(0,0,0);

// 如果着色点自己是光源,则直接返回
if(p.m->hasEmission())
{
return p.m->getEmission();
}

/*
否则,如果该着色点是非光源,按照课程讲的,则分为两部分来求:
1. 光源对该着色点的贡献(直接对光源取样,不用RR)
2. 其他非光源对该着色点的贡献(递归求解,用RR)
*/

// 光线的方向
Vector3f wo = ray.direction;
// 这个就是第 1 项,光源对该着色点的贡献
Vector3f L_indir(0);
// 这个就是第 2 项,光源对该着色点的贡献
Vector3f L_dir(0);

/*----------------- 1. 光源对该着色点的贡献(直接对光源取样,不用RR)---------------*/

// 对光源进行取样,得到光源的 pdf 和 光源上的点 和 光源的面积密度pdf
Intersection inter_l; // 在光源上的交点
float pdf_light; // 光源的 pdf
sampleLight(inter_l, pdf_light);
Vector3f x = inter_l.coords;

// 获得着色点p到光源(采样点)的 方向 与 距离
float p2lightDist = (x - p.coords).norm();
Vector3f p2lightDir = (x - p.coords).normalized();

// 从p点向取样得到的光源方向射出一条光线wi
Ray wi(p.coords, p2lightDir);
// 从p点向光源方向射出一条光线得到交点 inter_tmp,为判断光源与着色点是否有物体做准备
Intersection inter_tmp = intersect(wi);

// 判断 着色点p 和 光源之间 中间没有物体阻隔
if ((inter_l.coords - inter_tmp.coords).norm() < EPSILON * EPSILON)
{
// 这里我照着伪代码把变量都写出来了,清楚一点
Vector3f L_i = inter_l.emit;
Vector3f f_r = p.m->eval(wo, wi.direction, p.normal);
// (着色点法向量)和(着色点-光源采样点 向量)之间的夹角
float cos0 = dotProduct(wi.direction, p.normal);
// (光源平面法向量)和(光源采样点-着色点 向量)之间的夹角
float cos0_ = dotProduct(-wi.direction, inter_l.normal);

L_dir = L_i * f_r * cos0 * cos0_ / (p2lightDist * p2lightDist) / pdf_light;
}

/*----------------- 2. 其他非光源对该着色点的贡献(递归求解,用RR)---------------*/

// 俄罗斯轮盘获得随机概率
float P_RR = RussianRoulette;
int seed = rand() % 10;
// 生存概率
if (seed * 1.0 / 10 > P_RR) return L_dir;

// 在 着色点p 取样一个新的随机方向
Vector3f wi_new = p.m->sample(wo, p.normal);

// 发射出去求与其他物体的交点
Ray r(p.coords, wi_new);
Intersection q = intersect(r);

// 如果q点的物体不是光源
if (q.happened && !q.obj->hasEmit())
{
Vector3f f_r = p.m->eval(wo, wi_new, p.normal);
float cos0 = dotProduct(wi_new, p.normal);
float pdf_hemi = p.m->pdf(wo,wi_new,p.normal);

L_indir = castRay(r, depth+1) * f_r * cos0 / pdf_hemi / P_RR;
}

return L_dir + L_indir;
}

路径追踪结果:

可能是给虚拟机分配的内存和CPU数量少了… 跑了40分钟,主机的CPU也才跑了20多而已…

HomeWork 6:加速结构

首先,是 Render()Triangle::getIntersection,照着上一节课一样,只是有些地方的形式需要改改;

然后就是此次作业的要求:

  • 理解,并使用加速结构 BVH,找到光线和场景中物体的交点
    • getIntersection(BVHBuildNode node, const Ray ray)*
  • 判断光线和包围盒是否相交,
    • IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg)

对于知识点参考这一节课:

【Games 101】Lec 14:光线追踪 2(加速光线追踪 和 辐射度量学)

Renderer.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
// 这个还是一样的,照着上一个作业的写就行(注意有些东西变了)
void Renderer::Render(const Scene& scene)
{
// 帧缓冲区,用于保存返回的颜色
std::vector<Vector3f> framebuffer(scene.width * scene.height);

float scale = tan(deg2rad(scene.fov * 0.5));
float imageAspectRatio = scene.width / (float)scene.height;

// 这个视点变了
Vector3f eye_pos(-1, 5, 10);
int m = 0;
for (uint32_t j = 0; j < scene.height; ++j)
{
for (uint32_t i = 0; i < scene.width; ++i)
{
float x = (2 * (i + 0.5) / (float)scene.width - 1) * imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
dir = normalize(dir);

// 这里不是直接调用 castRay 了... 是先定义光线ray,然后在传到scene的castray方法中。
Ray ray(eye_pos,dir);
framebuffer[m++] = scene.castRay(ray,0);
}
UpdateProgress(j / (float)scene.height);
}
UpdateProgress(1.f);

// save framebuffer to file
FILE* fp = fopen("binary.ppm", "wb");
(void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
for (auto i = 0; i < scene.height * scene.width; ++i) {
static unsigned char color[3];
color[0] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].x));
color[1] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].y));
color[2] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].z));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}

Triangle::getIntersection

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
inline Intersection Triangle::getIntersection(Ray ray)
{
// 射线和三角形的交点(结构体,存了一些交点信息)
Intersection inter;

// 说明不是入射光线
if (dotProduct(ray.direction, normal) > 0) return inter;

double u, v, t_tmp = 0;

// 相当于 S1
Vector3f pvec = crossProduct(ray.direction, e2);
double det = dotProduct(e1, pvec);
if (fabs(det) < EPSILON) return inter;

// 相当于 1/dotProduct(S1,E1)
double det_inv = 1. / det;

// 相当于 S
Vector3f tvec = ray.origin - v0;
u = dotProduct(tvec, pvec) * det_inv;
if (u < 0 || u > 1) return inter;

// 相当于 S2
Vector3f qvec = crossProduct(tvec, e1);
v = dotProduct(ray.direction, qvec) * det_inv;
if (v < 0 || u + v > 1) return inter;

// 相当于 t
t_tmp = dotProduct(e2, qvec) * det_inv;
if (t_tmp <= 0) return inter;

// 下面完善一下交点信息
// 标记相交
inter.happened=true;

// 求一下坐标,Ray中重载了(), ray(t_tmp) = ray.origin + ray.direction * t_tmp
inter.coords = Vector3f(ray(t_tmp));
inter.distance = t_tmp;

// 交点是在这个三角形上,所以把这个三角形的材质、法线和指针传给交点。
inter.m = m;
inter.normal = normal;
inter.obj = this;

return inter;
}

BVH.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
/*
BVH 加速结构,获取 光线 和 以node为根结点的子树 的交点
这个照着课上的为代码写就行了
好像一个节点最多包含一个物体
规范一点,注意指针的判空
*/
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
Intersection inter;

if(node == nullptr) return inter;

// 当前节点是叶子节点,直接返回和这个物体的交点就行
if(node->left == nullptr && node->right == nullptr)
{
if(node->object != nullptr)
{
inter = node->object->getIntersection(ray);
}
return inter;
}

// 记录光线的方向
std::array<int, 3> dirIsNeg;
dirIsNeg[0] = int(ray.direction.x < 0);
dirIsNeg[1] = int(ray.direction.y < 0);
dirIsNeg[2] = int(ray.direction.z < 0);

Intersection inter1, inter2;

// 看看是否和左边的包围盒相交
if(node->left != nullptr && node->left->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg))
{
inter1 = getIntersection(node->left,ray);
}

// 看看是否和右边的包围盒相交
if(node->right != nullptr && node->right->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg))
{
inter2 = getIntersection(node->right,ray);
}

// 如果有两个交点,返回最近的那个
return inter1.distance < inter2.distance ? inter1 : inter2;
}

Bounds3.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 这个函数的作用是判断包围盒 BoundingBox 与光线是否相交
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) const
{
// invDir: ray direction(x,y,z), invDir = (1.0/x,1.0/y,1.0/z),使用这个是因为乘法比除法快
// dirIsNeg: ray direction(x,y,z), dirIsNeg = [int(x>0),int(y>0),int(z>0)], 用这个来简化你的逻辑

float tx_min = (pMin.x - ray.origin.x) * invDir.x;
float tx_max = (pMax.x - ray.origin.x) * invDir.x;
if (dirIsNeg[0]) std::swap(tx_min, tx_max);

float ty_min = (pMin.y - ray.origin.y) * invDir.y;
float ty_max = (pMax.y - ray.origin.y) * invDir.y;
if (dirIsNeg[1]) std::swap(ty_min, ty_max);

float tz_min = (pMin.z - ray.origin.z) * invDir.z;
float tz_max = (pMax.z - ray.origin.z) * invDir.z;
if (dirIsNeg[2]) std::swap(tz_min, tz_max);

float t_enter = std::max(std::max(tx_min, ty_min), tz_min);
float t_exit = std::min(std::min(tx_max, ty_max), tz_max);

return t_exit > 0.0f && t_enter < t_exit;
}

结果:

HomeWork 5:光线与三角形相交

这一个作业主要是光线追踪,我们要实现的是:

  • 从每个像素打出一条光线;
  • 判断光线和三角形是否相交

有的东西课上没有讲,具体的话可以看一下注释!

同步的知识点在这一片文章:

【Games 101】Lec 13:光线追踪 1(Shadow Mapping,Whitted-Style Ray Tracing)

Renderer.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
/*
主渲染函数。
在这里,我们迭代图像中的所有像素,从每个像素打出一条光线到场景中;
framebuffer 的内容被保存到一个文件中。

我的妈,这个是真的有点难哇... 主要是课上也没讲...
具体可以看这两篇文章:
https://zhuanlan.zhihu.com/p/475136497
https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays/generating-camera-rays.html

特别是第二篇,分析的特别清楚!
看完就知道要干嘛了(就是需要变换一下坐标)
*/
void Renderer::Render(const Scene& scene)
{
// 帧缓冲区,用于保存返回的颜色
std::vector<Vector3f> framebuffer(scene.width * scene.height);

float scale = std::tan(deg2rad(scene.fov * 0.5f));
float imageAspectRatio = scene.width / (float)scene.height;

// 使用这个变量作为眼睛的位置来开始你的光线。
Vector3f eye_pos(0);
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
/*
TODO:找到当前像素的x和y位置,以获得通过它的方向矢量。
此外,不要忘记将它们与变量*scale*相乘,并将x(水平)变量与*imageAspectRatio*相乘
*/
float x = (2.0 * i / scene.width - 1.0) * scale * imageAspectRatio;
float y = (1.0 - 2.0 *j / scene.height) * scale;

// 别忘了归一化这个方向
Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
dir = normalize(dir);

// 调用 castRay 来获取最终的颜色
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
}
// 输出一下?
UpdateProgress(j / (float)scene.height);
}

// 将缓冲区的颜色保存到文件中(帧缓冲区中的信息将被保存为图像)
FILE* fp = fopen("binary.ppm", "wb");
(void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
for (auto i = 0; i < scene.height * scene.width; ++i) {
static unsigned char color[3];
color[0] = (char)(255 * clamp(0, 1, framebuffer[i].x));
color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}

Triangle.hpp

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
/*
判断射线是否和三角形相交
用 Moller Trumbore 算法,这个直接套公式解出来就行
*/
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{
Vector3f E1 = v1 - v0;
Vector3f E2 = v2 - v0;
Vector3f S = orig - v0;
Vector3f S1 = crossProduct(dir, E2);
Vector3f S2 = crossProduct(S, E1);

float t = dotProduct(S2,E2) / dotProduct(S1,E1);
float b1 = dotProduct(S1,S) / dotProduct(S1,E1);
float b2 = dotProduct(S2,dir) / dotProduct(S1,E1);

// 都 >0 才表示有解
if(t > 0 && b1 > 0 && b2 > 0 && 1 - (b1 + b2) > 0)
{
tnear = t;
u = b1;
v = b2;
return true;
}

return false;
}

光线追踪结果:

HomeWork 8:质点弹簧系统

这节课比较简单,都是套公式就行;

有个点得提一下,一开始sb了,在src里面间的build文件夹,然后cmake,结果一直报错…

实际上是应该在上一级建的…(也就是 build 和 src 文件夹同一级)

知识点参考的课程:

【Games 101】Lec 21:动画

rope.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
110
111
112
113
114
115
/*
构造函数
这个构造函数应该可以创建一个新的绳子(Rope) 对象,该对象从start 开始,end 结束,包含 num_nodes 个节点。

- start:起始节点的坐标
- end:最终节点的坐标
- num_nodes:节点个数
- node_mass:节点的质量
- k:弹簧的弪度系数
- pinned_nodes:弹簧两个端点的索引
*/
Rope::Rope(Vector2D start, Vector2D end, int num_nodes, float node_mass, float k, vector<int> pinned_nodes)
{
// 初始化质点
for (int i = 0; i < num_nodes; ++i)
{
// 转成double再做除法,否则就会截断成int
Vector2D pos = start + (end - start) * ((double)i / ((double)num_nodes - 1.0));
masses.push_back(new Mass(pos, node_mass, false));
}

// 初始化弹簧
for (int i = 0; i < num_nodes - 1; ++i)
{
springs.push_back(new Spring(masses[i], masses[i + 1], k));
}

for (auto &i : pinned_nodes)
{
masses[i]->pinned = true;
}
}

// 显式 / 半隐式欧拉法
void Rope::simulateEuler(float delta_t, Vector2D gravity)
{
// 实现胡克定律:遍历所有的弹簧,根据拉伸的长度计算拉力(套公式就行)
for (auto &s : springs)
{
double len = (s->m1->position - s->m2->position).norm();
s->m1->forces += -(s->k) * (s->m1->position - s->m2->position) / len * (len - s->rest_length);
s->m2->forces += -(s->k) * (s->m2->position - s->m1->position) / len * (len - s->rest_length);
}

// 更新 加速度,速度,位置(也是套公式就行)
for (auto &m : masses)
{
if (!m->pinned)
{
/*
加上重力作用下的力,然后计算新的速度和位置
隐式:先更新速度,再更新位置
显式:先更新位置,再更新速度(直接就飞出去了...)
*/

// 加上质点的重力,重力 = 重力加速度 * 质量
m->forces += gravity * m->mass;

// 增加全局阻尼力
float k_d = 0.005;
Vector2D f_d = -k_d * m->velocity;

// 加上阻尼的力
m->forces += f_d;

// 加速度
Vector2D a = m->forces / m->mass;

m->velocity += a * delta_t; // 再更新速度
m->position += m->velocity * delta_t; // 先更新位置,就是用上一时刻的速度更新
}

// Reset all forces on each mass
m->forces = Vector2D(0, 0);
}
}

// 显式 Verlet 法
void Rope::simulateVerlet(float delta_t, Vector2D gravity)
{
for (auto &s : springs)
{
double len = (s->m1->position - s->m2->position).norm();
s->m1->forces += -(s->k) * (s->m1->position - s->m2->position) / len * (len - s->rest_length);
s->m2->forces += -(s->k) * (s->m2->position - s->m1->position) / len * (len - s->rest_length);
}

for (auto &m : masses)
{
if (!m->pinned)
{
/*
显示Verlet法:把弹簧的长度保持原长作为约束,移动每个质点的位置
也是套公式就行:
x(t+1) = x(t) + (1 - damping_factor) * ([x(t) - x(t-1)] + a * dt * dt)
可以看到需要 x(t-1),所以得先存一下
*/

// 加上质点的重力,重力 = 重力加速度 * 质量
m->forces += gravity * m->mass;
Vector2D a = m->forces/m->mass;

// x(t-1)
Vector2D lastPosition = m->position;

// 阻尼
float damping_factor = 0.00005f;

// x(t+1) = x(t) + (1 - damping_factor) * ([x(t) - x(t-1)] + a * dt * dt)
m->position += (1 - damping_factor) * (m->position - m->last_position + a * delta_t * delta_t);
m->last_position = lastPosition;
}
m->forces = Vector2D(0, 0);
}
}

结果:

HomeWork 0:虚拟机的使用

配环境…

真的是一波三折啊,捣鼓了好久…

还好虚拟机里面什么都有了

首先按照 pdf 里面的流程走就行了,下面记录一下一些问题:

黑屏…

我是重启之后就会黑屏,还没输入密码呢…

这个问题我直接删掉虚拟机,如何重新创建一个就好了;

安装 增强功能 显示 无法挂载光盘什么的

首先检查一下虚拟机是否有 VBox_GAs_… 光盘,有的话先弹出他,然后再尝试安装;

然后可能会失败,看下面这两篇:

VirtualBox 安装 ubuntu后安装增强工具无效的解决办法 - 知乎 (zhihu.com)

VirtualBox:unable to access “VBox_GAS_6.8.XXX-CSDN博客

成功安装之后就能:

调整虚拟机的分辨率;

主机和虚拟机之间拖拽文件;

等;

然后继续照着 pdf 里面操作就行了

代码

下面是作业0的代码:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include<cmath>
#include<eigen3/Eigen/Core>
#include<eigen3/Eigen/Dense>
#include<iostream>
using namespace std;

void Test()
{
// 这个一些基本运算
std::cout << "Example of cpp \n";
float a = 1.0, b = 2.0;
std::cout << a << std::endl;
std::cout << a/b << std::endl;
std::cout << std::sqrt(b) << std::endl;
std::cout << std::acos(-1) << std::endl;
std::cout << std::sin(30.0/180.0*acos(-1)) << std::endl;

// Example of vector
std::cout << "Example of vector \n";
//这个 Vector3f 是定义了一个三维向量,float(注意这里其实也就是一个 3x1 的矩阵)
Eigen::Vector3f v(1.0f,2.0f,3.0f);
Eigen::Vector3f w(1.0f,0.0f,0.0f);
// vector output
std::cout << "Example of output \n";
std::cout << v << std::endl;
// vector add
std::cout << "Example of add \n";
std::cout << v + w << std::endl;
// vector scalar multiply
std::cout << "Example of scalar multiply \n";
std::cout << v * 3.0f << std::endl;
std::cout << 2.0f * v << std::endl;

// Example of matrix
std::cout << "Example of matrix \n";
/*
这个 Matrix3f 是定义了一个3x3的float的矩阵;
其实跳进去看源码就知道了,通过模板和宏定义实现的;
*/
Eigen::Matrix3f i,j,ans;
i << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0;
j << 2.0, 3.0, 1.0, 4.0, 6.0, 5.0, 9.0, 7.0, 8.0;
// matrix output
cout << "Example of output \n";
cout << i << "\n\n";

// matrix add i + j
cout << "this is matrix add : \n";
ans = i + j;
cout << ans << "\n\n";

// matrix scalar multiply i * 2.0
cout << "this is matrix scalar multiply : \n";
ans = i * 20;
cout << ans << "\n\n";

// matrix multiply i * j
cout << "this is matrix multiply : \n";
ans = i * j;
cout << ans << "\n\n";

/*
matrix multiply vector i * v
这个就是定义一个自定义大小的float矩阵,我这里定义的是 1x3 的;
因为 v 是一个 1x3 的,i 是一个 3x3 的;
*/
cout << "this is matrix multiply vector : \n";
Eigen::MatrixXf ans1(1,3);
ans1 = i * v;
cout << ans1 << "\n\n";
}

/*
作业描述:
给定一个点 P=(2,1), 将该点绕原点先逆时针旋转 45◦,再平移 (1,2), 计算出变换后点的坐标(要求用齐次坐标进行计算)。


分析:
1. 首先说到要用齐次坐标表示,我们回顾一下,2维的点用齐次坐标表示,则需要加上一个1的维,也就是(2,1,1);
2. 然后平移旋转,我们可以构造一个旋转矩阵,然后将这个坐标和这个旋转矩阵相乘,完成旋转变换;
3. 最后对于平移,我们同样可以构造一个平移矩阵,然后相乘就行,完成平移变换;

想不起来的可以回顾一下这里:http://fjbq-blog.top/2023/12/30/GAMES%20101%20%E9%9A%8F%E7%AC%94/
*/

// 将角度转换为弧度
double DegToRad(double Deg)
{
return Deg / 180.0 * M_PI;
}

void solve()
{
// (2,1) 的点的齐次坐标表示(3 x 1 的矩阵)
Eigen::Vector3f Point(2.0, 1.0, 1.0);

// 旋转变换
double Deg = 45.0;
double Rad = DegToRad(Deg);

Eigen::Matrix3f RotationMatrix;
RotationMatrix << cos(Rad), -sin(Rad), 0,
sin(Rad), cos(Rad), 0,
0, 0, 1;

Point = RotationMatrix * Point;
cout << "旋转变换之后:\n" << Point << "\n\n";

// 平移变换
double tx = 1.0;
double ty = 2.0;

Eigen::Matrix3f TranslationMatrix;
TranslationMatrix << 1, 0, tx,
0, 1, ty,
0, 0, 1;

Point = TranslationMatrix * Point;
cout << "最终结果:\n" << Point <<'\n';
}

int main()
{
solve();

return 0;
}

结果:

HomeWork 4:Bézier 曲线

这个挺简单的,挺有意思的:

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
// 套公式求的 贝塞尔 曲线
void naive_bezier(const std::vector<cv::Point2f> &points, cv::Mat &window)
{
auto &p_0 = points[0];
auto &p_1 = points[1];
auto &p_2 = points[2];
auto &p_3 = points[3];

for (double t = 0.0; t <= 1.0; t += 0.001)
{
auto point = std::pow(1 - t, 3) * p_0 + 3 * t * std::pow(1 - t, 2) * p_1 +
3 * std::pow(t, 2) * (1 - t) * p_2 + std::pow(t, 3) * p_3;

window.at<cv::Vec3b>(point.y, point.x)[2] = 255;
}
}

// 递归做法的贝塞尔曲线
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t)
{
// 只剩一个控制点的时候就能返回了
if(control_points.size() == 1) return control_points[0];

std::vector<cv::Point2f> temp_control_points;
for(int i = 0; i < control_points.size()-1; i ++)
{
// 这个比较简单,看着图推一下就行
cv::Point2f temp_point = control_points[i] - (control_points[i] - control_points[i+1]) * t;
temp_control_points.push_back(temp_point);
}

return recursive_bezier(temp_control_points, t);
}

// 实现绘制 Bézier 曲线的功能(传进来一组控制点)
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
// 1000 个 t
for(float t = 0; t <= 1; t += 0.001f)
{
cv::Point2f point = recursive_bezier(control_points, t);
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}

下面是贝塞尔曲线:
红色:公式
绿色:递归
黄色:两个同时调用的结果

提高:(反走样)

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
/*
做一下反走样
https://zhuanlan.zhihu.com/p/464122963
*/
void bezier_2(const std::vector<cv::Point2f> &control_points, cv::Mat &window)
{
for (float t = 0.0; t <= 1.0; t += 0.001)
{
cv::Point2f point = recursive_bezier(control_points, t);

// 求距离点 point 最近的四个像素的像素交点中心(其实就是右下角的像素的起点)
cv::Point2i p0;
p0.x = point.x-std::floor(point.x) < 0.5 ? std::floor(point.x) : std::ceil(point.x);
p0.y = point.y-std::floor(point.y) < 0.5 ? std::floor(point.y) : std::ceil(point.y);

// 点 point 最近的四个像素点的起点
std::vector<cv::Point2i> ps;
ps.push_back(p0);
ps.push_back(cv::Point2i(p0.x-1, p0.y));
ps.push_back(cv::Point2i(p0.x, p0.y-1));
ps.push_back(cv::Point2i(p0.x-1, p0.y-1));

// 点 p 到相邻四个点的中心点的距离
float sum_d = 0.0f;
float max_d = sqrt(2);
std::vector<float> ds = {};
for (int i = 0; i < 4; i++)
{
// 像素点的中心
cv::Point2f cp(ps[i].x + 0.5f, ps[i].y + 0.5f);
float d = max_d - std::sqrt(std::pow(point.x - cp.x, 2) + std::pow(point.y - cp.y, 2));
ds.push_back(d);
sum_d += d;
}

// 求一下加权的颜色直就行
for (int i = 0; i < 4; i++)
{
float k = ds[i] / sum_d;
window.at<cv::Vec3b>(ps[i].y, ps[i].x)[1] = std::min(255.f, window.at<cv::Vec3b>(ps[i].y, ps[i].x)[1] + 255.f * k);
}
}
}

反走样对比,效果还是挺明显的:

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;
}

结果:

HomeWork 2:Triangles and Z-buffering(光栅化 和 抗锯齿)

代码

1
2
3
4
5
6
7
8
9
10
11
12
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
/*
这个里的 zNear,zFar 给的又是负数了...
所以这里是不用反转的...
不然结果会相反,大家可以试试
*/

Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
...
return projection;
}
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
// 判断点是否在三角形内(我这里用的是叉积)
static bool insideTriangle(int x, int y, const Vector3f* _v)
{
// 这个表示叉积后的方向,0 表示负,1 表示正
int flag = -1;

for(int i = 0; i < 3; i++)
{
// the current point
Eigen::Vector3f p0 = {x, y, 0};
// the 1st vertex
Eigen::Vector3f p1 = _v[i];
// the 2nd vertex
Eigen::Vector3f p2 = _v[(i+1)%3];

// 第一个向量 (p1-p0)
Eigen::Vector3f v1 = p1-p0;
// 第二个向量 (p1-p2)
Eigen::Vector3f v2 = p1-p2;

// 求一下叉积
float cp = v1.cross(v2).z();
if(cp == 0) continue;

int sign = cp < 0 ? 0: 1;
if(flag == -1) flag = sign;
if(flag != sign) return false;
}

return true;
}

// 三角形光栅化
void rst::rasterizer::rasterize_triangle(const Triangle& t)
{
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(min_x, point[0]);
max_x = max(max_x, point[0]);
min_y = min(min_y, point[1]);
max_y = max(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)])
{
Eigen::Vector3f p = { (float)x,(float)y, z_interpolated }; // 当前坐标
set_pixel(p, t.getColor());
depth_buf[get_index(x, y)] = z_interpolated; // 更新z值
}
}
}
}
}

提高的抗锯齿可以看这里:

Games101|作业2 + 光栅化 + SSAA vs MSAA + 黑边问题 - 知乎 (zhihu.com)

分析的太棒了!

结果

补一下提高 嘻嘻嘻

rasterizer.hpp 添加

1
std::vector<Eigen::Vector3f> color_list;

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 判断点是否在三角形内(我这里用的是叉积)
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
// 这个表示叉积后的方向,0 表示负,1 表示正
int flag = -1;

for(int i = 0; i < 3; i++)
{
// the current point
Eigen::Vector3f p0 = {x, y, 0};
// the 1st vertex
Eigen::Vector3f p1 = _v[i];
// the 2nd vertex
Eigen::Vector3f p2 = _v[(i+1)%3];

// 第一个向量 (p1-p0)
Eigen::Vector3f v1 = p1-p0;
// 第二个向量 (p1-p2)
Eigen::Vector3f v2 = p1-p2;

// 求一下叉积 float cp = v1.cross(v2).z();
if(cp == 0) continue;

int sign = cp < 0 ? 0: 1;
if(flag == -1) flag = sign;
if(flag != sign) return false;
}

return true;
}

/* ------------------------------------------------------------------------------- */

// 四个采样点 float dx[] = {0.25, 0.25, 0.75, 0.75};
float dy[] = {0.25, 0.75, 0.25, 0.75};
// 三角形光栅化
void rst::rasterizer::rasterize_triangle(const Triangle& t)
{
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++)
{
// 取四个点(MSAA)
for(int i=0;i<4;i++)
{
float xx = (double)x + dx[i];
float yy = (double)y + dy[i];

if (insideTriangle(xx, yy, 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)*4+i])
{
// 更新这个采样点 depth_buf[get_index(x, y)*4+i] = z_interpolated;
color_list[get_index(x, y)*4+i] = t.getColor();

// 重新算一下这个像素点的颜色(取平均)
Eigen::Vector3f new_color(0.0f, 0.0f, 0.0f);
for(int j=0;j<4;j++)
new_color += color_list[get_index(x, y)*4+j];
new_color /= 4;

// 更新当前像素的值 Eigen::Vector3f p = { (float)x,(float)y, z_interpolated };
set_pixel(p, new_color);
}
}
}
}
}
}

结果

结果对比:

太神奇啦!