在漫射材料章节,我们将多个球都模拟成漫射材料的颜色。那么问题来了,我们能不能将不同的球模拟成不同材料的颜色呢?可以哈!我们这一章节就干这事。
21.1总结一下设置颜色的几种方法
我们还是先回忆一下:ray tracing画图时,我们用过的设置颜色的方法:
1,第一张图(背景):
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white,light blue
将“(原始)光线的方向向量的标准向量的某一坐标(x、y、z都可以)”变换到(0,1)内,然后用变换后的值对两个固定颜色值进行插值,得到的结果便作为当前(原始)光线对应位置的颜色值。
2,第二张图(背景+一个球)
if(hit_sphere(vec3(0,0,-1), 0.5, r))
return vec3(1, 0, 0);
vec3 unit_direction =unit_vector(r.direction());
float t =0.5*(unit_direction.y() + 1.0);
return(1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white, light blue
背景颜色和1中是一样的画法;
球的颜色直接设置为红色。
3,第三张图(背景+一个球)
float t =hit_sphere(vec3(0,0,-1), 0.5, r);
if (t > 0.0){
vec3 N =unit_vector(r.point_at_parameter(t) - vec3(0,0,-1));
return 0.5*vec3(N.x()+1, N.y()+1, N.z()+1);
}
vec3 unit_direction =unit_vector(r.direction());
t =0.5*(unit_direction.y() + 1.0);
return(1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white, light blue
背景颜色和1中是一样的画法;
球的颜色为光线和球交点处的的单位法向量的色彩表映射值。
4,第四张图(背景+两个球)
if (world->hit(r,0.0, (numeric_limits<float>::max)(), rec)) {
return 0.5*vec3(rec.normal.x()+1, rec.normal.y()+1, rec.normal.z()+1);
}
else {
vec3unit_direction = unit_vector(r.direction());
float t =0.5*(unit_direction.y() + 1.0);
return(1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white, light blue
}
背景颜色和1中是一样的画法;
球的颜色为光线和球交点处的的单位法向量的色彩表映射值。(和3中画一个球时一样,只不过此处的交点为光线与所有球的所有交点中最近的那个交点)
5,第五张图(背景+两个漫射材料图)
vec3 color(const ray&r, hitable *world) {
hit_record rec;
if (world->hit(r,0.001, (numeric_limits<float>::max)(), rec)) {
vec3 target =rec.p + rec.normal + random_in_unit_sphere();
return 0.5*color( ray(rec.p, target-rec.p), world);
}
else {
vec3unit_direction = unit_vector(r.direction());
float t =0.5*(unit_direction.y() + 1.0);
return(1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white, light blue
}
}
背景颜色和1中是一样的画法:将“(原始)光线的方向向量的标准向量的某一坐标(x、y、z都可以)”变换到(0,1)内,然后用变换后的值对两个固定颜色值进行插值,得到的结果便作为当前(原始)光线对应位置的颜色值。
球的颜色:(注意啦~注意啦~注意啦~)和背景颜色的画法非常类似,只是不是基于原始光线,而是基于“最后一次反射的反射光线”,另外再乘以每次反射系数的累计乘积。将“最后一次反射的反射光线的方向向量的标准向量的某一坐标(x、y、z都可以)”变换到(0,1)内,然后用变换后的值对两个固定颜色值进行插值,(再乘以每次反射系数的累计乘积)得到的结果便作为当前(原始)光线对应位置的颜色值。物理意义即是:原始光线和漫射材料球交点处呈现的颜色是最后一次反射光线“采集”的背景颜色经过(多次)随机的反射衰减后的颜色。
总结一下设置颜色的几种方法:
法1,设置固定颜色;
法2,原始光线的方向向量的映射;
法3,交点处法向量的映射;
法4,反射光线的方向向量的映射;
21.2 模拟不同材料的颜色
前面章节,我们已经知道漫射材料颜色的设置:return 0.5*color( ray(rec.p, target-rec.p), world);也就是反射光线方向向量的映射值和衰减系数的乘积。
根据不同的反射方式,可将材料分为漫反射材料和镜面反射材料。各种反射材料又有不同的反射衰减系数。所以,我们有必要先定义一个material的抽象类,这个类包含反射光线和衰减系数。
----------------------------------------------material.h------------------------------------------
material.h
#ifndef MATERIAL_H
#define MATERIAL_H
#include "hitable.h"
class material
{
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const = 0;
};
#endif // MATERIAL_H
然后,定义lambertian漫反射类和metal镜面反射类,这两个类继承于material类。
----------------------------------------------lambertian.h------------------------------------------
lambertian.h
#ifndef LAMBERTIAN_H
#define LAMBERTIAN_H
#include <material.h>
class lambertian : public material
{
public:
lambertian(const vec3& a): albedo(a) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const;/*scatter()获取反射光线和衰减系数*/
vec3 albedo;/*保存衰减系数*/
};
#endif // LAMBERTIAN_H
----------------------------------------------lambertian.cpp------------------------------------------
lambertian.cpp
#include "lambertian.h"
vec3 random_in_unit_sphere() {
/*漫射材料章节中有介绍过这个函数。这个函数产生一个“起点在原点,长度小于1,方向随机”的向量,该向量和交点处单位法向量相加就得到交点处反射光线随机的方向向量*/
vec3 p;
do {
p = 2.0*vec3((rand()%(100)/(float)(100)),
(rand()%(100)/(float)(100)),
(rand()%(100)/(float)(100)))
- vec3(1,1,1);
} while (p.squared_length() >= 1.0);
return p;
}
bool lambertian::scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const {
/*这里具体实现lambertian::scatter()。做两件事情:获取漫反射的反射光线;获取材料的衰减系数。 */
vec3 target = rec.p + rec.normal + random_in_unit_sphere();
scattered = ray(rec.p, target-rec.p);
attenuation = albedo;
return true;
}
----------------------------------------------metal.h------------------------------------------
metal.h
#ifndef METAL_H
#define METAL_H
#include <material.h>
class metal : public material
{
public:
metal(const vec3& a) : albedo(a) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const;
vec3 albedo;
};
#endif // METAL_H
----------------------------------------------metal.cpp------------------------------------------
metal.cpp
#include "metal.h"
vec3 reflect(const vec3& v, const vec3& n) {
/*获取镜面反射的反射光线的方向向量。具体计算,后面解释*/
vec3 vp;
return v - 2*vp.dot(v,n)*n;
}
bool metal::scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const {
/*这里具体实现metal::scatter()。做两件事情:获取镜面反射的反射光线;获取材料的衰减系数。 */
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (reflected.dot(scattered.direction(), rec.normal) > 0);
}
镜面反射的反射的方向向量的计算:
接下来需要将“材料属性”添加到sphere类中。因为不同的球被撞击后,由于其材料不一样,反射光线方程不一样,还有就是光线的衰减系数不一样。
需要改动的code如下:
----------------------------------------------hitable.cpp------------------------------------------
hitable.cpp
#ifndef HITABLE_H
#define HITABLE_H
#include "ray.h"
class material;
struct hit_record{
/*在hit_record结构体中添加material类型的成员。所以接下来在用到过hit_record的地方都要做相应的添加。主要是:撞击小球时对其“填数据”。*/
float t;
vec3 p;
vec3 normal;
material*mat_ptr;
// vec3 c;
// float r;
};
----------------------------------------------sphere.cpp------------------------------------------
sphere.cpp
#include "sphere.h"
bool sphere::hit(const ray& r, float t_min, float t_max,hit_record& rec) const {
vec3 oc = r.origin() -center;
float a =oc.dot(r.direction(), r.direction());
float b = 2.0 * oc.dot(oc, r.direction());
float c = oc.dot(oc,oc) - radius*radius;
float discriminant =b*b - 4*a*c;
if (discriminant >0) {
float temp = (-b -sqrt(discriminant)) / (2.0*a);
if (temp <t_max && temp > t_min) {
rec.t = temp;
rec.p =r.point_at_parameter(rec.t);
rec.normal =(rec.p - center) / radius;
rec.mat_ptr = ma;
/*这个“ma”是哪来的呢?ma是初始化sphere对象时保存的材料信息,所以,接下来需要在sphere类定义中添加ma成员和通过构造函数对其赋值*/
return true;
}
temp = (-b +sqrt(discriminant)) / (2.0*a);
if (temp <t_max && temp > t_min) {
rec.t = temp;
rec.p =r.point_at_parameter(rec.t);
rec.normal =(rec.p - center) / radius;
rec.mat_ptr = ma;
return true;
}
}
return false;
}
----------------------------------------------sphere.h------------------------------------------
sphere.h
#ifndef SPHERE_H
#define SPHERE_H
#include "hitable.h"
#include "material.h"
class sphere: public hitable{
public:
sphere() {}
sphere(vec3 cen, floatr, material *m) : center(cen), radius(r), ma(m) {}
/*sphere的构造函数变了,所以之前创建的sphere对象的地方都会报错。都要添加材料属性*/
virtual bool hit(constray& r, float tmin, float tmax, hit_record& rec) const;
vec3 center;
float radius;
material *ma;
};
#endif // SPHERE_H
----------------------------------------------main.cpp------------------------------------------
main.cpp
int main(){
int nx = 200;
int ny = 100;
int ns = 100;
ofstream outfile(".\\results\\metal.txt", ios_base::out);
outfile <<"P3\n" << nx << " " << ny <<"\n255\n";
std::cout <<"P3\n" << nx << " " << ny <<"\n255\n";
hitable *list[2];
list[0] = newsphere(vec3(0,0,-1), 0.5, new lambertian(vec3(1, 1, 1)));
list[1] = newsphere(vec3(0,-100.5,-1), 100, new metal(vec3(1, 1, 1)));
/* new metal(vec3(0.8, 0.8,0.0)):创建一个反射衰减向量为(1, 1,1)的metal镜面材料对象。然后将这个对象的指针通过sphere类的构造函数的形参方式传递给sphere对象。*/
……
截至当前,我们已经将material的信息添加到球体对象上。接下来我们要做的是,怎么根据不同的材料,模拟其颜色。
----------------------------------------------main.cpp------------------------------------------
main.cpp
vec3 color(const ray&r, hitable *world, int depth) {
hit_record rec;
if (world->hit(r,0.001, (numeric_limits<float>::max)(), rec)) {
/*这个“rec”会在sphere::hit ()中带上来被撞击球的材料属性(指向一个材料对象的指针mat_ptr)。根据这个指针可以获取材料对象的成员方法scatter()和成员变量albedo(反射衰减向量)*/
ray scattered;
vec3 attenuation;
if (depth < 50 && rec.mat_ptr->scatter(r, rec,attenuation, scattered)) {
/*获取反射光线向量scattered和反射衰减向量attenuation*/
return attenuation*color(scattered, world, depth+1);
/*反射光线的强度需要乘以反射衰减向量(对应坐标相乘作为新的向量)。然后反射光线就扮演之前“原始光线”的角色。如果再次撞击到小球,就再次反射,直到不再撞击到任何球为止*/
}
else {
returnvec3(0,0,0);
}
}
else {
vec3unit_direction = unit_vector(r.direction());
float t =0.5*(unit_direction.y() + 1.0);
return(1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);//white, light blue
/*注意这里,原始光线和反射光线最后都会跑到这里来。
背景的颜色:原始光线的方向向量的映射
漫反射材料和镜面材料的颜色:最后一次反射光线的方向向量的映射 * 所有反射衰减系数的乘积。漫反射和镜面反射的区别在于,漫反射的每次反射方向是随机的。*/
}
}
乘以衰减系数的效果是什么呢?
考虑到,不管是原始光线还是反射光线的方向向量映射后的颜色都是两个固定颜色的插值(此处为白色和浅蓝色)。
但是,衰减系数是材料的特有属性。一方面,每次反射后的光强度肯定是弱了;另一方面每反射一次都会加入被撞物体的颜色。
我们可以这样理解:像漫反射球,原始光线碰撞漫反射球后经过多次无规则的反射进入环境,漫反射球的颜色就是环境颜色(最后一次反射光线方向向量的映射)和所有被碰撞到的其他物体颜色(衰减向量中隐含该信息)的叠加。
我们发现,镜面反射球上一般会有旁边物体的镜像,正是因为上述原因。但是漫反射球上不会有?因为其反射光线方向不规则嘛,所以成像太模糊了。
疑问?这么说的话,如果我们将漫反射球和镜面反射球的反射衰减系数都设置为(1, 1, 1),是不是就看不到球了呢?不会。为什么?
虽然反射衰减系数的乘积都是1,但是,最后一次反射光线的方向向量和原始光线的方向向量是不一样的(虽然最终颜色都是两个固定颜色的插值),所以是可以看出球的轮廓的。
下图两球的反射衰减系数都设置为(1, 1, 1),左球是漫反射球,右图是镜面反射球。
(左球确实看不太清哈,因为漫反射球的反射方向随机嘛,所以能够比较好地融入环境哈。右球就是一个完全的镜面球。)
书上是设置如下四个球:
list[0] = new sphere(vec3(0,0,-1), 0.5, new lambertian(vec3(0.8, 0.3,0.3)));
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)));
list[3] = new sphere(vec3(-1,0,-1), 0.5, new metal(vec3(0.8, 0.8,0.8)));
运行结果:
(左右两个球是镜面材料球,每个球中都有其他两个球的镜像。注意是镜像,不是透明)
我们四个球的材料属性换一下:
list[0] = new sphere(vec3(0,0,-1), 0.5, new lambertian(vec3(0.8, 0.3,0.3)));
list[1] = new sphere(vec3(0,-100.5,-1), 100, new metal(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)));
list[3] = new sphere(vec3(-1,0,-1), 0.5, new lambertian(vec3(0.8,0.8, 0.8)));
效果是这样的:
另外,镜面反射球的清晰程度是可以调节的,如果觉得镜面反射球上的其他球的镜像太清晰了,可以使它模糊化。
我们可以在反射向量的方向向量上添加一个“起点在原点,长度小于1,方向随机”的向量(相当于添加一个fuzziness parameter,随机向量的产生方式可以参考“问题十九”)。(长度为零,说明没有模糊)
需要添加的code如下:
random_in_unit_sphere()实在lambertian.cpp中实现的。在lambertian.h中声明了,然后如果要调用该函数,只需要#include “lambertian.h”
将两个镜面反射球的模糊系数都设置为0.3前后的对比图: