GAMES101homework记录

GAMES101homework记录

Ljy0109/Graphics_algorithms (github.com)

语法

inline

inline 是 C++ 中的关键字,用于提示编译器对函数进行内联展开。内联展开是指在调用函数的地方直接将函数的代码插入,而不是通过函数调用的方式进行执行。这样可以减少函数调用的开销,并提高程序的执行效率。

使用 inline 关键字声明的函数会被编译器视为候选进行内联展开的函数,但并不保证一定会被内联展开。编译器会根据一些策略来决定是否将函数内联展开,例如函数的大小、调用频率等因素。

示例:

1
2
3
4
5
6
7
8
inline int add(int a, int b) {
return a + b;
}

int main() {
int result = add(3, 4);
return 0;
}

在这个示例中,add 函数被声明为 inline,因此编译器可能会将 add 函数的代码直接插入到 main 函数中,而不是通过函数调用的方式执行。

需要注意的是,inline 关键字只是对编译器的建议,编译器并不一定会采纳。通常情况下,较小的、频繁调用的函数更容易被编译器选择进行内联展开。

throw

1
throw std::runtime_error("Invalid color values");

在满足条件后抛出错误。

std::runtime_error表示程序运行时的错误

std::transform()

示例:

1
2
3
4
5
6
7
8
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) {// const Vector3f& vec
return Vector4f(vec.x(), vec.y(), vec.z(), 1.f);
});
return res;
}

std::transform 是 C++ 标准库中的一个算法,它用于对一个容器(或者两个容器)中的元素应用指定的操作,并将结果存储到另一个容器中。该算法定义在 <algorithm> 头文件中。

std::transform 函数的常用形式有以下几种:

  1. 单容器变换:
1
2
3
template<class InputIterator, class OutputIterator, class UnaryOperation>
OutputIterator transform(InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperation op);

该函数接受一个输入迭代器范围 [first1, last1) 表示输入容器的范围,一个输出迭代器 result 表示输出容器的起始位置,以及一个一元操作函数 op,用于对输入容器的元素进行变换,并将结果存储到输出容器中。

  1. 双容器变换:
1
2
3
template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryOperation>
OutputIterator transform(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, OutputIterator result, BinaryOperation op);

该函数接受两个输入迭代器范围 [first1, last1)[first2, ... 来源于第二个容器的元素进行操作,并将结果存储到输出容器中。

在示例提供的代码中,使用的是第一种形式的 std::transform 函数,它对一个输入容器中的每个元素应用指定的操作,并将结果存储到输出容器中。

map

示例:

1
2
3
std::map<int, std::vector<Eigen::Vector3f>> pos_buf;

pos_buf.emplace(id, positions);

std::map 是 C++ 标准库中的一个关联容器,它提供了键-值对的映射关系,并且能够根据键的排序规则自动对键进行排序。std::map 的定义位于 <map> 头文件中。

std::map 的特点包括:

  1. 键值对:std::map 中的每个元素都是一个键值对,其中键和值可以是任意类型的数据,键和值之间存在映射关系。
  2. 排序:std::map 会根据键的排序规则自动对键进行排序,默认情况下是按照键的自然顺序进行排序,但也可以通过提供自定义的比较函数来定义排序规则。
  3. 唯一性:std::map 中的键是唯一的,每个键只能对应一个值,如果尝试插入一个已经存在的键,则插入操作会失败。
  4. 搜索:std::map 提供了高效的搜索操作,可以根据键来快速查找对应的值。

使用 std::map 的一般步骤包括:

  1. 包含头文件:#include <map>
  2. 定义 std::map 对象:std::map<KeyType, ValueType> myMap;
  3. 插入元素:myMap[key] = value; 或者 myMap.emplace(key, value);
  4. 访问元素:myMap[key] 或者 myMap.at(key)
  5. 遍历元素:使用迭代器进行遍历,或者范围遍历(C++11 及以上版本)。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <map>

int main() {
std::map<std::string, int> myMap;

// 插入元素
myMap["apple"] = 10;
myMap.emplace("banana", 20);

// 访问元素
std::cout << "Value of apple: " << myMap["apple"] << std::endl;
std::cout << "Value of banana: " << myMap.at("banana") << std::endl;

// 遍历元素
for (const auto& pair : myMap) {
std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
}

return 0;
}

这段代码创建了一个 std::map 对象 myMap,并向其中插入了两个键值对。然后使用下标运算符和 .at() 方法访问元素,并使用范围遍历方式遍历所有元素。

.emplace() 是 C++ STL 容器中的一个成员函数,用于在容器中直接构造一个新的元素。它的参数会被传递给容器中元素的构造函数,从而在容器中直接构造一个新的元素。.emplace() 函数通常用于避免不必要的拷贝或移动操作,因为它可以在容器中直接构造新的元素,而不需要先创建一个临时对象。

fabs()

fabs(dx) 是 C++ 标准库 <cmath> 中的函数,用于计算一个浮点数 dx 的绝对值。

在 C++ 中,fabs() 函数的功能是返回一个浮点数的绝对值。如果参数是整数类型,则会隐式转换为浮点数再计算绝对值。

static

static function()

static修饰的静态函数被限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说,可以被其它代码文件调用该函数。

静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:

  • 其他文件中可以定义相同名字的函数,不会发生冲突。
  • 静态函数不能被其他文件所用。
  • 静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
  • static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。

static 成员变量和成员函数

  1. private static 和 public static 都是静态变量,在类加载时就定义,不需要创建对象
  2. private static 是私有的,不能在外部访问,只能通过静态方法调用,这样可以防止对变量的修改
  3. static 成员函数只能访问static成员变量,非static成员函数可以访问static成员变量

static函数和私有函数的区别

  • static函数避免了调用函数的进栈出栈,速度比私有函数快。这在光栅化、渲染等实时任务里是一个有效加速的方法
  • 为什么不将私有函数变为静态函数?因为静态函数不能直接访问成员变量
  • 为什么不使用静态私有函数?因为静态私有函数只能访问静态成员变量

使用位运算同时表示两个种类

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum class Buffers
{
Color = 1, // 01
Depth = 2 // 10
};

inline Buffers operator|(Buffers a, Buffers b)
{ // 对color和depth进行位与和位或
// color | depth = 11 同时表示color和depth,也就是说
// (color | depth) & color = color
// (color | depth) & depth = depth
return Buffers((int)a | (int)b);
}

inline Buffers operator&(Buffers a, Buffers b)
{
return Buffers((int)a & (int)b);
}

使用operator关键词重构位运算符,以下是使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void rst::rasterizer::clear(rst::Buffers buff)
{ // 初始化缓冲器
if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
{
std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
}
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{
std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
}
}
// 同时初始化color和depth缓冲器
// 使用位运算同时表示两个种类,很精妙的用法
r.clear(rst::Buffers::Color | rst::Buffers::Depth);

v[0].w()

在C++中,v[0].w()通常表示对一个自定义类型或结构体中的第四个成员进行访问。

也就是说从0到3的元素默认表达为:v[0].x() v[0].y() v[0].z() v[0].w()

算法

Bresenham 直线光栅化算法

Bresenham布雷森曼算法 - Blog of Mr.Juan (ljy0109.github.io)

透视矫正系数

如何获得屏幕中二维点对应的深度信息?

通过二维点对应到三维空间所在的三角平面的三个顶点进行插值。

如何进行插值?

通过三角形的重心坐标:

V=αVa+βVb+γVcV=\alpha*V_a+\beta*V_b+\gamma*V_c

为什么不在二维平面进行插值?

首先,肯定不能使用二维点进行重心插值。因为二维点没有深度。

那么同一个点在二维平面的重心坐标和在三维空间中的重心坐标一样吗?

答案是不一样。重心坐标是三角形内部划分的三个三角形的面积。平面的三角形是三维空间三角形的投影,难道从不同视角看过去,三个三角形的面积比例都是不变的吗?当然不是

所以必须计算目标点在三维空间的重心坐标。

怎么计算呢?

思考一下从三维空间到平面坐标,顶点的变化:

假设在屏幕空间中求得得重心坐标$ (a,b,c)A,B,C,我们只需对三个屏幕空间中 A, B , C的重心权重(1/w_a,0,0),(0,1/w_b,0),(0,0,1/w_c)$插值即可(这里把它当作普通数据插值,之后再保证和为1),插值后的形式即是校正后的重心坐标:

那么插值公式就是:

此时插值的不一定是坐标值,也可以是颜色值,纹理值等等

法向量变换矩阵

法向量变换矩阵的推导_法向量变换矩阵推导-CSDN博客

使用矩阵对点进行空间变换是图形学中的常见操作,假设变换矩阵为MM,我们需要变换切向量TT(由点P2P1P_2-P_1定义) 以及与其垂直的法向量NN。(TTNN均为列向量)

假设点P2P_2经过MM变换后为P2P_2',点P1P_1经过MM变换后为P1P_1'TT'为变换后的切向量:

对于原切向量TT,我们希望找到一个矩阵MM',使得:

我们直接令M=MM'=M试一下:

可见对于切向量TT,我们可以直接使用MM对其进行变换。

对于法向量NN,我们有(注意,第一个公式中的点号表示点积):

假设变换后的法向量为NN',我们希望仍然保持其与TT'TT的变换后向量)的垂直(注意,第一个公式中的点号表示点积):

假设用于变换法向量的矩阵为G,则应有:

由于我们知道:

所以我们只要令(注意,这只是一种可能的取值,并不是唯一取值,我们的目的也仅是需要获得一种可能的取值):

便可以满足上面的等式 :

所以变换法向量,我们需要使用普通变化矩阵逆的转置(或者说转置的逆,对于可逆矩阵,其转置矩阵的逆矩阵等于其逆矩阵的转置矩阵

菲涅尔反射方程

当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。将其应用在BRDF当中,我们就可以更加精准的计算出渲染方程中Lo(p,wo)L_o(p,w_o)的值。

我们假设入射光与法线的夹角为θi\theta_i,折射光与法线的夹角为θo\theta_o 。由于折射还和介质的折射率有关,例如空气中的光射入水中,我们需要知道空气和水分别对应的折射率,我们再假设入射光所在介质的折射率为n1n_1 ,物体的折射率为n2n_2。由于光的偏振(极化)现象,我们可以得到 S偏振光 和 P偏振光 分别对应的菲涅尔方程,如下:

根据折射定律:

可以推导出:

因此上面的菲涅尔方程可以写成没有θt\theta_t的形式:

如果我们不考虑偏振的情况,那么菲涅尔方程即是上面两者的平均值:

利用菲涅尔方程,我们就可以根据不同的反射率画出 R 与θi\theta_i的对应关系图,如下:

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{ // 计算菲涅尔方程中的反射系数
float cosi = clamp(-1, 1, dotProduct(I, N));
float etai = 1, etat = ior;
if (cosi > 0) { std::swap(etai, etat); } // 从内部射出
// Compute sini using Snell's law
// sini = sqrtf(std::max(0.f, 1 - cosi * cosi))
float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
// Total internal reflection
if (sint >= 1) { // 全反射
return 1;
}
else {
float cost = sqrtf(std::max(0.f, 1 - sint * sint));
cosi = fabsf(cosi);
float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
return (Rs * Rs + Rp * Rp) / 2;
}
// As a consequence of the conservation of energy, transmittance is given by:
// kt = 1 - kr;
}

折射方程的计算逻辑

折射光线的强度因子

折射光线的强度因子(transmission coefficient)是指光线在介质界面发生折射时,入射介质和折射介质之间能量的传递比率。在一般情况下,该强度因子由折射角和入射角以及介质的折射率决定。

数学公式为:

k=1(ηinηout)2(1cos2(θin))k = 1 - \left( \frac{\eta_{\text{in}}}{\eta_{\text{out}}} \right)^2 \left( 1 - \cos^2(\theta_{\text{in}}) \right)

其中:

  • kk是强度因子;
  • ηin\eta_{\text{in}}是入射介质的折射率;
  • ηout\eta_{\text{out}}是折射介质的折射率;
  • θin\theta_{\text{in}}是入射角的余弦值。

这个公式的理解是,当 kk大于等于 0 时,光线能够穿透介质界面进行折射;而当kk小于 0 时,发生全反射,光线无法穿透介质界面。

折射光线的方向向量

折射光线的向量公式可以表示为:

refracted_ray=η×I+(η×cos(θin)k)×n\text{refracted\_ray} = \eta \times \text{I} + (\eta \times \cos(\theta_{\text{in}}) - \sqrt{k}) \times \text{n}

其中:

  • refracted_ray\text{refracted\_ray} 是折射光线的方向向量;
  • η\eta是入射介质的折射率与折射介质的折射率之比;
  • I\text{I}是入射光线的单位方向向量;
  • $ \cos(\theta_{\text{in}})$是入射角的余弦值;
  • k\sqrt{k}是折射光线的幅值,表示入射光线与法线夹角的正弦值;
  • n\text{n}是法线的单位方向向量。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{ // 实现了折射(refraction)的计算逻辑
// 接受三个参数:I:入射光线的方向向量。N:表面法线向量。ior:介质的折射率(index of refraction)。

// 计算入射光线与表面法线的夹角的余弦值
// clamp 函数确保其在 [-1, 1] 的范围内,定义在gobal.hpp中
float cosi = clamp(-1, 1, dotProduct(I, N));
float etai = 1, etat = ior; // etai 是入射介质的折射率,通常为1,etat 是出射介质的折射率
Vector3f n = N;

// 如果入射角 cosi 小于0,则将其取反
// 否则交换 etai 和 etat 的值,并将法线 n 取反。这是因为入射光线可能来自介质内部而不是外部。
// ~~ 为什么是这个逻辑?因为正常的入射光线是从光源到折射点,所以本来cosi 就是小于0 ~~
if (cosi < 0) { cosi = -cosi; } else { std::swap(etai, etat); n= -N; }
float eta = etai / etat;
float k = 1 - eta * eta * (1 - cosi * cosi);
// 如果 k 小于 0,表示全反射,此时返回零向量。
// 否则,根据折射定律计算折射光线的向量。
return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

MT算法:射线与三角形的交点

不用掌握推导,只用记住结论

推理过程:

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{ // 实现了射线和三角形的相交检测功能 MT算法
// v0、v1、v2:三角形的三个顶点。
// orig:射线的起始点。
// dir:射线的方向。
// tnear:相交点到射线起始点的距离。
// u、v:相交点在三角形上的重心坐标。
Vector3f E0=v1-v0,E1=v2-v0,B=orig-v0;
Vector3f B_Cross_E0=crossProduct(B,E0),D_Cross_E1=crossProduct(dir,E1);
tnear= dotProduct(B_Cross_E0,E1)/(dotProduct(D_Cross_E1,E0));
u=dotProduct(D_Cross_E1,B)/(dotProduct(D_Cross_E1,E0));
v=dotProduct(B_Cross_E0,dir)/(dotProduct(D_Cross_E1,E0));
return tnear>=-std::numeric_limits<float>::epsilon()&& u>=-std::numeric_limits<float>::epsilon()
&& v>=-std::numeric_limits<float>::epsilon()&& 1.0f-u-v >= -std::numeric_limits<float>::epsilon();
}

由局部坐标系到世界坐标系的转换

[补充知识]由局部坐标系到世界坐标系的转换 - 知乎 (zhihu.com)

在生成采样方向时,一般生成的采样方向都是定义在点的局部坐标系(也叫切线空间)中的,很多时候都需要把采样方向由局部坐标系转换到世界坐标系下进行后续计算(如下图)。

在光线追踪中,从相机光心打出射线与物体进行相交,然后需要在交点处随机采样一个反射光线。

怎么随机采样呢?那就是画一个以交点为中心的单位上半球,然后在球的表面随机选取一个点,从球心指向表面点的向量就是随机采样的光线。

代码示例:in Material.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vector3f Material::sample(const Vector3f &wi, const Vector3f &N){
switch(m_type){
case Microfacet:
case DIFFUSE:
{ // 在单位球面上随机取一个点
// uniform sample on the hemisphere
float x_1 = get_random_float(), x_2 = get_random_float();
float z = std::fabs(1.0f - 2.0f * x_1);
float r = std::sqrt(1.0f - z * z), phi = 2 * M_PI * x_2;
Vector3f localRay(r*std::cos(phi), r*std::sin(phi), z);
return toWorld(localRay, N);
break;
}
}
}

但是这个采样光线是在以球心作为原点的局部坐标系中表示。怎么把这个光线变换到世界坐标系?

通过交点平面的法向量。法向量N是世界坐标系的表示,那么使用N来表示采样光线的话,采样光线就是世界坐标系下的了。

怎么表示呢?思考这样一件事,法向量N是通过世界坐标系的基向量来表示的。现在如果以法向量N作为局部坐标系的基向量,那么使用N来表示采样光线是不是就相当于使用世界坐标系的基向量来表示。

但是法向量N就一个向量,所以需要通过法向量N来生成另外两个基向量,使其可以表示局部坐标系下的点。

引入切线空间 tangent space

构造方法:

计算在x-z平面或者y-z平面中与N正交的单位向量,获得副切线向量B,然后N×BN\times B获得切线向量T

NBT三个互相正交的向量构成局部坐标系。半球表面随机采样的点(x,y,z)对应NBT三个基向量,线性组合就变成世界坐标系了。变换后的采样光线仍然是单位向量。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
Vector3f toWorld(const Vector3f &a, const Vector3f &N){
Vector3f B, C;
if (std::fabs(N.x) > std::fabs(N.y)){
float invLen = 1.0f / std::sqrt(N.x * N.x + N.z * N.z);
C = Vector3f(N.z * invLen, 0.0f, -N.x *invLen);
}
else {
float invLen = 1.0f / std::sqrt(N.y * N.y + N.z * N.z);
C = Vector3f(0.0f, N.z * invLen, -N.y *invLen);
}
B = crossProduct(C, N);
return a.x * B + a.y * C + a.z * N;
}

思考代码中的C为什么这么计算?为了保证C和N正交,且C是单位向量

思考变换后的a和原本的a表示的是半球上相同的点吗?没想过,不过本来就是随机点,所以没关系

GAMES101:作业7的教程

Games101:作业7(含提高部分)_intersection scene::intersect(const ray &ray) cons-CSDN博客


GAMES101homework记录
http://example.com/2024/03/06/GAMES101homework记录/
作者
Mr.Yuan
发布于
2024年3月6日
许可协议