【Ray Tracing in One Weekend 超详解】 光线追踪1-7 Dielectric 半径为负,实心球体镂空技巧

今天讲这本书最后一种材质

Preface

水,玻璃和钻石等透明材料是电介质。当光线照射它们时,它会分裂成反射光线和折射(透射)光线。

处理方案:在反射或折射之间随机选择并且每次交互仅产生一条散射光线

(实施方法:随机取样,具体见后文)

调试最困难的部分是折射光线。如果有折射光线的话,我通常首先让所有的光折射。对于这个项目,我试图在我们的场景中放置两个玻璃球,我得到了这个:

   

上述图片是对的吗?显然,在实际生活中,那两个玻璃球看起来怪怪的,实际情况下,里面的内容应该将现在的进行上下颠倒,且没有黑色成分。

Ready

定量计算光的折射

-------------------------------------------- 数学分割线 --------------------------------------------

公式中的η为相对折射率:n2/n1

而由于入射光线方向的随机性和eta的不同,可能导致 1-η*η*(1-cosθ1 * cosθ1)小于0,此时取根号毫无意义

而事实上,这也就是全反射现象。即:当光线从光密介质进入光疏介质中如果入射角大于某个临界值的时候,就会发生全反射现象。

该临界角即折射角为90°时对应的入射角,也就是cosθ2恰好等于0的时候

------------------------------------------------ END ------------------------------------------------

正文

我们来封装一个电介质类

首先明确,它是材质的一种,即

#ifndef DIELECTRIC_H
#define DIELECTRIC_H

namespace rt
{
    class dielectric :public material
    {
    public:
        dielectric(rtvar RI) :_RI(RI) {  }

        virtual bool scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const;

        inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const;

    private:
        rtvar _RI;    //refractive indices
    };

    bool dielectric::scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const
    {
        rtvec outward_normal;
        rtvec refracted;
        rtvar eta;
        attenuation = rtvec(1., 1., 1.);
        if (dot(rIn.direction(), info._n) > 0)
        {
            outward_normal = -info._n;
            eta = _RI;
        }
        else
        {
            outward_normal = info._n;
            eta = 1. / _RI;
        }
        if (refract(rIn.direction(), outward_normal, eta, refracted))
        {
            scattered = ray(info._p, refracted);
            return true;
        }
        return false;
    }

    inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const
    {
        rtvec unitIn = rIn.ret_unitization();
        rtvar cos1 = dot(-unitIn, n);
        rtvar cos2 = 1. - eta*eta*(1 - cos1*cos2);

        if (cos2 > 0)
        {
            refracted = eta * rIn + n*(eta*cos1 - sqrt(cos2));
            return true;
        }

        return false;        //全反射
    }
}

#endif

dielectric.h

attenuation的值总是1,因为玻璃表面不吸收任何光,即没有rgb强度衰减

我们会很容易想到前言部分中的方法:如果有折射,那么让所有的光线折射,就像上面代码中scatter函数描述的那样,那么就会得到那张图

我们把metal中的reflect函数设置为静态的,或者是命名空间内“全局”函数,这样用起来比较方便,换句话讲,这个公式并不属于任何类,它是3D数学通用公式

main函数球体设置:

上述代码是前言中图像的生成代码

然而,它没有加入全反射,所以导致了黑色成分的出现,所以,我们将全反射加入到上述代码中

#ifndef DIELECTRIC_H
#define DIELECTRIC_H

namespace rt
{
    class dielectric :public material
    {
    public:
        dielectric(const rtvar RI) :_RI(RI) {  }

        virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override;

        inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const
        {
            rtvec unitIn = rIn.ret_unitization();    //将入射光线单位化

            rtvar cos1 = dot(unitIn, n);
            rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1);
            if (cos2 > 0)
            {
                refracted = eta * (rIn - n * cos1) - n * sqrt(cos2);
                return true;
            }
            return false;
        }
    private:
        rtvar _RI;
    };

    bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const
    {
        rtvec outward_normal;
        rtvec reflected = metal::reflect(InRay.direction(), info._n);
        rtvar eta;
        attenuation = rtvec(1., 1., 1.);
        rtvec refracted;

        if (dot(InRay.direction(), info._n) > 0)
        {
            outward_normal = -info._n;
            eta = _RI;
        }
        else
        {
            outward_normal = info._n;
            eta = 1. / _RI;
        }
        if (refract(InRay.direction(), outward_normal, eta, refracted))
        {
            scattered = ray(info._p, refracted);
        }
        else
        {
            scattered = ray(info._p, reflected);
            return false;
        }
        return true;
    }

}

#endif

dielectric.h

会得到如下图:

得到这张图是真的不容易,踩了一天坑

主要是,渲染一张图看下效果基本要7~10分钟,玩不起,放开双手~~

坑点

这里的反射公式有三种形式,但是它们化简之后都是一个式子

我们这里采用的是纸上推出来的,但是用哪个式子,我们都要注意三点:

1.向量的符号!!!

我们知道cos(theta1) = dot(- 入射向量,法线)

    折射向量 = eta * 入射 + 法线*eta*cos(theta1)- 法线 * cos(theta2)

但是,如果你代码中的cos(theta1) = dot(入射,法线)

那么,  折射向量 = eta * 入射 - ....  这里就不是+了

这是公式的符号的问题

2.入射向量的单位化

为什么要单位化呢,这个还是很重要的

因为你传入的入射向量是有长度的,你用你传入的入射向量计算出来的折射向量也是有长度的,显然,折射不会衰减光的强度,也不会平白无故缩短向量

这时候你就要考虑了,你传出的折射向量是要干嘛用的

折射向量是要作为新的视线的方向向量的对吧

而我们都知道,视线有三部分,eye的位置,方向向量,t系数(伸长长度)

还有一点,我们计算景物的画面的时候,计算的是视线延伸后的离眼球最近的点画在屏幕上

如果,你的视线最初的方向向量本身就有好长,你的眼球好大一颗,那么本来离eye点最近的点可能就被这颗偌大的眼球边界包在里面可能不是眼球之外最近的点了

所以,我们的方向向量一定是最短的,即单位1,这样,我们伸长之后,触碰到的第一个点才能保证是离眼球最近的点,如果方向向量过长,可能包在里面的点就被忽略了

第二点不注意就会出现下面这张图

  

左球的景象少了些,可能就是上述原因,视线的方向向量太长了,未经过单位化

3.向量统一

如果你要用入射向量的单位向量,那么,所有涉及入射向量的地方都用入射单位向量代替

如果不用入射单位向量,那么整个代码计算过程中就都不用,不要混用。

例: 下面是书上的折射函数代码

函数体第一行,它把入射光单位化,第二行用 uv 做了点乘,然而后面的五行却用的是 v 而不是uv ,没道理!!第五行的 dt 和discriminant都是用 uv 算出来的,前面突然用个v是什么操作??

我们把v改成uv就可以了

坑点结束

然而,还是存在玻璃内图像颠倒的现象

解释如下:

这里面有一个反射系数的问题,上面我们都考虑的是反射系数为0的情况,实际生活中的玻璃透明介质是有反射系数的。

此时,我们需要引入一个新的概念——反射系数

它是由 Christopher Schlick 提出的:

rtvar schlick(rtvar cosine, rtvar RI)
{
    rtvar r0 = (1-RI)/(1+RI);
    r0 *= r0;
    return r0 + (1-r0)*pow((1-cosine),5);
}

这里面还有一个问题

我们折射的 scatter 函数需要全反射的时候return 的 是false , 意思是 if 只计算折射情况,全反射是按照 rtvec(0,0,0)运算的,压根就没算

所以,我们改一下代码:

#ifndef DIELECTRIC_H
#define DIELECTRIC_H

namespace rt
{
    class dielectric :public material
    {
    public:
        dielectric(const rtvar RI) :_RI(RI) {  }

        virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override;

        inline static bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted);

    protected:
        rtvar _RI;

        rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const;
    };

    bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const
    {
        rtvec outward_normal;
        rtvec refracted;
        rtvec reflected = metal::reflect(InRay.direction(), info._n);
        rtvar eta;
        rtvar reflect_prob;
        rtvar cos;
        attenuation = rtvec(1., 1., 1.);

        if (dot(InRay.direction(), info._n) > 0)
        {
            outward_normal = -info._n;
            eta = _RI;
            cos = _RI*dot(InRay.direction(), info._n) / InRay.direction().normal();
        }
        else
        {
            outward_normal = info._n;
            eta = 1.0 / _RI;
            cos = -dot(InRay.direction(), info._n) / InRay.direction().normal();
        }

        if (refract(InRay.direction(), outward_normal, eta, refracted))
            reflect_prob = schlick(cos, _RI);    //如果有折射,计算反射系数
        else
            reflect_prob = 1.0;        //如果没有折射,那么为全反射

        if (rtrand01() < reflect_prob)
            scattered = ray(info._p, reflected);
        else
            scattered = ray(info._p, refracted);

        return true;
    }

    inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)
    {
        rtvec unitIn = rIn.ret_unitization();    //将入射光线单位化

        rtvar cos1 = dot(-unitIn, n);
        rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1);
        if (cos2 > 0)
        {
            refracted = eta * rIn + n * (eta * cos1 - sqrt(cos2));
            return true;
        }
        return false;
    }

    rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const
    {
        rtvar r0 = (1. - RI) / (1. + RI);
        r0 *= r0;
        return r0 + (1 - r0)*pow((1 - cosine), 5);
    }
}

#endif

dielectric.h

里面涉及到了rtrand01,还记得吗,这个是我们在学漫反射的时候弄的

那么放在这里作什么嘞?

还记得Preface中我们说过的处理方案吗

我们现在就是这么做的,我们得到一个reflect_prob,它介于0~1之间,如果我们取0~1之间的随机数,根据随机数确定选择反射还是折射,这个还是很科学的,为什么呢?因为我们做了100次采样!!,那么我们可以理直气壮的说,我们的透明电介质真正做到了反射和折射的混合(除了全反射现象),而且,前言也说过,光线照射透明电介质时,它会分裂为反射光线和折射光线。

主函数:

#define LOWPRECISION

#include ......
#define stds std::
using namespace rt;

rtvec lerp(const ray& sight, intersect* world, int depth)
{
    hitInfo info;
    if (world->hit(sight, (rtvar)0.001, rtInf(), info))
    {
        ray scattered;
        rtvec attenuation;
        if (depth < 50 && info.materialp->scatter(sight, info, attenuation, scattered))
            return attenuation * lerp(scattered, world, depth + 1);
        else
            return rtvec(0, 0, 0);
    }
    else
    {
        rtvec unit_dir = sight.direction().ret_unitization();
        rtvar t = 0.5*(unit_dir.y() + 1.);
        return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0);
    }
}

void build_9_1()
{
    stds ofstream file("graph9-1.ppm");
    size_t W = 400, H = 200, sample = 100;

    if (file.is_open())
    {
        file << "P3\n" << W << " " << H << "\n255\n" << stds endl;

        size_t sphereCnt = 4;
        intersect** list = new intersect*[sphereCnt];
        list[0] = new sphere(rtvec(0, 0, -1), 0.5, new lambertian(rtvec(0.1, 0.2, 0.5)));
        list[1] = new sphere(rtvec(0, -100.5, -1), 100, new lambertian(rtvec(0.8, 0.8, 0.)));
        list[2] = new sphere(rtvec(-1, 0, -1), 0.5, new dielectric(1.5));
        list[3] = new sphere(rtvec(1, 0, -1), 0.5, new metal(rtvec(0.8, 0.6, 0.2)));
        intersect* world = new intersections(list, sphereCnt);

        camera cma;

        for (int y = H - 1; y >= 0; --y)
            for (int x = 0; x < W; ++x)
            {
                rtvec color;
                for (int cnt = 0; cnt < sample; ++cnt)
                {
                    lvgm::vec2<rtvar> para{
                        (rtrand01() + x) / W,
                        (rtrand01() + y) / H };
                    color += lerp(cma.get_ray(para), world, 0);
                }
                color /= sample;
                color = rtvec(sqrt(color.r()), sqrt(color.g()), sqrt(color.b()));    //gamma 校正
                int r = int(255.99 * color.r());
                int g = int(255.99 * color.g());
                int b = int(255.99 * color.b());
                file << r << " " << g << " " << b << stds endl;
            }
        file.close();

        if (list[0])delete list[0];
        if (list[1])delete list[1];
        if (list[2])delete list[2];
        if (list[3])delete list[3];
        if (list)delete[] list;
        if (world)delete world;

        stds cout << "complished" << stds endl;
    }
    else
        stds cerr << "open file error" << stds endl;
}

int main()
{
    build_9_1();
}

/*********************************************************/

电介质球体的一个有趣且简单的技巧是要注意,如果使用负半径,几何体不受影响但表面法线指向内部,因此它可以用作气泡来制作空心玻璃球体:

我们实验一下书上的负半径:

得到这样的图:

为了能够看懂空心球是个啥玩意儿,我把eta 颠倒了一下

dot小于0,说明入射光线是从表面法线指向的方向空间入射到内部空间,例如:光从空气入射到水中

dot大于0,说明入射光线是从表面法线的反方向空间入射到表面法线指向的空间

这样,我们就可以看到那个内球了

感谢您的阅读,生活愉快~

原文地址:https://www.cnblogs.com/lv-anchoret/p/10217719.html

时间: 2024-10-09 16:36:45

【Ray Tracing in One Weekend 超详解】 光线追踪1-7 Dielectric 半径为负,实心球体镂空技巧的相关文章

【Ray Tracing The Next Week 超详解】 光线追踪2-6 Cornell box

Chapter 6:Rectangles and Lights 今天,我们来学习长方形区域光照  先看效果 light 首先我们需要设计一个发光的材质 /// light.hpp // ----------------------------------------------------- // [author] lv // [begin ] 2019.1 // [brief ] the areaLight-class for the ray-tracing project // from t

【Ray Tracing in One Weekend 超详解】 光线追踪1-10

<Ray Tracing in One Weekend>完结篇 最近课程上机实验,封面图渲染时间也超长,所以写东西就落下了,见谅 这篇之后,我会继续<Ray Tracing The Next Week>,还请多多关注 这几天我在渲染这本书的封面图,封面图还没出,不算结束,刚好安排了10节 今天呢,有两件事: 1.阐述整个工程的文件组织即内容 2.阐述封面,完结 12.1工程文件组织 试过很多方法,问过很多老师,无奈,子类继承实现的父类纯虚函数实在无法和类声明分成两个文件(即声明放于

【Ray Tracing The Next Week 超详解】 光线追踪2-3

 Preface 终于到了激动人心的纹理章节了 然鹅,看了下,并不激动 因为我们之前就接触过 当初有一个 attenuation 吗? 对了,这就是我们的rgb分量过滤器,我们画出的红色.蓝色.绿色等等,都是通过它来控制的 专业点的词语叫做rgb衰减比例,比如rtvec(1.,0.,0.),最后呈现出来的是红色,因为r保留了100% 它是怎么控制的呢,我们来回顾一下这个过程 首先,我们创建一个材质球 后面那个rtvec(0.4,0.2,0.1)就是衰减比例(衰减到原来的百分之..) 之后 进入数

【Ray Tracing The Next Week 超详解】 光线追踪2-7 任意长方体 &amp;&amp; 场景案例

上一篇比较简单,很久才发是因为做了一些好玩的场景,后来发现这一章是专门写场景例子的,所以就安排到了这一篇 Preface 这一篇要介绍的内容有: 1. 自己做的光照例子 2. Cornell box画质问题及优化方案 3. 新的场景几何体——长方体 轴平行长方体 任意长方体 我们这一篇重实践轻理论阐述 ready 1. 需要上一章的知识 但是,上一章的Cornell box画质优化仅限于盒子本身,如果作为场景和其他物体放在一起就不能那么优化画质 即,Cornell box像素计算失败应该返回黑色

POJ 1659 Frogs&#39; Neighborhood(可图性判定—Havel-Hakimi定理)【超详解】

Frogs' Neighborhood Time Limit: 5000MS   Memory Limit: 10000K Total Submissions: 9897   Accepted: 4137   Special Judge Description 未名湖附近共有N个大小湖泊L1, L2, ..., Ln(其中包括未名湖),每个湖泊Li里住着一只青蛙Fi(1 ≤ i ≤ N).如果湖泊Li和Lj之间有水路相连,则青蛙Fi和Fj互称为邻居.现在已知每只青蛙的邻居数目x1, x2, ..

CentOS6启动过程超详解分析

CentOS 6 开机流程--linux由kernel和rootfs组成.kernel负责进程管理.内存管理.网络管理.驱动程序.文件系统.安全等;rootfs由程序和glibc组成,完善操作系统的功能.同时linux内核的特点是模块化,通过对模块装载卸载可以对内核功能自定义.linux内核文件:/boot/vmlinuz-2.6.32-696.el6.x86_64 整体的流程 BIOS/开机自检 MBR引导(Boot Loader) 启动内核 启动第一个进程init 一.BIOS/开机自检 1

【RAY TRACING THE REST OF YOUR LIFE 超详解】 光线追踪 3-5 random direction &amp; ONB

 Preface 往后看了几章,对这本书有了新的理解 上一篇,我们第一次尝试把MC积分运用到了Lambertian材质中,当然,第一次尝试是失败的,作者发现它的渲染效果和现实有些出入,所以结尾处声明要通过实践,改进当前的效果 于是乎,就有了后面的章节,几乎整本书都在讲,如何一步一步地改进上一篇的画质,使其更加符合现实,上一篇其实是抛砖引玉 这本书的小标题名为the rest of your life 通过前面几章,我们可以更好地理解这句话:我们通过MC积分优化效果,采用的是pdf函数,之前说过,

高斯消元法(Gauss Elimination)【超详解&amp;模板】

高斯消元法,是线性代数中的一个算法,可用来求解线性方程组,并可以求出矩阵的秩,以及求出可逆方阵的逆矩阵.高斯消元法的原理是:若用初等行变换将增广矩阵 化为 ,则AX = B与CX = D是同解方程组. 所以我们可以用初等行变换把增广矩阵转换为行阶梯阵,然后回代求出方程的解. 1.线性方程组 1)构造增广矩阵,即系数矩阵A增加上常数向量b(A|b) 2)通过以交换行.某行乘以非负常数和两行相加这三种初等变化将原系统转化为更简单的三角形式(triangular form) 注:这里的初等变化可以通过

海量数据处理算法总结【超详解】

1. Bloom Filter [Bloom Filter]Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合.它是一个判断元素是否存在集合的快速的概率算法.Bloom Filter有可能会出现错误判断,但不会漏掉判断.也就是Bloom Filter判断元素不再集合,那肯定不在.如果判断元素存在集合中,有一定的概率判断错误.因此,Bloom Filter不适合那些“零错误”的应用场合. 而在能容忍低错误率的应用场合