普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月1日首页

Re: 0x01. 从零开始的光线追踪实现-光线、相机及背景

作者 壕壕
2025年6月30日 22:24

目标

书接上文,之前已经实现一个铺满整个窗口的红色填充,这趟来实现光线、相机及背景。

本节最终效果

image.png

计算物体在窗口坐标的位置

其实这个光追的思维模式很简单,就是从相机处开始发射一束射线,射线撞到哪些“物体”,就计算跟该“物体”相交的颜色。如图所示,从相机处发射射线,以左上角开始逐像素扫一遍,计算对应像素的颜色

fig-04-camera-view-space.svg

我们再看看 viewport 的坐标,假设一个窗口大小是 480272480 * 272(没错,PSP 的分辨率😁)的宽高,那么 xx 的区间就是 [0,479)[0, 479)yy 的区间就是 [0,271)[0, 271)

fig-03-viewport-coords.svg

现在我们要来处理一个标准化的像素坐标,处理像素在屏幕中的 2D 位置

struct Vertex {
  float4 position [[position]];
};

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
  // ...
}

上面这一步的作用是把像素级的屏幕坐标转成区间 [0,1][0, 1] 的归一化坐标。
假设现在有一个物体,它的坐标是 (240,135)(240, 135),通过上面的计算式子可以得出
uv=(240/479,135/271)(0.5,0.5)uv = (240 / 479, 135 / 271) ≈ (0.5, 0.5),说明它在屏幕的中间

接着我们假定相机的位置是原点 (0,0,0)(0, 0, 0),相机距离 viewport 11。我们计算出宽高的比例再套进这个计算 (2 * uv - float2(1)),等于讲把 [0,1][0, 1] 映射成 [1,1][-1, 1] 的范围,其实就是

原始 uv 变换后
(0, 0) (-1, -1) 左下角
(1, 0) (1, -1) 右下角
(0.5, 0.5) (0, 0) 居中
(1, 1) (1, 1) 右上角

再把 (2 * uv - float2(1))float2(aspect_ratio, -1) 相乘等于讲横向乘以 aspect_ratio 用来做等比例变换

至于纵向乘以 -1,那是因为在 Metal 中,yy 轴是向下为正,乘一下 -1 就可以把 yy 轴翻转变成向上为正,接下来计算方向就简单多了,因为 zz 轴面向相机,其实就是相机距离取反,上面假定相机距离为 1,所以取反再跟 uvuv 放一块就是方向,同时我们又假定相机的位置是原点 (0,0,0)(0, 0, 0),那么求光线就很容易了

struct Ray {
  float3 origin;
  float3 direction;
};

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  // ...
  const auto focus_distance = 1.0;
  // ...
  const auto direction = float3(uv, -focus_distance);
  Ray ray = { origin, direction };
}

现在既然有了光线,再就是要计算一下光线的颜色,因为目前场景中没有物体,所以就默认计算背景色,我们先把光线从 [1,1][-1, 1] 映射回 [0,1][0, 1],然后再线性插值计算渐变天空颜色,所以先要让光线经过归一化操作到 [1,1][-1, 1]

// [-1, 1]
normalize(ray.direction)

然后再给该向量加 11

// [-1, 1] + 1 = [0, 2]
normalize(ray.direction) + 1

然后把 [0,2][0, 2] 乘以 0.50.5 就转成 [0,1][0, 1] 了,之后再代入线性插值公式计算结果,具体渐变色值可以根据自己的需求调整,我这里直接使用 Ray Tracing in One Weekend 的色值 float3(0.5, 0.7, 1)

blendedValue=(1a)startValue+aendValueblendedValue = (1 − a) \cdot startValue + a \cdot endValue

float3 sky_color(Ray ray) {
  const auto a = 0.5 * (normalize(ray.direction).y + 1);
  return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}

最后总结一下代码

struct Ray {
  float3 origin;
  float3 direction;
};

float3 sky_color(Ray ray) {
  const auto a = 0.5 * (normalize(ray.direction).y + 1);
  return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  const auto origin = float3(0);
  const auto focus_distance = 1.0;
  const auto aspect_ratio = 480 / 272;
  auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
  uv = (2 * uv - float2(1)) * float2(aspect_ratio, -1);
  const auto direction = float3(uv, -focus_distance);
  Ray ray = { origin, direction };
  return float4(sky_color(ray), 1);
}
❌
❌