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

Preface

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

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

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

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

   

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

Chapter9:Dielectrics 

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(., ., .);
if (dot(rIn.direction(), info._n) > )
{
outward_normal = -info._n;
eta = _RI;
}
else
{
outward_normal = info._n;
eta = . / _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 = . - eta*eta*( - cos1*cos2); if (cos2 > )
{
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 = . - eta*eta*(. - cos1*cos1);
if (cos2 > )
{
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(., ., .);
rtvec refracted; if (dot(InRay.direction(), info._n) > )
{
outward_normal = -info._n;
eta = _RI;
}
else
{
outward_normal = info._n;
eta = . / _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 = (-RI)/(+RI);
r0 *= r0;
return r0 + (-r0)*pow((-cosine),);
}

这里面还有一个问题

我们折射的 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(., ., .); if (dot(InRay.direction(), info._n) > )
{
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 = . - eta*eta*(. - cos1*cos1);
if (cos2 > )
{
refracted = eta * unitIn + n * (eta * cos1 - sqrt(cos2));
return true;
}
return false;
} rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const
{
rtvar r0 = (. - RI) / (. + RI);
r0 *= r0;
return r0 + ( - r0)*pow(( - cosine), );
}
} #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 < && info.materialp->scatter(sight, info, attenuation, scattered))
return attenuation * lerp(scattered, world, depth + );
else
return rtvec(, , );
}
else
{
rtvec unit_dir = sight.direction().ret_unitization();
rtvar t = 0.5*(unit_dir.y() + .);
return (. - t)*rtvec(., ., .) + t*rtvec(0.5, 0.7, 1.0);
}
} void build_9_1()
{
stds ofstream file("graph9-1.ppm");
size_t W = , H = , sample = ; if (file.is_open())
{
file << "P3\n" << W << " " << H << "\n255\n" << stds endl; size_t sphereCnt = ;
intersect** list = new intersect*[sphereCnt];
list[] = new sphere(rtvec(, , -), 0.5, new lambertian(rtvec(0.1, 0.2, 0.5)));
list[] = new sphere(rtvec(, -100.5, -), , new lambertian(rtvec(0.8, 0.8, .)));
list[] = new sphere(rtvec(-, , -), 0.5, new dielectric(1.5));
list[] = new sphere(rtvec(, , -), 0.5, new metal(rtvec(0.8, 0.6, 0.2)));
intersect* world = new intersections(list, sphereCnt); camera cma; for (int y = H - ; y >= ; --y)
for (int x = ; x < W; ++x)
{
rtvec color;
for (int cnt = ; cnt < sample; ++cnt)
{
lvgm::vec2<rtvar> para{
(rtrand01() + x) / W,
(rtrand01() + y) / H };
color += lerp(cma.get_ray(para), world, );
}
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[])delete list[];
if (list[])delete list[];
if (list[])delete list[];
if (list[])delete list[];
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,说明入射光线是从表面法线的反方向空间入射到表面法线指向的空间

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

原著

本章节原书pdf图片,可放大

点此处查看或者翻阅相册内容

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

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

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

    我们来整理一下项目的代码 目录 ----include --hit --texture --material ----RTdef.hpp ----ray.hpp ----camera.hpp ---- ...

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

    Chapter 6:Rectangles and Lights 今天,我们来学习长方形区域光照  先看效果 light 首先我们需要设计一个发光的材质 /// light.hpp // ------- ...

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

    我们上一篇写了Chapter5 的第一个部分表面法线,那么我们来学剩下的部分,以及Chapter6. Chapter5:Surface normals and multiple objects. 我们 ...

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

    上一篇比较简单,很久才发是因为做了一些好玩的场景,后来发现这一章是专门写场景例子的,所以就安排到了这一篇 Preface 这一篇要介绍的内容有: 1. 自己做的光照例子 2. Cornell box画 ...

  5. 【Ray Tracing The Next Week 超详解】 光线追踪2-8 Volume

     Preface 今天有两个东东,一个是体积烟雾,一个是封面图 下一篇我们总结项目代码 Chapter 8:Volumes 我们需要为我们的光线追踪器添加新的物体——烟.雾,也称为participat ...

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

    Chapter 5:Image Texture Mapping 先看效果: 我们之前的纹理是利用的是撞击点p处的位置信息,比如大理石纹理 而我们今天的图片映射纹理采用2D(u,v)纹理坐标来进行. 在 ...

  7. 【Ray Tracing in One Weekend 超详解】 光线追踪1-8 自定义相机设计

    今天,我们来学习如何设计自定义位置的相机 ready 我们只需要了解我们之前的坐标体系,或者说是相机位置 先看效果   Chapter10:Positionable camera 这一章我们直接用概念 ...

  8. 【Ray Tracing The Next Week 超详解】 光线追踪2-4 Perlin noise

     Preface 为了得到更好的纹理,很多人采用各种形式的柏林噪声(该命名来自于发明人 Ken Perlin) 柏林噪声是一种比较模糊的白噪声的东西:(引用书中一张图) 柏林噪声是用来生成一些看似杂乱 ...

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

     Preface 终于到了激动人心的纹理章节了 然鹅,看了下,并不激动 因为我们之前就接触过 当初有一个 attenuation 吗? 对了,这就是我们的rgb分量过滤器,我们画出的红色.蓝色.绿色等 ...

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

    Chapter 2:Bounding Volume Hierarchies 今天我们来讲层次包围盒,乍一看比较难,篇幅也多,但是咱们一步一步来,相信大家应该都能听懂 BVH 和 Perlin text ...

随机推荐

  1. 写一个栈,实现出栈、入栈、求最小值,时间复杂度为O(1)

    #-*-coding:utf-8-*- ''' 需求:写一个栈,实现出栈.入栈.求最小值,时间复杂度为O(1) 思路:通过两个栈实现,一个栈stack,一个辅助栈min_stack,记录stack中的 ...

  2. Tomcat权威指南-读书摘要系列9

    从源代码组建Tomcat 安装Apache Ant ant是make的开放源代码的替代品,而且是专门为java程序语言设计. Ant的最初用途是作为Tomcat的组建工具: 之后,Ant成为Java软 ...

  3. 官方资料&一些好的博客与技术点

    https://technet.microsoft.com/zh-cn/library/hh848794.aspxzh   https://msdn.microsoft.com/en-us/power ...

  4. js调试系列: 源码定位与调试[基础篇]

    js调试系列目录: - 如果看了1, 2两篇,你对控制台应该有一个初步了解了,今天我们来个简单的调试.昨天留的三个课后练习,差不多就是今天要讲的内容.我们先来处理第一个问题:1. 查看文章下方 推荐 ...

  5. springMvc + Maven 项目提示 hessian 依赖包 无法下载;

    首先 从 https://github.com/alibaba/dubbo/archive/master.zip 下载最新的 dubbo 源码包到本地某个目录, 解压出来: cmd 进入该目录: 执行 ...

  6. django错误笔记——1242 Subquery returns more than 1 row

    在数据库查询操作过程中,子查询结果不唯一,导致外面的查询无法进行. 我的错误语句: rid = models.User.objects.filter(username=username).values ...

  7. 第10月第1天 storyboard uitableviewcell

    1. 如图,我们在Cell的属性界面对其进行了注册,identifier 为"TableViewCell" 不需要在 ViewDidLoad 对其进行注册了,如果进行注册的话,则对 ...

  8. spring如何管理mybatis(二) ----- SqlSession的线程安全性

    在之前的文章中我们了解到最终的数据库最终操作是走的代理类的方法: @Override public Object invoke(Object proxy, Method method, Object[ ...

  9. 【转】2019年3月 最新win10激活密匙 win10各版本永久激活序列号 win10正式版激活码分享

    现在市面上大致有两种主流激活方法,一种是通过激活码来激活,另外一种是通过激活工具来激活.但是激活工具有个弊端就是激活时间只有180天,很多网友都想要永久激活,现在已经过了win10系统免费推广期了,所 ...

  10. GBDT理解

    一.提升树 提升方法实际采用加法模型(即基函数的线性组合)与前向分布算法.以决策树为基函数的提升方法称为提升树,boosting tree.对分类问题的决策树是二叉分类树,对回归问题的决策树是二叉回归 ...