阅读视图

发现新文章,点击刷新页面。

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

目标

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

本节最终效果

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);
}

GPU 编程:用 CUDA 和 OpenCL 解锁算力超能力

想象一下,你的电脑里住着一群 “小工人”。CPU 是那个聪明绝顶、做事井井有条的首席执行官,它每次专注处理一项重要任务,深思熟虑地做出决策。而 GPU 则是工厂里数以千计的流水线工人,虽然单个工人不算特别聪明,但胜在数量庞大,能够齐心协力、不知疲倦地并行处理海量相似任务。这就是为什么在处理图像渲染、科学计算这类需要大量重复计算的工作时,GPU 能展现出惊人的速度优势,而 GPU 编程,就是指挥这群 “小工人” 高效工作的艺术。

为什么选择 GPU 编程?

在传统的 CPU 编程世界里,当我们面对大规模数据的计算任务,比如处理一张高清图片的每个像素点,或者进行复杂的物理模拟计算时,CPU 这位 “首席执行官” 就会显得力不从心。它只能按部就班地一个一个处理任务,就像让一个人独自搬运一整座仓库的货物,效率十分低下。

而 GPU 编程就像是给你配备了一支庞大的搬运队。它利用 GPU 强大的并行计算能力,将大任务拆解成无数个小任务,让数以千计的 “小工人” 同时开工。原本需要 CPU 花费很长时间完成的工作,在 GPU 的加持下,可能瞬间就能得出结果。无论是游戏开发中的实时渲染,还是人工智能领域的深度学习训练,GPU 编程都已经成为了不可或缺的技术。

CUDA 和 OpenCL:指挥 GPU 的两大 “语言”

CUDA:NVIDIA 家的 “方言”

CUDA(Compute Unified Device Architecture)是 NVIDIA 推出的一种并行计算平台和编程模型。它就像是 NVIDIA GPU 专属的 “方言”,只有 NVIDIA 的 GPU 能听得懂。使用 CUDA 进行编程,就好比你掌握了一门特殊的沟通技巧,可以直接和 NVIDIA 的 GPU “小工人” 对话,让它们按照你的要求高效工作。

在 CUDA 的世界里,我们通过编写 “内核函数” 来定义 GPU 需要执行的任务。这些内核函数会被大量复制,分配到 GPU 的各个计算核心上并行执行。下面是一个用 JavaScript 风格模拟 CUDA 内核函数的简单示例,展示如何对一个数组的每个元素进行加 1 操作:

// 模拟CUDA内核函数
function cudaKernel(array) {
    const threadId = getThreadId(); // 假设存在获取线程ID的函数
    if (threadId < array.length) {
        array[threadId]++;
    }
}
// 模拟调用CUDA内核函数
function callCudaKernel() {
    const dataArray = [1, 2, 3, 4, 5];
    const numThreads = dataArray.length;
    for (let i = 0; i < numThreads; i++) {
        cudaKernel(dataArray);
    }
    console.log(dataArray);
}
callCudaKernel();

在实际的 CUDA 编程中,我们需要安装 NVIDIA 的 CUDA Toolkit,并使用 C 或 C++ 等语言进行编写,然后通过编译器将代码编译成 GPU 能够执行的指令。

OpenCL:跨平台的 “世界语”

与 CUDA 不同,OpenCL(Open Computing Language)是一种跨平台的并行编程框架,它就像是编程语言中的 “世界语”,无论是 NVIDIA 的 GPU、AMD 的 GPU,甚至是 CPU、FPGA 等其他计算设备,都能理解 OpenCL 的指令。这使得开发者可以编写一套代码,在不同的硬件设备上运行,大大提高了代码的通用性和可移植性。

OpenCL 的编程模型相对复杂一些,它将计算设备抽象为 “平台”“设备”“上下文”“命令队列” 等概念。开发者需要先获取设备信息,创建上下文和命令队列,然后将计算任务(内核函数)编译并提交到命令队列中执行。以下是一个用 JavaScript 风格模拟 OpenCL 编程流程的示例:

// 模拟获取设备信息
function getOpenCLDevices() {
    return [/* 假设返回设备列表 */];
}
// 模拟创建上下文
function createOpenCLContext(devices) {
    return { /* 假设返回上下文对象 */ };
}
// 模拟创建命令队列
function createCommandQueue(context, device) {
    return { /* 假设返回命令队列对象 */ };
}
// 模拟OpenCL内核函数
function openCLKernel(array) {
    const workItemId = getWorkItemId(); // 假设存在获取工作项ID的函数
    if (workItemId < array.length) {
        array[workItemId]++;
    }
}
// 模拟编译内核函数
function compileOpenCLKernel(context, kernelSource) {
    return { /* 假设返回编译后的内核对象 */ };
}
// 模拟调用OpenCL内核函数
function callOpenCLKernel() {
    const devices = getOpenCLDevices();
    const context = createOpenCLContext(devices);
    const device = devices[0];
    const commandQueue = createCommandQueue(context, device);
    const dataArray = [1, 2, 3, 4, 5];
    const kernelSource = `
        __kernel void openCLKernel(__global int* array) {
            int workItemId = get_global_id(0);
            if (workItemId < get_global_size(0)) {
                array[workItemId]++;
            }
        }
    `;
    const kernel = compileOpenCLKernel(context, kernelSource);
    // 这里省略数据传输等实际操作
    console.log(dataArray);
}
callOpenCLKernel();

在真实的 OpenCL 编程中,我们通常使用 C 语言风格的语法来编写内核函数,并通过特定的 API 与计算设备进行交互。

GPU 编程的挑战与乐趣

虽然 GPU 编程能带来巨大的性能提升,但它也充满了挑战。由于 GPU 的架构与 CPU 有很大不同,开发者需要深入理解并行计算的原理,合理地分配任务和管理内存。比如,在 CUDA 和 OpenCL 编程中,我们需要考虑线程的同步问题,避免出现数据竞争;还要优化内存访问模式,充分利用 GPU 的高速缓存,以提高计算效率。

然而,正是这些挑战,让 GPU 编程充满了乐趣。当你经过一番努力,成功地让 GPU 以惊人的速度完成复杂的计算任务时,那种成就感是难以言喻的。而且,随着技术的不断发展,GPU 编程的应用场景也在不断拓展,从科学研究到工业生产,从娱乐传媒到医疗健康,它正在改变着我们生活的方方面面。

如果你对计算机科学充满热情,想要探索更广阔的计算领域,那么 GPU 编程绝对是一个值得深入学习的方向。无论是选择 CUDA 还是 OpenCL,都像是掌握了一把打开新世界大门的钥匙,让你能够充分发挥 GPU 的强大算力,创造出令人惊叹的应用和成果。快来加入 GPU 编程的奇妙之旅吧!

上述文章涵盖了 GPU 编程的基础概念与实践模拟。若你觉得内容深度、示例复杂度等方面需调整,或是有其他修改需求,欢迎随时告诉我。

❌