别沉浸在包围盒的那些递归函数里了,暂时忘掉它们。接下来几章的内容和包围盒毫无关系,甚至和动态模糊以及时空光追都没有什么关系。换句话说,丢弃掉之前的那些代码也不影响这几章程序的运行。
从这一章开始,我们将目光重新移回物体表面。回顾一下初篇中介绍的三大材质:磨砂、金属和玻璃——难道我们的世界仅限于此吗?渲染器还不能描述陶瓷的纹路,又或是婴儿细腻的皮肤,甚至这个世界上的所有物体表面!世界上所有的东西都是不完美的,你见过没有划痕的金属和不沾上人类指纹的玻璃球么?
换句话来说,这个世界上不存在仅仅使用材质就可以完美模拟的物体表面。是时候请出材质最好的搭档——“纹理”了!

纹理简谈

纹理,在图形学领域通常表示程序化的物体表面颜色。比如,通过一些参数和数据,来计算得到物体表面某一点的颜色。根据具体计算方式不同可以大致分为两种:
①:找一张图,对于这个物体表面上的任意一点,都赋予其一个二维坐标,通过这个二维坐标在这个二维平面图上去找,找到的那一点是什么颜色,那物体表面该处就是什么颜色。这个二维坐标如何找到呢?一般来说是由建模师进行安排,建模师在制作模型的时候会为模型上的一些点指定一个二维坐标,其他的点的二维坐标就由这些给定点的插值进行计算——当然我们的渲染器到目前为止尚未引入三角面片的概念,这里不多谈。
这种方式得到的纹理看上去就好像是把这张二维图片贴到三维表面上一样,所以这又叫做贴图纹理。它的应用相当的广泛。
image.png
②:另一种获取各点的颜色的方式就显得比较理科生了:利用某种数学函数直接得到某点的颜色。这种纹理我们已经接触过了。还记得“元气弹”么?这就是一种纹理,我们把物体表面的法线信息映射到颜色区间并显示出来,当然这种纹理还有很多,利用法线只是其中的一种。

纹理基类

每次引入一个新事物必定要先完成基类的编写,新建文件texture.h,并敲入如下代码:

  1. #ifndef TEXTURE_H
  2. #define TEXTURE_H
  3. #include "rtweekend.h"
  4. class texture {
  5. public:
  6. virtual color value(double u, double v) const = 0;
  7. };
  8. #endif

本纹理基类基于上小节中的第一种纹理类型:他只有一个函数,这个函数需要一个二维坐标u和v,返回一个颜色。当然,我们提到的拿着这个坐标去贴图中找颜色等等,这些都放到子类的value函数里来处理。从这个基类建立完成之后,如何得到颜色也就被规范化了,无论你是拿这个uv进行某种函数计算得到颜色,还是拿到某张图里去查找,这些都是子类该干的事情。
当然,仅仅传入uv并不能得到诸如“元气弹”之类的纹理,如果你需要通过纹理类创建元气弹,我们可以在之后再改造这个基类,加入诸如法线等等的参数。
至于这个u,v坐标从何而来,不是纹理类该管的事情,uv的计算不该放在这个类里,这个我们之后介绍。
uv坐标作为引入纹理之后碰撞中最重要的信息之一,需要把它写进record结构体:

  1. struct hit_record {
  2. vec3 p;
  3. vec3 normal;
  4. shared_ptr<material> mat_ptr;
  5. double t;
  6. //uv坐标。
  7. double u;
  8. double v;
  9. bool front_face;
  10. ...

纯色纹理

我们的金属和磨砂类都有名为albedo的变量来控制最终颜色。其实纯色也可以被当作一种纹理,就好像我们把二十一:简单纹理 - 图2也当作一种函数(常值函数)一样。
并不是所有的渲染器都会把纯色当作一种纹理,但在本渲染器中,我推荐这么做。这样意味着纹理类搭建完毕之后,对物体表面颜色的所有的解释都会被放到一个类里面。这会使代码架构变得更加易懂。
当然,把纯色解释为纹理会使得纯色材质会比过去更加耗时,因为有更多需要维护的变量,但不会比过去慢多少。
继续把这个类写在texture.h文件里

  1. #ifndef TEXTURE_H
  2. #define TEXTURE_H
  3. #include "rtweekend.h"
  4. class texture {
  5. ...
  6. };
  7. class solid_color : public texture {
  8. public:
  9. solid_color() {}
  10. solid_color(color c) : color_value(c) {}
  11. solid_color(double red, double green, double blue)
  12. : solid_color(color(red,green,blue)) {}
  13. //纯色纹理的uv参数仅仅是个摆设,无论吃到什么样的uv,直接吐颜色值就行,毕竟就一种颜色。
  14. virtual color value(double u, double v) const override {
  15. return color_value;
  16. }
  17. private:
  18. //纯色纹理的颜色值
  19. color color_value;
  20. };
  21. #endif

球的纹理坐标——经纬度

现在,我们来思考一下关于uv坐标的事情,因为现在我们的物体类只有球这一个子类,现在我们要讨论的是如何把球的表面上任意一点和一个二维坐标uv对应起来。
我们可以根据球的解析式轻松地把球的表面上的每一点和一个三维坐标对应,但是把球表面和一个二维平面对应有点反直觉。我们换一个生活中的例子去看你会明白很多——世界地图就好像是把空心球剪开之后平铺在平面上那样。你可以在世界地图上找到你家的位置,且你随机在地图上选一点也可以在现实世界中找到对应的真实的坐标,这简直就是我们这个问题的天然解决方案。
假设是球上任意一点代表的向量和-Y方向向量的夹角,而是围绕Y轴的方向角(从-X 方向到+Z 再到+X 到-Z 最后返回-X)。这个和就是常说的经纬度。