基本思路:由一个物体上一个点,找出纹理的u,v坐标返回颜色.
我们纹理类的主要方法是 color value(...) 方法,它会根据输入的坐标返回纹理颜色。除了接收点的纹理坐标 𝑢 和 𝑣 之外,我们还会提供相关点的位置信息.
恒定颜色纹理
texture.h类
#ifndef TEXTURE_H#define TEXTURE_H
#include"color.h"#include"vec3.h"
// texture 是所有纹理的抽象基类。// 给定 uv 坐标和命中点,返回该点的颜色值,支持纯色、棋盘格、图像纹理等。class texture { public: virtual ~texture() = default;
// 纹理采样:根据 uv 坐标和命中点 p 返回颜色。 virtual color value(double u, double v, const point3& p) const = 0;};
// solid_color 是纯色纹理实现:无论 uv 和命中点如何,始终返回同一颜色。class solid_color : public texture { public: solid_color(const color& albedo) : albedo(albedo) {}
solid_color(double red, double green, double blue) : solid_color(color(red,green,blue)) {}
color value(double u, double v, const point3& p) const override { return albedo; }
private: color albedo; // 纯色纹理的颜色值。};
#endif在hittable.h中为hit_record添加u、v坐标属性
double u; // 命中点纹理坐标 u:用于纹理贴图采样,范围 [0,1]。double v; // 命中点纹理坐标 v:与 u 一起构成 2D 纹理坐标,范围 [0,1]。实体纹理:棋盘格纹理
我们将实现一个空间 checker_texture 类,它实现的是三维棋盘格图案。由于空间纹理函数是由空间中的给定位置驱动的,因此纹理 value() 函数会忽略 u 和 v 参数,只使用 p 参数。
为了实现棋盘格图案,我们首先计算输入点各分量的向下取整值。虽然截断坐标也能实现,但这会将数值向零靠拢,导致零值两侧呈现相同颜色。而向下取整函数始终将数值向左(趋向负无穷方向)取整。得到这三个整数结果( ⌊𝑥⌋,⌊𝑦⌋,⌊𝑧⌋ )后,我们计算它们的总和并对 2 取模,得到 0 或 1 的结果。其中 0 映射为偶数色,1 映射为奇数色。
计算颜色
最后,我们为纹理添加一个缩放因子,以便控制场景中棋盘格图案的大小。
棋盘格纹理实现:
class checker_texture : public texture { public: // 构造函数:传入缩放系数和两个子纹理。 // inv_scale 的倒数决定棋盘格的大小,scale 越大格子越小。 checker_texture(double scale, std::shared_ptr<texture> even, std::shared_ptr<texture> odd) : inv_scale(1.0 / scale), even(even), odd(odd) {}
// 便捷构造函数:传入两种颜色,内部自动创建 solid_color 纹理。 checker_texture(double scale, const color& c1, const color& c2) : checker_texture(scale, std::make_shared<solid_color>(c1), std::make_shared<solid_color>(c2)) {}
color value(double u, double v, const point3& p) const override { auto xInteger = int(std::floor(inv_scale * p.x())); auto yInteger = int(std::floor(inv_scale * p.y())); auto zInteger = int(std::floor(inv_scale * p.z()));
// 将 x+y+z 的整数部分之和的奇偶性作为棋盘格判定依据。 bool isEven = (xInteger + yInteger + zInteger) % 2 == 0;
return isEven ? even->value(u, v, p) : odd->value(u, v, p); }
private: double inv_scale; // 缩放系数的倒数:值越大格子越小,用于将世界坐标映射到棋盘格网格。 std::shared_ptr<texture> even; // 偶数格纹理指针:多个物体可共享同一纹理实例。 std::shared_ptr<texture> odd; // 奇数格纹理指针:多个物体可共享同一纹理实例。};扩展lambertain类,使其能够处理纹理而非颜色.
颜色由value方法给出,我们只需要创建一个tex属性,用材质或反射率来初始化,颜色是根据光线所在位置决定的.
渲染立体棋盘格纹理
我们可以将main.cpp里的场景封装成一个函数,这样我们就能运行不同的场景了.
添加一个新场景,并在main函数里使用switch进行切换
void checkered_spheres() { hittable_list world;
auto checker = make_shared<checker_texture>(0.32, color(.2, .3, .1), color(.9, .9, .9));
world.add(make_shared<sphere>(point3(0,-10, 0), 10, make_shared<lambertian>(checker))); world.add(make_shared<sphere>(point3(0, 10, 0), 10, make_shared<lambertian>(checker)));
camera cam;
cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50;
cam.vfov = 20; cam.lookfrom = point3(13,2,3); cam.lookat = point3(0,0,0); cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(world);}
int main() { switch (2) { case 1: bouncing_spheres(); break; case 2: checkered_spheres(); break; }}结果看起来有点奇怪。由于
checker_texture是一种空间纹理,我们实际上看到的是球体表面切割三维棋盘空间的效果。在很多情况下,这种效果是完美的,或者至少是够用的。但在许多其他情况下,我们确实希望在物体表面获得一致的效果。接下来将介绍这种方法。
球体的纹理坐标
我们现在要实现根据点的坐标来计算纹理图的u,v坐标了.
对于球体,纹理坐标通常基于某种形式的经纬度,即球面坐标。因此我们计算 (𝜃,𝜙) 的球面坐标,其中 𝜃 是从底部极点(即从 -Y 方向)向上的角度,而 𝜙 是绕 Y 轴的角度(从 -X 到 +Z 到 +X 到 -Z 再回到 -X)
在球类中添加以下转换函数
static void get_sphere_uv(const point3& p, double& u, double& v) { // p: a given point on the sphere of radius one, centered at the origin. // u: returned value [0,1] of angle around the Y axis from X=-1. // v: returned value [0,1] of angle from Y=-1 to Y=+1. // <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50> // <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00> // <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50>
auto theta = std::acos(-p.y()); auto phi = std::atan2(-p.z(), p.x()) + pi;
u = phi / (2*pi); v = theta / pi; }在hit函数中击中球体后调用该方法,记录下点的u,v值
对于 𝑁𝑥 乘 𝑁𝑦 图像中的像素 (𝑖,𝑗) ,其图像纹理位置为:
访问纹理图像数据
直接使用书中给的图像加载类rtw_stb_image_h
接下来为texture类添加一个image_texture
#include "rtw_stb_image.h"
...
class checker_texture : public texture { ...};
class image_texture : public texture { public: image_texture(const char* filename) : image(filename) {}
color value(double u, double v, const point3& p) const override { // If we have no texture data, then return solid cyan as a debugging aid. if (image.height() <= 0) return color(0,1,1);
// Clamp input texture coordinates to [0,1] x [1,0] u = interval(0,1).clamp(u); v = 1.0 - interval(0,1).clamp(v); // Flip V to image coordinates
auto i = int(u * image.width()); auto j = int(v * image.height()); auto pixel = image.pixel_data(i,j);
auto color_scale = 1.0 / 255.0; return color(color_scale*pixel[0], color_scale*pixel[1], color_scale*pixel[2]); }
private: rtw_image image;};渲染图像纹理
main文件中修改
void earth() { auto earth_texture = make_shared<image_texture>("earthmap.jpg"); auto earth_surface = make_shared<lambertian>(earth_texture); auto globe = make_shared<sphere>(point3(0,0,0), 2, earth_surface);
camera cam;
cam.aspect_ratio = 16.0 / 9.0; cam.image_width = 400; cam.samples_per_pixel = 100; cam.max_depth = 50;
cam.vfov = 20; cam.lookfrom = point3(0,0,12); cam.lookat = point3(0,0,0); cam.vup = vec3(0,1,0);
cam.defocus_angle = 0;
cam.render(hittable_list(globe));}
int main() { switch (3) { case 1: bouncing_spheres(); break; case 2: checkered_spheres(); break; case 3: earth(); break; }}
