游戏开发 | ETH MSCS 在读

Shadow Map

Shadow Map的思想非常简单,就是通过两个Pass将阴影渲染出来。在第一个Pass中,从光源的视角去渲染场景,记录片元的深度,得到一张深度图。然后进行第二个Pass,从摄像机的视角去渲染场景,对于渲染的每一个片元,将其坐标转化为以光源为参照的坐标,可以得到当前的深度,将这个深度值和存储在深度图中的深度值进行比较,如果说当前深度大于深度图中的深度,说明这个位置的光源被物体遮挡了,是一个阴影。

但是,Shadow Map会造成摩尔纹的问题,如下图所示。这是由于采样率不足,多个不同位置但临近的片元有可能映射到同一个ShadowMap的像素上,如第二张图所示,这就会导致有些像素会被误认为是阴影(产生了错误的自遮挡)。为了解决这个问题我们可以增加一个非常小的bias,就好像让地面稍微悬空一点,让那些自遮挡的部分不被误判为阴影,这样子就可以解决摩尔纹的问题了。但同时,这样子做又会引出新的问题,就是物体看起来像是和地面悬空了一样。实际上有很多不同设置bias大小的办法。

img

img 比较简单的一种自适应bias方法是通过法线和入射光的夹角来确定。当入射光非常倾斜的时候,我们需要的bias是比较大的,反之会比较小。所以我们可以通过设定一个bias的最大值和最小值,然后通过法线和入射光的夹角使bias取一个合适的值。

img

更具体的,还可以采用更加细致的数学计算,可以参考自适应Shadow Bias算法 - 知乎中给出的公式。注意以上两种方法中,要考虑法线方向和光线方向在不同侧,会有负值,所以需要加一个abs(比如说小女孩的背面)。

PCF

PCF全称是Percentage Closer Filtering,是为了解决锯齿问题的一种最简单的处理方法。顾名思义,这种方法是采用了filtering的方法,当计算某个像素是否被遮挡时,首先先采样以该像素为中心周围一些点的遮挡情况,最后的Visibility值为被遮挡的采样点的比率。也就是说,对于每一个shading point,我们都要在shadow map上这个点周围一个范围内采样深度,并且判断和渲染视角深度的关系

Note

注意这里的filtering操作是在计算可见度的时候进行的操作,并非是直接在深度图上进行的操作(直接在深度图上进行卷积最终计算的结果还是硬阴影,没有意义)。

1
2
3
4
5
6
7
8
9
10
11
12
13
float PCF(sampler2D shadowMap, vec4 coords, float biasC, float filterRadiusUV) {
//uniformDiskSamples(coords.xy);
poissonDiskSamples(coords.xy); //使用xy坐标作为随机种子生成
float visibility = 0.0;
for(int i = 0; i < NUM_SAMPLES; i++){
vec2 offset = poissonDisk[i] * filterRadiusUV;
float shadowDepth = useShadowMap(shadowMap, coords + vec4(offset, 0., 0.), biasC, filterRadiusUV);
if(coords.z > shadowDepth + EPS){
visibility++;
}
}
return 1.0 - visibility / float(NUM_SAMPLES);
}

PCSS

在PCF中,filtering的范围大小是预先确定的,然而在实际情况中,距离更近的阴影更影,远处则更远,如下图 img

在PCF的基础上,PCSS会根据阴影距离物体的远近调整filtering的大小。PCSS分为三个步骤:

  1. Blocker Search,在某个特定范围内得出平均遮挡物的深度;
  2. 估计半影 (Penumbra)区域的大小,根据相似三角形得出;
  3. 使用计算得到的半影大小作为FilterRadius进行PCF
img

那么在第一步中,在哪一块区域中计算那遮挡物的平均深度呢?这块区域取决于光源的大小和阴影接收平面距离光源的距离。同样类似的,根据相似三角形,在shadow map上可以得到对应的区域,如下图所示

image.png

对应的代码实现

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
float findBlocker(sampler2D shadowMap, vec2 uv, float zReceiver) {

int blockerNum = 0;
float blockDepth = 0.;
float posZFromLight = vPositionFromLight.z;
// blocker search的大小
// NEAR_PLANE是近平面,即shadow map所在的平面
float searchRadius = LIGHT_SIZE_UV * (posZFromLight - NEAR_PLANE) / posZFromLight;

poissonDiskSamples(uv);
for(int i = 0; i < NUM_SAMPLES; i++){
float shadowDepth = unpack(texture2D(shadowMap, uv + poissonDisk[i] * searchRadius));
if(zReceiver > shadowDepth){
blockerNum++;
blockDepth += shadowDepth;
}
}
if(blockerNum == 0)
return -1.;
else
return blockDepth / float(blockerNum);
}

float PCSS(sampler2D shadowMap, vec4 coords, float biasC){
float zReceiver = coords.z;

// STEP 1: avgblocker depth
float avgBlockerDepth = findBlocker(shadowMap, coords.xy, zReceiver);
if(avgBlockerDepth < -EPS)
return 1.0;

// STEP 2: penumbra size
float penumbra = (zReceiver - avgBlockerDepth) * LIGHT_SIZE_UV / avgBlockerDepth;
float filterRadiusUV = penumbra;

// STEP 3: filtering
return PCF(shadowMap, coords, biasC, filterRadiusUV);
}

Variance Soft Shadow Mapping (VSSM)

VSSM在PCF和PCSS的基础上通过统计学方法进行了改进。

Step 3

在PCSS中第三步即PCF中,我们需要对任意一个shading point通过卷积计算其visibility。这一步通常比较耗时。我们可以将其转化为一个概率, 其中可以代表shadowmap的采样值,可以表示shading point的深度。我们可以将深度的概率分布粗略的估计为一个Gaussian distribution,更进一步的,我们甚至可以直接采用Chebyshev不等式进行估计

接下来的问题就是如何计算均值和方差,对于方差我们可以利用公式,这样就只需要知道均值以及平方的均值就可以计算方差

Step 1

在PCSS第一步Blocker search中,我们也需要对shadow map进行采样来计算遮挡物的平均深度,同样也是非常耗时的步骤。这里同样可以用概率进行估计以提高效率。首先定义以下相关变量

  • 遮挡物 ()的平均深度

  • 非遮挡物 ()的平均深度

  • 总样本数量

  • 遮挡物的样本数量

  • 非遮挡物的样本数量

  • 区域平均深度

    因此我们有以下公式

    使用概率作以下估计和假设

  • 然后就可以用Chebyshev不等式解决。另外这里使用的假设直接假定非遮挡物都和shading point在同一平面上,可以理解为因为绝大部分阴影接收者都是一个平面,所以这样的假设也具有一定的合理性。

到此为止,用统计学方法,我们可以解决PCSS中两步采样较为耗时的问题。

MIPMAP and Summed-Area Variance Shadow Maps

在具体实现算法的时候,我们需要计算shadow map中一块区域的方差和均值,可以通过Mipmap或者SAT(Summed Area Table)实现。

  • Mipmap可以快速的对Texture进行范围查询,在查询某块方形范围时,需要根据方形大小选择对应的Level,根据方形中心位置选择周围四个像素进行插值,以及上下Level之间作三线性插值(Trillinear interpolation)。由于多次插值,所以得到的结果是不准确的。另外,Mipmap不适合做非方形区域的范围查询。
image.png
  • SAT使用了类似前缀和的思想,维护一个table,每个位置上存储从原点到当前位置的矩形区域的和,如下图所示,可以很方便的得到某一块区域的范围之和。预计算和存储所需要的额外复杂度都是(逐行逐列累加),可以采用并行计算进行加速。SAT得到的结果对比Mipmap是准确的,但缺点就是预处理比较耗时。
image.png

问题

由于VSSM中使用了大量的假设,在这些估计不准确的时候,会造成渲染的结果存在明显的artifact。比如由于我们假设了在filtering的范围内的深度是一个正态分布,但如果在并不是的情况下,就会造成估计的结果不准确。通常有两种情况,过大或者过小。过小会导致画面偏黑,而过大会导致漏光的现象,比如下面一个例子

image.png

假设右图中三块板从上到下A、B、C的深度分别为a、b、c,那么在Filter region中我们计算得到深度的均值和方差

因此,根据Chebyshev不等式,B板的Visibility就是

C板的Visibility是

可以看出当很小时,也就是多个遮挡物体之间的深度变化很大的时候,本来应该很小的Visibility在上面的例子中会接近0.5,就会造成漏光问题。

除了这个问题以外,另一个假设,也就是假设未造成遮挡的物体和阴影接收平面处于同一个平面上,在一些并不符合的情况下就会造成漏光的问题,如下图。

image.png

Moment Shadow Map

为了解决VSSM中存在的问题,Moment Shadow map使用了更高阶的矩(Moment)去拟合一个分布的CDF。Moment shadow mapping使用矩的最简单的形式 VSSM在本质上就是使用了两阶的Moment,

image.png

从图中可以看到,通常情况下四阶moment已经可以很好的拟合一个CDF了。但是虽然 Moment Shadow Mapping 效果相当不错,很好的解决了 VSSM 绝大部分缺陷,但是它仍需要相当的额外空间开销和重建矩的额外性能开销。

image.png

Distance Field Soft Shadow

Distance Field Shadow Map采用了Signed Distance Function (SDF)进行阴影的计算。使用SDF可以有效解决Shadow map存在的诸多问题,比如摩尔纹,peter panning,锯齿等等问题。

SDF用一个Volume Texture存储每个点到最近的mesh表面的距离,如果在mesh外则为正,在mesh内部则为负。

SDF的性质可以用于进行ray tracing。从起点出发,每一次trace步进当前点查询到的最近距离,这样子可以确保每一次步进都不会穿过物体。下图展示了从起点不断trace光线直到找到了光线和树的交点的过程。

image.png

使用这个性质,可以通过ray marching的方式可以大致估算出visibility。如下图所示,假设有一个面光源,从shading point出发,trace一根光线,使用SDF可以计算出这根光线在多大角度的范围内没有被遮挡,越小的角度代表越小的visibility。具体做法是,从起点出发,按照上面的办法不断的trace光线,在这个过程中计算角度,也就是

image.png

但在实际情况下,由于计算比较耗时,可以粗略的通过简单的线性关系表示遮挡程度,而并非严格的计算未遮挡角度和光源最大角度的比值

其中用来控制阴影的软硬程度,如下图所示 image.png

shader代码实现如下,来源Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float k )
{
float res = 1.0;
float t = mint;
for( int i=0; i<256 && t<maxt; i++ )
{
float h = map(ro + rd*t);
if( h<0.001 )
return 0.0;
res = min( res, k*h/t );
t += h;
}
return res;
}

参考资料

  1. GAMES202-作业1:实时阴影 - 知乎
  2. GAMES 202 - 作业 1: 实时阴影_games202 作业1_CCCCCCros____的博客-CSDN博客
  3. 自适应Shadow Bias算法 - 知乎
  4. Chapter 8. Summed-Area Variance Shadow Maps | NVIDIA Developer
  5. 实时阴影技术(1)Shadow Mapping - KillerAery - 博客园
  6. Mesh Distance Fields in Unreal Engine | Unreal Engine 5.5 Documentation | Epic Developer Community

在实际的项目开发中,我们有时候需要进行一些操作比如说序列化。而如果我们对所有的内容都进行hardcode,显然这样的工作比较繁琐。因此,使用反射可以让我们在运行时获得类型信息,从而更方便的进行开发。在C++中原生不能提供反射机制,因此,通常需要我们手动去实现。

在C++中,常见的反射实现通常分为动态反射和静态反射。静态反射将类型信息在编译时进行导出,通常情况下使用parser在编译时期生成一些辅助代码,通过对类型、字段和函数的标记,生成相应的反射类型信息,如Unreal Engine、Qt等都是通过静态反射去实现反射的功能。动态反射是在运行时将类型信息记录下来,通常需要我们手动去register所需要的反射信息。

一种简单的反射实现

首先,我们可以从简单开始,考虑一个最基本的问题,我们如何通过一个类名的字符串创建出对应的类。我们可以通过一个Class工厂类,存储我们注册的类名和类的构造信息。在创建类的同时,我们实现一个类的回调构造函数,这就是我们在工厂类中保存的类的构造信息,这样我们就可以使用类名字符串创建对应的类了。代码如下 (源自 我所理解的 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <map>
#include <iostream>
#include <string>
using namespace std;

typedef void* (*PTRCreateObject)(void);

class ClassFactory {
private:
map<string, PTRCreateObject> m_classMap ;
ClassFactory(){}; //构造函数私有化

public:
void* getClassByName(string className);
void registClass(string name, PTRCreateObject method) ;
static ClassFactory& getInstance() ;
};

void* ClassFactory::getClassByName(string className){
map<string, PTRCreateObject>::const_iterator iter;
iter = m_classMap.find(className) ;
if ( iter == m_classMap.end() )
return NULL ;
else
return iter->second() ;
}

void ClassFactory::registClass(string name, PTRCreateObject method){
m_classMap.insert(pair<string, PTRCreateObject>(name, method)) ;
}

ClassFactory& ClassFactory::getInstance(){
static ClassFactory sLo_factory;
return sLo_factory ;
}

class RegisterAction{
public:
RegisterAction(string className,PTRCreateObject ptrCreateFn){
ClassFactory::getInstance().registClass(className,ptrCreateFn);
}
};

#define REGISTER(className) \
className* objectCreator##className(){ \
return new className; \
} \
RegisterAction g_creatorRegister##className( \
#className,(PTRCreateObject)objectCreator##className)

// test class
class TestClass{
public:
void m_print(){
cout<<"hello TestClass"<<endl;
};
};
REGISTER(TestClass);

int main(int argc,char* argv[]) {
TestClass* ptrObj=(TestClass*)ClassFactory::getInstance().getClassByName("TestClass");
ptrObj->m_print();
}

Taichi 反射库

Taichi Training类似于RTTR实现了一套动态反射库作为教程,教程链接40 分钟搓一个 C++ 反射库【原理、用法、实现都在这里了!】_哔哩哔哩_bilibili

首先,我们可以先看一段测试代码

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 Foo {
public:
void PassByValue(std::string s) const {
std::cout << "Foo::PassByValue(`" << s << "`)" << std::endl;
}

void PassByConstRef(const std::string &s) const {
std::cout << "Foo::PassByConstRef(const `" << s << "` &)" << std::endl;
}

std::string Concat(const std::string &head, const std::string &tail) {
auto res = head + tail;
return res;
}

// std::unique_ptr will result in compile-time error
std::shared_ptr<float> MakeFloatPtr(float i) {
return std::make_shared<float>(i);
}

static void MakeReflectable() {
reflect::AddClass<Foo>("Foo")
.AddMemberVar("name", &Foo::name)
.AddMemberVar("x_", &Foo::x_)
.AddMemberFunc("PassByValue", &Foo::PassByValue)
.AddMemberFunc("PassByConstRef", &Foo::PassByConstRef)
.AddMemberFunc("Concat", &Foo::Concat)
.AddMemberFunc("MakeFloatPtr", &Foo::MakeFloatPtr);
}

int x() const { return x_; }

std::string name;

private:
int x_{0};
};

注册类型信息的代码就在MakeReflectable中,可以看到这里采用了一个建造者模式,可以链式的对成员函数和成员变量进行register。首先,在AddClass中会创建出这样的一个Builder

1
2
3
4
5
template <typename T>
details::TypeDescriptorBuilder<T> AddClass(const std::string &name) {
details::TypeDescriptorBuilder<T> b{name};
return b;
}

这样的Builder最终想要创建的就是一个TypeDescriptor

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
class TypeDescriptor {
public:
const std::string &name() const {
return name_;
}

const std::vector<MemberVariable> &member_vars() const {
return member_vars_;
}

const std::vector<MemberFunction> &member_funcs() const {
return member_funcs_;
}

MemberVariable GetMemberVar(const std::string &name) const {
for (const auto &mv : member_vars_) {
if (mv.name() == name) {
return mv;
}
}
return MemberVariable{};
}

MemberFunction GetMemberFunc(const std::string &name) const {
for (const auto &mf : member_funcs_) {
if (mf.name() == name) {
return mf;
}
}
return MemberFunction{};
}

private:
friend class RawTypeDescriptorBuilder;

std::string name_;
std::vector<MemberVariable> member_vars_;
std::vector<MemberFunction> member_funcs_;
};

成员变量

MemberVariable的实现中,使用了C++中的一种Type Eraser的方法,这样在编译时我们不需要类型信息,将类型信息的处理放在了运行时进行,也就是将类型的处理交给了使用者。也就是说这里MemberVariable本身不包含任何模板,类型的传递在构造函数以及getter和setter中使用模板进行推导。getter_setter_的参数和返回值都使用std::any,也就是任意类型,在更古老的版本中可以用void*。在getter_setter_的具体实现中,使用std::any_cast对类型进行强制类型转化。

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
class MemberVariable {
public:
MemberVariable() = default;

template <typename C, typename T>
MemberVariable(T C::*var) {
getter_ = [var](std::any obj) -> std::any {
return std::any_cast<const C *>(obj)->*var;
};
setter_ = [var](std::any obj, std::any val) {
// Syntax: https://stackoverflow.com/a/670744/12003165
// `obj.*member_var`
auto *self = std::any_cast<C *>(obj);
self->*var = std::any_cast<T>(val);
};
}

const std::string &name() const {
return name_;
}

template <typename T, typename C>
T GetValue(const C &c) const {
return std::any_cast<T>(getter_(&c));
}

template <typename C, typename T>
void SetValue(C &c, T val) {
setter_(&c, val);
}

private:
friend class RawTypeDescriptorBuilder;

std::string name_;
std::function<std::any(std::any)> getter_{nullptr};
std::function<void(std::any, std::any)> setter_{nullptr};
};

成员函数

成员函数中,类似的,也采用了Type Eraser的策略。这里的构造函数从一个变成了四个,分别对应 - void func(params...) - return_type func(params...) - void func(params...) const - return_type func(params...) const 四种情况。具体的实现中,C::*funcC时类的类型,func时成员函数,Args...时参数列表。绑定函数到fn_上调用时,首先会将类C和参数列表Args...转化成一个tuple的指针,然后调用std::apply函数执行函数返回结果。

另外,在调用的时候,使用std::reference_wrapper对类型进行了引用封装,用于确保该对象是作为一个引用保存在tuple中而不是值。

std::make_tuple Creates a tuple object, deducing the target type from the types of arguments. For each Ti in Types..., the corresponding type Vi in VTypes... is std::decay<Ti>::type unless application of std::decay results in std::reference_wrapper<X> for some type X, in which case the deduced type is X&.

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
class MemberFunction {
public:
MemberFunction() = default;

template <typename C, typename R, typename... Args>
explicit MemberFunction(R (C::*func)(Args...)) {
fn_ = [this, func](std::any obj_args) -> std::any {
using tuple_t = std::tuple<C &, Args...>;
// How to debug compile-time types...
// static_assert(std::is_same<tuple_t, void>::value, "Hoi!");
auto *tp_ptr = std::any_cast<tuple_t *>(obj_args);
return std::apply(func, *tp_ptr);
};
}

template <typename C, typename... Args>
explicit MemberFunction(void (C::*func)(Args...)) {
fn_ = [this, func](std::any obj_args) -> std::any {
using tuple_t = std::tuple<C &, Args...>;
auto *tp_ptr = std::any_cast<tuple_t *>(obj_args);
std::apply(func, *tp_ptr);
return std::any{};
};
}

template <typename C, typename R, typename... Args>
explicit MemberFunction(R (C::*func)(Args...) const) {
fn_ = [this, func](std::any obj_args) -> std::any {
using tuple_t = std::tuple<const C &, Args...>;
// How to debug compile-time types...
// static_assert(std::is_same<tuple_t, void>::value, "Hoi!");
auto *tp_ptr = std::any_cast<tuple_t *>(obj_args);
return std::apply(func, *tp_ptr);
};
is_const_ = true;
}

template <typename C, typename... Args>
explicit MemberFunction(void (C::*func)(Args...) const) {
fn_ = [this, func](std::any obj_args) -> std::any {
using tuple_t = std::tuple<const C &, Args...>;
auto *tp_ptr = std::any_cast<tuple_t *>(obj_args);
std::apply(func, *tp_ptr);
return std::any{};
};
is_const_ = true;
}

const std::string &name() const {
return name_;
}

bool is_const() const {
return is_const_;
}

template <typename C, typename... Args>
std::any Invoke(C &c, Args &&... args) {
if (is_const_) {
auto tp = std::make_tuple(std::reference_wrapper<const C>(c), args...);
return fn_(&tp);
}
auto tp = std::make_tuple(std::reference_wrapper<C>(c), args...);
return fn_(&tp);
}

private:
friend class RawTypeDescriptorBuilder;

std::string name_;
bool is_const_{false};
std::function<std::any(std::any)> fn_{nullptr};
};

最终的测试和使用

测试代码如下,这里需要注意一点,对于const reference和reference的参数传递,需要手动添加一个std::refstd::cref的包装,否则会crash。这是因为否则存在tuple中的类型不是一个引用类型,这和我们希望的是冲突的,所以会产生问题。

这里还有另一个解决办法,作者通过一层中间层ArgWrap对形参到实参的转化作了处理,细节参考代码和视频

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
void TestFoo() {
std::cout << ">>> TestFoo\n" << std::endl;

Foo::MakeReflectable();
auto foo_t = reflect::GetByName("Foo");
for (const auto &mv : foo_t.member_vars()) {
std::cout << "member var: " << mv.name() << std::endl;
}
std::cout << std::endl;
for (const auto &mf : foo_t.member_funcs()) {
std::cout << "member func: " << mf.name() << ", is_const=" << mf.is_const()
<< std::endl;
}
std::cout << std::endl;

Foo f;
// Test member variables
auto name_var = foo_t.GetMemberVar("name");
name_var.SetValue(f, std::string{"taichi"});
std::cout << "f.name=" << f.name << std::endl;
auto x_var = foo_t.GetMemberVar("x_");
x_var.SetValue(f, 42);
std::cout << "f.x=" << f.x() << std::endl;
std::cout << std::endl;

// Test member functions
auto foo_make_float_ptr = foo_t.GetMemberFunc("MakeFloatPtr");
auto res = foo_make_float_ptr.Invoke(f, 123.4f);
auto float_sptr = std::any_cast<std::shared_ptr<float>>(res);
std::cout << "MakeFloatPtr res: " << *float_sptr << std::endl;

std::string hello_s{"hello"};
std::string world_s{" world"};

auto foo_pass_by_val = foo_t.GetMemberFunc("PassByValue");
foo_pass_by_val.Invoke(f, hello_s);

auto foo_pass_by_cref = foo_t.GetMemberFunc("PassByConstRef");
// foo_pass_by_cref.Invoke(f, hello_s); // Crash, value
// foo_pass_by_cref.Invoke(f, std::ref(hello_s)); // Crash, non-const ref
foo_pass_by_cref.Invoke(f, std::cref(hello_s)); // OK: const ref

auto foo_concat = foo_t.GetMemberFunc("Concat");
// foo_concat.Invoke(f, hello_s, world_s);
res = foo_concat.Invoke(f, std::cref(hello_s), std::cref(world_s));
std::cout << "Concat got: " << std::any_cast<std::string>(res) << std::endl;
std::cout << std::endl;

std::cout << "<<< TestFoo OK\n" << std::endl;
}

参考资料

Path tracing笔记以及Games101作业7代码相关解读以及实现。笔记包括

  1. 辐射度量学相关概念
  2. 渲染方程
  3. 蒙特卡洛积分
  4. Path Tracing逻辑。

阅读全文 »

生成树指的是一个连通图G的覆盖所有顶点的无环子图,最小生成树指的是所有生成树中加权和最小的生成树。

最小生成树的应用:聚类分析、网络架构设计、VLSI布线设计等诸多实际应用问题,都可转化并描述为最小支 撑树的构造问题。在这些应用中,边的权重大多对应于某种可量化的成本,因此作为对应优化问 题的基本模型,最小支撑树的价值不言而喻。——《数据结构C++版》

求最小支撑树的算法主要采用贪心算法,最著名的两个算法分别是Prim算法和Kruskal算法。

本篇主要参考Kruskal’s Minimum Spanning Tree Algorithm | Greedy Algo-2 - GeeksforGeeks以及以及Prim’s Minimum Spanning Tree (MST) | Greedy Algo-5 - GeeksforGeeks,GeeksforGeeks上的数据结构今天刚好搜到了,确实讲的不错,转载收藏一下作为笔记。

阅读全文 »

最近在跑实验的时候遇到了这样一个Bug花了很久才解决,记录一下学习一波以免以后再遇到。

1
2
3
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.cuda.FloatTensor [3
, 64, 7, 7]] is at version 2; expected version 1 instead. Hint: enable anomaly detection to find the operation that failed to compute it
s gradient, with torch.autograd.set_detect_anomaly(True).

阅读全文 »

图片的上采样和下采样分别对应图片的放大和缩小,在对图像进行采样的过程中会造成不同程度的模糊。图像的像素画生成采用的方法就是图片下采用的方法。本文会对图像像素化的方法进行总结。

阅读全文 »

在Rider中配置UnLua环境就可以使用同一个IDE同时进行c++和Lua的开发,不用切换vscode和Visual Studio,并且Rider更加美观,操作逻辑和代码提示也比Visual Studio加番茄插件更加好用。所以还是非常推荐使用Rider进行UE的开发的。

阅读全文 »

Games101作业5实现笔记以及Witted Style Ray Tracing和Moller Trumbore Algorithm笔记

Witted Style 光线追踪

最基础最原始的光线追踪算法。根据光路的可逆性,我们从眼睛处朝着像素点发射一道光线,这道光线与场景中的物体相交会发生反射和折射,这里我们默认反射是镜面反射,产生的两道光线会继续和场景中的物体相交产生新的折射光线和反射光线,这就是光线追踪的基本原理。如果不加以限制,光路的反射和折射会不断进行下去,所以我们需要设置一个反射深度限制。

阅读全文 »

vscode编译.cpp文件出现中文乱码解决方案

在vscode中编译.cpp文件,可以使用vscode中的插件code runner点击按钮运行,如果熟悉如何使用g++的命令也可以直接使用命令行语句编译运行。但是如果文件中出现了中文字符,就会出现编译之后生成乱码的问题。比如这一段代码(来源于菜鸟教程

阅读全文 »