Materials
Diffuse Reflection
漫反射是投射在粗糙表面上的光线被弥散地向四周反射的现象。为了在光线追踪器中实现漫反射,需要遵循以下两点原则:
1. 光线只会向向外法线指向的半球发生漫反射,即光线不可能穿透物体向另一面反射。
2.部分光照的颜色会被材质表面吸收。
使用如下思路实现漫反射:向指定像素映射到的世界坐标发射光线,并计算对应的颜色;同时,如果这一光线击中了任何物体(而非背景/天空球),则从击中点发射一条随机方向的光线,将这一光线得到的颜色和物体本身的颜色按比例相乘。这个发射光线的过程是递归的,随机方向的光线需要满足和交点的向外法线同向。
可以对方向三个分量分别生成\([-1,1]\)范围内的随机值,以实现生成随机光线的效果。
注意到,如果从用这种方法发射随机光线,向所在单位立方体的八个角发射的概率较高。原理是对于边长为\(1\)的立方体,其内部最大球体的体积是大约是\(0.52\)(\(4/3\pi r^3\)),这意味着球体外的八个角占据了该立方体大约\(48\%\)的体积,每次发射光线有接近一半的概率会发射向立方体的角落。
由于方向向量的分布不均匀发生在角落,而球体中的向量分布是均匀的,可以只选择终点位于球体内部的向量。
为了解决这一问题,一种方案是拒绝法(Rejection Method):生成\([-1,1]\)范围内的随机光线,并进行拒绝法的检查。拒绝法需要进行以下两项检查:
1. 根据球体判定公式\(x^2+y^2+z^2=r^2\),如果该向量三个分量的平方和大于1.0,则该向量不合法,重新随机;
2. 如果向量合法,但通过和向外法线的点积计算,得到两者方向相反,则返回该向量归一化的反向向量;
3. 如果以上两项测试都通过,则返回归一化的向量本身。
为此,需要在vec3.h中添加以下方法辅助进行拒绝检查:
vec3.h中的新增方法
| //生成随机单位向量
inline vec3 random_unit_vector() {
while (true) {
auto p=vec3::random(-1,1);
auto lensq=p.length_squared();
if (lensq>1e-160&&lensq<1.0)return unit_vector(p);
}
}
//生成随机和传入法线向量方向相同的单位向量
inline vec3 random_on_hemishpere(const vec3& normal) {
auto on_unit_vec=random_unit_vector();
if (dot(on_unit_vec,normal)>0) return on_unit_vec; //同向/位于相同半球
else return -on_unit_vec;
}
|
根据光学理论,如果击中材质的光线被全部反射,则称这种材质为白色;如果击中材质的光线被全部吸收,则称这种材质为黑色。
在这里,假定材质吸收了50%的光,反射了50的光。它应该显示为一种灰色。
修改camera类中的ray_color方法以进行漫反射和拒绝检查:
ray_color()
| color ray_color(const ray& r,const hittable& world) {
//检查当前射线是否和实体对象相交
hit_record temp_rec;
if (world.hit(r,interval(0,infinity),temp_rec)) {
//获取随机的漫反射光线
auto direction=random_on_hemishpere(temp_rec.normal);
//返回交点的法线映射的颜色
return 0.5*ray_color(ray(temp_rec.position,direction),world);
}
//如果没有交点,则绘制渐变背景
//将光线转换为单位向量
vec3 unit_direction=unit_vector(r.direction());
//将y坐标的范围从[-1,1]映射到[0,1],便于之后按比例混合颜色
//由于主函数规定的视口高度为2,以视口中心为0,则高度的范围就是【-1,1】
auto a=0.5*(unit_direction.y+1.0);
//返回白色和蓝色混合后的颜色
return (1.0-a)*color(1.0,1.0,1.0)+a*color(0.5,0.7,1.0);
}
|
观察上述方法可以发现,ray_color()仅在击中背景时停止递归。如果当前场景中存在大量物体,则完全有可能发生栈溢出。
因此可以引入一个public的成员max_depth,并修改render()方法中的调用和ray_color()方法:
修改后的render()方法
| void render(std::ostream& out ,const hittable& world) {
init();
//PPM文件头
out<<"P3\n"<<image_width<<' '<<image_height<<'\n'<<"255\n";
//逐像素渲染 i-水平偏移 j-垂直偏移
for (int j=0;j<image_height;j++) {
//输出进度,\r表示光标回到当前行行首
std::clog<<"\r当前进度:"<<(image_height-j)<<' '<<std::flush;
for (int i=0;i<image_width;i++) {
//重采样并计算颜色平均值
color pixel_color=color(0.0,0.0,0.0);
for (int k=0;k<sample;k++) {
ray r=get_ray(i,j);
pixel_color+=ray_color(r,world,max_depth);
}
pixel_color*=pixel_sample_scale;
//将颜色输出到文件
write_color(out,pixel_color);
}
}
std::clog<<"\r结束 \n";
}
|
修改后的ray_color()方法
| color ray_color(const ray& r,const hittable& world,int depth) {
//如果达到最大深度,直接返回黑色
if (depth<=0)return color(0.0,0.0,0.0);
//检查当前射线是否和实体对象相交
hit_record temp_rec;
if (world.hit(r,interval(0.001,infinity),temp_rec)) {
//获取随机的漫反射光线
auto direction=random_on_hemishpere(temp_rec.normal);
//返回交点的法线映射的颜色
return 0.5*ray_color(ray(temp_rec.position,direction),world,depth-1);
}
//如果没有交点,则绘制渐变背景
//将光线转换为单位向量
vec3 unit_direction=unit_vector(r.direction());
//将y坐标的范围从[-1,1]映射到[0,1],便于之后按比例混合颜色
//由于主函数规定的视口高度为2,以视口中心为0,则高度的范围就是【-1,1】
auto a=0.5*(unit_direction.y+1.0);
//返回白色和蓝色混合后的颜色
return (1.0-a)*color(1.0,1.0,1.0)+a*color(0.5,0.7,1.0);
}
|
Lambertian Reflection