Maya Python - Vscode
在maya python当中的脚本编写与运行都十分麻烦。在vscode中编写则更容易。
也可以搜到一些关于maya python在vscode中使用的教程,但是本文会完善一些细节以及整合更优的工作流选择。
对于一个参数化的单位球面
$$
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$ 和我是反的)

回忆球面到平面参数化的展开图(如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
维基百科上这张图很形象
![]()
其实就是假定某种变换$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里面还给了很多神奇的采样方法
$$
选择u = \cos\theta 均匀分布,所以du = \sin\theta d\theta\
x = \sqrt{1-u^2}\cos\phi\
y = \sqrt{1-u^2}\sin\phi\
z=u
$$
$$
选取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)
$$
$$
选取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}\
$$
$$
生成三个高斯随机变量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
创建一个类始终需要在C++中占用内存,即使是空类,也会占用一个字节
1 | using String std::string; |
New的主要功能是在heap堆上分配内存
1 | int a = 2; |
new的实质是一个operator
1 | delete b; |
1 | class Entity { |
显示关键字禁止隐式转换
1 | class Entity { |
运算符就是一种函数
1 | struct Vector2 { |
在类中,this是一个指向当前实例的指针
1 | void PrintEntity(Entity* e); |
一个Scope就是一个独立的stack栈,声明周期结束,栈和栈上所有的东西都被销毁。
new关键字使变量创建在heap堆上,栈销毁不会影响堆
1 | class Entity { |
1 | int* CreateArray() { |
1 | class ScopedPtr { |
在讲述new和delete的过程中,我们为了在Scope结束时自动释放new分配的内存,在Scope内定义了一个类,储存new分配内存的指针,并在这个类中的析构函数使用delete释放,使得new分配的内存生命周期和Scope一致
而智能指针就是自动完成这一功能
智能指针就是包裹一个原生的(real raw)指针,使用new分配内存,并且基于使用智能指针的scope使用delete释放内存
unique指针就是最简单的智能指针,完成上述任务。
unique指针无法copy(因为是unique),如果copy这个指针,它们指向同一块内存地址。如果一个指针释放,那第二个指向同一个地址的指针也被释放了。
1 |
|
问题在于,如果想要复制指针、或者把它传入到一个函数中等等,unique无法做到
1 | std::unique_ptr<Entity> e0 = entity;//这是不行的,在uniquePtr的定义中,=操作符被删除了。 |
shared_ptr工作的方式是使用reference counting引用计数,引用计数可以跟踪指针使用了多少引用,只要引用数为0就释放
1 | std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>(); |
shared_ptr在内存中会另外分配一块地址control block,用于储存引用计数,如果先创建new entity(),然后传递给shared_ptr构造函数,这样就是两块地址,也是exception safety的。
1 | { |
weak_ptr在复制上和shared_ptr是相同的,但是它没有引用计数,也不会增加shared_ptr的引用计数
1 | { |
1 | struct vec2 { |
这些过程都是copy,包括reference
1 | class String { |
在对string直接拷贝然后输出后,尽管有正确的结果,但是运行结束后程序出错了。
在内存中我们有这两个string,它们进行的拷贝叫做shallow copy浅拷贝,它拷贝的是指针。
因此这两个string在内存中有相同的child pointer value子指针值(应该是指成员变量的指针)。
即string和second两个实例的m_Buffer地址是相同的。
那么这里问题就很好理解了。因为写了析构函数,在程序结束时删除m_Buffer,但是两个实例共用一个m_Buffer,自然在第二次删除的时候删除不了东西,就报错了。
==那么这里我的思考是,如果说两个实例本来就需要用同一个变量,那么可否用static关键字来解决这个报错(至于在这里对string这个类有没有意义就不管了)==
到这里应用上的问题已经很明显了,也就是说如果类中的成员是指针的话,不同的实例之间在拷贝的时候使用的是shallow copy,无法达到我们想要的deep copy深拷贝。
解决方案就是使用拷贝构造函数。
1 | class String { |
1 | class Entity { |
1 | struct vec3 { |
关于standard template,它就像一个容器,可以包含各种类型的数据。
std::vector其实本身和vector没啥关系。
1 |
|
std::vector 在每次push_back时可能会进行拷贝,然后重新分配一段内存,删除原来的内存。因此速度会变慢
1 |
|
选择库的环境(32bit,64bit)并不意味着开发的环境需要是这个,而是开发的对象,目标的运行环境。
我们下载预编译版本的glfw(32-bit windows binaries)
通常libraries会有两部分
static linking
dynamic linking
把需要的include和对应版本的lib文件放进项目依赖文件夹中

所以这就是预编译的意思,对于这个库已经帮你编译成lib文件了,运行的时候只需要link就行了,不用自己编译。
dll文件相当于一个字典储存了有哪些函数
lib就是一个static library静态库
在C++ 常规属性的附加包含目录就是inlude的目录,可以使用解决方案的相对路径
D:\学习\浙大\C++\Cherno tutorial\Practice\HelloWorld\Dependencies\GLFW\include
$(SolutionDir)Dependencies\GLFW\include

然后就可以include了

使用引号会先检索目录地址,然后再查找外部依赖库
如果是外部依赖External Dependencies,建议使用<>
如果是和项目一起编译,再使用“”
这时已经可以正常编译了ctrl+7
但是运行时,在link阶段就出错了

这说明还没有对library进行link
因为我们include文件里,只有对这个函数的声明,但我们没有link去找到这个函数的定义。
1 |
|
假如我这样搞,也是可以运行的。
那么在项目属性的链接器linker中,可以添加附加依赖库

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


==这一段暂时有点看不懂在讲什么,贴个链接在这==,这个链接讲得很好
https://blog.csdn.net/alexhu2010q/article/details/106264237
上面的linking都是静态的,在编译时linking
dynamic linking就是在运行时linking,即启动程序的时候进行link,但它实际上不是exe的一部分。
当一般exe执行的时候,它加载进内存。如果有Dynamic link lib,这样会在运行时链接另外一个lib和额外的二进制文件(dll中),加载进内存。
C++的库文件分为两种:
glfw3dll.lib储存的是dll中的指针,用来记录glfw3.dll里面的函数等内容
他把link依赖库的glfwd.lib删掉了,然后换成了glfw3dll.lib
这时出现

只需要把dll文件和可执行文件放一起就可以了。exe在执行时就能自己去找到。所以这就是为什么那么多程序有好多dll文件,他们都采用了动态链接。
当然也可以自己设置地址,但是exe始终是自动搜索当前目录的。
当我们关注glfw库中的声明时,它们一律使用了一个GLFWAPI命名空间,从它的定义可以看到在这里使用静态和动态库的区别,静态库就直接define GLFWAPI了,动态库使用了一个__declspec(dllimport)命令,(第一部分是export导出成dll的)
1 | /* GLFWAPI is used to declare public API functions for export |
但是问题在于,我们明明没有在预处理器定义里些GLFW_DLL,GLFWAPI定义为nothing,我们仍然能成功运行,这是为什么,教程把它留作小作业希望我们自己处理
在预处理器这里加上GFLW_DLL,和不加并没有任何不同

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

目前的理解暂时是,由于glfw3dll.lib的存在,它储存了dll中的指针,因为是静态链接,在编译的时候自己就找到了。
首先把我们要输出的库文件配置类型改为静态类

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

这样engine已经可以在另一个项目中编译了,但还无法进行link
但是其实在生成engine项目时我们可以看到,它生成了一个lib文件

可以在helloWorld项目右键

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

使用静态链接生成的exe包含了所有的二进制文件,可以独立运行
对于同种变量,你可以返回一个数组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
但是总之建议
使用指针/引用来处理返回值
或者使用数据结构
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,这个学懂了来看着玩可以,不懂的不要妄想从这个来学习离线渲染(本末倒置了属于是)
首先作业分成两部分
这次的作业离线预计算部分使用了nori的光线追踪框架(既然这么巧遇到,正好202结束,渲染方向就做这个吧)
首先是项目构建,就cmake构建。
在编译时就遇到了问题

但是这儿明明啥都没有

解决方法如下,这是由于MSVC对中文字符编码的支持问题。
https://games-cn.org/forums/topic/guanyuzuoye2kaitoubianyidekunhuo/
编译成功后是这样的输出


这里输出的信息有一些值得关注的
主要是这里讲光照的信息

来自于这个light.txt
而我们的SH系数最后输出到了transport.txt里面

根据作业说明,我们知道这次的环境光照也还是来源于环境贴图,也就是说,我们这次其实不是完全计算的环境光,其实只是把环境贴图的环境光照转化成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
今天算是阶段性的结束,稍微来写点总结。
全局光照 = 直接光照(光线弹射一次)+ 间接光照(光线弹射大于一次)
用间接光照照亮一个点p需要知道什么?
假设被直接光照亮的次级光源是diffuse的
(它这里分母四次方是因为点乘当中没有归一化)
忽略次级光源对shading point的可见性。
总结:其实就是shadow mapping的思想,把所有次级光源看作diffuse,着色的时候进行对次级光源采样
这是对于单个光源。问题是无法处理过多光源,也没有考虑间接光照部分的可见项,并且有非常多的采样。
可以看作Virtual point light(VPL)方法的光栅化版
CryEngine3引入的技术,快速、质量高
核心问题
思想
步骤
==Generation== of radiance point set scene representation生成radiance点集
==Injection== of point cloud of virtual light source into radiance volume
Volumetric radiance ==propagetion==

Scene ==lighting== with final light propagation volume
问题


two-pass algorithm
Two main differences with RSM
问题:体素化的复杂度
Screen space:利用从相机渲染场景得到的直接光照信息Direct illumination
相当于post processing on existing rendering
环境光遮蔽AO,Crytek
AO就是场景中物体之间的contact shadow,易于实现,增强场景中的相对位置信息
SSAO
Key idea 1
Key idea 2&3

$K_a * L_i^{indir}*albedo$
在diffuse、间接光照为常数的情况下,这是准确的
投影立体角(Projected solid angle)
(实际上解释了$cos\theta dw_i$ 半球积分为$\pi$ )
但是这样是没有加权平均的
只考虑一定范围内有没有遮挡物



其他问题:False occlusions
Choosing samples
Horizon based ambient occlusion(HBAO)

类似HBAO,考虑点p的局部半球
采样到的点不被挡住,则没有间接光照,被挡住,则有间接光照,并且把它们加起来。
如图3,会出这样的问题,P-A没有被挡住,但在屏幕空间的深度判断下,将它认为是挡住的,P-B则相反。
反射本身就是全局光照
假设场景没有反射,要加入反射,反射出来的东西都是屏幕上已有的东西。
对于地面任何一个点,可以描述它的反射光,如何求交?
先保守一点只走一步,没有交点,就可以多走一些,如果终于遇到了最小深度交点,就有可能和场景相交了,就需要少走一些。


最小层级都有交点,或者离开了屏幕,就可以停止了。
局限:Mipmap上判断不了起点不在2^k上像素的深度最小值


和path tracing没有差别,只需要假设diffuse reflectors/secondary lights
可以解决
Sharp、bulrry reflections
Contact hardening(specular的lobe在非常近的距离,也只会采样很少的范围)
Specular elongation(被拉长,还是brdf的lobe采样的原因)
Per-pixel roughness and normal
感觉有点难找到合适的教程。
直接看pbrt有点头大,于是选择了从论文开始。这里的内容主要是论文翻译
PPM相对于其他无偏的离线渲染方法(主要指PT,BPT,MLT)和一些有偏的方法(PM),解决的是SDS的问题(SDS中的caustic现象)。
是一个2pass 的方法
对于一个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}
$$
在标准的光子映射中,这个结果是理论正确的,但是光子储存在内存中,这使得无法获得精确的结果。
渐进式光子映射将解决这个问题。
刚才又去浏览了一下PBRT,我知道我为什么看起来难受了,Literate Programming是什么鬼啊……文学编程……从零开始直接跳去看PM的内容着实有点难受了。还是接着从论文开始吧。
PPM是多pass的算法,先是ray tracing,然后子序列的pass用来做photon tracing,每一次的photon tracing pass 都会提升全局光照结果的准确性。
使用标准的ray tracing通过图像中的每个像素找到场景中所有可见表面,每一条ray path都包含了所有specular的反弹,直到遇到第一个non-specular的表面。
场景中specular表面比较多时,也可以用俄罗斯轮盘赌(Russian Roulette)来停止。而如果击中的表面BRDF有non-specular的部分,对于每个ray path我们都储存路径上所有的hit points(对于这句话我的理解是,如果不是完全镜面的表面,就会有能量的吸收,有一部分光子停留在这里)
1 | struct hitpoint { |
这个步骤是用来累计光子能量的。可以分成很多的pass去做,每个pass追踪一系列光子,每个 photon tracing pass结束后,就去查看所有hit points,找到半径区域的光子。使用新加入的光子来修正光照计算。光子的贡献一经记录,就可以把光子丢掉了,然后再去处理下一个photon tracing pass。直到累积了足够数量的光子。
我们甚至可以在每个PTP后渲染一遍场景,累积的光子越多,场景的质量就越高。
传统的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后,通过使用光子图来估计半径。
每个hit point都有一个半径R(x),我们的目标是,半径内的光子数量累计增加时,减少半径。

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,这个公式都是独立计算的。
当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。
每一次光子追踪后,我们都可以估计击中点的辐射度。回调储存的数据,包括当前半径、当前的乘以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,光子数量增长至无穷,辐射度也会正确收敛。渐进式的辐射度估计保证了每次迭代中每个击中点光子密度的增加,和辐射度估计方程是一致的。
再往后就是论文对不同方法的效果的比较了,差不多到这里就结束了。
需要纪念一下,这个日子。
成年人的崩溃,往往只在一瞬间。
原来前面那篇就是昨天写的,太不可思议了,但其实已经过了快48小时了。
感觉这40多个小时经历了好多……
今天拍摄完了雷人的元宇宙视频,虽然其实已经超出预期了,效果竟然还不错。全程非常欢乐。
有好多需要处理的事,开始一件件冒头了。