在线查看器
参考教程
项目代码
Chapter 1:A Simple Monte
Carlo Program
蒙特卡罗方法(MC)是一种统计模拟方法,是一类很重要的数值计算方法,它是一种使用随机数解决好很多实际问题的方法
1.1 估计\(\pi\)
#include <iostream> #include "Math/Random.h" void estimate_π(const size_t points) { int inside = 0 ; for (int i = 0 ; i < points; ++i) { double x = 2 * Random::rand01 () - 1 ; double y = 2 * Random::rand01 () - 1 ; if (x*x + y*y < 1 ) inside++; } std::cout << "Estimate of π by" << points << "test points is " << 4 * double (inside) / points << std::endl; } int main () { estimate_π(1000 ); estimate_π(10000 ); estimate_π(100000 ); estimate_π(1000000 ); estimate_π(10000000 ); estimate_π(10000000 /2 ); return 0 ; }
1.2 分层方法
一开始非常快速的逼近\(π\) ,之后变化就比较缓慢了,这是一个收益递减法(Law
of Diminishing Returns)的例子
即每一个样本对结果的收益少于后面一个,这个是MC的一个缺点,我们可以通过对样本进行分层来减轻这种递减收益,此法通常称为抖动
我们进行网格划分,并在每个网格中选取一个样本:
我们采用边长为1e4的方框进行测试
void stratify () { size_t inside{ 0 }; size_t circle_stratified{ 0 }; size_t sqrtAll = 1e4 ; for (int i = 0 ; i < sqrtAll; ++i) for (int j = 0 ; j < sqrtAll; ++j) { double x = 2 * Random::rand01 () - 1 ; double y = 2 * Random::rand01 () - 1 ; if (x*x + y*y < 1 ) inside++; x = 2 * ((i + Random::rand01 ()) / sqrtAll) - 1 ; y = 2 * ((j + Random::rand01 ()) / sqrtAll) - 1 ; if (x*x + y*y < 1 ) circle_stratified++; } std::cout << "Regular Estimate of π by 1e8 test points is " << 4 * double (inside) / 1e8 << stds endl; std::cout << "Stratified Estimate of π by 1e8 test points is " << 4 * double (circle_stratified) / 1e8 << stds endl; }
分层方法能更好地收敛于渐近率。不足之处是,这个优势随着问题的维度而降低(例如,对于3D球体积版本,差距会更小)。
这被称为维度诅咒(=.=)
我们的工程将是非常高的维度(每个反射增加两个维度),所以我不会在本书中进行分层。
但是,如果你做的是单反射或阴影或某些严格的2D问题,分层是个很好的选择
Chapter 2:One
Dimensional MC Integration
2.1 相关概念
PDF:\(p(x)\) ,表示随机变量取值为\(x\) 的概率,值域为\((0,∞)\)
CPDF:\(P(x)=\int_{-∞}^x
p(x)\) ,表示从负无穷到x的累积概率,值域为\((0,1)\)
当我们使用rand01()
获得满足\((0,1)\) 之间平均分布的变量\(Y\) 时,其对应的是\(P(x)\)
因此我们需要使用CPDF的反函数\(P^{-1}(Y)\) ,获得满足我们需要的随机变量\(x\)
\(x\) 满足\(p(x)\) 定义的分布
假设我们希望随机变量\(x\) 满足\(p(x)=\frac{x}{2}\)
首先求出CPDF:\(P(x)=\frac{x^2}{4}\)
然后求反函数:\(P^{-1}(Y)=\sqrt{4Y}\)
从而得到生成随机变量\(x\) 的代码为:x = sqrt(4 * rand01())
2.2 重要性采样 & 蒙特卡洛积分
\[
S = \frac{1}{n} * \sum\frac{F(x_i)}{p(x_i)}
\]
2.3 示例:\(\int
\cos^2 θ\ dθ\)
PDF:均匀采样
\(S = \frac{1}{n} * \sum\frac{\cos^2
θ}{p(direction)} = \frac{1}{n} * \sum\frac{\cos^2
θ}{1/4\pi}\)
void sphereMC () { auto pdf = []() { return 1 / (4 * π); }; size_t n = 10000000 ; double sum = 0 ; for (int i = 0 ; i < n; i++) { dvec3 d = Random::random_unit_sphere (); double cosine = d.z ()*d.z (); sum += cosine / pdf (); } std::cout << "I = " << sum / n << stds endl; }
Chapter 3:Light Scaterring
3.1 光的散射
对于光的散射而言,可以基于立体角,设计一个PDF来描述光线散射的方向分布
记为:\(s(direction)\)
这个散射PDF会随着入射方向的变化而变化
对于表面色彩而言,有如下积分形式: \[
color=\int albedo * s(direciont) * color(direction)\ dθ
\]
其中,\(albedo\) 、\(s(direction)\) 都是依赖于观察方向的,因此\(color\) 也会随着观察方向的变化而变化
\(albedo\) 、\(s(direction)\) 也可能随着表面或物体的位置而变化
使用蒙特卡洛方法计算该积分: \[
color=\frac{1}{n} * \sum \frac{albedo * s(direciont) *
color(direction)}{pdf(direction)}
\]
3.2
Lambertian材质的表面散射PDF
Lambertian材质的表面密度,呈余弦规律
因此,Lambertian表面的光散射PDF是正比于\(\cos θ\)
此外,我们定义:\(\cos θ <
0\) 时,\(s(direction)=0\)
半球面积的计算如下: \[
\begin{aligned}
S &= \iint_{A} \cosθ dA \\
&= \int_{0}^{2\pi} \int_0^{\frac{\pi}{2}} \cosθ \sinθ \ dθdΦ\\
&= \int_{0}^{2\pi} dΦ \int_0^{\frac{\pi}{2}} \cosθ \sinθ \ dθ\\
&= \int_{0}^{2\pi} dΦ \int_0^{\frac{\pi}{2}} \frac{1}{2} \sin2θ
dθ\\
&= \pi
\end{aligned}
\] 因此,Lambertian材质的表面散射PDF为: \[
s(direction) = \frac{\cosθ}{\pi}
\]
为了保证采样的最优性,我们也使用同样的PDF函数:\(p(direction) = \frac{\cosθ}{\pi}\)
此时\(s\) 和\(p\) 可以约去,得到\(color=albedo *
color(direction)\) ,这也是我们原始的color函数中的设定
使用BRDF描述表面散射为: \[
BRDF=\frac{albedo*s(direction)}{\cosθ}
\]
对于Lambertian材质,\(BRDF=\frac{albedo}{\pi}\)
Chapter 4:Important
Sampling Materials
4.1 相关概念
4.2 对material基类的修改
class Material {public : virtual bool scatter (const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const = 0 ; virtual bool scatter (const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out, double & pdf) const { return scatter (r_in, info, attenuation, r_out); } virtual double scatter_pdf (const Ray& r_in, const HitInfo& info, const Ray& r_out) const { return 1 ; } virtual Color emitted (double u, double v, const Point3& p) const { return Color (0 , 0 , 0 ); } };
4.3 对Lambertian的修改
bool Lambertian::scatter (const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out) const { Vec3 target = info.position + info.normal + Random::rand_unit_sphere (); r_out = Ray (info.position, target - info.position, r_in.Time ()); attenuation = albedo->Value (info.u, info.v, info.position); return true ; } bool Lambertian::scatter (const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out, double & pdf) const { Vec3 target = info.position + info.normal + Random::rand_unit_sphere (); r_out = Ray (info.position, target - info.position, r_in.Time ()); attenuation = albedo->Value (info.u, info.v, info.position); pdf = info.normal.dot (r_out.Direction ()) / PI; return true ; } double Lambertian::scatter_pdf (const Ray& r_in, const HitInfo& info, const Ray& r_out) const { double cosine = info.normal.dot (r_out.Direction ()); if (cosine < 0 ) cosine = 0 ; return cosine / PI; }
Chapter 5:Generating
Random Direction
5.1 三维随机方向向量的生成
假定:
Z轴
:表面法线
θ
:与法线的夹角
仅处理关于z轴旋转对称 的分布,其他相关的量,均设为均匀分布
5.2 \(pdf(direction)=f(θ)\)
\[
\begin{aligned}
g(Φ)&=\frac{1}{2\pi}\\
h(θ)&=2\pi f(θ) \sin θ
\end{aligned}
\]
对于满足\([0,1]\) 之间均匀分布的随机变量\(r_1,r_2\) 而言: \[
\begin{aligned}
r_1 &= \int_{0}^{Φ} \frac{1}{2\pi} dθ = \frac{Φ}{2\pi} \\
r_2 &= \int_{0}^{θ} 2\pi f(t) \sin t \ dt
\end{aligned}
\]
5.2.1 \(pdf(direction)=f(θ)=\frac{1}{4\pi}\)
\[
\begin{aligned}
r_1 &= \int_{0}^{Φ} \frac{1}{2\pi} dθ = \frac{Φ}{2\pi} \\
r_2 &= \int_{0}^{θ} 2\pi \frac{1}{4\pi} \sin t \ dt\\
&= \int_{0}^{θ} \frac{1}{2} \sin t \ dt\\
&= \frac{1-\cos θ}{2}
\end{aligned}
\]
从而: \[
\begin{aligned}
Φ &= 2\pi r_1 \\
\cos θ &= 1 - 2r_2
\end{aligned}
\] 带入球坐标得: $$
\[\begin{aligned}
x &= \cosΦ\sinθ = 2\cos(2\pi r_1) * \sqrt{r_2(1-r_2)} \\
y &= \sinΦ\sinθ = 2\sin(2\pi r_1) * \sqrt{r_2(1-r_2)}& \\
z &= \cosθ = 1-2r_2
\end{aligned}\]
$$
5.2.2 \(pdf(direction)=f(θ)=\frac{1}{2\pi}\)
\[
\begin{aligned}
r_1 &= \int_{0}^{Φ} \frac{1}{2\pi} dθ = \frac{Φ}{2\pi} \\
r_2 &= \int_{0}^{θ} 2\pi \frac{1}{2\pi} \sin t \ dt\\
&= \int_{0}^{θ} \sin t \ dt\\
&= 1-\cos θ
\end{aligned}
\]
从而: \[
\begin{aligned}
Φ &= 2\pi r_1 \\
\cos θ &= 1 - r_2
\end{aligned}
\] 带入球坐标得: $$
\[\begin{aligned}
x &= \cosΦ\sinθ = \cos(2\pi r_1) * \sqrt{r_2(2-r_2)} \\
y &= \sinΦ\sinθ = \sin(2\pi r_1) * \sqrt{r_2(2-r_2)}& \\
z &= \cosθ = 1-r_2
\end{aligned}\]
$$
5.2.3 \(pdf(direction)=f(θ)=\frac{\cos
θ}{4\pi}\)
\[
\begin{aligned}
r_1 &= \int_{0}^{Φ} \frac{1}{2\pi} dθ = \frac{Φ}{2\pi} \\
r_2 &= \int_{0}^{θ} 2\pi \frac{\cos t}{4\pi} \sin t \ dt\\
&= \int_{0}^{θ} \frac{1}{2} \sin t \cos t \ dt\\
&= 1-\cos^2 θ
\end{aligned}
\]
从而: \[
\begin{aligned}
Φ &= 2\pi r_1 \\
\cos θ &= \sqrt{1 - r_2}
\end{aligned}
\] 带入球坐标得: $$
\[\begin{aligned}
x &= \cosΦ\sinθ = 2\cos(2\pi r_1) * \sqrt{r_2} \\
y &= \sinΦ\sinθ = 2\sin(2\pi r_1) * \sqrt{r_2}& \\
z &= \cosθ = \sqrt{1 - r_2}
\end{aligned}\]
$$
static Vec3 rand_cosine_direction () { double r1 = rand01 (); double r2 = rand01 (); double x = cos (2 * PI * r1) * sqrt (r2); double y = sin (2 * PI * r1) * sqrt (r2); double z = sqrt (1 - r2); return Vec3 (x, y, z); }
Chapter 6:Ortho-normal Bases
目标:将Chapter 5
中基于z轴的随机方向,转化为基于表面法线的随机方向
6.1 建立基于表面法线的坐标系
\(\bold{n}\) :表面法线
\(\bold{t}\) :\(\bold{t}=cross(\bold{vup},\bold{n})\)
\(\bold{s}\) :\(\bold{s}=cross(\bold{t},\bold{n})\)
坐标系中的任意一点\((x,y,z)\) 表示如下:
随机向量 = \(x\bold{s}+y\bold{t}+z\bold{n}\)
class ONB {public : ONB (Vec3 normal); Vec3 local (double x, double y, double z) const ; Vec3 local (const Vec3& v) const ; private : void build_from_w (const Vec3& normal) ; public : const Vec3& operator [](int index) const { return axis[index]; } const Vec3& u () const { return axis[0 ]; } const Vec3& v () const { return axis[1 ]; } const Vec3& w () const { return axis[2 ]; } private : Vec3 axis[3 ]; };
ONB::ONB (Vec3 normal) { build_from_w (normal); } Vec3 ONB::local (double x, double y, double z) const { return x * u () + y * v () + z * w (); } Vec3 ONB::local (const Vec3& v) const { return local (v[0 ], v[1 ], v[2 ]); } void ONB::build_from_w (const Vec3& normal) { Vec3 n = normal.normalize (); Vec3 vup (1 , 0 , 0 ) ; if (fabs (n.x ()) > 0.9 ) vup = Vec3 (0 , 1 , 0 ); axis[2 ] = n; axis[1 ] = n.cross (vup).normalize (); axis[0 ] = n.cross (axis[1 ]).normalize (); }
6.2 修改 Lambertian
材质的scatter函数
bool Lambertian::scatter (const Ray& r_in, const HitInfo& info, Color& attenuation, Ray& r_out, double & pdf) const { ONB uvw (info.normal) ; do { Vec3 direction = uvw.local (Random::rand_cosine_direction ()); r_out = Ray (info.position, direction.normalize (), r_in.Time ()); pdf = uvw.w ().dot (r_out.Direction ()) / PI; } while (pdf == 0 ); attenuation = albedo->Value (info.u, info.v, info.position); return true ; }
Chapter 7:Sample Lights
Directly
即直接朝光源方向发射光线
重点在于计算\(pdf(direction)\)
7.1 直接光源采样
对于一个光源区域A,如果均匀采样该区域,则\(p\_q(q)=\frac{dA}{A}\)
但是由于积分区域是物体表面的单位球,因此我们需要将\(pdf\) 进行转换,即将光源处的微分\(dA\) 转化为方位角微分\(dΩ\) \[
dΩ=\frac{\cos α}{distance^2}dA
\]
由于\(dA\) 和\(dΩ\) 对应的区域的概率是相同的,因此有如下等式:
\[
p(direction) * dΩ = p\_q(q) * dA \\
\]
因此有 \[
p(direction) = \frac{distance^2}{\cos α *A}
\]
7.2 修改GetColor
的采样
Color ObjectWorld::GetColor (const Ray& r, int & depth) { HitInfo info; if (this ->hit (r, 0.01 , INFINITY, info)) { Ray r_out; Color emit = info.material->emitted (r, info, info.u, info.v, info.position); Color attenuation; double pdf; if (depth < max_depth && info.material->scatter (r, info, attenuation, r_out, pdf)) { Point3 on_light = Vec3 (213 + Random::rand01 () * (343 - 213 ), 554 , 227 + Random::rand01 () * (332 - 227 )); Vec3 direction = on_light - info.position; double distance_squared = direction.length_squared (); direction = direction.normalize (); if (direction.dot (info.normal) < 0 ) return emit; double A = (343 - 213 ) * (332 - 227 ); double cos_alpha = fabs (direction.y ()); if (cos_alpha < 1e-6 ) return emit; pdf = distance_squared / (cos_alpha * A); r_out = Ray (info.position, direction, r.Time ()); return emit + attenuation * info.material->scatter_pdf (r, info, r_out) * GetColor (r_out, ++depth) / pdf; } else return emit; } else { return Color (0 ); } }
Chapter 8:混合概率密度
设计pdf的一个很重要的原则:使得累积概率密度达到且只达到1
8.1 pdf基类
主要的任务
获取\(pdf(direction)\)
按照当前模型,生成\(direction\)
class PDF {public : virtual double value (const Vec3& direction) const = 0 ; virtual Vec3 generate () const = 0 ; };
8.1.1 \(pdf(direction)=f(θ)=\frac{\cos
θ}{4\pi}\)
class PDF_cos : public PDF {public : PDF_cos (const Vec3& normal) : uvw (normal) {} virtual double value (const Vec3& direction) const override ; virtual Vec3 generate () const override ; private : ONB uvw; };
double PDF_cos::value (const Vec3& direction) const { double cosine = direction.normalize ().dot (uvw.w ()); if (cosine > 0 ) return cosine / PI; else return 0 ; } Vec3 PDF_cos::generate () const { return uvw.local (Random::rand_cosine_direction ()); }
8.1.2 直接光源采样
class PDF_hit : public PDF {public : PDF_hit (Ref<Object> light, Point3 origin) : light (light), origin (origin) {} virtual double value (const Vec3& direction) const override ; virtual Vec3 generate () const override ; private : Point3 origin; Ref<Object> light; };
double PDF_hit::value (const Vec3& direction) const { return light->pdf_value (origin, direction); } Vec3 PDF_hit::generate () const { return light->random (origin); }
对光源的修改:RectXZ
double RectXZ::pdf_value (const Point3& origin, const Vec3 direction) const { HitInfo info; if (this ->hit (Ray (origin, direction), 1e-3 , INFINITY, info)) { double area = (x2 - x1) * (z2 - z1); double distance_squared = info.t * info.t * direction.length_squared (); double cosine = fabs (direction.dot (info.normal) / direction.length ()); return distance_squared / (cosine * area); } else return 0 ; } Vec3 RectXZ::random (const Point3& origin) const { Point3 on_light = Vec3 (Random::rand_between (x1, x2), k, Random::rand_between (z1, z2)); return on_light - origin; }
8.2 混合概率密度
class PDF_mixture : public PDF {public : PDF_mixture (Ref<PDF> pdf1, Ref<PDF> pdf2, double weight1); virtual double value (const Vec3& direction) const override ; virtual Vec3 generate () const override ; private : Ref<PDF> pdf[2 ]; double weight1; };
PDF_mixture::PDF_mixture (Ref<PDF> pdf1, Ref<PDF> pdf2, double weight1) { pdf[0 ] = pdf1; pdf[1 ] = pdf2; this ->weight1 = weight1; } double PDF_mixture::value (const Vec3& direction) const { return weight1 * pdf[0 ]->value (direction) + (1 - weight1) * pdf[1 ]->value (direction); } Vec3 PDF_mixture::generate () const { if (Random::rand01 () < weight1) return pdf[0 ]->generate (); else return pdf[1 ]->generate (); }
8.3 对GetColor的修改
Color ObjectWorld::GetColor (const Ray& r, int & depth) { HitInfo info; if (this ->hit (r, 1e-3 , INFINITY, info)) { Ray r_out; Color emit = info.material->emitted (r, info, info.u, info.v, info.position); Color attenuation; double pdf; if (depth < max_depth && info.material->scatter (r, info, attenuation, r_out, pdf)) { Ref<RectXZ> light = New <RectXZ>(213 , 343 , 227 , 332 , 554 , nullptr ); Ref<PDF_hit> pdf_hit = New <PDF_hit>(light, info.position); Ref<PDF_cos> pdf_cos = New <PDF_cos>(info.normal); PDF_mixture pdf_mixture (pdf_hit, pdf_cos, 0.8 ) ; r_out = Ray (info.position, pdf_mixture.generate (), r.Time ()); pdf = pdf_mixture.value (r_out.Direction ()); return emit + attenuation * info.material->scatter_pdf (r, info, r_out) * GetColor (r_out, ++depth) / pdf; } else return emit; } else { return Color (0 ); } }