回忆一下,一个球是怎么画的呢?
第一步:光线撞上球。找“撞点”
第二步:将“撞点”的像素位置设置为球的颜色。之前采用过两种方式设置颜色:1,简单粗暴,将整个球的颜色设置为红色(return vec3(1, 0, 0));2,将球面上每个像素位置的颜色设置为球在该点的单位法向量的色彩表映射值(每个点的法向量都不一样,所以,每个点的颜色被设置得不一样)。
所以,那么,现在怎么画多个球呢?一样一样的。也是先找撞点,再设置颜色。
第一步:找撞点。一条光线和N个球的交点可能有0~2N个。之前画一个球时,我们取的是大于零的较小的那个根(因为只考虑球完全不透明)。所以,现在画多个球,我们取所有交点中大于零最小的那个交点(因为我们向前看,只能看到最近的那个交点,后面所有的交点都被一个球体或者多个球体挡住了。)。
第二步:设置颜色。这个和画一个球时一样,设置为球在该点的单位法向量的色彩表映射值。这样的话,有个问题:球和球的相交边界处,两个球的颜色会不会很接近导致边界模糊呢?不会~不会~不会。因为,虽然交点坐标非常接近,但是球的法向量是等于交点坐标减去球心坐标。从而,每个球在边界点的法向量是不一样的。如果两个球的球心坐标相差较大时,法向量坐标也会相差比较大,颜色也会相差比较大。
废话少说,所有代码贴上来!
----------------------------------------------hitable.h----------------------------------------------
hitable.h
#ifndef HITABLE_H
#define HITABLE_H
#include "ray.h"
struct hit_record{
float t;
vec3 p;
vec3 normal;
};/*该结构体记录“撞点”处的信息:离光线起点的距离t、撞点的坐标向量p、撞点出的法向量normal。*/
class hitable
{/*hitable这个类表示能够被光线撞上的任何物体。比如,球体*/
public:
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const = 0;
/*hit()在此被声明为虚函数,则hitable为抽象类。抽象类的子类中必须实现其虚函数*/
};
----------------------------------------------hitable.cpp----------------------------------------------
hitable.cpp
为空
----------------------------------------------sphere.h----------------------------------------------
sphere.h
#ifndef SPHERE_H
#define SPHERE_H
#include "hitable.h"
class sphere: public hitable{
/*: public hitable表示sphere继承hitable。即:sphere为hitable的子类*/
public:
sphere() {}
sphere(vec3 cen, float r) : center(cen), radius(r) {}
/*此处为使用初始化列表的构造函数来初始化成员变量*/
virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
/*必须实现父类的虚函数。在此出声明,后续在sphere.cpp中具体实现*/
vec3 center;
float radius;
};
#endif // SPHERE_H
----------------------------------------------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.orgin() - 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;
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;
return true;
}
}
/*判断小根和大根是否在范围内。首先判断小根是否在范围内。是:保存相关信息,然后直接返回;否,判断大根是否在范围内。也就是优先选小根,小根不行再考虑大根 */
return false;
}
----------------------------------------------hitable_list.h----------------------------------------------
hitable_list.h
#ifndef HITABLE_LIST_H
#define HITABLE_LIST_H
#include "hitable.h"
class hitable_list: public hitable{
/*: public hitable表示hitable_list继承hitable。即:sphere为hitable的子类*/
public:
hitable_list() {}
hitable_list(hitable **l, int n) {list = l; list_size = n; }
virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
/*必须实现父类的虚函数。在此出声明,后续在sphere.cpp中具体实现*/
hitable **list;
int list_size;
};/*hitable_list是所有能够被光线撞击的物体的列表/集合*/
#endif // HITABLE_LIST_H
----------------------------------------------hitable_list.cpp----------------------------------------------
hitable_list.cpp
#include "hitable_list.h"
bool hitable_list::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
hit_record temp_rec;
bool hit_anything = false;
double closest_so_far = t_max;
for (int i = 0; i < list_size; i++) {
if (list[i]->hit(r, t_min, closest_so_far, temp_rec)){
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}
/*依次判断列表中所有物体是否被光线撞到,每次判断一个。若有被撞到,则将撞点信息保存在hit_record结构体中。我们可以看到rec是可能被写多次的,最终保存的值是后一次的值,也就是真正有效的值是后一次的值,也就是离观测点最近的物体的有效撞点(“有效撞点”:对于单个物体,会筛选出一个局部有效撞点;对于多个物体,从所有单个物体各自的局部有效撞点筛选出最终一个整体有效撞点)。因为不管这条光线依次撞击了多少个物体产生多少个撞点,我们能看到的只是离我们最近的撞点*/
/*如果当前撞点在范围内,则将当前撞点的距离设置为范围的最大值。也就是后面只考虑比该撞点更近的撞点。趋势是:找到的撞点是越来越近的,最终找到最近的撞点。*/
return hit_anything;
}
----------------------------------------------main.cpp----------------------------------------------
main.cpp
#include <iostream>
#include <fstream>
#include <limits>
#include "sphere.h"
#include "hitable_list.h"
#include "float.h"
using namespace std;
vec3 color(const ray& r, hitable *world) {
hit_record rec;
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);
/*有撞点:即为球体,将球体颜色设置为球在该点的单位法向量的色彩表映射值。另外numeric_limits<float>::max)()表示最大浮点数*/
}
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);//white, light blue
// 无撞点:将颜色设置为背景色
}
}
int main(){
int nx = 200;
int ny = 100;
ofstream outfile( ".\\results\\SeveralSpheres.txt", ios_base::out);
outfile << "P3\n" << nx << " " << ny << "\n255\n";
std::cout << "P3\n" << nx << " " << ny << "\n255\n";
vec3 lower_left_corner(-2.0, -1.0, -1.0);
vec3 horizontal(4.0, 0.0, 0.0);
vec3 vertical(0.0, 2.0, 0.0);
vec3 origin(0.0, 0.0, 0.0);
hitable *list[2];
list[0] = new sphere(vec3(0,0,-1), 0.5);
list[1] = new sphere(vec3(0,-100.5,-1), 100);
hitable *world = new hitable_list(list,2);
/*将所有能够被撞击的物体信息保存在列表中*/
for (int j = ny-1; j >= 0; j--){
for (int i = 0; i < nx; i++){
float u = float(i) / float(nx);
float v = float(j) / float(ny);
ray r(origin, lower_left_corner + u*horizontal + v*vertical);
/*产生光线*/
// vec3 p = r.point_at_parameter(2.0);
vec3 col = color(r, world);
/*设置光线颜色*/
int ir = int (255.99*col[0]);
int ig = int (255.99*col[1]);
int ib = int (255.99*col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
std::cout << ir << " " << ig << " " << ib << "\n";
}
}
}
设置颜色思路如下:(多个球的各自的颜色和背景颜色)
1,光线是否撞上球?是:设置为球的颜色;否:设置为背景颜色
2,如果光线撞上了球。N个球,可能有2N撞点,到底哪一个撞点是有效的呢?可以被最终设置颜色呢?原则是:找出最近的撞点。
3,怎么找?一个球一个球依次找。每个球可能有2个撞点,选出较近的有效的撞点。
4,然后,从所有这些每个球的较近的有效的撞点中,选出最近的那个撞点。
整体运行的最终结果图如下: