1605 字
8 分钟
纹理映射

基本思路:由一个物体上一个点,找出纹理的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值

对于 𝑁𝑥 乘 𝑁𝑦 图像中的像素 (𝑖,𝑗) ,其图像纹理位置为: u=iNx1u = \frac{i}{N_x-1} v=jNy1v = \frac{j}{N_y-1}

访问纹理图像数据#

直接使用书中给的图像加载类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;
}
}

纹理映射
https://dingfengbo.vercel.app/posts/rtiow/纹理映射/
作者
Eureka
发布于
2026-05-21
许可协议
CC BY-NC-SA 4.0