所有光线追踪都包含射线类(ray class)和计算沿射线所见颜色的过程。我们可以将射线视为函数 P(t)=A+tb。在这里,P是三维空间中一条直线上的位置。A是射线的起点,b是射线的方向。射线参数t是一个实数(在代码中通常为双精度浮点数)。通过输入不同的t值,P(t)会沿着射线移动点的位置。加入负的t值,你可以在三维线上到达任何位置。对于正的t值,你只能得到A前方的部分,这通常被称为半线段或射线
我们可以将射线表示为一个类,并将函数P(t)表示为我们称之为ray::at(t)的函数:
#ifndef RAY_H
#define RAY_H
#include "vec3.h"
class ray {
public:
ray(){ }
ray(const point3& origin, const vec3& direction) : orig(origin), dir(direction) {}
point3 origin() const { return orig; }
vec3 direction() const { return dir; }
point3 at(double t) const {
return orig + t*dir;
}
private:
point3 orig;
vec3 dir;
};
#endif
4.2 将光线投射到场景中
现在我们准备转向并制作一个光线追踪器。在其核心部分,光线追踪器通过像素发送光线,并计算在这些光线方向上所见的颜色。涉及的步骤包括:
1. 计算从“眼睛”穿过像素的光线
2. 确定光线与哪些物体相交
3. 为最近的交点计算颜色
当初次开发光线追踪时,我通常会使用简单的相机来让代码快速运行起来。
使用正方形图像进行调试时,我经常因为频繁转置 x 和 y 而遇到麻烦,因此我们将使用非正方形图像。正方形图像具有1∶1的长宽比,因为其宽度与高度相同。由于我们想要一个非正方形图像,我们将选择16∶9,因为它非常常见。16∶9的长宽比意味着图像宽度与图像高度的比例为16∶9。换句话说,给定一个16∶9长宽比的图像,
width/height=16/9=1.7778
以一个实际例子来说,一个宽度为800像素、高度为400像素的图像具有2∶1的长宽比。
图像的长宽比可以通过其高度与宽度的比例确定。然而,由于我们已经有一个给定的长宽比,更容易设置图像的宽度和长宽比,然后使用这个信息计算其高度。这样,我们可以通过改变图像的宽度来放大或缩小图像,而不会破坏我们所期望的长宽比。我们需要确保求解得到的图像高度至少为1。
除了设置渲染图像的像素尺寸之外,我们还需要设置一个virtual viewport来传递场景光线。viewport是3D世界中的一个虚拟矩形,包含了图像像素位置的网格。如果像素在水平和垂直方向上的间距相等,边界视口将具有与渲染图像相同的长宽比。两个相邻像素之间的距离称为像素间距,正方形像素是标准。
首先,我们将选择一个任意的 viewport 高度为2.0,并根据所需的长宽比来缩放viewport宽度。下面是代码的一部分示例:
auto aspect_ratio = 16.0 / 9.0;
int image_width = 400;
// Calculate the image height, and ensure that it's at least 1.
int image_height = static_cast<int>(image_width / aspect_ratio); // 225
image_height = (image_height < 1) ? 1 : image_height;
// Viewport widths less than one are ok since they are real valued.
auto viewport_height = 2.0;
Auto viewport_width=viewport_height*(static_cast<double>(image_width)/image_height);//3.555556
如果你想知道为什么我们在计算viewport宽度时不直接使用aspect_ratio,那是因为aspect_ratio所设定的值是理想的比例,它可能并不是image_width和image_height之间的实际比例。如果允许image_height为实数而不仅仅是整数,那么使用aspect_ratio是可以的。但是image_width和image_height之间的实际比例可以根据代码的两个部分而变化。首先,integer_height被向下取整到最近的整数,这可以增加比例。其次,我们不允许integer_height小于1,这也会改变实际的长宽比。
请注意,aspect_ratio是一个理想的比例,我们用基于整数的image宽度与image高度的比例尽量逼近它。为了使我们的viewport比例与图像比例完全匹配,我们使用计算得到的图像长宽比来确定最终的viewport宽度。
接下来,我们将定义相机中心:一个在3D空间中的点,所有场景光线都将从这个点发出(这通常也被称为眼点)。从相机中心到viewport中心的向量将与viewport垂直。我们将初始设置viewport与相机中心点之间的距离为一个单位。这个距离通常被称为焦距。
为了简单起见,我们将相机中心设置在(0,0,0)处。我们将y轴朝上,x轴朝右,负z轴指向观察方向。(这通常被称为右手坐标系。)
现在是不可避免的棘手部分。虽然我们的3D空间遵循上述规定,但这与我们的图像坐标冲突,我们希望将第一个像素设在左上角,并从上到下逐渐移动到右下角的最后一个像素。这意味着我们的图像坐标Y轴是反向的:Y增加时向下移动图像。
当我们扫描图像时,我们将从左上角的像素(像素0,0)开始,从左到右扫描每一行,然后逐行从上到下扫描。为了帮助在像素网格中导航,我们将使用从左边缘到右边缘的向量(Vu),以及从上边缘到下边缘的向量(Vv).
我们的像素网格将从viewport边缘向内缩进半个像素间距。这样,我们的视口区域就被均匀地分成宽度×高度个相同的区域。以下是我们的视口和像素网格的样子:
在这个图中,我们有视口(viewport),用于7*5分辨率图像的像素网格,视口左上角Q,像素P0,0位置,视口向量Vu(viewport_u),视口向量Vv(viewport_v),以及像素增量向量Δu和Δv。
基于这些信息,下面是实现相机功能的代码。我们将插入一个名为ray_color(const ray& r)的函数,该函数返回给定场景光线的颜色——目前我们将其设置为始终返回黑色。
#include "color.h"
#include "vec3.h"
#include "ray.h"
#include <iostream>
color ray_color(const ray& r) {
return color(0,0,0);
}
int main() {
// Image
auto aspect_ratio = 16.0 / 9.0;
int image_width = 400;
// Calculate the image height, and ensure that it's at least 1.
int image_height = static_cast<int>(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
// Camera auto focal_length = 1.0;
auto viewport_height = 2.0;
auto viewport_width = viewport_height * (static_cast<double>(image_width)/image_height);
auto camera_center = point3(0, 0, 0);
// Calculate the vectors across the horizontal and down the vertical viewport edges. auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
auto pixel_delta_u = viewport_u / image_width;
auto pixel_delta_v = viewport_v / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left = camera_center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2;
auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = 0; j < image_height; ++j) {
std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v);
auto ray_direction = pixel_center - camera_center;
ray r(camera_center, ray_direction);
color pixel_color = ray_color(r);
write_color(std::cout, pixel_color);
}
}
std::clog << "\rDone. \n";
}
请注意,在上面的代码中,我没有将ray_direction转换为单位向量,因为我认为这样做可以使代码更简洁且略微更快。
现在我们将填写ray_color(ray)函数来实现一个简单的渐变效果。该函数将根据将射线方向缩放为单位长度后的y坐标的高度,线性混合白色和蓝色。因为我们是在对向量进行归一化后查看y坐标的高度,所以除了垂直渐变之外,您还会注意到水平渐变的颜色。
我将使用一个标准的图形技巧来线性缩放0.0≤a≤1.0。当a=1.0时,我想要蓝色。当a=0.0时,我想要白色。在两者之间,我希望进行混合。这形成了一个“线性混合”或“线性插值”。这通常被称为两个值之间的插值。
插值始终采用下面的形式:
blendedValue=(1−a)⋅startValue+a⋅endValue,
将a从0到1进行线性插值。
将所有这些组合在一起,我们得到以下结果:
#include "color.h"
#include "ray.h"
#include "vec3.h"
#include <iostream>
color ray_color(const ray& r) {
vec3 unit_direction = unit_vector(r.direction());
auto a = 0.5*(unit_direction.y() + 1.0);
return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0);
}...
这样会得到(根据射线Y坐标产生一个从蓝色到白色的渐变。):