【光线追踪系列六】反射与金属类特性

本文深入探讨光线追踪技术,实现金属材质的真实感渲染。通过封装材质类material,引入Lambert和Metal材质,利用反射函数模拟真实世界的反射行为。文章详细介绍了如何通过递归在光线散射过程中应用材质属性,实现不同材质的视觉效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本次光线追踪系列从基础重新开始,主要参照 Ray Tracing in One Weekend ,具体实现代码框架见 https://github.com/RayTracing/raytracing.github.io/。本文只是主要精炼光追相关理论,具体实现可参照原文。

本部分主要来实现反射函数及金属特性;

在这里插入图片描述

一、材质抽象类 material

接下来我们会涉及到处理不同的材质,所以我们可以封装一个材质类material。
其中的抽象函数叫scatter(),意思为散射,因为射线起点和方向的改变,不一定是反射,还可能是折射等,所以这种行为统称为散射。函数返回值为bool,表明这次的散射是否成功。此外,该方法要能够产生散射射线scattered,且能反映散射过程中各颜色通道衰减了多少,用attenuation表示。

注意:虽然这个变量叫作attenuation(衰减),但在代码逻辑中,该变量其实是反射量。特此说明。

class material
{
public:
    //r_in为入射光线, scattered为散射光线, attenuation 意思为衰减量,实际为各通道的反射率
    virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const = 0;
};

hit_record里面也要加上一个material的指针,以记录此次命中物体的材质。

//记录命中信息
struct hit_record
{
    float t;     //命中射线的长度
    vec3 p;      //命中终点坐标
    vec3 normal; //命中点的法向量
    material *mat_ptr; //new
};

球体类也要加上material指针:

class sphere : public hittable
{
public:
    vec3 center;
    float radius;
    material *mat_ptr; /* NEW */
    ...
    sphere(vec3 cen, float r, material *m) : center(cen), radius(r), mat_ptr(m){}; //new
    //如果命中了,命中记录保存到rec
    virtual bool hit(const ray &r, float t_min, float t_max, hit_record &rec) const
    {
        ...
        if (discriminant > 0)
        {
            float temp = (-b - sqrt(discriminant)) / a; //小实数根
            if (temp < t_max && temp > t_min)
            {
             	...
                rec.mat_ptr = mat_ptr; /* NEW */
                return true;
            }
            temp = (-b + sqrt(discriminant)) / a; //大实数根
            if (temp < t_max && temp > t_min)
            {
                ...
                rec.mat_ptr = mat_ptr; /* NEW */
                return true;
            }
        }
        return false;
    }
};

这样一来,当射线命中我们的球体表面时,mat_ptr会指向该物体的材质。以便在color()能够获取hit_record中的mat_ptr,对不同材质的散射光线做进一步处理。

1.1 封装Lambert(兰伯特)材质类

上一节我们实现了不光滑的表面产生均匀散射而形成的材质类型,这叫做Lambert材质,把它封装成一种材质,继承自材质类material。

跟上一节的代码相比,新增了一个叫做albedo(反射率)的变量,光线能量的衰减量(即吸收量)应该为1-albedo,但不知道为什么代码中albedo和attenuation划等号了,特此说明。

class lambertian : public material
{
public:
    vec3 albedo; //反射率
    lambertian(const vec3 &a) : albedo(a) {}
    virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const
    {
        vec3 s_world = rec.p + rec.normal + random_in_unit_sphere();
        scattered = ray(rec.p, s_world - rec.p); //scattered为散射光线
        attenuation = albedo;	//注意这是各通道的反射率!
        return true;
    }
};

1.2 更新color()函数

color()新增了一个depth参数,用于限制递归深度,例如限制最多递归50层。此外,直接调用mat_ptr->scatter()来获取当前命中材质的反射率变量attenuation和下一步的散射光线scattered,然后通过反射率变量attenuation和递归color()的颜色值进行向量乘法,从而将下一条射线的采样结果,与本次命中材质上各通道发生的衰减,结合起来。

//发射一条射线,并采样该射线最终输出到屏幕的颜色值值
vec3 color(const ray &r, hittable *world, int depth){
    hit_record rec;
    if (world->hit(r, 0.001, MAXFLOAT, rec)) //射线命中物体
    {
        ray scattered; //散射光线
        vec3 attenuation; //其实是反射率!
        if (depth < 50 && rec.mat_ptr->scatter(r, rec, attenuation, scattered))	//不超过递归深度,且发生了散射
        {
            return attenuation * color(scattered, world, depth + 1);
        }
        else
        {
            return vec3(0, 0, 0);
        }
    }
    else
    {
        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);
    }
}

可能递归理解起来有点绕,但我们这样来理解,每一次执行color()函数,(不超过递归深度,且发生了散射时)会返回:
当前射线命中的材质本身的反射率 * (射线散射后所产生的)新射线所采样到的最终返回值。

例如上一节漫反射材质反射的color()函数:

//发射一条射线,并采样该射线最终输出到屏幕的颜色值值
vec3 color(const ray &r, hittable *world)
{
 	   ...
       vec3 s_world = rec.p + rec.normal + random_in_unit_sphere(); //1.单位球内的一个点s
       return 0.5 * color(ray(rec.p, s_world - rec.p), world);	//2.递归采样
       ...
}

其实是将反射率定义为了vec3(0.5, 0.5, 0.5),然后

return vec3(0.5,0.5,0.5) * color(漫反射材质的反射射线, world);

1.3 更新光追入口函数

void RayTracing()
{
   	list[0] = new sphere(vec3(0, 0, -1), 0.5, new lambertian(vec3(0.5, 0.5, 0.5)));
	list[1] = new sphere(vec3(0, -100.5, -1), 100, new lambertian(vec3(0.5, 0.5, 0.5)));//new
	list[2] = new sphere(vec3(1, 0, -1), 0.3, new lambertian(vec3(0.5, 0.5, 0.5)));    //new
	list[3] = new sphere(vec3(-1, 0, -1), 0.3, new lambertian(vec3(0.5, 0.5, 0.5))); //new
    world = new hittable_list(list, 4);
    ns = 200;   
    camera cam;     

    for (int j = ny - 1; j >= 0; j --)
    {
        for (int i = 0; i < nx; i++)
        {
            ...
            for(int s = 0; s < ns; s++)
            {
                ...
                ray r = cam.get_ray(u, v);
                col += color(r, world, 0); new
            }
            ...
        }
    }
}

尽管得到的效果和上节完全一样,但我们现在可以进一步继承材质类material,实现更多不同材质的散射结果。同时也可以设置不同的反射率通道的值,保留(=1)或削弱(<1)散射光线采样到的各个颜色通道。
在这里插入图片描述

例如,改变上面两个球体的反射率:

	list[0] = new sphere(vec3(0, 0, -1), 0.5, new lambertian(vec3(1.0, 0.7, 0.0)));
	list[1] = new sphere(vec3(0, -100.5, -1), 100, new lambertian(vec3(0.0, 1.0, 1.0)));//new
	list[2] = new sphere(vec3(1, 0, -1), 0.3, new lambertian(vec3(0.8, 0.6, 0.2)));//new
	list[3] = new sphere(vec3(-1, 0, -1), 0.3, new lambertian(vec3(0.8, 0.8, 0.8)));//new

在这里插入图片描述

接下来,实现金属材质类。

二、反射函数

绝对光滑的金属表面,并不会像Lambert材质那样随机散射,而是会像镜子那样,反射出一个关于表面法向量对称的反射向量,如下图所示:
在这里插入图片描述

其中,N为单位法向量,由上图可知(红色箭头对应的)反射向量:
在这里插入图片描述

v在N上面的投影的绝对值,就是B的长度。因为v和N的夹角大于90度,导致v⋅N<0,因此B的长度为−(v⋅N),综上可得反射公式为:
在这里插入图片描述

写成反射函数:

vec3 reflect(const vec3& v, const vec3& n) {
    return v - 2*dot(v,n)*n;
}

因为reflect()返回的是反射光线的向量,也就是反射光线的方向,所以反射光线scattered的代码表达为:

vec3 p = 反射点;
vec3 r = reflect(v,n);	//反射向量
ray scattered = ray(p, r);	//反射光线

目前已经可以实现镜面反射的效果了。此外,我们可以让反射光线稍微偏移一点,来实现金属的哑光效果。具体做法就是在反射光线的t=1的位置,偏移一个小球的量,这个偏移量比单位球小一点:
在这里插入图片描述

如果希望偏移量比单位球小一点,可以设fuzz为一个0到1的系数,然后乘以单位球内的某个点的坐标,得到偏移量x。加上偏移后的反射射线逻辑修改如下:

vec3 p = 反射点;
vec3 r = reflect(v, n);	//反射向量
vec3 x = fuzz * random_in_unit_sphere(); //偏移量
ray scattered = ray(p, r+x); //反射光线

三、金属材质类Metal

class metal : public material
{
public:
    vec3 albedo;
    float fuzz;

    metal(const vec3 &a, float f) : albedo(a)
    {
        fuzz = f < 1 ? f : 1;
    }

    virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const
    {
        vec3 v = unit_vector(r_in.direction());
        vec3 n = rec.normal;
        vec3 p = rec.p;
        vec3 r = reflect(v, n);
        vec3 offset = fuzz * random_in_unit_sphere();
        scattered = ray(p, r + offset);

        attenuation = albedo;
        return (dot(scattered.direction(), rec.normal) > 0);
    }
};

新增两个金属球体,fuzz分别设置为0.3和1.0:

hittable *list[4];
...
list[2] = new sphere(vec3(1, 0, -1), 0.3, new metal(vec3(0.8, 0.6, 0.2), 0.3));    //new
list[3] = new sphere(vec3(-1, 0, -1), 0.3, new metal(vec3(0.8, 0.8, 0.8), 1.0));    //new

在这里插入图片描述

如果将fuzz直接设置为0,则表示完全反射:

...
list[2] = new sphere(vec3(1, 0, -1), 0.3, new metal(vec3(0.8, 0.6, 0.2), 0.0));    //new
list[3] = new sphere(vec3(-1, 0, -1), 0.3, new metal(vec3(0.8, 0.8, 0.8), 0.0));    //new

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值