Decorative image frame

【笔记】球面均匀采样的方法

球面均匀采样

对于一个参数化的单位球面
$$
x=\cos\theta \cos\phi\
y=\cos\theta \sin\phi\
z=\sin\theta
$$

如果很自然地对$\theta \sim U[0,\pi],\phi \sim U[0,2\pi]$ 均匀采样的话,这张可视化的图可以很容易理解(这里 $\theta,\phi$ 和我是反的)

image

回忆球面到平面参数化的展开图(如HDR图、地图),在顶部和底部的的两条线会被压缩成极点,因此靠近这两个边缘的点的分布会变得非常密集。

那么一般的做法是,因为我们考虑球面均匀采样,是面积均匀,对于某个采样点v,因此我们希望它的概率密度函数$pdf(v) = \frac{1}{4\pi}$ ,

然后我们需要将这个采样点参数化,利用概率密度函数在积分域上积分为1
$$
pdf(v)dA = \frac{1}{4\pi}dA = pdf(\theta,\phi)d\theta d\phi
$$
那么这样我们需要找到$dA$ 和$d\theta d\phi$ 的映射关系,而面积微元$dA = \sin\theta d\theta d\phi$

所以联合概率密度函数
$$
pdf(\theta,\phi) = \frac{1}{4\pi}\sin\theta
$$
可以求出边际概率密度函数,由于$\theta,\phi$ 是独立的,其实也就是$\theta,\phi$ 各自的概率密度函数
$$
pdf(\theta) = \int_0^{2\pi}pdf(\theta,\phi)d\phi = \frac{\sin\theta}{2}\
pdf(\phi)=\int_0^\pi pdf(\theta,\phi)d\theta = \frac{1}{2\pi}
$$
概率分布函数/累积分布函数
$$
F(\theta) = \int_0^\theta pdf(\theta)d\theta = \frac{1-\cos\theta}{2}\
F(\phi) = \int_0^\phi pdf(\phi)d\phi = \frac{\phi}{2\pi}
$$
也就是说,我们的参数化平面这张图上,$\theta,\phi$ 的分布就应该长这个样子了。

但是如何来获得一个这种分布呢?

方法有很多

逆变换采样

就实际应用上来说,我们最容易做的random也就是均匀分布。

利用一个均匀分布来得到另外一种分布,我们需要用到一个东西叫做逆变换采样Inverse_transform_sampling

维基百科上这张图很形象

img

其实就是假定某种变换$T$,使得$T(U) \sim F(\theta)$
$$
\因为U是均匀分布,因此有P(U\leq y) = y\
F(\theta) = P(\xi \leq \theta) = P(T(U)\leq\theta)=P(U\leq T^{-1}(\theta)) = T^{-1}(\theta)\
那么自然知道,F(\theta) = T^{-1}(\theta)\
那么这种变换T(U) = F^{-1}(U)\
取两个随机变量\xi_1,\xi_2 \sim U\
F_{\theta}^{-1}(\xi_1) = \arccos (1-2\xi_1)\
F_{\phi}^{-1}(\xi_2) = 2\pi \xi_2
$$
至此推导完毕。

其他采样方法

Wolfram MathWorld里面还给了很多神奇的采样方法

Marsaglia(1972)

(1)

$$
选择u = \cos\theta 均匀分布,所以du = \sin\theta d\theta\
x = \sqrt{1-u^2}\cos\phi\
y = \sqrt{1-u^2}\sin\phi\
z=u
$$

(2)

$$
选取x_1,x_2\sim U(0,1),进行剔除x_1^2+x_2^2\geq1\
x = 2x_1\sqrt{1-x_1^2-x_2^2}\
y = 2x_2\sqrt{1-x_1^2-x_2^2}\
z = 1-2(x_1^2+x_2^2)
$$

Cook 1957,Marsaglia 1972

$$
选取x_0,x_1,x_2,x_3\sim U(-1,1),拒绝域x_0^2+x_1^2+x_2^2+x_3^2\ge1\
利用四元数的变换规则\
x = \frac{2(x_1x_3+x_0x_2)}{x_0^2+x_1^2+x_2^2+x_3^2}\
y = \frac{2(x_2x_3-x_0x_1)}{x_0^2+x_1^2+x_2^2+x_3^2}\
z = \frac{x_0^2+x_3^2-x_1^2-x_2^2}{x_0^2+x_1^2+x_2^2+x_3^2}\
$$

Muller 1959,Marsaglia 1972

$$
生成三个高斯随机变量x,y,z\
向量\frac{1}{\sqrt{x^2+y^2+z^2}}\left[\begin{array}{} x\y\z\end{array}{}\right]
\在表面S^2上是均匀的
$$

【参考资料】

http://corysimon.github.io/articles/uniformdistn-on-sphere/

https://zhuanlan.zhihu.com/p/360420413

https://zhuanlan.zhihu.com/p/49746076

球体点拾取 – 来自 Wolfram MathWorld

逆变换采样 - 维基百科,自由的百科全书 (wikipedia.org)

https://www.bilibili.com/read/cv5661231/

【笔记】Cherno C++ Tutorial note 02

Create/Instantiate Objects 对象的实例化

创建一个类始终需要在C++中占用内存,即使是空类,也会占用一个字节

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
using String std::string;

class Entity
{
private:
String m_Name;
public:
Entity() : m_Name("Unknown") {}
Entity(const String& name) : m_Name(name) {}

const String& GetName() const { return m_Name; }
};

void Function() {
Entity entity = Entity("Cherno");//当前函数运行结束时,该实例会销毁
//如果希望对象在当前函数的生命周期之外存在,
}
int main() {
Funtion();
Entity entity;//调用默认构造函数
Entity entity2 = Entity("Cherno");
Entity entity3("Cherno");
std::cout << entity.GetName() << std::endl;

Entity* e;
{
Entity entity("Cherno");
//即便如此,括号结束后,e指向的地址上的对象也会被销毁。
e = &entity;
std::cout << entity.GetName() << std::endl;
}//如果要让entity一直存在,就不能这样实例化在(stack)堆栈中。

Entity* e;
{
Entity* entity = new Entity("Cherno");//在Array部分也说过,new关键字的特性,即使当前生命周期结束也不会销毁内存,必须手动释放。(heap堆)
e = entity;
std::cout << (*entity).GetName() << std::endl;
std::cout << entity->GetName() << std::endl;
}
delete e;

}

New Keyword New关键字

New的主要功能是在heap堆上分配内存

  • new int在内存上连续的一部分分配一个4bytes的空间,并返回一个指针指向这个地址
1
2
3
4
5
6
7
8
9
int a = 2;
int* b = new int;
int* b = new int[50];// 200 bytes

Entity* e = new Entity();//根据Entity()的大小,获得在内存上连续的一块空间
//********同时会调用构造函数********
Entity* e = new Entity[50];//50倍大小

(Entity*)malloc(sizeof(Entity));//不会调用构造函数

new的实质是一个operator

  • 使用new分配内存,必须使用delete进行释放
1
2
delete b;
delete[] b;

Implicit Conversion and the Explicit Keyword隐式转换和显式关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Entity {
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name):m_Name(name),m_Age(-1){}
Entity(int age) : m_Name("Unknown"),m_Age(age){}
};
void PrintEntity(const Entity& entity){
//printingfunction
}
int main() {
Entity a("Cherno");
Entity b(22);
//利用隐式转换可以写成
Entity a = "Cherno";
Entity b = 22;

PrintEntity(22);
PrintEntity("Cherno");//这样不行
//Entity 构造函数需要的string 是STD string,是一个char array
//C++必须要做两次类型转换才能匹配,char-string-Entity,一次只能做一个
PintEntity(std::string("Cherno"));
}

显示关键字禁止隐式转换

1
2
3
4
5
6
7
8
9
10
11
class Entity {
private:
std::string m_Name;
int m_Age;
public:
explicit Entity(const std::string& name):m_Name(name),m_Age(-1){}
Entity(int age) : m_Name("Unknown"),m_Age(age){}
};
void PrintEntity(const Entity& entity){
//printingfunction
}

OPERATORS and OPERATOR OVERLOADING运算符和运算符重载

运算符就是一种函数

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
struct Vector2 {
float x,y;
Vector2(float x,float y) :x(x),y(y){}

Vector2 Add(const Vector2& other) const {
return Vector2(x+other.x,y+other.y);
}
Vector2 Multiply(const Vector2& other) const {
return Vector2(x*other.x,y*other.y);
}
Vector2 operator+ (const Vector2& other) const {
return Vector2(x+other.x,y+other.y);
}
Vector2 operator* (const Vector2& other) const {
return Vector2(x*other.x,y*other.y);
}
};
std::ostream& operator<<(std::stream& stream,const Vector2& other){
stream<<otehr.x<<", "<<other.y;
return stream;
}

int main() {
Vector2 pos(4.0f,4.0f);
Vector2 dir(3.0f,5.0f);
Vector2 speed(1.1f,1.1f);

Vector2 resul=pos.Add(dir.multiply(speed));
Vector2 resul2 = pos+dir*speed;

std::cout << result <<std::endl;//<<运算符不能直接输出Vector类型,需要重载
}

“this”关键字

在类中,this是一个指向当前实例的指针

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
void PrintEntity(Entity* e);
void PrintEntity(const Entity& e);
class Entity {
public:
int x,y;
Entity(int x,int y) {
Entity* e = this;
//或Entity* const e = this;
x = x;
y = y;
//====可以写
e->x = x
e->y = y
//因此可以直接写
this->x = x;
this->y = y;
PrintEntity(this);//this是一个指针
Entity& e =*this
PrintEntity(*this);//解指针,引用
}

int GetX() const {
//在const方法中并非Entity*e = this
const Entity* e = this;
const Entity& e = *this;
return x;
}
}

Object Lifetime(Stack/Scope Lifetimes)

一个Scope就是一个独立的stack栈,声明周期结束,栈和栈上所有的东西都被销毁。

new关键字使变量创建在heap堆上,栈销毁不会影响堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Entity {
public:
Entity() {}
~Entity() {}
}
int main {
{
Entity e;//创建实例,调用构造函数
//stack based variable
}//Scope结束,e生命周期结束,调用析构函数

{
Entity* e = new Entity();//创建实例,调用构造函数
//heap based variable
}//Scope结束,但是并没有调用e的析构函数,因为e以new关键字声明,必须要delete才能释放内存
}
1
2
3
4
5
6
7
8
9
int* CreateArray() {
int array[50];//这个变量内存是创建在栈上的
//必须以new声明
//int* array = new int[50];
return array;
}
int main() {
int* a = CreateArray();//这是没用的
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ScopedPtr {
private:
Entity* m_Ptr;
public:
ScopedPtr(Entity* ptr) : m_Ptr(ptr){}
~ScopedPtr(){
delete m_Ptr;
}
}
int main() {
{
ScopedPtr e = new Entity();
Entity* a = new Entity();
//ScopedPtr e是创建在栈上的,因此Scope结束,会调用析构函数,这个指针的内存会通过delete被释放。
}
//这就是智能指针unique_ptr的作用
}

Smart Pointers智能指针

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

在讲述new和delete的过程中,我们为了在Scope结束时自动释放new分配的内存,在Scope内定义了一个类,储存new分配内存的指针,并在这个类中的析构函数使用delete释放,使得new分配的内存生命周期和Scope一致

而智能指针就是自动完成这一功能

智能指针就是包裹一个原生的(real raw)指针,使用new分配内存,并且基于使用智能指针的scope使用delete释放内存

std::unique_ptr

unique指针就是最简单的智能指针,完成上述任务。

unique指针无法copy(因为是unique),如果copy这个指针,它们指向同一块内存地址。如果一个指针释放,那第二个指向同一个地址的指针也被释放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<memory>
class Entity {
public:
Entity() {}
~Entity() {}

void Print(){}
}
int main {
{
std::unique_ptr<Entity> entity(new Entity());
//std::unique_ptr<Entity> entity = new Entity();这样是不行的
//unique_ptr的定义是显式声明的,因此必须显式调用构造函数,而不能隐式转换
//当然也可以这样,这是更安全的做法,如果构造函数有exception,不会出现dangling pointer悬空指针
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
entity->Print();

}


}

问题在于,如果想要复制指针、或者把它传入到一个函数中等等,unique无法做到

1
std::unique_ptr<Entity> e0 = entity;//这是不行的,在uniquePtr的定义中,=操作符被删除了。

std::shared_ptr

shared_ptr工作的方式是使用reference counting引用计数,引用计数可以跟踪指针使用了多少引用,只要引用数为0就释放

1
2
3
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
//在uniquePtr中不建议使用std::unique_ptr<Entity> entity(new Entity())
//因为会存在exception unsafety,但是sharedPtr不会

shared_ptr在内存中会另外分配一块地址control block,用于储存引用计数,如果先创建new entity(),然后传递给shared_ptr构造函数,这样就是两块地址,也是exception safety的。

1
2
3
4
5
6
7
8
9
{
std::shared_ptr<Entity> e0;
{
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
std::weak_ptr<Entity> weakEntity = sharedEntity;
//调用构造函数
e0 = sharedEntity;
}//scope结束,sharedEntity结束,但是内存未被销毁,因为e0还保持着它的引用
}//e0结束,内存才被销毁,调用析构函数

std::weak_ptr

weak_ptr在复制上和shared_ptr是相同的,但是它没有引用计数,也不会增加shared_ptr的引用计数

1
2
3
4
5
6
7
8
{
std::weak_ptr<Entity> e0;
{
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
//调用构造函数
e0 = sharedEntity;
}//调用析构函数
}

Copying and Copying Constructors拷贝与拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct vec2 {
float x,y
}
int main() {
int a = 2;
int b = a;//这就是一个拷贝,a,b具有不同的地址

vec2* a = new vec2();
vec2* b = a;//a是一个指针指向一个地址,b指针现在指向同样的地址,因此这里拷贝的不是变量,而是a指向的地址(这个地址就是的地址储存的变量)
b++;//因此这里b改变的是b自己的地址
b->x = 2;//那么这里就是b储存的地址里的变量,因此a指向的变量也会改变。
//根据指针的概念这些很容易理解

}

这些过程都是copy,包括reference

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
class String {
public:
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* string) {
m_Size = strlen(string);
m_Buffer = new Char[m_Size+1];
memcpy(m_Buffer,string,m_Size+1);
m_Buffer[m_Size] = 0;
//这里教程中演示的时候,输出的最后出现了一堆乱码,还记不记得之前讲字符串的时候说char在内存中末尾需要有0填充作终止符,因此把m_Size+1用来储存终止符
}
~String() {
delete[] m_Buffer;
}

char& operator[](unsigned int index) {
return m_Buffer[index];
}

friend std::ostream& operator<<(std::ostream& stream, const String& string);
}
std::ostream& operator<<(std::ostream& stream, const String& string) {
//stream<< string.GetBuffer();通常来说m_Buffer是私有变量,因此需要一个接口函数,但是这里我们使用了友元函数friend关键字,让这个方法可以访问私有变量。
stream<<string.m_Buffer;
return stream;
}
int main() {
String string = "Cherno";
String second = string;

second[2] = 'a';//那么在这里由于m_Buffer的地址相同,两个实例都会发生改变
std::cout<<string<<std::endl;
std::cout<<second<<std::endl;
std::cin.get();
}

在对string直接拷贝然后输出后,尽管有正确的结果,但是运行结束后程序出错了。

在内存中我们有这两个string,它们进行的拷贝叫做shallow copy浅拷贝,它拷贝的是指针。

因此这两个string在内存中有相同的child pointer value子指针值(应该是指成员变量的指针)。

即string和second两个实例的m_Buffer地址是相同的。

那么这里问题就很好理解了。因为写了析构函数,在程序结束时删除m_Buffer,但是两个实例共用一个m_Buffer,自然在第二次删除的时候删除不了东西,就报错了。

==那么这里我的思考是,如果说两个实例本来就需要用同一个变量,那么可否用static关键字来解决这个报错(至于在这里对string这个类有没有意义就不管了)==

到这里应用上的问题已经很明显了,也就是说如果类中的成员是指针的话,不同的实例之间在拷贝的时候使用的是shallow copy,无法达到我们想要的deep copy深拷贝。

解决方案就是使用拷贝构造函数。

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
class String {
public:
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* string) {
m_Size = strlen(string);
m_Buffer = new Char[m_Size+1];
memcpy(m_Buffer,string,m_Size+1);
m_Buffer[m_Size] = 0;
}
String(const String& other);
//C++为这种构造函数提供了接口,把它认为是拷贝构造函数,完整写出来就是下面这样
String(const String& other)
: m_Buffer(other.m_Buffer), m_Size(other.m_Size){
}
//或者
String(const String& other) {
memcpy(this, &other, sizeof(String));
}
//这些都是浅拷贝
//也可以禁用拷贝构造函数,这样就不能拷贝了,比如unique_ptr
String(const String& other) = delete;
//深拷贝
String(const String& other)
: m_Size(other.m_Size) {
m_Buffer = new CHar[m_Size+1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

~String() {
delete[] m_Buffer;
}

char& operator[](unsigned int index) {
return m_Buffer[index];
}

friend std::ostream& operator<<(std::ostream& stream, const String& string);
}
void PringString (String string) {//这样实际上又执行了拷贝构造函数,可以使用引用传递
std::cout<<string <<std::endl;
}
void PringString (const String& string) {//建议总是使用const reference来传递对象
std::cout<<string <<std::endl;
}
int main() {
String string = "Cherno";
String second = string;

second[2] = 'a';
PringString(string);
PringString(second);
std::cin.get();
}

Arrow Operator箭头操作符

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
class Entity {
public:
int x;
public:
void Print() const {}//const版本中,这里也需要是const函数才可以被调用
}
class ScopedPtr {
private:
Entity* m_Obj;
public:
ScopedPtr(Entity* e) : m_Obj(e) {

}
~ScopedPtr() {
delete m_Obj
}
Entity* GetObject() { return m_Obj }

Entiyt* operator->() {
retrun m_Obj;
}

const Entity* operator->() const {//const版本
return m_Obj
}
}
int main () {
Entity e;
e.Print();
Entity* ptr = &e;
(*ptr).Print();
ptr->Print();

ScopePtr entity = new Entity();
entiyt.GetObject()->Print();//这是可以的
entity->Print();//这是不行的,还记得吗,这个包裹的类的地址指向成员指针,
//因此为了更简便,我们需要进行重载

const ScopePtr entity = new Entity();//const版本
}
1
2
3
4
5
6
7
8
9
10
struct vec3 {
float x,y,z;
}

int main() {
((vec3*)0)->x;
((vec3)nullptr)->;
int offset = (int*)&((vec3)nullptr)->x;//可以用箭头函数看到类储存成员变量的相对位置
vec3
}

Dynamic Arrays动态数组

关于standard template,它就像一个容器,可以包含各种类型的数据。

std::vector其实本身和vector没啥关系。

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
#include<vector>
struct Vertex {
float x,y,z;
}



int main() {
Vertex* vertices = new Vertex[5];
//限制就是我们想持续往里加东西的时候是不行的,除非把数组设很大。

std::vector<Vertex> vertices;
vertices.push_back({1,2,3});
for (int i = 0; i< vertices.size();i++) {
std::cout<<vertices[i] <<std::endl;
}
for (Vertex v : vertices) {//这样每一次都对vertices的元素进行了拷贝
std::cout<< v <<std::endl;
}
for (Vertex& v : vertices) {
std::cout<< v <<std::endl;
}
vertices.erase(vertices.begin() + 1);//erase不能传递数字,需要传递iterator

vertices.clear();

}

Optimizing the useage of std::vector

std::vector 在每次push_back时可能会进行拷贝,然后重新分配一段内存,删除原来的内存。因此速度会变慢

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
#include<vector>
struct Vertex {
float x,y,z;
Vertex(float x,float y,float z) : x(x), y(y), z(z) {}
Vertex(const Vertex& vertex) :x(vertex.x),y(vertex,y),z(vertex.z) {
std::cout<<"copied"<<std::endl;
}
}

int main() {
vertices.push_back({1,2,3});
//一次拷贝
//我们首先在mian的stack上创建了vertex,然后把它拷贝到vertices里
vertices.push_back({4,5,6});
//+两次拷贝,vertices进行resize,capacity变为2,然后进行两次拷贝
//
vertices.push_back(Vertex(7,8,9));//上面这段发生了6次拷贝
//capacity变为3,+三次拷贝

std::vector<Vertex> vertices;
vertices.reserve(3);
vertices.push_back({1,2,3});
vertices.push_back({4,5,6});
vertices.push_back(Vertex(7,8,9));
//一共三次拷贝

std::vector<Vertex> vertices;
vertices.reserve(3);
vertices.emplace_back(1,2,3);
vertices.emplace_back(4,5,6);
vertices.emplace_back(7,8,9);
//直接在vector中进行构造,而不是main中。0次拷贝
//注意这里不再传递vertex,而是传递构造的参数。

}

Using Libraries(static linking)静态链接

选择库的环境(32bit,64bit)并不意味着开发的环境需要是这个,而是开发的对象,目标的运行环境。

我们下载预编译版本的glfw(32-bit windows binaries)

通常libraries会有两部分

  • includes
    • header files
  • lib
    • pre-built binaries

static linking

dynamic linking

把需要的include和对应版本的lib文件放进项目依赖文件夹中

image-20220630103950872

所以这就是预编译的意思,对于这个库已经帮你编译成lib文件了,运行的时候只需要link就行了,不用自己编译。

dll文件相当于一个字典储存了有哪些函数

lib就是一个static library静态库

在C++ 常规属性的附加包含目录就是inlude的目录,可以使用解决方案的相对路径

D:\学习\浙大\C++\Cherno tutorial\Practice\HelloWorld\Dependencies\GLFW\include

$(SolutionDir)Dependencies\GLFW\include

image-20220630104355277

然后就可以include了

image-20220630104812733

使用引号会先检索目录地址,然后再查找外部依赖库

如果是外部依赖External Dependencies,建议使用<>

如果是和项目一起编译,再使用“”

这时已经可以正常编译了ctrl+7

但是运行时,在link阶段就出错了

image-20220630105501781

这说明还没有对library进行link

因为我们include文件里,只有对这个函数的声明,但我们没有link去找到这个函数的定义。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <GLFW//glfw3.h>

int glfwInit() {
return 0;
}
int main() {

int a = glfwInit();

std::cin.get();
}

假如我这样搞,也是可以运行的。

那么在项目属性的链接器linker中,可以添加附加依赖库

image-20220630110022357

我可以输入lib的相对地址

也可以直接输入名字就行了,只需要在常规选项中添加一个附加库目录

image-20220630110240741

image-20220630110149324

Using Dynamic Libraries动态链接

==这一段暂时有点看不懂在讲什么,贴个链接在这==,这个链接讲得很好

https://blog.csdn.net/alexhu2010q/article/details/106264237

上面的linking都是静态的,在编译时linking

dynamic linking就是在运行时linking,即启动程序的时候进行link,但它实际上不是exe的一部分。

当一般exe执行的时候,它加载进内存。如果有Dynamic link lib,这样会在运行时链接另外一个lib和额外的二进制文件(dll中),加载进内存。

C++的库文件分为两种:

  • lib文件
    • 静态的
    • 在build时就被打包到exe内
    • 单独的一个exe文件就可以运行
  • dll文件
    • 动态的
    • 不会被打包到exe内
    • 除了exe,还需要对应的dll文件一起才可以运行

glfw3dll.lib储存的是dll中的指针,用来记录glfw3.dll里面的函数等内容

他把link依赖库的glfwd.lib删掉了,然后换成了glfw3dll.lib

这时出现

image-20220630112323377

只需要把dll文件和可执行文件放一起就可以了。exe在执行时就能自己去找到。所以这就是为什么那么多程序有好多dll文件,他们都采用了动态链接。

当然也可以自己设置地址,但是exe始终是自动搜索当前目录的。

当我们关注glfw库中的声明时,它们一律使用了一个GLFWAPI命名空间,从它的定义可以看到在这里使用静态和动态库的区别,静态库就直接define GLFWAPI了,动态库使用了一个__declspec(dllimport)命令,(第一部分是export导出成dll的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* GLFWAPI is used to declare public API functions for export
* from the DLL / shared library / dynamic library.
*/
#if defined(_WIN32) && defined(_GLFW_BUILD_DLL)
/* We are building GLFW as a Win32 DLL */
#define GLFWAPI __declspec(dllexport)
#elif defined(_WIN32) && defined(GLFW_DLL)
/* We are calling GLFW as a Win32 DLL */
#define GLFWAPI __declspec(dllimport)
#elif defined(__GNUC__) && defined(_GLFW_BUILD_DLL)
/* We are building GLFW as a shared / dynamic library */
#define GLFWAPI __attribute__((visibility("default")))
#else
/* We are building or calling GLFW as a static library */
#define GLFWAPI
#endif

但是问题在于,我们明明没有在预处理器定义里些GLFW_DLL,GLFWAPI定义为nothing,我们仍然能成功运行,这是为什么,教程把它留作小作业希望我们自己处理

在预处理器这里加上GFLW_DLL,和不加并没有任何不同

image-20220630112720473

可以看到不加这个定义,它是作为静态库的

image-20220630144531511

目前的理解暂时是,由于glfw3dll.lib的存在,它储存了dll中的指针,因为是静态链接,在编译的时候自己就找到了。

Making and Working with Libraries in C++生成库文件

首先把我们要输出的库文件配置类型改为静态类

image-20220701100834701

在所需要的项目里,include目录加上engine.h所在目录

image-20220701102241940

这样engine已经可以在另一个项目中编译了,但还无法进行link

但是其实在生成engine项目时我们可以看到,它生成了一个lib文件

image-20220701102527029

可以在helloWorld项目右键

image-20220701102922814

引用选择Engine

然后生成时我们可以发现它首先生成Engine.lib,然后再生成Helloworld

image-20220701102956503

使用静态链接生成的exe包含了所有的二进制文件,可以独立运行

How to Deal with Multiple Return Values处理多个返回值

对于同种变量,你可以返回一个数组std::<array,2> 或者用std::<vector>

不同种变量还可以使用tuple/pari

std::tuple<std::string,std::string>

定义的方式是std::make_pari<std::string,std::string>()

调用:std::get<0>(sources),std::get<1>(sources)

这需要使用

#include

#include

但是总之建议

使用指针/引用来处理返回值

或者使用数据结构

【过程记录】【GAMES202】PRT

【过程记录】【GAMES202】PRT

https://zhuanlan.zhihu.com/p/337319903

离线渲染的书籍:
\1. Physically Based Rendering (third edition),这个不用多说了
\2. Advanced Global Illumination,整本书只有300多页,却基本覆盖了所有的Light Transport Method
\3. Robust Monte Carlo Methods for Light Transport Simulation,离线渲染必读: http://graphics.stanford.edu/papers/veach_thesis/

课程:
UWaterloo蜂须贺先生的CS888: https://cs.uwaterloo.ca/~thachisu/CS888_W21/,重点是页面中的paper list

不建议:
\1. Ray Traing三部曲 (in One Weekend, …) ,没啥用(默认已经从101之类的课程学到了基本的Monte Carlo方法和Path tracing的话)
\2. SmallPT,这个学懂了来看着玩可以,不懂的不要妄想从这个来学习离线渲染(本末倒置了属于是)

首先作业分成两部分

  1. 预计算部分(cpp)
  2. 实时环境光照部分(webgl)

这次的作业离线预计算部分使用了nori的光线追踪框架(既然这么巧遇到,正好202结束,渲染方向就做这个吧)

预计算部分

首先是项目构建,就cmake构建。

在编译时就遇到了问题

image-20221006012253027

但是这儿明明啥都没有

image-20221006012301544

解决方法如下,这是由于MSVC对中文字符编码的支持问题。

https://games-cn.org/forums/topic/guanyuzuoye2kaitoubianyidekunhuo/

编译成功后是这样的输出

image-20221006013619106

image-20221006013558409

这里输出的信息有一些值得关注的

主要是这里讲光照的信息

image-20221006013929438

来自于这个light.txt

而我们的SH系数最后输出到了transport.txt里面

image-20221006014056237

根据作业说明,我们知道这次的环境光照也还是来源于环境贴图,也就是说,我们这次其实不是完全计算的环境光,其实只是把环境贴图的环境光照转化成SH的表达而已。

3.54535 3.54535 3.54535
4.92763e-07 4.92763e-07 4.92763e-07
-1.9416e-07 -1.9416e-07 -1.9416e-07
2.29642e-07 2.29642e-07 2.29642e-07
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0

【笔记】【GAMES202】Real-time Global Illumination实时全局光照

Real-time Global Illumination(in 3D)

全局光照 = 直接光照(光线弹射一次)+ 间接光照(光线弹射大于一次)

Reflective Shadow Maps(RSM)

用间接光照照亮一个点p需要知道什么?

  • 哪些surface patches are directly lit
    • shadow mapping可以提供这个信息
  • 每个surface patch对p的贡献是什么
    • 每个surface patch作为area light,把他们都加起来
image-20220512222030784

假设被直接光照亮的次级光源是diffuse的

image-20220512222601390 image-20220512225823209

(它这里分母四次方是因为点乘当中没有归一化)

忽略次级光源对shading point的可见性。

image-20220512230920138 image-20220512230929309

总结:其实就是shadow mapping的思想,把所有次级光源看作diffuse,着色的时候进行对次级光源采样

这是对于单个光源。问题是无法处理过多光源,也没有考虑间接光照部分的可见项,并且有非常多的采样。

可以看作Virtual point light(VPL)方法的光栅化版

Light Propagation Volumes(LPV)

CryEngine3引入的技术,快速、质量高

  • 核心问题

    • shading point来自不同方向的radiance
  • 思想

    • Radiance在空间中沿直线传播并且不改变
    • 使用3D网格将直接光照的表面传输到其他地方
  • 步骤

    • ==Generation== of radiance point set scene representation生成radiance点集

      • 找到直接光照表面
      • 应用RSM
      • 可能使用简化的diffuse surface patches(virtual light sources)
    • ==Injection== of point cloud of virtual light source into radiance volume

      • 预细分场景为3D网格
      • 对于每个格子,找到包含的virtual light source
      • Sum up their directional light distribution
      • Project to first 2 orders of SHs(4 in total)投影到2阶SH
    • Volumetric radiance ==propagetion==

      • 对于每个网格,收集6个表面接收到的radiance
      • Sum up,and again use SH to represent
      • 重复传播若干次至收敛
      • image-20220514211324813
    • Scene ==lighting== with final light propagation volume

      • 对于每个shading point,找到所在的grid cell
      • 取网格中的incident radiance(from all directions)
      • Shade
  • 问题

    • image-20220514211513120
    • 因为认为同一网格radiance相同 ——light leaking
    • image-20220514213435555
    • 同样假设格子间的visibility不计算

Voxel Global Illumination(VXGI)

  • two-pass algorithm

  • Two main differences with RSM

    • Directly illuminated pixels -> (hierachical) voxels
    • Sampling on RSM -> tracing reflected cones in 3D(Note the inaccuracy in sampling RSM) (Cone tracing)
image-20220514220033906 image-20220514220348504 image-20220514220600084

问题:体素化的复杂度

Real-time Global Illumination(Screen Space)

Screen space:利用从相机渲染场景得到的直接光照信息Direct illumination

相当于post processing on existing rendering

Screen Space Ambient Occlusion(SSAO)

环境光遮蔽AO,Crytek

AO就是场景中物体之间的contact shadow,易于实现,增强场景中的相对位置信息

  • SSAO

    • An approximation of GI
    • in screen space
  • Key idea 1

    • 假设不知道间接光照
    • 假设它是一个常数,对于所有shading points,来自不同方向
    • idea1相当于Ambient的思路
  • Key idea 2&3

    • 考虑different visibility(towards all directions)at different shading points
    • image-20220514222931272
    • (3D建模软件里大概叫天光)
    • 此外,假设是diffuse材质
image-20220514223032378

Theory

image-20220514223158769 image-20220514223535289

$K_a * L_i^{indir}*albedo$

在diffuse、间接光照为常数的情况下,这是准确的

image-20220514223901474 image-20220514224020238

投影立体角(Projected solid angle)

(实际上解释了$cos\theta dw_i$ 半球积分为$\pi$ )

image-20220514224235118 image-20220514224537665

但是这样是没有加权平均的

如何实时计算ka项

  • 世界空间
    • ray casting
  • 屏幕空间
    • post-rendering pass
    • 不用预计算
    • 不依赖场景复杂度
image-20220514224953129

只考虑一定范围内有没有遮挡物

  • SSAO
  • 假设任何一个shading point 在周围体积采样点,判断点被shading point看到的结果
  • 使用相机的depth buffer来判断
  • image-20220514225151550
  • 按理说只需要考虑法线方向的半球就可以了,但法线信息可能是不知道的
  • 因此做法是,只有过半的点在内部(红点),才开始使用AO,使用球而不是半球,但是只有过半的点才开始计算
  • image-20220514225603651
  • 也没有cos项的权重,但这一点可以忽略了
  • image-20220514230015563

其他问题:False occlusions

image-20220514230147163
  • Choosing samples

    • 采样越多越精确,但考虑速度只用16个
    • 少量sample得到结果,再降噪
  • Horizon based ambient occlusion(HBAO)

    • 也是在屏幕空间
    • 近似ray tracing 深度缓冲
    • 需要知道法线(normal map),只采样半球
    • (有法线可以对不同方向进行加权计算)

Screen Space Directional Occlusion(SSDO)

  • SSDO
    • SSAO的提高
    • 考虑更多的实际间接光照
  • 思路
    • 不必假设接受的间接光照是uniform的
    • 一部分间接光照信息是已知的(间接光照=次级光源提供的直接光照(RSM))
  • SSDO使用的直接光照信息不来自于RSM,来自于摄像机
  • 类似于path tracing
    • 在shading point发射随机的光线
    • 如果没有遇到障碍物,是直接光照
    • 如果遇到障碍,是间接光照
  • 比较
    • AO假设间接光照来源非常远,DO假设间接光照来源周围
    • image-20220515131429453
image-20220515131937025

类似HBAO,考虑点p的局部半球

image-20220515132410638

采样到的点不被挡住,则没有间接光照,被挡住,则有间接光照,并且把它们加起来。

如图3,会出这样的问题,P-A没有被挡住,但在屏幕空间的深度判断下,将它认为是挡住的,P-B则相反。

  • SSDO质量更接近离线渲染
  • 仍然只是小范围的GI
  • 可见性偏差
  • 屏幕空间的问题,丢失了不可见表面的信息(一切SS都会出现这种问题)
image-20220515132917610

Screen Space Reflection(SSR)

  • SSR
    • RTR中实施GI的方式
    • 在屏幕空间进行光线追踪
    • 不需要3D信息
  • 基本任务
    • 求交
      • 光线和场景
    • 着色
      • 从交点像素到着色点

反射本身就是全局光照

假设场景没有反射,要加入反射,反射出来的东西都是屏幕上已有的东西。

  • 对于每个像素(镜面反射)
    • 计算反射光线
    • 和屏幕上的物体求交
    • 使用交点颜色做反射色
  • 对于一定roughness的材质,根据BRDF考虑采样的反射光线数量
  • 甚至可以加上法线信息
image-20220515133949777

对于地面任何一个点,可以描述它的反射光,如何求交?

  • Linear Raymarch
    • 每一步检测深度值
    • 质量取决于step size
image-20220515134334564
  • Hierarchy ray trace
    • Generate Depth Mip map
      • 下采样使用最小深度而不是平均
      • 类似于3D空间的层次结构(BVH,KD-tree)
      • 能快速舍弃不可能相交的像素
      • 最小值建立一个保守的判断逻辑
image-20220515134751855 image-20220515134931819

先保守一点只走一步,没有交点,就可以多走一些,如果终于遇到了最小深度交点,就有可能和场景相交了,就需要少走一些。

image-20220515135234170 image-20220515135328284 image-20220515135417034

image-20220515135432576

image-20220515135443096

最小层级都有交点,或者离开了屏幕,就可以停止了。

局限:Mipmap上判断不了起点不在2^k上像素的深度最小值

  • 问题:,因为只在屏幕空间中计算
    • Hidden Geometry Problem
    • image-20220515140428156
    • Edge Cutoff
    • image-20220515140620129

Shading using SSR

和path tracing没有差别,只需要假设diffuse reflectors/secondary lights

  • 是否引入平方衰减?
    • 不需要,只要是算radiance,就不用,只有irradiance才会有伴随面积变大,能量衰减的效果。
  • 是否考虑了着色点和次级光源之间的可见性?
    • 是的,因为ray trace
image-20220515140938022

可以解决

  • Sharp、bulrry reflections

  • Contact hardening(specular的lobe在非常近的距离,也只会采样很少的范围)

  • Specular elongation(被拉长,还是brdf的lobe采样的原因)

  • Per-pixel roughness and normal

【笔记】Progressive Photon Mapping渐进式光子映射

渐进式光子映射Progressive Photon Mapping

感觉有点难找到合适的教程。

直接看pbrt有点头大,于是选择了从论文开始。这里的内容主要是论文翻译

PPM相对于其他无偏的离线渲染方法(主要指PT,BPT,MLT)和一些有偏的方法(PM),解决的是SDS的问题(SDS中的caustic现象)。

光子映射Photon mapping

是一个2pass 的方法

  • 1st pass
    • photon tracing
  • 2nd pass
    • rendering using photon map

对于一个photon map,任何一个表面位置x的exitant radiance估计为
$$
L(x,\overrightarrow w)\approx\sum_{p=1}^n\frac{f_r(x,\overrightarrow w,\overrightarrow w_p)\phi_p(x_p,\overrightarrow w_p)}{\pi r^2}
$$
$n$ :邻近的光子数量,用来估计incoming radiance

$\phi_p$ :第p个光子的flux(power)

这种估计假设了局部的光子代表了x接收到的radiance,并且x周围的表面是平坦(flat)的。

这也是Photon mapping方法偏差的来源。photon tracing过程本身是无偏的,但是photon分布的结果在radiance estimate的过程中进行平均(blurred)。

光子密度(photon density)增加,radiance的估计也会收敛到正确的结果,这说明光子映射是一致的(consistent)。

为了保证收敛到正确结果,需要在photon map中储存无限的光子,并且radius半径需要收敛到0。我们可以通过一种操作满足这些要求:

在photon map 中使用$N$个光子,但是只有$N^\beta$ ($\beta\in [0,1]$)个光子用来进行radiance 估计,当$N$趋近无穷时,$N$和$N^\beta$ 都趋近无穷,但是$N^\beta$是N的高阶无穷小,保证了r收敛到0,以下会把它称为辐射度估计方程。
$$
L(x,\overrightarrow w)\approx \lim {N\to\infin}\sum{p=1}^{N^\beta}\frac{f_r(x,\overrightarrow w,\overrightarrow w_p)\phi_p(x_p,\overrightarrow w_p)}{\pi r^2}
$$
在标准的光子映射中,这个结果是理论正确的,但是光子储存在内存中,这使得无法获得精确的结果。

渐进式光子映射将解决这个问题。

渐进式光子映射Progressive Photon Mapping

刚才又去浏览了一下PBRT,我知道我为什么看起来难受了,Literate Programming是什么鬼啊……文学编程……从零开始直接跳去看PM的内容着实有点难受了。还是接着从论文开始吧。

PPM是多pass的算法,先是ray tracing,然后子序列的pass用来做photon tracing,每一次的photon tracing pass 都会提升全局光照结果的准确性。

Ray Tracing Pass

使用标准的ray tracing通过图像中的每个像素找到场景中所有可见表面,每一条ray path都包含了所有specular的反弹,直到遇到第一个non-specular的表面。

场景中specular表面比较多时,也可以用俄罗斯轮盘赌(Russian Roulette)来停止。而如果击中的表面BRDF有non-specular的部分,对于每个ray path我们都储存路径上所有的hit points(对于这句话我的理解是,如果不是完全镜面的表面,就会有能量的吸收,有一部分光子停留在这里)

1
2
3
4
5
6
7
8
9
10
11
struct  hitpoint {
position x;//击中位置
normal n;//x所在的法线
vector w;//入射光线方向
integer BRDF;//BRDF的index
float x,y;//像素位置
color wgt;//像素权重
float R;//当前的光子查找半径
integer N;//累计光子数
color t;//累计反射的flux能量
}

Photon Tracing Pass

这个步骤是用来累计光子能量的。可以分成很多的pass去做,每个pass追踪一系列光子,每个 photon tracing pass结束后,就去查看所有hit points,找到半径区域的光子。使用新加入的光子来修正光照计算。光子的贡献一经记录,就可以把光子丢掉了,然后再去处理下一个photon tracing pass。直到累积了足够数量的光子。

我们甚至可以在每个PTP后渲染一遍场景,累积的光子越多,场景的质量就越高。

Progressive Radiance Estimate渐进的辐射度估计

传统的PM算法估计着色点的局部光子密度
$$
d(x) = \frac{n}{\pi r^2}
$$
这个估计的假设是周围是平面,在一个半径为r的圆盘上去估计。如果我们在新的光子图上,要产生新的光子,在同样的圆盘上去估计密度,那就是
$$
d’(x)=\frac{n’}{\pi r^2}
$$
把$d(x)$ 和$d’(x)$ 进行平均,我们可以获得半径 r上更准确的估计。这种方法可以获得更加平滑的radiance估计,但是最终结果由于平均计算会失去很多细节。并且平均过程会破坏一致性,使得无法收敛到正确结果。

渐进式的辐射度估计结合多个光子图,能够收敛到正确结果,也能解决细节问题。关键方法是在每个hit point的辐射度估计中,随着光子数量的累计减少半径。这有效地保证了光子密度在极限估计趋于无穷。

接下来会描述光子密度是如何渐进式增长的。我们在ray tracing pass生成的每个hit points上都会计算辐射度估计。初始化时,x对应的半径R(x)会设置一个非零值,比如说对应像素的footprint(虎书中对像素footprint的解释是,屏幕空间像素映射到纹理空间的形状,这里可以看作世界空间)。也可以第一次photon tracing pass后,通过使用光子图来估计半径。

Radius Reduction半径缩减

每个hit point都有一个半径R(x),我们的目标是,半径内的光子数量累计增加时,减少半径。

image-20220515211027302

hit point x的密度d(x),使用上面的公式就可以算,假设已经做了一些photon tracing了,在x处累计了N(x)的光子,如果这个时候在做一个photon tracing pass,并且在R(x)范围内又找到M(x)个光子,我们可以把新的M(x)个光子加上去
$$
\hat d(x)=\frac{N(x)+M(x)}{\pi R(x)^2}
$$
下一步就是用dR(x)来减少半径R(x),如果我们假设半径R(x)内的光子密度是常数,我们可以算出新的圆盘半径$\hat R(x)=R(x)-dR(x)$ 内光子总数
$$
\hat N(x) = \pi\hat R(x)^2\hat d(x) = \pi(R(x)-dR(x))^2\hat d(x)
$$
为了满足辐射度估计方程中的条件 ,每一次迭代的光子总数都需要有增加的($\hat N(x)>N(x)$,为了简便,使用了一个系数$\alpha \in [0,1]$ 来控制光子的比例
$$
\hat N(x) = N(x) + \alpha M(x)
$$
也就是说,我们每次迭代可以把$\alpha M(x)$ 个新的光子加上去,可以计算出对应需要减少的半径$dR(x)$
$$
\pi(R(x)-dR(x))^2\hat d(x) = \hat N(x)
\\Leftrightarrow \pi(R(x)-dR(x))^2\frac{N(x)+M(x)}{\pi R(x)^2} = N(x) + \alpha M(x)
\\Leftrightarrow dR(x) = R(x) - R(x)\sqrt{\frac{N(x) + \alpha M(x)}{N(x)+M(x)}}
$$
所以更新的$\hat R(x)$
$$
\hat R(x)=R(x)-dR(x)=R(x)\sqrt{\frac{N(x) + \alpha M(x)}{N(x)+M(x)}}
$$
注意,在每个hit point,这个公式都是独立计算的。

Flux Correction能量修正

当hit point接收到新的M(x)个光子,我们还需要加上这些光子所携带的能量。还需要把前面计算的半径缩减考虑在内。每个hit point储存接收到的BRDF预乘后未归一化的能量。把它叫做$\tau(x,\vec w)$ ,对于N(x)个光子
$$
\tau_N(x,\vec w) = \sum_{p=1}^{N(x)}f_r(x,\vec w,\vec w_p)\phi_p’(x_p,\vec w_p)
$$
$\vec w$是hit point的入射光线的方向,$\vec w_p$是入射光子的方向,$\phi_p’(x_p,\vec w_p)$ 是光子p携带的未归一化的能量。注意!这个阶段的能量在标准光子映射中,是没有被发出光子的数量除掉的。

同样的,新的M(x)个光子提供的能量
$$
\tau_M(x,\vec w) = \sum_{p=1}^{M(x)}f_r(x,\vec w,\vec w_p)\phi_p’(x_p,\vec w_p)
$$
如果半径是常数,那我们可以干嘛,直接把这两个能量加起来了。但是半径减少了,我们还要考虑到已经变成在半径外面的那些光子。

一种方法是,维护一个圆盘内所有光子的列表,半径衰减后,不在圆盘内的,就把它们移出去。 但是这个方法不实用,因为光子列表消耗太多内存了。因此,我们假设圆盘内的光照和光子密度是常数,会有以下的结果
$$
\tau_{\hat N}(x,\vec w) = (\tau_N(x,\vec w)+\tau_M(x,\vec w))\frac{\pi \hat R(x)^2}{\pi R(x)^2}
\=\tau_{N+M}(x,\vec w)\frac{\pi(R(x)\sqrt{\frac{N(x)+\alpha M(x)}{N(x)+M(x)}})^2}{\pi R(x)^2}
\=\tau_{N+M}(x,\vec w)\frac{N(x)+\alpha M(x)}{N(x)+M(x)}
$$
$\tau_{\hat N}$ 就是半径缩减后$\hat N$ 个光子相关的缩减后的能量。最开始的时候,假设的是半径内的光子密度和光照是常数,这可能不正确,但是随着半径越来越小,这个结果会变得越来越正确,除了恰好位于照明不连续处的点。但这不构成问题,因为不连续的光照是未定义的,击中点恰好在不连续的位置的概率是0。

Radiance Evaluation辐射度估计

每一次光子追踪后,我们都可以估计击中点的辐射度。回调储存的数据,包括当前半径、当前的乘以BRDF后的截断能量。估计的辐射度要乘以对应像素的权重,再加到对应像素上。

为了估计辐射度,我们还需要知道发射出的光子总数$N_{emitted}$ 用来归一化$\tau (x,\vec w)$

辐射度估计如下
$$
L(x,\vec w) = \int_{2\pi}f_r(x,\vec w,\vec w’)L(x,\vec w’)(\vec n\cdot\vec w’)dw’
\\approx\frac{1}{\Delta A}\sum_{p=1}^nf_r(x,\vec w,\vec w’)\Delta\phi_p(x_p,\vec w_p)
\=\frac{1}{\pi R(x)^2}\frac{\tau(x,\vec w)}{N_{emitted}}
$$
和正常的光子映射相似,这个公式没有限制为Lambertian材质,因为我们要把能量预先乘上BRDF再储存为$\tau(x,\vec w)$ 。

如果R(x)定义的圆盘位于未照明区域内,则半径R(x)不会减小(因为M(x)= 0 )。虽然这种情况看起来破坏了一致性,它仍然会收敛到正确的结果$L(x,\vec w) = 0$ ,因为随着$N_{emitted}\to \infin, \tau(x,\vec w)也不会增加,L(x,\vec w)\to0$ ,

总之论文根据实验数据给出N(x)和R(x)是正确收敛的,半径衰减至0,光子数量增长至无穷,辐射度也会正确收敛。渐进式的辐射度估计保证了每次迭代中每个击中点光子密度的增加,和辐射度估计方程是一致的。

再往后就是论文对不同方法的效果的比较了,差不多到这里就结束了。

2022年5月11日 周三

原来前面那篇就是昨天写的,太不可思议了,但其实已经过了快48小时了。
感觉这40多个小时经历了好多……

今天拍摄完了雷人的元宇宙视频,虽然其实已经超出预期了,效果竟然还不错。全程非常欢乐。
有好多需要处理的事,开始一件件冒头了。

Read More...