感觉学的还是有点慢啊,刚刚往后看了一点,感觉后面的知识越来越难,我得赶快点学。
法向量和多物体处理
根据法向量进行表面着色
我们需要计算出表面法线进而进行着色。(法线是垂直于交点处的表面的向量)
法向量的计算使用决策有以下两种情况:
法向量可以有任意长度,这些长度可以附带额外的信息,比如反射啥的
法向量全部进行单位化,此时法向量仅仅代表其法线方向
我们可以看到法线的单位化过程中有开方操作,这个操作可能会花费较多的时间,但是我们仍然保留它,理由有三:
你不应该等到要用的时候再进行单位化,设置条件可能会导致程序更加复杂
我们会经常用到单位化的法向量
法向量的单位化比想象中的简单,根据特定的几何类可以定制不同的单位化策略,如在球体中可以通过除以radius实现法向量的单位化
综上考虑,我们对所有的法向量都进行单位化。
球体的外法线的计算方式也很简单:
1 2 vec(d) = point(P) - point(C) unit(vec(d)) = vec(d)/radius
image.png
由于我们的单位向量的大小是1
也就是说,单位法向量在其他任意方向的分量范围都在[-1,1]内,所以我们可以将其每个分量都映射到[0,1]的区间内,然后将(x,y,z)映射到
(red,green,blue)
我们需要计算出t从而计算出法向量的信息,我们确保球体是在摄像机的前方,所以不必考虑t的负值。当t有多个解的时候,我们只考虑离我们最近的交点(最小的t值),这样我们可以计算出我们的法向量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 double hit_sphere (const point3& center,double radius,const ray& r) { vec3 oc = center - r.origin (); auto a = dot (r.direction (),r.direction ()); auto b = -2.0 * dot (r.direction (),oc); auto c = dot (oc,oc) - radius*radius; auto discriminant = b*b - 4 *a*c; if (discriminant < 0.0 ){ return -1.0 ; }else { return (-b - std::sqrt (discriminant)) / (2.0 *a); } } color ray_color (ray & r) { auto t = hit_sphere (point3 (0 ,0 ,-1 ),0.5 ,r); if (t > 0.0 ){ vec3 N = unit_vector (r.at (t) - point3 (0 ,0 ,-1 )); return 0.5 *color (N.x ()+1 ,N.y ()+1 ,N.z ()+1 ); } vec3 unit_direction = unit_vector (r.direction ()); auto a = 0.5 *(unit_direction.y () + 1.0 ); return (1.0 -a)*color (1.0 ,1.0 ,1.0 ) + a*color (0.5 ,0.7 ,1.0 ); }
我们将程序修改后运行渲染得到了新的图片:
image.png
光线计算的简化
我们查看原本的交点计算方程:
1 2 3 4 5 6 7 8 9 10 11 12 13 double hit_sphere (const point3& center,double radius,const ray& r) { vec3 oc = center - r.origin (); auto a = dot (r.direction (),r.direction ()); auto b = -2.0 * dot (r.direction (),oc); auto c = dot (oc,oc) - radius*radius; auto discriminant = b*b - 4 *a*c; if (discriminant < 0.0 ){ return -1.0 ; }else { return (-b - std::sqrt (discriminant)) / (2.0 *a); } }
我们注意到b中有一个-2
的因子,那么我们考虑将其分离出来,令b = -2h
于是方程可以进行以下变换实现简化:
image.png
同时我们知道向量的自己和自己的点积,等同于向量的平方dot(vec(v),vec(v)) = vec(v)^2
所以我们的程序可以改进成以下形式(没看出改进太多啊):
1 2 3 4 5 6 7 8 9 10 11 12 13 double hit_sphere (const point3& center,double radius,const ray& r) { vec3 oc = center - r.origin (); auto a = r.direction ().length_squared (); auto h = dot (r.direction (),oc); auto c = oc.length_squared () - radius*radius; auto discriminant = h*h - a*c; if (discriminant < 0.0 ){ return -1.0 ; }else { return (h - std::sqrt (discriminant)) / a; } }
可被(光线)击中类的抽象
接下来我们需要考虑当有多个球体的情况,你可能觉得很简单,可以创建一个数组来实现,但实际上有更好的选择——创建一个抽象类,这里我们称其为hittable
类。
这个抽象类将接受ray
作为参数。同时我们添加一个有效的命中区间(t_min到t_max),这样便于我们对t的计算,只有在有效命中区内,t才被采用,命中才计数。同时我们还要考虑一个问题,那就是我们只需要考虑并计算离我们较近的东西的法线,所以我们需要采取一些方案来计算他们,以下是一个抽象类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef RENDER_C___HITTABLE_H #define RENDER_C___HITTABLE_H #include "ray.h" class hit_record {public : point3 p; vec3 normal; double t; }; class hittable {public : virtual ~hittable () = default ; virtual bool hit (const ray& r, double ray_min,double ray_max,hit_record& rec) const = 0 ; }; #endif
在此基础上实现一个球体的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #ifndef RENDER_C___SPHERE_H #define RENDER_C___SPHERE_H #include "vec3.h" #include "hittable.h" class sphere : public hittable{public : sphere (const point3& center, double radius) : center (center), radius (std::fmax (0 ,radius)) {} bool hit (const ray& r, double ray_min,double ray_max,hit_record& rec) const override { vec3 oc = center - r.origin (); auto a = r.direction ().length_squared (); auto h = dot (r.direction (),oc); auto c = oc.length_squared () - radius*radius; auto discriminant = h*h - a*c; if (discriminant < 0.0 ){ return false ; } auto sqrtd = std::sqrt (discriminant); auto root = (h - sqrtd) / a; if (root <= ray_min || ray_max <= root){ root = (h + sqrtd) / a; if (root <= ray_min || ray_max <= root) return false ; } rec.t = root; rec.p = r.at (rec.t); rec.normal = (rec.p - center) / radius; return true ; } private : point3 center; double radius; }; #endif
前表面与后表面
我们先前提到了关于法线的几个决策,现在我们需要决定法线是否应该始终指向外面。当然,我们可以利用法线始终指向外面的方式来进行光线来向的判断,并帮助我们区分前后表面。
image.png
以这张图为例,我们可以通过计算光线方向和法线(始终朝外)的点积,并判断它的正负来实现其方向的判断。如果其点积为正,说明光线与法线同向,照射到的是内表面。如果点积为负,说明光线与法线反向,照射到的是外表面。我们可以通过以下程序实现判断:
1 2 3 4 5 6 7 8 9 10 bool front_face;if (dot (ray_direction,outward_normal) > 0.0 ){ normal = -outward_normal; front_face = false ; }else { normal = outward_normal; front_face = true ; }
我们将其添加到我们的hittable.h
和sphere
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 class hit_record {public : point3 p; vec3 normal; double t; bool front_face; void set_face_normal (const ray& r,const vec3& outward_normal) { front_face = dot (r.direction (),outward_normal) < 0 ; normal = front_face ? outward_normal : -outward_normal; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class sphere : public hittable { public : ... bool hit (const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const { ... rec.t = root; rec.p = r.at (rec.t); vec3 outward_normal = (rec.p - center) / radius; rec.set_face_normal (r, outward_normal); return true ; } ... };
可击中对象的列表
我们之前创建了一个抽象类hittabel.h
,用来描述光线可以相交的物体。现在我们再创建一个类来存储具有hittable
特性的列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #ifndef RENDER_C___HITTABLE_LIST_H #define RENDER_C___HITTABLE_LIST_H #include "hittable.h" #include <memory> #include <vector> using std::make_shared;using std::shared_ptr;class hittable_list : public hittable {public : std::vector<shared_ptr<hittable>> objects; hittable_list (){} hittable_list (shared_ptr<hittable> object) {add (object);} void clear () {objects.clear ();} void add (shared_ptr<hittable> object) { objects.push_back (object); } bool hit (const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override { hit_record temp_rec; bool hit_anything = false ; auto closest_so_far = ray_tmax; for (const auto & object : objects){ if (object->hit (r,ray_tmin,closest_so_far,temp_rec)){ hit_anything = true ; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } }; #endif
这里有些地方用到了C++的一些特性,需要特别强调一下。
shared_ptr<>
是一个智能指针,被包含在<memory>
中,用来指向已经分配的类型,它可以自动管理内存,当对象不再被任何shared_ptr
引用时,自动释放内存。我们可以使用make_shared<thing>(...)
为数据类型thing
创建一个实例,并返回一个shared_ptr<thing>
例如:
1 2 3 auto double_ptr = make_shared <double >(0.41 );auto vec3_ptr = make_shared <vec3>(1.1 ,1.2 ,1.3 );auto sphere_ptr = make_shared <sphere>(point3 (0 ,0 ,0 ),1.0 )
还有一个需要注意的用到的特性是std::vector
,其被包含在<vector>
中,其用来生成一个动态数组,并且支持对其集合仅从指定的操作。例如:
1 2 3 4 5 6 7 8 9 std::vector<shared_ptr<hittable>> objects; objects.push_back (object); objects.clear (); objects.size (); objects[]
大概涉及到这些吧,剩下的之后再慢慢了解
常用常量和实用函数
我们设置一个头文件,我们放置一些常用的数学常量和一些常用的头文件,这样方便我们的调取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #ifndef RENDER_C___RTWEEKEND_H #define RENDER_C___RTWEEKEND_H #include <cmath> #include <iostream> #include <limits> #include <memory> using std::make_shared;using std::shared_ptr;const double infinity = std::numeric_limits<double >::infinity ();const double pi = 3.1415926535897932385 ;inline double degree_to_radius (double degrees) { return degrees * pi / 180.0 ; } #include "color.h" #include "ray.h" #include "vec3.h" #endif
现在我们的rtweekend.h
里面已经包含了大多数常用的文件头,
我们现在利用上述新创建文件头,写一个新的main函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include "rtweekend.h" #include "hittable.h" #include "hittable_list.h" #include "sphere.h" #include <iostream> color ray_color (ray & r,const hittable& world) { hit_record rec; if (world.hit (r,0 ,infinity,rec)){ return 0.5 *(rec.normal + color (1 ,1 ,1 )); } vec3 unit_direction = unit_vector (r.direction ()); auto a = 0.5 *(unit_direction.y ()+1.0 ); return (1.0 - a)*color (1.0 ,1.0 ,1.0 ) + a*color (0.5 ,0.7 ,1.0 ); } int main () { auto aspect_radio = 16.0 /9.0 ; int image_width = 400 ; int image_height = int (image_width / aspect_radio); image_height = (image_height < 1 ) ? 1 : image_height; hittable_list world; world.add (make_shared <sphere>(point3 (0 ,0 ,-1 ),0.5 )); world.add (make_shared <sphere>(point3 (0 ,-100.5 ,-1 ),100 )); auto focal_length = 1.0 ; auto viewport_height = 2.0 ; auto viewport_width = viewport_height * (double (image_width)/image_height); auto camera_center = point3 (0 ,0 ,0 ); auto viewport_u = vec3 (viewport_width,0 ,0 ); auto viewport_v = vec3 (0 ,-viewport_height,0 ); auto pixel_delta_u = viewport_u/image_width; auto pixel_delta_v = viewport_v/image_height; auto viewport_upper_left = camera_center - vec3 (0 ,0 ,focal_length) - viewport_v/2 - viewport_u/2 ; auto pixel00_loc = viewport_upper_left + 0.5 *(pixel_delta_u+pixel_delta_v); std::cout << "P3\n" << image_width << " " << image_height << "\n255\n" ; for (int j=0 ;j<image_height;j++){ std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; for (int i=0 ;i<image_width;i++){ auto pixel_center = pixel00_loc + (i*pixel_delta_u) + (j*pixel_delta_v); auto ray_direction = pixel_center - camera_center; ray r (camera_center,ray_direction) ; color pixel_color = ray_color (r,world); write_color (std::cout,pixel_color); } } std::clog << "\rDone. \n" ; }
这个是我们渲染出来的新图片,我们设置了一个大的球体在下面作为一个地面。
image.png
区间类
我们在判断是否击中的hit()
函数中我们经常设置一个区间(tmin,tmax)
,为了方便之后的使用,我们将这个区间类给抽象出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #ifndef RENDER_C___INTERVAL_H #define RENDER_C___INTERVAL_H #include "rtweekend.h" class interval {public : double min,max; interval () : min (+infinity),max (-infinity) {} interval (double min,double max): min (min),max (max) {} double size () const { return max - min; } bool contains (double x) const { return min <= x && x <= max; } bool surrounds (double x) const { return min < x && x < max; } static const interval empty,universe; }; const interval interval::empty = interval (+infinity,-infinity);const interval interval::universe = interval (-infinity,+infinity);#endif
我们现在可以用这个头文件去更新之前用到了区间的程序
1 2 3 4 5 6 class hittable { public : ... virtual bool hit (const ray& r, interval ray_t , hit_record& rec) const = 0 ; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class hittable_list : public hittable { public : ... bool hit (const ray& r, interval ray_t , hit_record& rec) const override { hit_record temp_rec; bool hit_anything = false ; auto closest_so_far = ray_t .max; for (const auto & object : objects) { if (object->hit (r, interval (ray_t .min, closest_so_far), temp_rec)) { hit_anything = true ; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } ... };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class sphere : public hittable { public : ... bool hit (const ray& r, interval ray_t , hit_record& rec) const override { ... auto root = (h - sqrtd) / a; if (!ray_t .surrounds (root)) { root = (h + sqrtd) / a; if (!ray_t .surrounds (root)) return false ; } ... } ... };
1 2 3 4 5 6 7 8 9 10 color ray_color (const ray& r, const hittable& world) { hit_record rec; if (world.hit (r, interval (0 , infinity), rec)) { return 0.5 * (rec.normal + color (1 ,1 ,1 )); } vec3 unit_direction = unit_vector (r.direction ()); auto a = 0.5 *(unit_direction.y () + 1.0 ); return (1.0 -a)*color (1.0 , 1.0 , 1.0 ) + a*color (0.5 , 0.7 , 1.0 ); }
以上就是需要更新的地方啦
这一篇的内容比较多,写了差不多两天,得好好消化一下啦。