33.1 怎么用ray tracing画特殊长方体
在光线追踪中被用到的一种常见形态是长方体盒子。这种基本物体被用于可见物体和包围盒,包围盒被用于加速复杂物体的相交测试。
吐槽:单词都认识,就是不知道讲的是什么。“包围盒”是什么鬼?不懂!不过感觉好像很厉害的样子。
表面法向量和坐标轴平行的长方体是最简单的形式之一。
我们先就画“表面法向量和坐标轴平行”的长方体。
先回忆一下,已经学过的基本物体的画法:球、多边形。
球:
第一步:一条光线出去,判断是否撞上球?(联立光线和球的方程,判别式大于零)
第二步:是撞上了,求得实根,判断实根是否离光线起点最近?(二次方程求根公式求得实根,然后判断实根是否在有效范围)
第三步:是最近,将该光线在画面中的颜色设置为球的颜色。(根据球的材质和交点处的法向量设置球的颜色)
多边形:
第一步:一条光线出去,判断是否撞上多边形所在的平面?(联立光线和平面的方程,只要光线方向向量和平面法向量不垂直,就会有实根)
第二步:是撞上了,求得实根,根据实根求得光线和平面的交点,交点是否在多边形内?(1,根据平面法向量确定主坐标;2,去掉交点和多边形所有顶点坐标中的主坐标,完成所有点在UV平面的投影;3,将交点移至UV平面原点;4,从原点沿着+U轴引一条射线,射线与多边形的交点个数为奇数,则原交点在多边形内,反之在多边形外)
第三步:交点在多边形内,才算光线撞上了多边形。判断实根是否离光线起点最近?(判断实根是否在有效范围)
第四步:是最近,将该光线在画面中的颜色设置为多边形的颜色。(根据多边形的材质和交点处的法向量设置多边形的颜色)
根据“球”和“多边形”的画法经验得出:要画一个物体,只要判断光线是否和物体撞上和求得撞击点到光线起点的距离,然后根据物体的材质和交点处的法向量设置物体的颜色。
现在我们要画是表面法向量与坐标轴平行的长方体,这类长方体的特殊之处在于:
1,六个平面的法向量是已知的:(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)------------------------------交点处的法向量基本搞定;
2,同时,三对平面垂直分别空间平面XOY,YOZ,ZOX,所以长方体在个平面上的投影不会变形。
法向量搞定,材质先不用管,
接下来只要确定:光线撞上了长方体、撞击点到光线起点的距离。
光线和长方体是定义如下:
书上提供了一种很巧妙的方法来判定光线是否撞上长方体。
由于这类长方体的特殊性,光线和长方体的位置关系在坐标平面的投影是这种情况:
然后,书上给出了如下测试项,若其中某一项测试失败,则光线不会撞上长方体;反之,若通过所有测试项,则光线撞上长方体。
测试项如下:
如上,对于每一坐标(X或者Y或者Z),测试项中都有三个可能“FALSE”的地方,所有长方体和光线的坐标关系通过这3*3=9个测试之后,则表示光线撞上了长方体。
理解和分析:
1,t1x, t1y, t1z, t2x, t2y, t2z表示的不是什么鬼投影或者垂直距离,而是光线起点到六个平面的距离。
2,将这些距离分成三组:t1x, t2x;t1y, t2y;t1z, t2z
算法的步骤:
第一步:
t_far = t_x_大
t_near=t_x_小
第二步:
t_y_大 < t_far(即t_x_大),则t_far = t_y_大;反之t_far不变,还是等于t_x_大。
t_y_小 > t_near(即t_x_小),则t_near=t_y_小;反之t_near不变,还是等于t_x_小。
经过这一步之后,t_far等于“t_x_大”和“t_y_大”中较小的,t_near等于“t_x_小”和“t_y_小”中较大的。(也就是,t_far在慢慢变小,t_near在慢慢变大)。
判断t_far和t_near的大小:若t_far<t_near,即是如下情况:
测试顺利完成时(光线撞上长方体了),
t_far={t_x_大,t_y_大,t_z_大}中最小的那个
t_near={t_x_小,t_y_小,t_z_小}中最大的那个
OK,书上只介绍了怎么判断光线是否撞击到长方体,当然“材质”我们先不用考虑。但是,为了画出长方体,我们还必须知道光线起点到撞击点的距离(具体是那六个距离中的哪个,当然,实际t=t_near)和交点处的法向量。
“立体图示”中,最终t_near=t2x
现在我们要求交点处的法向量。特殊长方体的特殊之处:右、左、上、下、前、后,六个平面的法向量分别是:(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)
但是交点处法向量对应的是哪一个呢?
考虑到:“t_near={t_x_小,t_y_小,t_z_小}中最大的那个”
若t_near为X的值,则所交平面为左平面或者右平面;
若t_near为Y的值,则所交平面为上平面或者下平面;
若t_near为Z的值,则所交平面为前平面或者后平面;
但是,到底具体是左还是右?是上还是下?是前还是后?
怎么判断?????
这个可以根据光线方向向量和法向量点乘的正负来判断,交点是在面向光线的平面上,所以点乘是小于零的那个。
截至当前,画长方体的条件应该齐了:
1,光线撞上长方体
2,光线起点到撞击点的距离
3,撞击点处的法向量
4,(材质)
33.2 看C++代码实现
----------------------------------------------box.h------------------------------------------
box.h
#ifndef BOX_H
#define BOX_H
#include <hitable.h>
class box : public hitable
{
public:
box() {}
box(vec3 vl, vec3 vh, material *m) : vertex_l(vl), vertex_h(vh), ma(m) {
normals[0] = vec3(-1, 0, 0);//left
normals[1] = vec3(1, 0, 0);//right
normals[2] = vec3(0, 1, 0);//up
normals[3] = vec3(0, -1, 0);//down
normals[4] = vec3(0, 0, 1);//front
normals[5] = vec3(0, 0, -1);//back
/*按照这个顺序将六个表面的法向量保存在数组中。注意:保存顺序和后面获取交点处法向量是对应的。*/
}
virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
vec3 vertex_l;//前左下顶点坐标
vec3 vertex_h;//后右上顶点坐标
vec3 normals[6];
material *ma;
};
#endif // BOX_H
----------------------------------------------box.cpp------------------------------------------
box.cpp
#include <iostream>
#include <limits>
#include "float.h"
#include "box.h"
#include "log.h"
using namespace std;
bool box::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
float t_near = (numeric_limits<float>::min)();
float t_far = (numeric_limits<float>::max)();
int near_flag, far_flag;
vec3 direction = r.direction();
vec3 origin = r.origin();
vec3 bl = vertex_l;
vec3 bh = vertex_h;
float array1[6];
/*光线方向向量和坐标轴平行。若光线起点不在两个平行平面之间,则光线不可能撞击到长方体,return false;反之,[Xd, Yd, Zd]三个坐标有出现零,零是不能作除数的,所以,可能除以零的交点坐标需要提前算出来*/
if(direction.x() == 0) {
if((origin.x() < bl.x()) || (origin.x() > bh.x())) {
return false;
}
array1[0] = (numeric_limits<float>::min)();
array1[1] = (numeric_limits<float>::max)();
}
if(direction.y() == 0) {
if((origin.y() < bl.y()) || (origin.y() > bh.y())) {
return false;
}
array1[2] = (numeric_limits<float>::min)();
array1[3] = (numeric_limits<float>::max)();
}
if(direction.z() == 0) {
if((origin.z() < bl.z()) || (origin.z() > bh.z())) {
return false;
}
array1[4] = (numeric_limits<float>::min)();
array1[5] = (numeric_limits<float>::max)();
}
/*计算六个(三组)交点坐标,一次保存在数组中*/
if((direction.x() != 0) && (direction.y() != 0) && (direction.z() != 0)) {
array1[0] = (bl.x()-origin.x())/direction.x();
array1[1] = (bh.x()-origin.x())/direction.x();
array1[2] = (bl.y()-origin.y())/direction.y();
array1[3] = (bh.y()-origin.y())/direction.y();
array1[4] = (bl.z()-origin.z())/direction.z();
array1[5] = (bh.z()-origin.z())/direction.z();
}
for (int i=0; i<6; i=i+2){
/*i=i+2,说明,只循环3次(和X、Y、Z三组六个交点对应)。顺利完成测试循环后(说明光线撞击到长方体),我们获得t_far、t_near和其对应的位置(对着X或者Y或者Z值)far_flag、near_flag*/
if(array1[i] > array1[i+1]) {//将每组中较小的放在i位置,较大的放在i+1位置
float t = array1[i];
array1[i] = array1[i+1];
array1[i+1] = t;
}
if(array1[i] >= t_near) {t_near = array1[i]; near_flag = i;}
/*较小的值比t_near大时,把较小的值赋给t_near,同时记住当前t_near的位置near_flag。这里的near_flag可能的值0(t_x_小)、2(t_y_小)、4(t_z_小)(交点位置数组(array1[6])中的每组中较小值的位置),后面我们会根据该值去法向量数组(vec3 normals[6])中获取撞击点的法向量,这里可以看出array1[6]和normals[6]是对应的:0、1对应X对应左右,2、3对应Y对应上下,4、5对应Z对应前后*/
if(array1[i+1] <= t_far) {t_far = array1[i+1]; far_flag = i+1;}
/*较大的值比t_far小时,把较大的值赋给t_far,同时记住当前t_far的位置far_flag*/
if(t_near > t_far) {//t_near>t_far,测试失败,光线不会撞击到长方体
return false;
}
if(t_far < 0) {
return false; //t_far<0,测试失败,光线不会撞击到长方体
}
}
if (t_near < t_max && t_near > t_min) {
rec.t = t_near;//t_near就是光线起点到撞击点的距离
rec.p = r.point_at_parameter(rec.t);
rec.mat_ptr = ma;
/*接下来求撞击点处的法向量*/
vec3 normals_choose[6];
for(int j=0; j<6; j++) {
normals_choose[j] = vec3(0,0,0);
}
for(int i=0; i<6; i++) {
if(dot(normals[i], r.direction()) < 0) {
/*选出面向光线起点的表面的法向量(最多三个),保存在新的数组normals_choose[]中*/
normals_choose[i] = normals[i];
}
}
for(int k=near_flag; k<6; k++) {
/*前面求得near_flag可能的值0(t_x_小)、2(t_y_小)、4(t_z_小)(也就是t_near可能出现的位置)。0、1对应X对应左右,2、3对应Y对应上下,4、5对应Z对应前后。在normals_choose[6]中只在对应位置保存了面向光线起点的长方体表面的法向量。所以,根据t_near值可以在该数组中取的唯一的不等于零的向量,这个向量即为撞击点处的法向量*/
if(!vector_equ(normals_choose[k], vec3(0,0,0))) {
rec.normal = normals_choose[k];
break;
}
}
return true;
}
return false;
}
----------------------------------------------main.cpp------------------------------------------
main.cpp
//triangle2, the green lambertian one
vec3 vertexes3_2[3];
vertexes3_2[0] = vec3(1.5,0.5,1.0);
vertexes3_2[1] = vec3(2.5,0.5,1.0);
vertexes3_2[2] = vec3(2.0,2.0,1.0);
hitable *list[7];
list[0] = new sphere(vec3(0.0,-100.5,-1), 100, new lambertian(vec3(0.8, 0.8, 0.0)));
list[1] = new box(vec3(-2.0,-0.5,4.0), vec3(-1.0,1.0,2.0), new lambertian(vec3(0.0, 1.0, 0.5)));
list[2] = new box(vec3(-0.25,-0.5,0.0), vec3(0.75,0.5,-1.0), new metal(vec3(0.8, 0.2, 0.2), 0.0));
list[3] = new box(vec3(-5.0,-0.5,-5.0), vec3(5.0,3.0,-6.0), new metal(vec3(0.8, 0.6, 0.4), 0.0));
list[4] = new sphere(vec3(2.0,0.0,1.0), 0.5, new lambertian(vec3(0.5, 0.7, 0.6)));
list[5] = new sphere(vec3(0.75,-0.25,5.0), 0.25, new lambertian(vec3(0.8, 0.7, 0.6)));
list[6] = new polygon(vertexes3_2, 3, new lambertian(vec3(0.3, 0.8, 0.0)));
hitable *world = new hitable_list(list,7);
vec3 lookfrom(0,0,12);
vec3 lookat(0,1,-1);
float dist_to_focus = (lookfrom - lookat).length();
float aperture = 0.0;
camera cam(lookfrom, lookat, vec3(0,1,0), 20, float(nx)/float(ny), aperture, 0.7*dist_to_focus);
三个长方体,一个漫射材料,两个镜面材料。
输出图片如下:
心血来潮,输出了对应的2048*1024分辨率的图片: