本文主要参照 Ray Tracing: The Next Week,其中只是主要精炼光追相关理论,具体实现可参照原文。
一、纹理实现
实现之前,你应该已经充分理解了【光线追踪系列六】反射与金属类特性。
在定义兰伯特材质时,我们将各个通道的反射率赋值给lambertian的构造函数,然后发射的射线命中该位置后,如果产生了散射光线,将该位置的反射率作为系数,乘以散射光线所采样得到的颜色值,从而实现该材质对射线的颜色吸收与反射。
1.1 纹理类实现
新增texture抽象类:
class texture {
public:
//返回一个三通道的颜色值,p为命中终点坐标
virtual vec3 value(float u, float v, const vec3& p) const = 0;
};
hit_record结构体增加两个变量u和v,目前还不会用上,后面贴图才会用上,加进来先:
struct hit_record
{
...
float u;
float v;
};
之后我们修改之前的lambertian类以实现原有效果:
class lambertian : public material
{
public:
texture *albedo; //反射率
lambertian(texture *a) : albedo(a) {}
virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const
{
vec3 s_world = rec.p + rec.normal + random_in_unit_sphere();
scattered = ray(rec.p, s_world - rec.p, r_in.time()); //scattered为散射光线
attenuation = albedo->value(rec.u, rec.v, rec.p); //注意这是各通道的反射率!
return true;
}
};
其中我们可以定义一个纯色纹理来替换之前直接写入颜色:
class constant_texture : public texture {
public:
vec3 color;
constant_texture() { }
constant_texture(vec3 c) : color(c) { }
//返回一个三通道的color
virtual vec3 value(float u, float v, const vec3& p) const {
return color;
}
};
在创建物体时,我们也需要对应修改lambertian的实现:
hittable *random_scene() {
int n = 500;
hittable **list = new hittable*[n + 1];
list[0] = new sphere(vec3(0, -1000, 0), 1000, new lambertian(new constant_texture(vec3(0.5, 0.5, 0.5))));
//texture *checker = new checker_texture(new constant_texture(vec3(0.2, 0.3, 0.1)), new constant_texture(vec3(0.9, 0.9, 0.9)));
//list[0] = new sphere(vec3(0, -1000, 0), 1000, new lambertian(checker));
int i = 1;
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
float choose_mat = random_double();
vec3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());
if ((center - vec3(4, 0.2, 0)).length() > 0.9) {
if (choose_mat < 0.8) { // diffuse
if (b % 2 == 0) //动态模糊的球体
{
auto center2 = center + vec3(0, random_double(), 0);
list[i++] = new moving_sphere(center, center2, 0.0, 1.0, 0.2,
new lambertian(new constant_texture(vec3(random_double()*random_double(),
random_double()*random_double(),
random_double()*random_double()))
)
);
}
else
{
list[i++] = new sphere(center, 0.2,
new lambertian(new constant_texture(vec3(random_double()*random_double(),
random_double()*random_double(),
random_double()*random_double()))
)
);
}
}
else if (choose_mat < 0.95) { // metal
list[i++] = new sphere(center, 0.2,
new metal(vec3(0.5*(1 + random_double()),
0.5*(1 + random_double()),
0.5*(1 + random_double())),
0.5*random_double()));
}
else { // glass
list[i++] = new sphere(center, 0.2, new dielectric(1.5));
}
}
}
}
list[i++] = new sphere(vec3(0, 1, 0), 1.0, new dielectric(1.5));
list[i++] = new sphere(vec3(-4, 1, 0), 1.0, new lambertian(new constant_texture(vec3(0.4, 0.2, 0.1))));
list[i++] = new sphere(vec3(4, 1, 0), 1.0, new metal(vec3(0.7, 0.6, 0.5), 0.0));
list[i++] = new sphere(vec3(5, 0.35, 4), 0.35, new dielectric(1.2));
list[i++] = new sphere(vec3(0, 0.4, 3), 0.4, new lambertian(new constant_texture(vec3(0.0, 1.0, 1.0))));
list[i++] = new sphere(vec3(-6, 0.5, 2), 0.5, new metal(vec3(0.8, 0.8, 0.8), 0.1));
//return new hittable_list(list, i);
return new bvh_node(list, i, 0.0, 1.0);
}
constant_texture的物体表面的反射率不会随着射线命中点的位置而变化,仅仅只是返回一个三通道的color而已,所以即使反射率已经和命中点的位置挂钩了,着色还是跟之前一样是纯色。
注意,lambertian中的反射率变量,当texture为constant_texture的时候,该值的意义为constant_texture中的color,也就是说,反射率就相当于color。其实光追是一个逆向的过程,当前的反射率乘以散射光线采样到的颜色值,其实是散射光线逆向照在命中点所对应的物体表面后反射的颜色,因为颜色值相乘可以用来模拟光源照在物体后反射的颜色。
实现效果如下图:
1.2 棋盘纹理实现
如果要实现棋盘纹理的话,就要让表面的反射率跟射线命中点的位置关联起来:
//棋盘纹理
class checker_texture : public texture {
public:
texture *odd;
texture *even;
checker_texture() { }
checker_texture(texture *t0, texture *t1): even(t0), odd(t1) { }
virtual vec3 value(float u, float v, const vec3& p) const {
float sines = sin(10*p.x())*sin(10*p.y())*sin(10*p.z());
if (sines < 0)
return odd->value(u, v, p);
else
return even->value(u, v, p);
}
};
其中,sines为了区分球体纹理位置。
这样一来,我们可以将棋盘的两种constant_texture(纯色材质)的指针赋值给checker_texture,实现棋盘纹理。
修改random_scene():
hittable *random_scene() {
...
hittable **list = new hittable*[n + 1];
texture *checker = new checker_texture(new constant_texture(vec3(0.2, 0.3, 0.1)), new constant_texture(vec3(0.9, 0.9, 0.9)));
list[0] = new sphere(vec3(0, -1000, 0), 1000, new lambertian(checker));
...
return new bvh_node(list, i, 0.0, 1.0);
}
实现后效果如下:
二、球面纹理贴图
2.1 球面贴图映射公式
在直角坐标中,对于一个宽高为nx*ny的图片,坐标为( i , j )的像素点的纹理坐标( u , v ) 定义如下:
这样就能够将( i , j ) 映射到( u , v ) ,并缩放到[0,1]
在球坐标中,我们同样可以将角度映射到( u , v ) :
假设( θ , ϕ )为球坐标上的一点,将球坐标想象成地球,则ϕ 为环绕着地轴旋转的角度(共360度),θ 为球心从北极点方向到南极点方向的角度(共180度),纹理坐标( u , v ) 为:
对于单位球表面上的一个命中点,球坐标转换到直角坐标的转换关系如下;
我们希望通过单位球上的命中点的直角坐标(x,y,z),得到球坐标,变形得:
atan2()返回值范围为[ − π , π ]
asin()的返回值范围为[ − π / 2 , π / 2 ]
综上,在我们的场景中(y轴朝上),单位球上一个命中点的直角坐标,到纹理坐标的映射为:
写成代码如下:
//输入命中点p的坐标,输出纹理坐标u,v
void get_sphere_uv(const vec3& p, double& u, double& v) {
auto phi = atan2(p.z(), p.x());
auto theta = asin(p.y());
u = 1-(phi + pi) / (2*pi);
v = (theta + pi/2) / pi;
}
同时记住要更新圆类,在hit函数中记录uv:
class sphere : public hittable
{
public:
vec3 center;
float radius;
material *mat_ptr; /* NEW */
sphere() {}
sphere(vec3 cen, float r, material *m) : center(cen), radius(r), mat_ptr(m) {}; //new
//如果命中了,命中记录保存到rec
virtual bool hit(const ray &r, float t_min, float t_max, hit_record &rec) const
{
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction());
float b = dot(oc, r.direction());
float c = dot(oc, oc) - radius * radius;
float discriminant = b * b - a * c;
if (discriminant > 0)
{
float temp = (-b - sqrt(discriminant)) / 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;
rec.mat_ptr = mat_ptr; /* NEW */
vec3 outward_normal = (rec.p - center) / radius;
get_sphere_uv(outward_normal, rec.u, rec.v);
return true;
}
temp = (-b + sqrt(discriminant)) / 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;
rec.mat_ptr = mat_ptr; /* NEW */
vec3 outward_normal = (rec.p - center) / radius;
get_sphere_uv(outward_normal, rec.u, rec.v);
return true;
}
}
return false;
}
bool bounding_box(float t0, float t1, aabb &box) const
{
box = aabb(center - vec3(radius, radius, radius), center + vec3(radius, radius, radius));
return true;
}
};
2.2 读取图片
使用stb_image的stbi_load()函数读取图片,该函数返回一个unsigned char数组,该数组按顺序保存了图片的RGB颜色值,范围为[0,255]。
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
unsigned char *imgEarth = stbi_load("earthmap.jpg", &nx, &ny, &nn, 0);
需要注意的是:注意png与jpg图片读取的区别。
2.3 贴图纹理类的构造和使用
贴图纹理类image_texture继承自texture,定义如下:
class image_texture : public texture
{
public:
unsigned char* data;
int nx, ny;
image_texture(){}
image_texture(unsigned char* pixels, int A, int B) : data(pixels), nx(A), ny(B) {}
//输入u和v,输出对应图片像素的rgb值
virtual vec3 value(float u, float v, const vec3 &p) const
{
int i = int((u)* nx);//求出像素索引
int j = int((1 - v)*ny - 0.001f);
if (i < 0) i = 0;
if (j < 0) j = 0;
if (i > nx - 1) i = nx - 1;
if (j > ny - 1) j = ny - 1;
float r = int(data[3 * i + 3 * nx*j]) / 255.0f;
float g = int(data[3 * i + 3 * nx*j + 1]) / 255.0f;
float b = int(data[3 * i + 3 * nx*j + 2]) / 255.0f;
return vec3(r, g, b);
}
};
lambertian材质的贴图纹理球体定义示例:
hittable *random_scene() {
hittable **list = new hittable*[n + 1];
texture *checker = new checker_texture(new constant_texture(vec3(0.2, 0.3, 0.1)), new constant_texture(vec3(0.9, 0.9, 0.9)));
list[0] = new sphere(vec3(0, -1000, 0), 1000, new lambertian(checker));
int nx, ny, nn;
unsigned char *earthmapjpg = stbi_load("F://earthmap.jpg", &nx, &ny, &nn, 0);
material *earthmapJpg = new lambertian(new image_texture(earthmapjpg, nx, ny));
unsigned char *Cristiano = stbi_load("F://Cristiano.jpg", &nx, &ny, &nn, 0);
material *CristianoJpg = new lambertian(new image_texture(Cristiano, nx, ny));
unsigned char *earthmappng = stbi_load("F://earthmap.png", &nx, &ny, &nn, 0);
material *earthmapPng = new lambertian(new image_texture(earthmappng, nx, ny));
list[i++] = new sphere(vec3(5, 1, 0), 1.0, earthmapJpg);
list[i++] = new sphere(vec3(0, 1, 0), 1.0, CristianoJpg);
list[i++] = new sphere(vec3(-5, 1, 0), 1.0, earthmapPng);
list[i++] = new sphere(vec3(5, 0.35, 4), 0.35, new dielectric(1.2));
list[i++] = new sphere(vec3(0, 0.4, 3), 0.4, new lambertian(new constant_texture(vec3(0.0, 1.0, 1.0))));
list[i++] = new sphere(vec3(-6, 0.5, 2), 0.5, new metal(vec3(0.8, 0.8, 0.8), 0.1));
return new bvh_node(list, i, 0.0, 1.0);
}
运行效果如下: