目录
一、创建Ray类,实现背景
从相机到画布的每一个像素,创建一条Ray,每个像素点都有属于自己的Ray,并且根据自己的Ray计算出自身颜色。
可以看到Ray方向的y分量越多,像素越蓝,y分量越少则越白
下面是效果图
二、加入一个球
光线如果和球有交点,后面的背景则会被遮挡,显示球的颜色。求交的方式是:
用A*t+B表示直线上某一点,如果这个点到球心的距离等于球的半径,则有交点,可以列出方程式:
t有解,则有交点。
化简之后,就是一个二次方程,求根公式即可
计算每个像素颜色时(color(ray)),先判断和球是否交,没有交再渲染背景颜色
则结果:
三、让球的颜色和其法线信息相关
球面上的法线=球面上的点坐标-球心坐标
但是有时候会和球面交有两点,因此需要舍去背面的点,才能得到正确的法线。
让球的颜色和法线相关
四、多种形状,多个碰撞体
sphere,hitable_list继承hitable抽象类
之前创建sphere,是直接在Color函数里面,加入了球形的碰撞检测
现在有很多个hitable object,可以在main中创建他们,封装成list,再传给color函数
每个hitable的子类都继承hit方法
bool hit(const ray& r, float t_min, float t_max, hit_record& rec)
t_min
,t_max
指定了t的求解范围,只在直线的一定范围内判断是否有交点。
rec
用于返回碰撞信息:
五、封装相机类
之前创建每个像素对应的光线是:
其中origin为相机投影屏中心,uv为屏幕坐标系下的横纵坐标
可以把光线创建封装到camera类中
在main函数中调用
六、抗锯齿
一个像素内随机取ns个点,ns个点颜色的平均值,作为该点颜色。
这样边缘像素的颜色,有一部分是前景的,有一部分是背景的。看起来更加自然。
七、漫发射
光线在一个碰撞点发生碰撞,部分吸收,部分反射,并且反射的方向是随机的。
反射出的光线可以用于二次颜色计算(递归调用color)
在与碰撞点P相切的圆中随机取一个点S。PS即为一条新的光线。
随机取点的方法:
先在随机生成[-1,1],范围内的点,再保留在球内部的即可。
得到的坐标是球心为(0,0,0)的坐标,S的世界坐标还需加上球心的世界坐标(P+n)
现在把法线相关的颜色信息替换成漫反射:
不论递归多少次,最终终止时,返回的颜色都是背景的蓝色。再层层乘0.5,表示每次反射都有50%的光线被吸收。
光线被层层吸收后,最后上图的漫反射看上去很暗,进行gama校正即可,gama校正本质是一种图像幂率变换
γ<1图像将变亮
γ>1变暗
这里不妨取1/2:
回顾之前的hit函数:
hit(const ray& r, const float t_min, const float t_max, hit_record& rec)
我们指定了t_ min,t_max限制t的范围。
理论上从相机发出的光线,不接受t<0的交点,因为交点位于视线后方,但由于浮点数的缘故,计算的t并不是完全的精确,所以t_min取0.001更加保险。
原文描述为:This gets rid of the shadow acne problem. (暂时不是很理解为什么)
八、抽象出材料类(编写metal类)
将求反射光线的部分放到了材质类的Scatter(),输入入射光线,碰撞信息,衰减强度,反射光线用scattered返回:
attenuation 代表了衰减强弱,之前的案例是50%,这里是vec3,和color的每个分量对应相乘即可。
attenuation是物体表面材质信息,如果绿色,红色吸收的多(r,g分量数值较小),那么该物体就会呈现蓝色。如果三个分量数值相等,则会呈现灰色。
下面的(0.8,0.6,0.2)则呈现黄色。
我们将之前写的diffuse材料抽象到Lambertian类中,作为material的子类。
对于铁而言,并不像漫反射一样,反射的光方向随机散开。如果将铁视为纯反射,则满足光的反射定律:
反射光的方向计算如下:
因此铁的class为:
其中return的值,测试反射光的方向,和法向量是否成锐角(成钝角就指向表面内部了,显然不合理)
返回为false则不会参与颜色计算。
scattered光线衰减后,再进入二次计算,depth记录了递归的深度,递归次数小于50次。
color和main更新如下
但铁并不是完全的镜面反射,反射方向可以有部分的偏移。
fuzzier是上图球的半径大小
把左边改成
list[3] = new sphere(vec3(-1, 0, -1), 0.5, new metal(vec3(0.3, 0.3, 0.3),0.3));
结果还是不够真实。
铁的高光反射范围应该更狭小,高光强度应该更高,其他物体映射在表面的投影不应该如此明显,需要如何修改呢?
九、介质材料(折射光)
这里的“介质”是指光可以通过的物质。比如,水,玻璃等。也就是我们常说的具有一定透明度的物质。
入射透明材料的光线,部分折射,部分反射,折射满足Snell’s law
满足下面条件时,发生全反射(无折射,Snell’s law无解):
- 光从光密介质入射光疏介质;
- 入射角大于等于临界角。
求解折射光线的函数:
以上函数推导过程详见:
https://blog.csdn.net/Pabebe/article/details/83308190
这里的scatter只选取折射或者反射的一种
如果不发生全反射,则只产生反射光,否则只产生折射光。
考虑到可能是入射透明物体,也可能是出射透明物体,故需要用入射方向与法线方向的夹角进行判断,调节法线方向和折射比率。
此处全部发生折射:
可以发现透过介质球看到的像的位置和球背后景的位置是上下颠倒的,原因如图:
https://blog.csdn.net/libing_zeng/article/details/54428732
在右侧的介质球完全看不到蓝球的反射,为什么呢?
我们目前所有的散射光(折射、反射)都是由scatter函数计算的
而scattered函数的返回,只有一条光线scattered,意味着反射,折射只能取其中一条,这里似乎全部选择了折射。
来看看refract函数:
选择反射的条件是,Snell’s law无解,即发生全反射的条件,我们可以推导该条件:
https://blog.csdn.net/Pabebe/article/details/83308190
顺着此链接的推导步骤,发生反射的条件是,根号下的式子小于0
易得,要满足根号下小于0的条件,首先n2/n1需要小于1,因为sinα1<1,即需从光密介质入射光疏介质
,其次α1需要大于arcsin(n2/n1),和全反射的发生条件一致。
由于从空气入射介质,往往是从光疏介质入射光密介质,因此不满足全反射条件,故此处全为折射。
但在真实世界里,折射和反射往往是并存的,只是这里由于模型的局限性无法实现。
下面再画一个位置形状材质都相同,但|半径|略小,且半径为负值的介质球。半径为负值,意味着外表面的填充,即表面法线方向指向球心。
这样我们就有了一个空心介质球。相当于再画一个,让倒着的成像正过来。
十、可指定位置的相机
目前我们在main中创建相机,仅用:
camera cam;
相机的属性,已经提前写好在相机类中了
class camera {
public:
vec3 origin;
vec3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
camera()
{
lower_left_corner = vec3(-2.0, -1.0, -1.0);
horizontal = vec3(4.0, 0.0, 0.0);
vertical = vec3(0.0, 2.0, 0.0);
origin = vec3(0.0, 0.0, 0.0);
}
ray get_ray(float s, float t) {
return ray(origin, lower_left_corner + s * horizontal + t * vertical - origin);
}
};
如果我们希望指定这些属性,则需要更多的工作。
这里选择输入vfov和aspect来计算horizontal,vertical,vfov是纵向张角,aspect是横纵比。
下面的构造函数中给出了计算方法
绘制结果如图
上面的代码让我们确定了horizontal和vertical,但尚未确定相机看向的方向。
确定相机方向,需有lookfrom
,lookat
,但如果只确定lookfrom,lookat,相机可以绕着lookfrom,lookat的轴,进行旋转,得到的视野仍不是确定的,因而还需要第三个分量vup
,指定相机的上方。
输入lookfrom
,lookat
,vup
,vfov
,aspect
就能完全确定一个相机啦
十一、景深
在现实的相机中,为了增加图片的亮度,我们需要更大的光圈而不是现有模型中的理想小孔,而大光圈会造成图片散焦,进而模糊。
但是我们可以调节呈像的位置,以改变图片的清晰度。
在之前,呈像的位置是-w平面,引入景深后,呈像位置记为-focus_dist
平面。由相似三角形知,图片的大小也被整体放大focus_dist,因此有:
同时由于小孔成像,变为光圈成像,光线的起点和终点会在光圈半径的范围内偏移,故有: