材质决定光线和物体如何作用
一个物体的材质,可以分成两部分来看,因为物体没有绝对光滑和绝对粗糙
漫反射:由于物体粗糙,对于微小平面,光线会向四周反射,光源的一部分光线传回人眼
镜面反射:假设物体绝对光滑,反射方向都是一致的,因此当特定角度观察,光线很大一部分传到人眼
物体越粗糙,漫反射材质部分占比越大
1·Lambertian漫反射材质
为了模拟漫反射材质,我们生成随机反射方向,在单位hemisphere半球中生成随机向量
inline vec3 random_unit_vector() {/* 生成一个随机的终点在单位球体内的向量 */
while (true) {
auto p = vec3::random(-1,1);
auto lensq = p.length_squared();/* 计算长度的平方通常比计算实际长度要快,sqrt函数比较昂贵,所以我们仅在if成立再执行sqrt */
if (1e-160 < lensq && lensq <= 1)/* 1e-160是一个非常小的数,接近于零但不等于零 */
return p / sqrt(lensq);/* 返回单位向量 */
}
}
inline vec3 random_on_hemisphere(const vec3& normal) {/* 生成一个随机的终点在单位半球体内的向量 */
vec3 on_unit_sphere = random_unit_vector();
/* 如果法线为正面不用反转,否则反转到正半球内 */
if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal
return on_unit_sphere;
else
return -on_unit_sphere;
}
反射光线为交点和随机半球方向
ray(rec.p, random_on_hemisphere(rec.normal))
为了防止自相交,t==0.000……1的情况 ,我们将深度测试的范围控制在t>0.001
结果:
2`Lambertian朗伯漫反射
真实世界的反射光线最有可能在接近表面法线的方向上散射,而不太可能 在远离法线的方向上散布,因此我们可以
这是沿着法线形成的单位球体随机向量
ray(rec.p, rec.normal + random_unit_vector())
可以看到阴影更加明显:
3·伽马校正(gamma corrected)
我们看到的球体比较暗,这是因为屏幕CRT2.2会自动校正颜色,因此我们应该进行gamma校正(1/2.2次幂),让我们看到实际颜色值
这里简单的应用x^1/2次幂,也就是 sqrt(x),应用在write_color()最终的输出颜色计算中
inline double linear_to_gamma(double linear_component)
{
if (linear_component > 0)
return std::sqrt(linear_component);
return 0;
}
这回才是真正的颜色
4·金属材质
我们要添加其他材质,材质部分又变的繁杂了,让我们把材质类抽象出来
虚函数scatte()根据入射光,交点信息,返回应该返回的颜色和散射向量scattered
创建一个新的lambertian和metal材质继承material材质基类,
metal散射光线遵守反射定律:
首先点乘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(- 入射向量 * 法线)
这两个投影向量的和就是折射向量,
创建一个新的dielectric绝缘体材质,ref_idx是入射介质折射率,如果法线是正面的,那么eta / eta prime = 1 / ref_idx(空气折射率为1),否则法线反面,代表光线从物体内部折射出去,也就是ref_idx / 1 = ref_idx
inline vec3 refract(const vec3 &uv, const vec3 &n, double etai_over_etat)
{ /* 折射向量 */
auto cos_theta = std::fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta * n);
vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}
让我们的折射率设置为 1.00 / 1.33,渲染比较奇怪的
全内反射:
所有的光线都不发生折射, 仅发生反射,它经常在实心物体的内部发生
玻璃的折射率约为 1.5-1.7,金刚石约为 2.4,空气的折射率为 小折射率为 1.000293。
当eta > eta prime时,比如从玻璃1.3进入空气,如果eta / eta prime * sintheta >1,也就是sintheta primer>1这根本不可解,我们应该在不会发生折射时进行反射
Schlick:
现实世界中的玻璃, 发生折射的概率会随着入射角而改变,从一个很狭窄的角度去看玻璃窗, 它会变成一面镜子
有个数学上近似的等式, 它是由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·摄像机
我想要轻松调整摄像机视野,位置和旋转
视野:高度视角/2,y/x * x = y
auto theta = degrees_to_radians(vfov);
auto h = std::tan(theta/2);
auto viewport_height = 2 * h * focal_length;
为了调整摄像机位置和旋转,我们需要额外的lookform,lookat,up3个向量
void camera::initialize()
{
/* 图像高度分辨率 */
image_height = int(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
/* 采样点的间隔 */
pixel_samples_scale = 1.0 / samples_per_pixel;
/* 摄像机位置 */
center = lookfrom;
/* 视角,高度,宽度 */
// Determine viewport dimensions.
auto theta = degrees_to_radians(vfov);
auto h = std::tan(theta / 2);
auto viewport_height = 2 * h * focus_dist; /* 根据tan(y/x) * x求得屏幕高度,focus_dist焦点距离越大,屏幕越小 */
auto viewport_width = viewport_height * (double(image_width) / image_height); /* 根据屏幕宽高比求得宽度 */
/* 摄像机的坐标轴 */
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
w = unit_vector(lookfrom - lookat); /* 加入看向原点,注意方向,从原点指向lookfrom */
u = unit_vector(cross(vup, w)); /* 只要它们不是共线的,叉乘都会得到一个与它们都垂直的新向量。*/
v = cross(w, u);
// Calculate the vectors across the horizontal and down the vertical viewport edges.计算横向和纵向视口边缘的矢量。
vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge
vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge/* 这里v向量取反,如果原v指向上,那么新v指向下 */
// Calculate the horizontal and vertical delta vectors from pixel to pixel.计算从像素到像素的水平和垂直增量向量。
pixel_delta_u = (viewport_width * u) / image_width; /* 如果viewport_u小则像素增量《1,否则》1 */
pixel_delta_v = (viewport_height * -v) / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left = center - (focus_dist * w) - (viewport_width * u) / 2 - (viewport_height * -v) / 2; /* 左上角:屏幕中心左上: */
pixel00_loc = viewport_upper_left + 0.5 * (static_cast<vec3>(pixel_delta_u) + static_cast<vec3>(pixel_delta_v)); /* 左上第一个像素坐标向量 */
// Calculate the camera defocus disk basis vectors.计算相机……。
double defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2));
defocus_disk_u = u * defocus_radius;
defocus_disk_v = v * defocus_radius;
}
不同fov大小
7·depth of field景深 / 散焦模糊(defocus blur)
我们原来相机是针孔相机,无法模拟景深,因此不再从lookfrom发射光线,而是指定aperture孔径大小,模拟薄片透镜相机来实现景深效果
我们只要从一个虚拟的透镜范围中发射光线到我们的摄像机平面(成像平面)就能近似模拟了,这个透镜与平面的距离成为焦距(focus_dist)
我们通过从随机球面生成lookfrom光线起点方向来模拟透镜
vec3 random_in_unit_disk() {
while (true) {
auto p = vec3(random_double(-1,1), random_double(-1,1), 0);
if (p.length_squared() >= 1) continue;
return p;
}
}
我们定义一些属性:aperture光圈/透镜直径,lens_radius光圈半径,focus_dist焦距
首先生成单位球内随机点,然后*radius变为光圈大小,我们的偏移后的向量(透镜)对齐UV成像平面的方向
vec3 rd = lens_radius * random_in_unit_disk();
vec3 offset = u * rd.x() + v * rd.y();
然后从一定的(光圈范围)偏移位置发射光线,
return ray(
origin + offset,lower_left_corner + s*horizontal + t*vertical - origin - offset
);
结果:
8·渲染大量球体,应用不同材质
利用之前所有的框架,渲染多个球体,使用不同的材质,设置球体大小和位置