1·Lambertian漫反射材质
一个物体的材质,可以分成两部分来看,因为物体没有绝对光滑和绝对粗糙
漫反射:由于物体粗糙,那么对于微小平面,光线会向四周反射,光源的一部分光线传回人眼
镜面反射:假设物体绝对光滑,那么反射方向都是一致的,因此当特定角度观察,光线很大一部分传到人眼
物体越粗糙,漫反射材质部分占比越大
步骤:
为了模拟漫反射材质,我们生成随机反射方向,这个方向根据,交点的法线终点坐标为中心的单位球体内,生成随机点,求得反射光线(随机点 - 交点)
如何在单位球体生成随机点?
首先根据在一个-1---1的单位立方体内,生成随机点,如果这个点在球外(向量长度>1,超出了单位球的半径)就重新生成直到该点在球内:
然后 ,交点法线 + 随机点向量 = 反射方向
vec3 random_in_unit_sphere() {
while (true) {
auto p = vec3::random(-1,1);
if (p.length_squared() >= 1) continue;
return p;
}
}
递归求光线颜色
- 如果有交点,生成随机反射光线,递归 * 0.5,因此次数越多会越暗
- 如果递归超过50次,那么返回0,防止一直有有交点不断反射的情况
- 如果没有交点,返回背景颜色
结果:
2·伽马校正(gamma corrected)
我们看到的球体比较暗,这其实和计算的数据不符合,因为屏幕CRT2.2会自动校正颜色,因此我们应该进行gamma校正(1/2.2次幂),让我们看到实际颜色值
这里简单的应用x^1/2次幂,也就是 sqrt(x)
3`优化
为了防止自相交,t==0.000……1的情况 ,我们将t的范围控制在>0.001
我们生成的随机点是单位球体积内, 这样生成的向量大概率上会和法线方向相近, 并且极小概率会沿着入射方向反射回去。
但是真正的反射分布率会更加均衡。这是因为我们选取的是单位球面上的点。我们可以通过在单位球内选取一个随机点, 然后将其单位化来获得该点。
vec3 random_unit_vector() {
auto a = random_double(0, 2*pi);
auto z = random_double(-1, 1);
auto r = sqrt(1 - z*z);
return vec3(r*cos(a), r*sin(a), z);
}
兰伯特球体:
还有一种方式,选取随机反射方向:通过在交点取单位球体的随机方向,然后再和物体的normal点乘,判断是否在物体表面的半球
4·金属材质
我们将材质类抽象出来,虚函数scatter散射函数,它会返回反射率(albedo),和散射光线
Lambertian材质继承material材质基类,散射光线是随机生成的
另外创建一个新的metal材质继承material材质基类,散射光线遵守反射定律,通过:
首先点乘vn,因为n是单位向量,点乘表示向量v在这个单位向量方向n上的投影长度,||v||costheta(三角函数)
因为vn方向相反,因此点乘cos结果为负数,即在n的反向方向的投影,因此需要加符号
反射向量 = v + 2(v在n 的投影长度)
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}
可以让金属模糊,方法是以反射向量终点为单位球心,生成随机点,用这个点作为最终的反射方向
通过fuzz变量控制模糊程度,随机向量 * fuzz,fuzz越大随机球的半径越大
可以看到越来越接近漫反射材质,左边0.3,右边1.0
5·绝缘体材质
透明的材料, 例如水, 玻璃, 和钻石都是绝缘体。当光线击中这类材料时, 一条光线会分成两条, 一条发生反射, 一条发生折射。我们假设它每次要么是反射,要么是折射
斯内尔定律/折射定律:eta和eta prime是折射率,theta和theta prime是入射光线与折射光线距离法相的夹角
我们要表示折射向量,首先将折射向量Rprimer分解为,两个投影方向的加法,下面是一个经过推导后的结论,其中costheta(- 入射向量 * 法线)
根据这个结论,写出refract折射函数,返回折射向量,
创建一个新的dielectric绝缘体材质,ref_idx是入射介质折射率,如果法线是正面的,那么eta / eta prime = 1 / ref_idx(空气折射率为1),否则法线反面,代表光线从物体内部折射出去,也就是ref_idx / 1 = ref_idx
设置散射光线为折射光线,但是这是会出现黑点,这是由于eta > eta prime导致的,比如从玻璃1.3进入空气,如果eta / eta prime * sintheta >1,也就是sintheta primer>1这根本不可能求解,
因此无法计算折射光线的结果,我们应该在不会发生折射时进行反射
全内反射:所有的光线都不发生折射, 转而发生了反射,它经常在实心物体的内部发生
double cos_theta = ffmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
if (etai_over_etat * sin_theta > 1.0)
{
vec3 reflected = reflect(unit_direction, rec.normal);
scattered = ray(rec.p, reflected);
return true;
}
优化:
最左侧是玻璃材质,这和现实世界的玻璃还不同,现实世界中的玻璃, 发生折射的概率会随着入射角而改变,从一个很狭窄的角度去看玻璃窗, 它会变成一面镜子
有个数学上近似的等式, 它是由Christophe Schlick提出的(几何函数:微平面间相互遮蔽的比率):
其中如果光线和物体法线,以一定角度观察,也会执行反射
double schlick(double cosine, double ref_idx) {
auto r0 = (1-ref_idx) / (1+ref_idx);
r0 = r0*r0;
return r0 + (1-r0)*pow((1 - cosine),5);
}
double reflect_prob = schlick(cos_theta, etai_over_etat);
if (random_double() < reflect_prob)
{
vec3 reflected = reflect(unit_direction, rec.normal);
scattered = ray(rec.p, reflected);
return true;
}
对于想要渲染通透的玻璃,需要两个球体,把一个小球套在大球里,半径设置为负数
6·摄像机
优化我们的摄像机类,使得在创建相机时,可以轻松调整高度方向的视角fov theta,并且指定aspect 屏幕宽高比,和调整摄像机位置和旋转
那么屏幕(摄像机)一半高度可以由这两个参数表示 == half_height(==tan(fov theta / 2) == h / z,假设-z为1时省略,z越大,h越大 ),一半宽度 == aspect * half_height
调整摄像机位置和旋转,为了描述位置,我们建立lookfrom摄像机的位置,lookat看向的点,为了描述旋转,建立正上方向向量vup,可以绕着lookfrom--lookat轴旋转
class camera
{
public:
camera(
vec3 lookfrom, vec3 lookat, vec3 vup, /* 摄像机位置,屏幕的中心,上向量 */
double vfov, /* 高度视角 */
double aspect) /* 宽高比 */
{
origin = lookfrom; /* 摄像机原点 */
vec3 u, v, w;
/* 根据fov视角,计算宽高的长度 */
auto theta = degrees_to_radians(vfov);
auto half_height = tan(theta / 2);
auto half_width = aspect * half_height;
w = unit_vector(lookfrom - lookat); /* lookfrom-lookat */
u = unit_vector(cross(vup, w)); /* 屏幕x坐标方向 */
v = cross(w, u); /* 屏幕y坐标方向 */
/*
摄像机起点到屏幕平面形成锥形区域,我们计算左下点向量,实际是从摄像机向屏幕左下的向量
左下角:屏幕一半的宽高分别*uv向量,有了两个从lookfrom出发的向量
origin - 宽向量,向量取反, -高向量,获得从uv平面lookfrom指向左下的向量,
-w获得从lookfrom指向以lookat为中心屏幕的左下角
*/
lower_left_corner = origin - (half_width * u) - (half_height * v) - w; /* */
horizontal = 2 * half_width * u; /* 水平和垂直向量 */
vertical = 2 * half_height * v;
}
ray get_ray(double s, double t)
{
/*
向量位置无关性,那么为什么摄像机的origin的位置会影响渲染结果?
因为ray的创建指定了origin,在世界空间从origin位置发射光线,求这个光线的交点
*/
/*
左下向量 + s向量 + t向量 获得从origin为起点,指向新的像素坐标
*/
return ray(origin, lower_left_corner + (s * horizontal) + (t * vertical) - origin);
}
public:
vec3 origin; /* 原点 */
vec3 lower_left_corner; /* 左下 */
vec3 horizontal; /* 宽度 */
vec3 vertical; /* 高度 */
};
不同fov大小
7·散焦模糊
不在从lookfrom发射光线,而是指定aperture孔径大小,随机球面生成lookfrom光线起点方向
focus_dist交点距离
8·渲染大场景
利用之前所有的框架,渲染多个球体,使用不同的材质,设置球体大小和位置