此文写于今年5月份,写了一些感觉会对之后读者有帮助的东西,以及一些笔记。因为要参加秦春林的线下交流会,所以看得比较匆忙。有些错误在所难免,还请见谅。
原作者的代码使用的是浮点类型是float,但实际上c++默认的浮点类型是double(IDE里可以设置默认浮点类型),如果直接抄代码渲染结果会不正确。
解决方案:
- 使用f作为字面量的后缀,例如:1.0f。
- 将所有类型都改成double。
原作者书中的sphere.h代码错误
double temp = (-b - sqrt(b*b-a*c)) / a;
其实这个并不是错误,b变量少乘以了2,这里其实是简化了计算步骤,将2约去了。
详细推导过程
抗锯齿
原作者用的抗锯齿方式是SSAA,没必使用100次采样,64次足以。
提高渲染速度
如果用原作者渲染2000X1000的图,只怕渲染10MIN都没完成。原因是系统只使用了一个核心来渲染。可以采用多线程的库来运用所有核心来渲染。同时使用release模式来渲染速度会更快一些(测试下来结果是快很多)
反射模糊
反射模糊采用了给反射光线添加随机方向(向量)偏移来实现。
折射
最后部分的修改的代码作者没有写全
hitable *list[5];
list[0] = new sphere(vec3(0,0,-1), 0.5, new lambertian(vec3(0.1, 0.2,0.5)));
list[1] = new sphere(vec3(0,-100.5,-1), 100, new lambertian(vec3(0.8,0.8, 0.0)));
list[2] = new sphere(vec3(1,0,-1), 0.5, new metal(vec3(0.8, 0.6, 0.2),0.0));
list[3] = new sphere(vec3(-1,0,-1), 0.5, new dielectric(1.5));
list[4] = new sphere(vec3(-1,0,-1),-0.45, new dielectric(1.5));
hitable *world = new hitable_list(list,5);
推荐去看 https://www.cnblogs.com/night-ride-depart/p/7429618.html
使用M_PI
使用M_PI,除了需要#include
简单的说只要把camera.h包含放到最前位置就可以了(预编译头文件后面)
FOV
double theta = vfor*M_PI / 180;
double half_height = tan(theta / 2);
double half_width = aspect*half_height;
lower_left_corner=vec3(-half_width,-half_height, -1.0);
horizontal = vec3(2*half_width, 0.0, 0.0);
vertical = vec3(0.0, 2*half_height, 0.0);
相机焦距模糊
通过设置焦距以及随机向量,来模拟光圈大小。光圈越大,景深越大。
最终封面图
在pdf中并没有说摄像机参数
vec3 lookfrom(13, 2, 3);
vec3 lookat(0, 0, 0);
double dist_to_focus = 10;
double aperture = 0.1;
camera cam(lookfrom , lookat, vec3(0, 1, 0), 20, nx / ny,aperture,dist_to_focus);
动态模糊
首先在camera类中计算快门时间,同时乘以一个随机值(0~1.0)赋予给ray,以及生成随机射线。
创建一个moving_sphere类,通过time计算出位置(这样就可以模拟去物体运动了,所以不需要配合摄像机快门的时间。)。并且在hit函数中的光线撞击判断中也使用
最终在随机场景函数中加入随机center0~1的moving_sphere来实现动态模糊。
层次包围盒BVH Bounding Volume Hierarchy
使用平行于轴线的平面构建一个包围盒。用于加快射线求交效率。下文较为详细的介绍了3D游戏引擎中常见的三维场景管理方法
3D游戏引擎中常见的三维场景管理方法 - 水煮鱼丸 - 博客园
AABB(axis-aligned bounding boxes,轴对齐包围盒)的碰撞解析:
AABB(axis-aligned bounding box)
注意:作者并没有在代码中使用BVH
hitable_list::bounding_box 有误
if (list[i]->bounding_box(t0, t1, temp_box)) {
box = surrounding_box(box, temp_box);
}
贴图球面映射
$\theta$ 是纵向角(俯仰角)$-\pi/2 \pi/2$,$$\phi$ 是横向角(方位角)$2\pi$。
书中的公式一下子较难理解,这里你需要用投射的方式来思考,因为p(x,y,z)是球上任意一点,所以向量pCenter是一个三维向量,对于x,y你只有,具体的可以参考这个网址的图(他坐标系与书中的不同)。球面UV坐标 - aa20274270的博客 - CSDN博客
然后通过反三角函数反求。
这样我们就可以通过hit点的三维坐标来求贴图UV,在此之前还需要对UV进行区间转换:
//航向角之类的是以球体中心向外为准,而我们观察方向是相反的,所以:
//PI~-PI=>1~0=>0~1
u = 1 - (phi + M_PI) / (2 * M_PI);
//-PI/2~PI/2=>0~1
v = (theta + M_PI / 2) / M_PI;
作者最后将get_sphere_uv函数写到了hitable中了,然后在sphere中使用。
体渲染
//雾计算公式,t为距离
//Transmittance = e ^ (-t * d).
double hit_distance = -(1 density)*log(uni_dist(reng));
作者在这里有个细节没说:那就是当color有一个通道的数值超过1的时候该如何处理。我这里是这么做的:
- 当其中一个通道超过1,进行normalize操作。
- 如果三个通道都超过1,这强制返回vec3(1,1,1)
分层抽样
作者推荐:对于单次反射、阴影、一些2D问题可以使用使用分层方式的蒙特卡洛方法,来增加效率。但是这种方式会因为维度的增加而效率降低。
材质采样
灯光的随机采样会造成许多噪点,这是因为采样不足所造成的,我们可以只采样需要采样的地方,所以我们需要一个概率密度函数。
probability density function =pdf
计算出对应样本的概率,其反函数,通过概率计算出对应的样本。
推荐去看这个蒙特卡洛(Monte Carlo)法求定积分
灯光采样
//这里是因为是与Y轴垂直的平面才能这么求的
double light_cosine = fabs(dot(to_light,rec.normal));
//A为灯光的的面积
//如果我们在面光源上均匀采样,则pdf 为1/A
//立体角:dw = dA cos(alpha) / (distance(p,q)^2)
//因为立体角dw与dA是相同的
//可得:p(direction)*cos(alpha)*dA / (distance(p,q)^2 ) = p_q(q)*dA = dA / A
//pdf=distance(p,q)^2 / ( cos(alpha) *A )
混合pdf
因为需要混合pdf,所以设计一个pdf类,这个类需要做:
- 在对应位置的值
- 返回适当分配的随机数。
看到第三本书时,只推荐敲一下代码。他这里的重要性采样会让英文不好的人相当困惑。其实就是给光线追踪计算出来的值乘以一个权重,这个权重就是当前计算光线与采样平面,贡献辐射照度的比率。这里涉及到辐射度的问题,推荐去看文刀秋二的PBR系列文章。
Ph0en1x:Importance Sampling (重要性采样)
文刀秋二:基于物理着色(一)