【光线追踪系列十一】纹理贴图

本文主要参照 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);
}

运行效果如下:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值