Path tracing笔记以及Games101作业7代码相关解读以及实现。笔记包括
- 辐射度量学相关概念
- 渲染方程
- 蒙特卡洛积分
- Path Tracing逻辑
辐射度量学 (Radiometry)
-
辐射通量 (Radiant flux (power)) Φ = d Q d t \Phi=\frac{\mathrm{d}Q}{\mathrm{d}t} Φ=dtdQ Energy per unit time,表示单位时间内光穿过一个截面的光能。
-
辐射强度 (Radiant intensity), I ( ω ) = d Φ d ω I(\omega)=\frac{\mathrm{d}\Phi}{\mathrm{d}\omega} I(ω)=dωdΦ Power per unit solid angle,表示单位立体角上的辐射通量。
-
辐射率 (Radiance), L ( p , ω ) = d 2 Φ ( p , ω ) d ω ⋅ d A cos θ L(\mathrm p,\omega)=\frac{\mathrm d^2\Phi(p,\omega)}{\mathrm d \omega\cdot\mathrm dA\cos\theta} L(p,ω)=dω⋅dAcosθd2Φ(p,ω) Power emitted, reflected, transmitted or received by a surface, per unit solid angle, per projected unit area,表示单位立体角,单位投影面积上的辐射通量。这里多出来一个 cos θ \cos \theta cosθ是因为Lamber’s Cosine Law,即阳光在斜照的时候强度会由于倾角变成原来的 cos θ \cos \theta cosθ倍。
-
辐照度 (Irradiance), E ( x ) = d Φ ( x ) d A E(\mathrm{x})=\frac{\mathrm d\Phi(\mathrm x)}{\mathrm d A} E(x)=dAdΦ(x) Power per unit area incident on a surface point,表示单位面积上的辐射通量。这里有一种记忆方法,在Games104上学到的,将Irradiance拆分成Ir(In)和Radiance记为进入到表面的光照强度。
-
立体角 (Solid Angles)
Bidirectional Reflectance Distribution Function (BRDF)
- 渲染方程 (Rendering Equation)
L o ( p , ω 0 ) = L e ( p , ω 0 ) + ∫ Ω + L i ( p , ω i ) f r ( p , ω i , ω 0 ) ( n ⋅ ω i ) d ω i L_o(p,\omega_0)=L_e(p,\omega_0)+\int_{\Omega+}L_i(p,\omega_i)f_r(p,\omega_i,\omega_0)(n\cdot\omega_i)\mathrm d\omega_i Lo(p,ω0)=Le(p,ω0)+∫Ω+Li(p,ωi)fr(p,ωi,ω0)(n⋅ωi)dωi
其中第一项表示物体本身的发出的光照强度,第二项的积分所包括的内容就是我们的反射函数 (Reflection Function),即
L r ( p , ω r ) = ∫ Ω + L i ( p , ω i ) f r ( p , ω i , ω 0 ) cos θ i d ω i L_r(\mathrm p, \omega_r)=\int_{\Omega+}L_i(p,\omega_i)f_r(p,\omega_i,\omega_0)\cos\theta_i\mathrm d\omega_i Lr(p,ωr)=∫Ω+Li(p,ωi)fr(p,ωi,ω0)cosθidωi
其中第一项为入射方向的Radiance,代表从单位立体角方向入射到单位面积上的光照强度,第二项就是双向反射分布函数,他表示以下比值
f r ( ω i → ω r ) = d L r ( ω r ) d E i ( ω i ) = d L r ( ω r ) L i ( ω i ) cos θ i d ω i [ 1 sr ] f_r(\omega_i\rightarrow\omega_r)=\frac{\mathrm dL_r(\omega_r)}{\mathrm dE_i(\omega_i)}=\frac{\mathrm dL_r(\omega_r)}{L_i(\omega_i)\cos\theta_i\mathrm d\omega_i}~~[\frac{1}{\text{sr}}] fr(ωi→ωr)=dEi(ωi)dLr(ωr)=Li(ωi)cosθidωidLr(ωr) [sr1]
也就是出射光Radiance与入射光Irradiance的比值,也就是代表了在各个方向上多少光照会被反射出去。这里的积分是对入射光的Radiance进行积分,得到的就是入射光在半球面上的Irradiance,然后和BRDF相乘得到的就是反射出的在单位立体角上的Radiance。
蒙特卡洛积分 (Monte Carlo Integration)
- Monte Carlo estimator F N = 1 N ∑ i = 1 N f ( X i ) p ( X i ) F_N=\frac{1}{N}\sum^N_{i=1}\frac{f(X_i)}{p(X_i)} FN=N1∑i=1Np(Xi)f(Xi), Example, Basic (Uniform) Monte Carlo estimator F N = b − a N ∑ i = 1 N f ( X i ) F_N=\frac{b-a}{N}\sum_{i=1}^Nf(X_i) FN=Nb−a∑i=1Nf(Xi)
- Monte Carlo Integration ∫ f ( x ) d x = 1 N ∑ i = 1 N f ( X i ) p ( X i ) \int f(x)\mathrm dx=\frac{1}{N}\sum_{i=1}^N\frac{f(X_i)}{p(X_i)} ∫f(x)dx=N1∑i=1Np(Xi)f(Xi) The more samples, the less variance.
路径追踪 (Path Tracing)
Whitted-style 光线追踪始终采用镜面反射,在光线与diffuse表面相交时停止反射,这一假设会造成一些问题。首先,比如说诸如Glossy (感觉类似磨砂)的效果就无法表现出来。其次,由于不在diffuse表面进行反射,物体之间由于光线反射形成的现象比如说康奈尔Box中的墙面会映射出左右两面墙的红绿两种颜色(如下图)就无法表现出来。
Path tracing使用Rendering Equation进行光照的计算。在计算Rendering Equation中的积分部分时,我们就可以使用Monte Carlo Integration计算。一个简单的Monte Carlo的方法如下图所示 (Direct Illumination),其中的pdf最直观的办法我们可以视为uniform的,也就是 p d f = 1 2 π \mathrm{pdf}=\frac{1}{2\pi} pdf=2π1。
以上是最简单的直接光照的蒙特卡洛积分,然后我们再加上递归的间接光照以及控制递归深度就完成了最终的Path Tracing。为了控制深度,我们使用一种叫做俄罗斯轮盘赌 (Russian Roulette)的方法。非常简单,这种方法就是在每次光线进行反射的时候进行一次随机选择,p的概率继续递归,(1-p)的概率停止,如果继续反射,则我们计算反射的光照强度为 L r ( p , ω r ) p \frac{L_r(p,\omega_r)}{p} pLr(p,ωr),这样我们最后根据数学期望得到的光照强度仍然是 L r ( p , ω r ) L_r(p,\omega_r) Lr(p,ωr)。使用后RR的Path Tracing伪代码如下所示。
最后还有一个问题需要解决,使用以上方法进行采样的效率实际上是不高的,因为很多光线实际上是反射不到光源上的,那样就会造成很多浪费。因此,我们可以使用另外一种采样方式,Sampling the Light,从光源处采样。为了实现光源采样,我们可以重写反射方程,将对入射角度
ω
\omega
ω的积分转化为对光源面积
A
A
A的积分,由于
d
ω
=
d
A
cos
θ
′
∥
x
′
−
x
∥
2
\mathrm d\omega=\frac{\mathrm dA\cos\theta'}{\Vert x'-x\Vert^2}
dω=∥x′−x∥2dAcosθ′ (由立体角的定义得出),因此
L
r
(
p
,
ω
r
)
=
∫
Ω
+
L
i
(
x
,
ω
i
)
f
r
(
x
,
ω
i
,
ω
0
)
cos
θ
i
d
ω
i
=
∫
A
L
i
(
x
,
ω
i
)
f
r
(
p
,
ω
i
,
ω
0
)
cos
θ
i
cos
θ
i
′
∥
x
′
−
x
∥
2
d
ω
i
\begin{align} L_r(\mathrm p,\omega_r)&=\int_{\Omega+}L_i(x,\omega_i)f_r(x,\omega_i,\omega_0)\cos\theta_i\mathrm d\omega_i \\ &=\int_{A}L_i(x,\omega_i)f_r(p,\omega_i,\omega_0)\frac{\cos\theta_i\cos\theta'_i}{\Vert x'-x\Vert^2}\mathrm d\omega_i \end{align}
Lr(p,ωr)=∫Ω+Li(x,ωi)fr(x,ωi,ω0)cosθidωi=∫ALi(x,ωi)fr(p,ωi,ω0)∥x′−x∥2cosθicosθi′dωi
最终版的Path Tracing伪代码如下
作业7
首先,我们需要将上一次作业的几个函数的实现复制粘贴到这里来,包括Triangle::getIntersection()
in Triangle.hpp,IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg)
in Bounds3.hpp以及getIntersection(BVHBuildNode* node, const Ray ray)
in BVH.cpp。代码如下所示
// Triangle.hpp
inline Intersection Triangle::getIntersection(Ray ray)
{
Intersection inter;
if (dotProduct(ray.direction, normal) > 0)
return inter;
double u, v, t_tmp = 0;
Vector3f pvec = crossProduct(ray.direction, e2);
double det = dotProduct(e1, pvec);
if (fabs(det) < EPSILON)
return inter;
double det_inv = 1. / det;
Vector3f tvec = ray.origin - v0;
u = dotProduct(tvec, pvec) * det_inv;
if (u < 0 || u > 1)
return inter;
Vector3f qvec = crossProduct(tvec, e1);
v = dotProduct(ray.direction, qvec) * det_inv;
if (v < 0 || u + v > 1)
return inter;
t_tmp = dotProduct(e2, qvec) * det_inv;
// find ray triangle intersection
if (t_tmp < 0)
return inter;
inter.happened = true;
inter.distance = t_tmp;
inter.coords = ray(t_tmp);
inter.normal = normal;
inter.obj = this;
inter.m = m;
return inter;
}
// Bounds3.hpp
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir,
const std::array<int, 3>& dirIsNeg) const
{
// invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
// dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
// test if ray bound intersects
Vector3f t_min = (this->pMin - ray.origin) * invDir;
Vector3f t_max = (this->pMax - ray.origin) * invDir;
if (!dirIsNeg[0]) std::swap(t_min.x, t_max.x);
if (!dirIsNeg[1]) std::swap(t_min.y, t_max.y);
if (!dirIsNeg[2]) std::swap(t_min.z, t_max.z);
float t_enter = fmax(t_min.x, fmax(t_min.y, t_min.z));
float t_exit = fmin(t_max.x, fmin(t_max.y, t_max.z));
return t_enter <= t_exit && t_exit >= 0;
}
//BVH.cpp
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
Intersection inter;
if (node == nullptr)
return inter;
std::array<int, 3> dirIsNeg = {int(ray.direction.x > 0), int(ray.direction.y > 0), int(ray.direction.z > 0)};
if (!node->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg))
{
return inter; // No intersection in the current node
}
if (node->left == nullptr && node->right == nullptr)
{
if (node->object == nullptr)
throw std::runtime_error("NULL Object Error");
inter = node->object->getIntersection(ray);
return inter;
}
Intersection left_inter = getIntersection(node->left, ray);
Intersection right_inter = getIntersection(node->right, ray);
return left_inter.distance < right_inter.distance ? left_inter : right_inter;
}
完成后确保编译运行都没有问题后,我们再正式开始这次作业的实现,我们只需要对castRay(const Ray ray, int depth)
in Scene.cpp进行实现,代码如下所示
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
Vector3f L_dir = {0, 0, 0}, L_indir = {0, 0, 0};
Intersection intersection = Scene::intersect(ray);
if (!intersection.happened)
return {};
if (intersection.m->hasEmission())
return intersection.m->getEmission();
Intersection light_pos;
float light_pdf = 0.0f;
sampleLight(light_pos, light_pdf);
Vector3f collision_light = light_pos.coords - intersection.coords;
float dis = dotProduct(collision_light, collision_light);
Vector3f collision_light_dir = collision_light.normalized();
Ray light_to_object_ray(intersection.coords, collision_light_dir);
Intersection light_ray_inter = Scene::intersect(light_to_object_ray);
auto f_r = intersection.m -> eval(ray.direction, collision_light_dir, intersection.normal);
// Pay attention to the precision here
if (light_ray_inter.distance - collision_light.norm() > -0.005){
L_dir = light_pos.emit * f_r * dotProduct(collision_light_dir, intersection.normal)
* dotProduct(-collision_light_dir, light_pos.normal) / dis / light_pdf;
}
if (get_random_float() > RussianRoulette)
return L_dir;
Vector3f w0 = intersection.m -> sample(ray.direction, intersection.normal).normalized();
Ray object_to_object_ray(intersection.coords, w0);
Intersection islight = Scene::intersect(object_to_object_ray);
if (islight.happened && !islight.m->hasEmission())
{
float pdf = intersection.m->pdf(ray.direction, w0, intersection.normal);
f_r = intersection.m->eval(ray.direction, w0, intersection.normal);
L_indir = castRay(object_to_object_ray, depth + 1) * f_r * dotProduct(w0, intersection.normal) / pdf / RussianRoulette;
}
return L_dir + L_indir;
}
最终生成的图片,这里为了效果好一点我跑了128的SPP,大概需要跑几个小时,如下图
未完待续
生成一张这样的图片几个小时确实有点太慢了,提高篇要求我们使用多线程来提高渲染的速度,并且采用微表面模型的材质。这也是十分重要的概念,不过还是没有深入理解,准备等到以后学习完Games202之后再回来看看如何实现。