Decorative image frame

2022年5月9日 周一

E星计划结束了。就在今天凌晨。
一个月的时间,在课程和实践中都收获了很多。
但也有一些遗憾。
最后一天的时间完成shadow mapping,原理都明白,之前opengl上也做过。但是由于对bgfx的引擎框架不熟悉,它的framebuffer完全不会用,提取出来的深度图是错误的。
最终shadow mapping 也没有完成。

这学期也是同时在做一大堆事情。上学期像这样同时做很多事情的时候,最终是没有一件做到满意,给每件平均打70分吧。
这学期同样如此,这是完成的第一件,虽然也有遗憾,但我给自己能打到85分了。剩下还有好多事情,DIP,元宇宙(虽然这些不太放在心上),还有很希望做好的图形学研究进展的作业和报告。
要抽时间去研究PRT和photon mapping了。
我总是劝告自己,不要同时把很多事情揽在身上,不然最后一件都做不好。
但这么多事情就是同时出现了,又不能不去做(不做DIP和元宇宙行不行),只能接受了,希望这学期都能有好的结果吧

Read More...

【笔记】【GAMES202】Real-time Shadows实时阴影

Real-time Shadows

Shadow Mapping

2-pass Algorithm

  • The light pass generates the SM
  • The camera pass uses the SM

Image-space algorithm

  • 不需要场景的几何信息
  • 自遮挡、走样

Pass1:Render from Light

Pass2:Render from Eye

Issues: Self occlusion

一个像素内记录一个深度,在场景中,属于一个像素的一片区域,光源深度并不相同,更远的就被认为成阴影了。

解决方式:Bias

Adding a bias to reduce self occlusion,

比较深度时,增加bias,距离“明显”更远时才记为阴影。

image-20220501231928346

新的问题:Bias引发的悬浮Peter Panning

(找一个合适的Bias)或:

Second-depth shadow mapping

用最小深度和次小深度的中间深度做最终的比较。(可用性不高)

Issues: Aliasing

Math behind shadow mapping数学解释

$$
\int_{\Omega}f(x)g(x)dx \simeq\frac{\int_{\Omega}f(x)dx}{\int_{\Omega}dx}\cdot \int_{\Omega}g(x)dx
$$

当g(x)足够光滑,g(x)的support较小,二者满足一个,该近似比较准确。

在Esp的IBL-Specular中讲述了同样的==split sum approximate==,
$$
L_o(p,\omega_o) = \int_{\Omega^+}L_i(p,\omega_i)f_r(p,\omega_i,\omega_o)\cos \theta_iV(p,\omega_i)d\omega_i
\\simeq\frac{\int_{\Omega^+}V(p,\omega_i)d\omega_i}{\int_{\Omega^+}d\omega_i}\cdot\int_{\Omega^+}L_i(p,\omega_i)f_r(p,\omega_i,\omega_o)\cos \theta_id\omega_i
$$
右边代表正常的着色,左边的可见项代表阴影,分开来算再乘起来,正是shadow mapping的思想。

Small support: point / directional lighting

Smooth integrand: diffuse bsdf / constant radiance area lighting

面光源、环境光照、glossy会不太准确

Percentage closer soft shadows(PCSS)

Percentage Closer Filtering(PCF)

  • 用于阴影边缘的反走样,并不适用于软阴影

  • 阴影比较的结果进行滤波

  • 不能将shadow map滤波,因为记录的是深度。

对于世界空间的点,判断比较深度不光查找shadow map对应的一个像素,而是周围的局部区域的像素,每一个深度都进行比较,将这些值作平均

(如果是在阴影边缘,那么对应深度图上有阴影部分,也有照亮部分,这样能达到滤波效果)

Filter Size:

  • Small->sharper
  • Large->softer

将一个较大的PCF应用于硬阴影,就类似于软阴影效果

Percentage closer soft shadows

Filter size <-> blocker distance

阴影的软硬和遮挡物距离有关,也将决定filter size的不同。
$$
w_{Penumbra} =(d_{Receiver}-d_{Blocker})\cdot w_{Light}/d_{Blocker}
$$
image-20220502002132527

$w_{Penumbra}$ 即衡量了软阴影的范围

  • blocker size
  • light size
完整流程

Tips: W_light是指定的,Shadow mapping需要用点光源。

  • Step1:Blocker search(Slow)
    • 对于一个着色点,在特定区域计算平均blocker深度(类似PCF)
    • 这个区域可以是常量,也可以是启发式:image-20220502003207293
  • Step2: Penumbra estimation
    • 使用平均blocker深度,决定filter size
  • Step3:Percentage Closer Filtering(Slow)
  • 可以减少sample的数量来加速,但是会有噪声

Deeper Look at PCF

$V(x) = \sum_{q\in N(p)} w(p,q)\cdot\chi^+[D_{SM}(q)-D_{scene}(x)]$

而不是filter shadow map:

$V(x)\neq\chi^+{w*D_{SM}-D_{scene}(x)}$

也不是在图像上filter

$V(x)\neq\sum_{q\in N(p)}w(p,q)V(q)$

Variance soft shadow mapping(VSSM)

解决PCSS在step1和step3慢的问题

PCSS中做平均,其实就是知道区域内阴影和照亮的比例

Accelerate PCF

将shading point对应周围的点的最近深度看作正态分布

  • 计算该区域深度的均值和方差
    • 均值
      • MipMap
      • Summed Area Tables(SAT)
    • 方差
      • $Var(X) = E(X^2) - E^2(X)$
      • $E(X^2)$ 项需要另外一个shadow map(square-depth map) 记录(总共只需要两个通道)
    • 如果假设是正态分布CDF(x)(只有数值解,没有解析解)
      • 切比雪夫不等式 $P(x>t)\leq \frac{\sigma^2}{\sigma^2+(t-\mu)^2}$ (甚至不需要知道分布函数)
      • image-20220502131303998

Accelerate PCSS

计算遮挡物的平均深度

image-20220502132700742

$$
\frac{N_1}{N}z_{unocc}+\frac{N_2}{N}z_{occ}=z_{Avg}
$$
Approximation:N1/N = P(x>t) -> Chebyshev

需要 $Z_{unocc}$ 将其假设为t(shading point的深度->导致只有阴影接收物为平面时效果比较好)

MipMap and Summed-Area Variance Shadow Maps

构建SAT范围查询

前缀和算法

image-20220502134821180

2D情况:

image-20220502135327231

Moment shadow mapping

VSSM:在一些特定的遮挡物场景,分布与假设差别太大,可能偏黑,也可能过亮

image-20220502140118793

切比雪夫不等式只有t>z_avg时有效

解决方式:Moment(矩) Shadow Mapping

VSSM只用了前二阶矩

用前m阶矩描述,可以恢复有m/2个阶跃的CDF

image-20220502140821983

4阶也正好可以用4个通道存储。

问题:主要在于利用Moment的Reconstruction需要比较大的开销,至于如何reconstruction,比较复杂,如果要深入了解,去看论文。

Distance field soft shadows距离场阴影

  • Distance functions:(Optimal transform)
    • 空间任何一个点到物体表面的最小距离

Usage1:Ray marching

  • Ray marching(sphere tracing) to perform ray-SDF intersection
    • 在任意一点,到物体最小距离之内,不会和物体相交
    • 因此在任何一点P,都可以走SDF(P)的距离

image-20220502154558204

Usage2:Approximate percentage of occlusion

软阴影始终定义在面光源下

image-20220502160104196
  • During ray marching

    • 任意一点的SDF,能够提供一个“safe angle”
      • Safe angle越小,能够看到的东西越少
    • 在每一个step,计算眼睛出发的safe angle
      • $\arcsin \frac{SDF(p)}{p-o}$ 运算量较大
      • $min{\frac{k\cdot SDF(p)}{p-o},1.0}$ ,k越大,earlier cutoff of penumbra,阴影越硬
      • image-20220502155552433
    • 保留最小的safe angle,用来决定阴影软硬
  • 特点

    • 快速
    • 高质量
  • 问题

    • 需要预计算
    • 大量的存储空间
    • Artifact

2022年4月24日 周日

刚好又是一个周末。

这周怎么说呢,花了很多功夫去做N星的笔试题,之前固定好睡前看的C++教程也落下了。八选二最终也只完成了一个。抛开平时其他的杂事占用的时间,确实对这八个题目没有什么熟练度。本来简单的效果和作品就还没做过几个,突然要做完整的demo,确实比较困难。

Read More...

【笔记】Cherno C++ Tutorial note 01

@[TOC]

指针Pointers

指针是一个整数,储存一个内存地址。

类型没有意义,只是在这个地址的数据是这个类型

1
2
3
4
5
6
7
8
9
int var = 8;
void* ptr = &var;
*ptr = 10;// 指针解引用,访问对应地址储存的值
=========================
char* buffer = new char[8];
// 一个char占用1个字节,分配了8个字节的内存,并返回一个指向这块内存的开始地址的指针
memset(buffer, 0, 8 );// 为指定地址填充值
delete[] buffer;// 删除值
buffer = nullptr;// 回收指针

指针本身也只是变量,这些变量也储存在内存中(double pointers,triple pointers-指针的指针)

1
2
3
char* buffer = new char[8]; 
memset(buffer, 0, 8 );
char** ptr = &buffer;// 在32位程序里,一个内存地址是32位的

引用Reference

指针和引用基本是一回事,引用是对某个存在变量的引用。引用本身不是一个新的变量,并不真正占用内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int a = 5;
int* b = &a;
int& ref = a;//这里引用的&符号不同于指针,它是变量类型的一部分,紧跟着变量类型
//在这里创建了一个a的别名,ref其实不是一个实际的变量,只是a所在地址的数据的别称
void increment(int value) { // 在这里是传值调用(passing by value)
value++;
}
increment(a);//将a拷贝到了函数中,创造一个全新的变量value,a并不会发生改变

void increment(int* value) {//传递地址,调用a的内存地址而不是a本身给函数
(*value)++;//解引用,改变地址储存的数值而不是地址本身.
//递增符号会优先执行,所以添括号保证先解引用再递增
}
increment(&a)//传递变量地址

void increment(int& value) {//引用传递,passing by reference
//和传递地址是一样的,但是看起来更简单了
value++;
}
increment(a)

引用只是让代码更好看了,没有什么事情是引用能做但指针不能的

一旦声明一个引用,就不能更改它所引用的对象

类Class

C++支持面向过程,基于对象,面向对象,泛型编程

类是一种将数据和函数组织在一起的方式

由类类型制成的变量叫对象,新创建对象的过程叫做实例化

默认情况下,类成员的访问控制都是私有的,只有类内部的函数才能访问——提供公有函数访问接口

类内的函数称为方法(method)

与结构体的区别

默认情况下,类是私有的,结构体是私有的

一般不对结构体使用继承,或过于复杂的功能,仅仅保持结构体为数据和简单的方法

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
class Log{
public:// 将公有私有变量、方法分开。
const int LogLevelError = 0;
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo;
public:
void SetLevel(int level){
m_LogLevel = level;
}
void Error(const char* message){
if (m_LogLevel >= LogLevelError)
std::cout << "[ERROR]: " << message << std::endl;
}
void Warn(const char* message){
if (m_LogLevel >= LogLevelWarning)
std::cout << "[WARNING]: " << message << std::endl;
}
void Info(const char* message){
if (m_LogLevel >= LogLevelInfo)
std::cout << "[Info]: " << message << std::endl;
}
}
int main(){
Log log;
log.SetLevel(log.LogLevelWarning);
Log.Error("Hello!");
Log.warn("Hello!");
Log.Info("Hello!");
std::cin,get();

}

Static静态关键字

类外或结构体外的static修饰符在link阶段是局部的,它只对定义它的编译单元(.obj)可见

类内或结构体里面的static表示这部分内存是这个类的所有实例共享的,即就算实例化了很多次这个类或结构体,这个静态static变量只会有一个实例,并被共享。

静态方法也是如此,没有该实例的指针(this)

类外的static。

static.cpp:

1
2
static int s_Variable = 5; // 这个变量在link的时候只对这个编译单元(.obj)里的东西可见
// static变量或函数表示在link到它实际的定义时,linker 不会再这个编译单元.obj外面寻找它的定义

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
int s_Variable = 10;
int main() {
std::cout << s_Variable << std::endl;
//输出10 不会与static.cpp中的static变量发生重复定义
std::cin.get();
}
=========================
#include <iostream>
extern int s_Variable;//外部链接,会在其他编译单元里寻找定义,但如果static.cpp中是静态声明,就找不到了,静态声明使其他编译单元不能访问s_Variable,相当于私有变量
int main() {
std::cout << s_Variable << std::endl;
std::cin.get();
}

类内Static

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
#include <iostream>
struct Entity{
int x,y;
void Print() {
std::cout << x << ", " << y <<std:endl;
}
}

int main() {
Entity e;
e,x = 2;
e.y = 3;
Entity e1 = { 5, 8 };
e.Print();
e1.Print();//2, 3 /n 5, 8
std::cin.get();
}
============================================
#include <iostream>
struct Entity{
static int x,y;
void Print() {
std::cout << x << ", " << y <<std:endl;
}
}
int Entity::x;
int Entity::y;
int main() {
Entity e;
e.x = 2;
e.y = 3;
Entity e1// = { 5, 8 };//x和y不再是类的成员, 这种写法会报错
e1.x = 5;
//等同于Entity::x = 5,虽然写了e1.x,但其实并不属于类成员
e1.y = 8;
e.Print();
e1.Print();//5, 8 /n 5, 8
std::cin.get();
//static 修饰的struct全局会共用一块内存
//无法通过静态方法访问非静态的成员变量
}

局部静态Local Static

变量的

  • 生命周期:变量被删除前在内存中停留多久

  • 作用域:在哪里能访问这个变量

一个静态局部变量允许我们定义一个变量,生命周期是整个程序,但作用于被限制在这个函数里或任何作用域。和类内作用域的静态变量是一样的。

1
2
3
4
5
6
7
#include <iostream>
void Function() {
static int i = 0;
i++;//如果没有静态,则每次调用函数时,i的值被设为0,自增后输出1
//局部静态变量和在函数外定义全局变量的区别在于作用域
std::cout << i << std::endl;
}

ENUMS枚举

1
2
3
4
5
6
7
8
9
10
11
12
enum Example {
A,B,C
//第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。
// 可以指定任意数组,默认顺序
};
int main() {
Example value = B
}
============
enum Example : unsigned char {// 可以指定任意类型

}
1
2
3
4
5
6
7
8
9
class Log{
public:
enum Level{
Error, Warning, Info
};
// const int LogLevelError = 0;
// const int LogLevelWarning = 1;
// const int LogLevelInfo = 2;
}

Constructor构造函数

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
class Entity{
public:
float X, Y;
void Init() {//需要一种方式,每当构造Entity实例时,需要将X,Y初始化
X = 0.0f;
Y = 0.0f;
}
void Print() {
std::cout<< X<< ", " << Y << std::endl;
}
}
int main() {
Entity e;
e.Init();
e.Print();
}
=================================================
class Entity{//构造函数就是每当构建一个对象时都会调用的方法
public:
float X, Y;
// Entity() = delete;删除默认的构造函数
Entity(){
X = 0.0f;
Y = 0.0f;
}// 可以写许多构造函数(重载)
Entity(float x, float y){
X = x;
Y = y;
}
}
int main() {
Entity e;
Entity e2(10.0f, 5.0f);
e.Print();
e2.Print()
}

当使用static声明类中元素时,并不会调用构造函数(因为static是所有实例共有的)

特殊类型的构造函数:拷贝构造函数Copy Constructor, Move Constructor

Destructors析构函数

constructor 在创建一个对象实例时运行,而析构函数在摧毁一个对象时运行

构造函数通常用来设置变量或者进行某些需要的初始化

当对象即将结束生命周期时,需要清理原本占用的内存,

析构函数同时使用堆和栈来给对象分配空间,所以如果使用new给对象分配空间,而对应的需要使用delete调用析构函数释放空间

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
class Entity{//构造函数就是每当构建一个对象时都会调用的方法
public:
float X, Y;
Entity(){
std::cout << "Created Entity!" << std::endl;
X = 0.0f;
Y = 0.0f;
}
Entity(float x, float y){
X = x;
Y = y;
}
~Entity(){
std::cout << "Destroyed Entity!" << std::endl;
}
}
void Function(){
Entity e;
e.Print();
}
int main() {
Function();
/*
Created Entity!
0, 0
Destroyed Entity!
实例e的生命周期只在Function函数中,因此Function结束后,就调用析构函数释放了空间
*/
std::cin.get();
}

Inheritance继承

(Code Duplication)

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
class Entity{
public:
float X,Y;
void Move(float xa, float ya) {
X += xa;
Y += ya;
}
};
class Player : public Entity{// Entity中任何非私有的变量或函数,Player都可用
public:
const char* Name;
/*float X,Y;
void Move(float xa, float ya) {
X += xa;
Y += ya;
}*/
void PrintName() {
std::cout<< Name <<std::endl;
}
};
int main() {
Player palyer;
player.PrintName();
palyer.Move(5, 10);
}

Virtual Functions虚函数

虚函数允许我们覆盖子类中的方法

比如B是A的派生类(子类),如果指定A中某虚函数方法,则可以在B中覆盖该方法做别的事

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 {
public:
std::string GetName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_Name;
public:
Player(const std::string& name)//这里使用了初始化列表,在构造时先于函数体执行,代表将name赋值给m_Name
: m_Name(name) {}
std::string Getname() { retrun m_Name; }
};

void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
Entity* e = new Entity();
PrintName(e)//Entity

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Entity
}

虚函数引入了动态分派Dynamic Dispatch,通常使用VTable虚函数表实现此编译

虚函数表包含基类中所有虚函数的映射,在运行时映射向正确的覆写函数

总之,如果要覆盖函数,需要在基类中将基函数标记为虚函数

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
class Entity {
public:
virtual std::string GetName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_Name;
public:
Player(const std::string& name)
: m_Name(name) {}
//std::string Getname() { retrun m_Name; }
std::string Getname() override { retrun m_Name; }
//在C++11中,可以将覆写的函数用关键词override标记
};

void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
Entity* e = new Entity();
PrintName(e)//Entity

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Cherno
}

虚函数的性能代价,是需要遍历整个虚函数表查看映射

Pure virtual function纯虚函数

纯虚函数允许我们定义一个在基类中没有实现的函数,然后迫使在子类中实际实现

在接口中,类仅仅包含未实现的方法并充当一种勉强的模板,这种类无法实例化,必须在子类中实现

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
class Entity {
public:
virtual std::string GetName() = 0;
};
class Player : public Entity {
private:
std::string m_Name;
public:
Player(const std::string& name)
: m_Name(name) {}
//std::string Getname() { retrun m_Name; }
std::string Getname() override { retrun m_Name; }
//在C++11中,可以将覆写的函数用关键词override标记
};

void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
// Entity* e = new Entity();
// PrintName(e)//Entity无法再实例化Entity类
Entity* e = new Player("");
PrintName(e);

Player* p = new Player("Cherno");
std::cout << p->GetName() << std::endl;//Cherno
PrintName(p)//Cherno
}

只有当一个类提供所有纯虚函数的实现,才能实例化。

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
class Printable {
public:
virtual std::string GetClassName() = 0;
};
class Entity : public Printable {
public:
virtual std::string GetName() { return "Entity"; }
std::string GetClassName override { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_Name;
public:
Player(const std::string& name)
: m_Name(name) {}
std::string Getname() override { retrun m_Name; }
std::string GetClassName override { return "Player"; }
};

void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
void Print(Printable* obj) {
std::cout << obj->GetClassNmae <<std::endl;
}
int main() {
Entity* e = new Entity();
// PrintName(e)//Entity
Print(e)//Entity
Player* p = new Player("Cherno");
// std::cout << p->GetName() << std::endl;//Cherno
// PrintName(p)//Cherno
Print(p)// Player,如果没有在Player中定义,调用的是Entity中的override,输出Entity
}

Visibility访问控制

本质指类中的成员数据及函数的可访问性

基本的访问控制修饰符:public公有, protected保护, private私有

1
2
3
4
5
6
7
8
9
10
class Entity {
int X,Y;// 默认私有,意味着只有Entity类内才能访问这些变量,包括读写(除非是friend)
public:
Entity() {
X = 0;
}
};
class Player : public Entity {//Player的构造函数也不能访问Entity的私有变量

};

Array数组

1
2
3
4
5
6
7
int example[5]; //声明并分配空间
example[0] = 0;

std::cout << example[0] << std::endl;
std::cout << example << std::endl;//输出example的地址,这是一个指针类型
int* ptr = example;
*(ptr+2) = 6;//example[2] = 6,example地址后移两个int的空间并解引用,得到example[2]的值赋值为6

new 运算符动态分配内存空间,

delete 运算符释放,否则即便局部作用域的程序运行完毕,内存也不会被回收。

为class分配内存空间时,还会同时运行构造函数。

实例的地址所指向的就是成员变量的地址;但如果用new关键字来分配,为成员变量建立了一个指针,实例的地址指向的是指针的地址。

Strings字符串

1
2
3
4
5
6
7
8
const char* name = "Cherno";
// 字符串在内存中末尾需要有0 填充作终止符
const char name2[6] = {'C','h','e','r','n','o'}
std::cout<<name2<<std::endl;//输出的Cherno后面会跟随后面内存所对应的无意义的ASCII码,如锟斤拷
const char name2[7] = {'C','h','e','r','n','o'0}
std::cout<<name2<<std::endl;// 加上终止符,能正常输出Cherno
name[2] = 'a';

standard string

1
2
3
4
5
6
7
8
9
10
11
#include <string>
int main() {
std::string name = "Cherno";
//std string 有一个构造函数,接受一个char ptr或const char ptr
//这里是(const char[7])
//std string 还有一些成员函数可以使用
//std::string name = "Cherno" + "hello!";这是不可以的,这相当于把两个数组相加
name += "hello!";//这是可以的,将一个指针和name相加,利用string的重载+ 拼接字符串
std::string name2 = std::string("Cherno") + “hello!";
//这也是可以的
}

string literal

字符串是不可变的。默认为const,在内存中只读

1
2
3
//正如上面所说,字符串的末尾默认有\0的填充,作为终止符
//"Che\0rno" ="Che\0rno\0"
//实际的字符长度因为中间的终止符,只有3
1
char name[] = "Cherno"// 将它定义为数组,而不是字符串或字符串指针,就可以修改了
1
2
3
4
const char* name = u8"Cherno";//每个字符一个字节
const wchar_t* name2 = L"Cherno";//宽指针
const char16_t* name3 = u"Cherno";//每个字符两个字节的16位字符串
const char32_t* name4 = U"Cherno";//32位字符或每个字符四个字节

在C++14中,还可以

1
2
3
4
5
6
7
8
9
10
using namespace std::string_literals;
std::string name0 = "Cherno"s + "hello";
std::wstring name0 = L"Cherno"s + L"hello";
std::u32string name0 = U"Cherno"s + U"hello";
//R前缀
const char* example = R"(Line1
Line2
Line3
Line4)"

Const常量

就像一个承诺,认为一个变量不改变,但是否信守承诺还是取决于你自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int MAX_AGE = 90
const int a =5;
int* a = new int;
const int* a = new int;// 意味着不能再更改指针的内容,但是可以读取,也可以更改指针。
*a = 2//不可操作
a = (int*)&MAX_AGE; //可以操作
================================
//也等同于:
int const* a = new int;
=================================
int* const a = new int;// 完全相反,不能更改指针,但可以更改指针的内容
*a = 2;// 可以操作
a = (int*)&MAX_AGE;// 不可操作
================================
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
class Entity{
private:
int* m_X,m_Y;
mutable int var;
public:
int GetX() const {// 此方法不会修改任何类成员变量,是一种只读方法
return m_X;
}
int GetX() {
m_X = 0
return m_X;
}
void SetX(int x) {
m_X = x;
}
const int* const GetX() const {// 此方法的指针和此方法都不能更改,且不会修改任何成员变量。
retrun m_X;
}
};

void PrintEntity(const Entity& e){
std::cout<<e.GetX()<<std::end;;
//因为这里是引用了常量,因此不能对类有任何修改,在调用类方法时,也只能调用只读方法,如果没有类方法没有const,则不能调用,因为无法保证没有更改类成员。
//有时同名的类方法会有const版本和无const版本,在这里调用时会自动使用const版本
}

Mutable关键字

与const共同使用

1
2
3
4
5
6
7
8
9
10
11
class Entity
{
private:
std::string m_Name;
mutable int m_DebugCount = 0;
public:
const std::string& GetName() const {
m_DebugCount++;//在这里,本来该方法不能修改类成员变量,但mutable是例外,可以修改了,mutable 将其标记为可变的。
return m_Name;
}
};

lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int x = 8;
auto f = []() {
std::cout<<"Hello"<<std::endl;
}//lambada函数
auto f = [&]() {//传地址
std::cout<<x<<std::endl;
}
auto f = [=]() {//传值
x++;//在这里因为是值传递,相当于用新的变量储存,x不会发生改变
std::cout<<x<<std::endl;
}
auto f = [=]() mutable {
x++;//外部的x变量仍然不会改变
std::cout<<x<<std::endl;//9
};
f();//x = 8

Member Initializer Lists成员初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Entity
{
private:
std::string m_Name;
int m_Score;
public:
Entity() {
m_Name = "Unknown";
}//可以使用初始化列表来完成:
Entity() : m_Name("Unknown"),m_Score(0)//需要按照声明顺序定义
{
}
Entity(const std::string& name)
m_Name(name)
{
//m_Name = name;
}
}
int main() {
Entiyt e0;
}

此外,实际上在不同的构造函数中,如果对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Entity
{
private:
std::string m_Name;
Example m_Example;
public:
Entity() {
m_Name = "Unknown";
}
Entity()
{
m_Name = "Unknown";
m_Example = Example(8)////调用了构造函数,不带参数,来源于m_Example声明。也调用了参数为8的构造函数。
}
Entity() : m_Name("Unknown"),m_Example(Example(8)),m_Example(8)//只调用一次参数为8的构造函数,节约性能。
{
}
Entity(const std::string& name)
m_Name(name)
{
//m_Name = name;
}
}

Ternary Operators三元操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (s_Level>5){
s_Speed = 10;
} else {
s_Speed = 5;
}
s_Speed = s_Level>5 ? 10 : 5;

std::string rank = s_Level > 10 ?"Master": "Beginner";//三元操作符能做到更快
std::string rank;//需要先声明,并调用构造函数
if(s_Level>10)
rank = "Master";//还要进行重载
else
rank = "Beginner";

s_Speed = s_Level > 5 ? s_Level > 10 ? 15 : 10 : 5;
s_Speed = s_Level > 5 ? (s_Level > 10 ? 15 : 10 ) : 5;
s_Speed = s_Level > 5 && s_Level < 100 ? s_Level > 10 ? 15 : 10 : 5;

2022年4月10日 周日

​ 原本以为结束了可视化的项目,就能放松一段时间,谁知道,清明节的假期就是仅有的放松时间。

​ 在三月的时候,其实投递的很多简历都没有消息了,还挂了几家。但是那时候由于重心放在可视化项目,因此没有放在心上。(就连说好的更新博客也鸽了很长时间)直到现在,剩下的几家也都没有消息……就只有耐心等了,希望暑假能有班上。

Read More...

【笔记】【GAMES101】Ray Tracing Note

@[TOC]

Whitted-Style Ray Tracing

Ray Casting

基于针孔相机模型

从眼睛向某个像素投射光线,击中场景中最近的物体相交点,(解决深度测试),连接交点与光源,判定遮挡(解决阴影),如果没有遮挡,则依据着色模型进行着色。

Recursive (Whitted-Style)Ray Tracing

依据镜面反射和折射,计算出反射光线和折射光线,进行递归计算

image-20220304230619622

光线求交

Ray Equation

$r(t)=o+td , 0\le t\le+ \infin$

与球求交

$p:(p-c)^2-R^2=0$ ,联立$(o+td-c)^2-R^2=0$,求解得到正的、较小的t值,可推广至一般隐式曲面。

与三角形求交

一般方法
  1. 光线与平面求交

​ 定义平面:一个方向(法线方向)与一个点(平面上的点),

​ $p:(p-p^{‘}) \cdot N \to ax+by+cz+d=0$ ,同样代入求解,$t=\frac{(p^{‘}-o)\cdot N}{d\cdot N}$,检验$0\le t\le+ \infin$

  1. 判定交点是否在三角形内(叉乘)
Moller Trumbore Algorithm
image-20220304233012291

假设光线和三角形平面有一个交点,则该点既可以用光线方程描述,也可以用三角形的重心坐标描述,以此构建方程,确定重心坐标非负,即可判定光线是否和三角形相交

光线与表面求交的加速

简单的光线-场景求交
  1. 测试光线和每个物体的交点
  2. 找到最近的交点(最小的t)
包围盒Bounding Volumes
轴对齐包围盒AABB
image-20220304234531792

对于三维的情况

  • 只有当光线进入了所有的投影对面,光线才进入了盒子
  • 当光线离开了任意一个对面,光线就离开了盒子

对于每个对面,计算$t_{min}和t_{max}$,对于三维盒子,$t_{enter}=max(t_{min}),t_{exit}=min(t_{max})$

如果进入时间小于离开时间,光线则在这段时间内在盒子里,即有交点

因为正的t才是有意义的,只要$t_{enter}<t_{exit} {&&}t_{exit}\ge0$,则光线与盒子有交点,如果此时$t_{enter}<0$,表示光线出发点在盒子当中。

image-20220304235816614

加速结构

空间划分(Spatial Partitions)

八叉树Oct-Tree / KD-Tree / BSP-Tree

KD-Tree

物体只存在叶子节点中

光线只与被判定相交的叶子结点中所有物体判定求交

image-20220305161528199

BVH(Bounding Volume Hierarchy)

  • 解决物体与包围盒相交、一个物体出现在多个叶子结点的问题

  • 找到一个包围盒,递归地把物体进行划分,重新构建包围盒

  • 选取节点中最长的轴进行划分

  • 取在中间的物体进行节点划分

image-20220305162233644

场景发生变化,则需要重新构建BVH

image-20220305162838657 image-20220305162847780

辐射度量学

Why,What,How

概念

Radiant Energy and Flux

  • Radiant Energy是电磁辐射的能量,$Q[J=Joule]$

  • Radiant flux(power) 是单位时间的能量,$\Phi =\frac{dQ}{dt} [W=Watt][lm=lumen]$,lumen衡量光源亮度

Solid Angles

Anlge:$\theta=\frac{l}{r}$

Solid Angle:$\Omega=\frac{A}{r^2}$

球有 $4\pi$ 的立体角

Differential Solid Angle:

$dA=(rd\theta)(r \sin\theta d\phi)=r^2\sin\theta d\theta d\phi$

$d\omega=\frac{dA}{r^2}=\sin\theta d\theta d\phi$

Radiant Intensity

单位立体角的power
$$
I(\omega)=\frac{d\Phi}{d\omega}[\frac{W}{sr}][\frac{lm}{sr}=cd=candela]
$$

Irradiance(辐射照度)

单位面积的power:The power per(perpendicular/projected) unit area incident on a surface point
$$
E(x)=\frac{d\Phi(x)}{dA}[\frac{W}{m^2}][\frac{lm}{m^2}=lux]
$$

Lambert’s Cosine Law

$E=\frac{\Phi}{A}\cos\theta$

image-20220305181403183

Radiance(辐射率)

物体表面单位立体角、单位投射面积的power:The power emitted,reflected,transmitted or received by a surface, per unit solid angle,per projected unit area

==这里为什么cos在下面呢?这部分光线本身所在面积A,而由于角度,表面测量的面积为$\frac{A}{\cos\theta}$,因此,代表着该表面上的辐射照度Irradiance,其实增加了==,而上面Lambert‘s Cosine Law定义的是表面面积为A,刚好相反。
$$
L(p,\omega)=\frac{d^2\Phi(p,\omega)}{d\omega dA \cos\theta}[\frac{W}{sr\ m^2}][\frac{cd}{m^2}=\frac{lm}{sr\ m^2}=nit]
$$
image-20220305234631162

Incident Radiance

光线从某个方向到达表面单位立体角的Irradiance
$$
L_i(p,\omega)=\frac{dE(p)}{d\omega\cos\theta}
$$

image-20220305234924339
Exiting Radiance

光线离开表面单位投射面积上的Intensity
$$
L(p,\omega)=\frac{dI(p,\omega)}{dA\cos\theta}
$$

image-20220305235512827 image-20220306001005140

BRDF

微小表面从某个微小方向上接受的irradiance,分配到某个方向

image-20220306002133557

BRDF告诉我们,光线从每个吸收方向反射多少到每个出射方向

任意一个出射方向的radiance微分,除以入射点上的irradiance微分

image-20220306002710943

反射方程

对于一个出射方向的radiance,等于BRDF乘以每一个入射方向的radiance进行积分(把每个方向的入射光线对出射方向radiance的贡献加起来)

image-20220306003241967

渲染方程

即反射方程加自发光项

image-20220306003933911

深入理解渲染方程:

考虑一个点光源在一个反射方向上的渲染方程

image-20220306004433528 image-20220306004443153

那么由这种情况可以推测,如果是面光源,则只需要对面光源进行积分(计算直接光照时可以使用)

image-20220306004607488 image-20220306004823056 image-20220306005126998 image-20220306005304523

蒙特卡洛路径追踪——求解渲染方程

蒙特卡洛积分

计算定积分 $\int_a^bf(x)dx$ ,在积分域随机采样 $X_i \sim p(x)$ ,蒙特卡洛积分 $F_N=\frac{1}{N}\sum_{i=1}^N \frac{f(X_i)}{p(X_i)}$,以采样点的概率为权重,对采样的函数值进行加权求和。

推导
$$
利用样本均值对随机变量f(x)的无偏估计
\E(g(x))=\int_{x\in S}g(x)p(x)d\mu \approx \frac{1}{N}\sum_{i=1}^N g(x_i)
\通常更希望估计单个函数的积分,因此令f(x)=g(x)p(x)
\\int_{x\in S}f(x)d\mu\approx\frac{1}{N}\sum_{i=1}^N\frac{f(x_i)}{p(x_i)}
$$

路径追踪

背景

Whitted Style ray tracing 中总是光滑的物体,产生reflections和refractions,在表面根据光照模型计算颜色,直到diffuse表面才发生停止。无法渲染复杂材质,也无法表现diffuse表面之间的反射。

求解渲染方程

image-20220306191509519
忽略自发光项的直接光照情况,

$L_o(p,\omega_o)=\int_{\Omega^+}L_i(p,\omega_i)f_r(p,\omega_o,\omega_i)(n\cdot \omega_i)d\omega_i$ ,使用蒙特卡洛积分求解

此时 $f(x)=L_i(p,\omega_i)f_r(p,\omega_o,\omega_i)(n\cdot \omega_i)$ ,$p(\omega_i)=\frac{1}{2\pi}$(半球面均匀采样)
$$
L_o(p,\omega_o)=\int_{\Omega^+}L_i(p,\omega_i)f_r(p,\omega_o,\omega_i)(n\cdot \omega_i)d\omega_i
\\approx\frac{1}{N}\sum_{i=1}^{N}\frac{L_i(p,\omega_i)f_r(p,\omega_o,\omega_i)(n\cdot \omega_i)}{p(\omega_i)}
$$
image-20220306192828414

间接光照

等同于从P点观测Q点的直接光照。

image-20220306193020106 image-20220306193055533
递归光线数量

取N=1,反射的光线则不会爆炸。(在pdf采样出的光线中,随机选取一个)

与之相对的,需要增加每个像素的光线路径采样数,将radiance取平均。

image-20220306193813503

在这一部分当中,将摄像机位置和像素连线,就隐式构成了透视投影。如果直接按像素平面的法向投射光线,则形成正交投影。

递归出口

真实场景光线弹射无数次,但是在计算机中需要递归出口。因此引入了RR(Russian Roulette)

image-20220306194500300 image-20220306194555964

在生存概率为P的情况下,光线弹射次数的期望是多少?$1/p$

采样光源
image-20220306205859562

很难击中光源。

image-20220306210040279

渲染方程是在半球面上进行采样,因此需要将渲染方程写成对光源上的dA积分

因为只对光源部分进行积分,所以使用蒙特卡洛积分求解时,积分域只在光源上,因此取光源面积A,pdf=1/A

只需要求dw与dA的关系

将dA投影在半球面上

$d\omega=\frac{dA\cos\theta^{‘}}{\abs{\abs{x^{‘}-x}}^2}$ ,
$$
L_o(p,\omega_o)=\int_{\Omega^+}L_i(p,\omega_i)f_r(p,\omega_o,\omega_i)(n\cdot \omega_i)d\omega_i
\=\int_AL_i(p,\omega_i)f_r(p,\omega_o,\omega_i)\frac{\cos\theta\cos\theta^{‘}}{\abs{\abs{x^{‘}-x}}^2}dA
$$
着色结果来源于两部分

  • 光源(直接光照,不需要RR)
  • 其他反射物(借鉴光照,需要RR)
image-20220306211237150

直接光照部分,是使用了光源采样的改写渲染方程, $pdf=1/A$ ,间接光照部分,还是原来的渲染方程, $pdf=1/2\pi$ ,

最后加上直接光源部分计算光源是否被遮挡

其他问题
半球均匀采样
重要性采样

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

​ 选择更适合的pdf

多重重要性采样

采样球面和光源,将其结合起来

像素重建滤波(pixel reconstruction filter)
gamma矫正,曲线,色彩空间,HDR

radiance和颜色的换算过程