WebGPU 光线追踪器

需求
该应用目前仅在 Chrome 上运行,因为它无需标志即可支持 WebGPU,并完全遵循 WebGPU 规范。Firefox 和 Safari 对其具有实验性支持,但此演示目前无法在它们上运行。
警告!该应用需要性能较强的 GPU。你的手机或平板电脑可能无法满足要求。旧款基于 Intel 的 Mac 应该没问题。我是在 M1 Max Mac Studio 上开发的。
摘要
在从事光栅化工作多年之后,我对光线追踪产生了浓厚的兴趣。毕竟,它是计算机图形学的圣杯,能够生成令人惊叹的逼真图像,并带有柔和的阴影、环境光遮蔽和模糊的反射。这些效果在使用实时 3D 光栅化器时很难实现,但在这里,你基本上可以免费获得它们,几乎不需要任何渲染技巧。
最终,我得到了一个所谓的路径追踪器。它需要以随机方式向每个像素发射大量光线以实现收敛,从而消除渲染图像中的噪声。
应用架构
Typescript
这部分代码在 CPU 上运行。它从 Wavefront OBJ 和 MTL 文件中读取三角形信息,创建 Bounding Volume Hierarchy 树,并将所有数据推送到 GPU。然后,它负责在每一帧向 GPU 提交工作,处理用户输入以及与摄像机的交互。
Compute Shader
这是光线追踪器的核心,在 GPU 上并行运行。它在场景中反弹光线,并收集最终写入图像缓冲区中像素的累积颜色。随后,图像缓冲区被 blit 到设备屏幕上。
已经存在在 fragment shader 中完成所有路径追踪的 WebGPU 光线追踪器。我为什么选择使用 compute shader?纯粹是为了学习练习。我很好奇更深入地探索 WebGPU compute shader。话虽如此,这个光线追踪器可以轻松地移植到在 fragment shader 中运行。
Bounding Volume Hierarchy
将每条光线与场景中的每个三角形进行相交测试是天真且缓慢的。这正是 BVH 等加速结构发挥作用的地方。你用包围盒包裹每个对象,然后递归地为对象的不同部分添加更多包围盒。想象一个人:你添加一个包围他们的盒子,然后为每个肢体添加一个盒子,为每根手指添加一个盒子。如果光线没有与这个人相交,你就不检查肢体或手指中的任何三角形。如果它击中右臂,你就不检查左脚中的三角形,依此类推。这显著加快了此应用中的渲染时间。

不使用纹理,改用存储缓冲区
为了存储光线追踪器所需的所有状态,例如随机数生成和图像状态,使用 WebGPU 存储缓冲区比使用纹理更容易,因为您可以在存储缓冲区中放置任意数据。
例如,我希望在多个帧中累积像素样本。需要一个缓冲区来保存累积的样本。使用存储纹理来存储像素值似乎很自然,但存储纹理目前不允许从计算着色器中读取。texture_storage_2d<rgba8unorm, write> 已得到支持,但 texture_storage_2d<rgba8unorm, read> 隐藏在一个标志之后。
因此,我改用存储缓冲区:
// read_write is important, we want to read the old frame and write the new frame
@group(0) @binding(0) var<storage, read_write> raytraceImageBuffer: array<vec3f>;
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) globalInvocationId : vec3<u32>) {
let pos = globalInvocationId.xy;
let idx = pos.x + pos.y * cameraUniforms.viewportSize.x;
let oldFrameContents = raytraceImageBuffer[idx];
let color: vec3f = /* calculate new color value somehow */
// accumulate the new frame contents with the old frame content value
raytraceImageBuffer[idx] += color;
}
通过循环实现递归
在 CPU 上进行光线追踪很容易通过递归实现。类似于:
color ray_color(const ray& r, const hittable& world) const {
hit_record rec;
if (world.hit(r, interval(0, infinity), rec)) {
vec3 direction = random_on_hemisphere(rec.normal);
return 0.5 * ray_color(ray(rec.p, direction), world);
}
return color(0, 0, 0);
}
然而,GPU 上不允许使用递归。因此,该应用使用循环在计算着色器中实现递归。
屏幕 Blit 片段着色器
在每一帧中,光线追踪计算着色器完成后,其结果会通过运行在全屏四边形上的片段着色器 Blit 到设备屏幕上。此时也会应用色调映射。
使用的库
gl-matrix用于通用的矩阵 / 向量操作mtl-file-parser用于 Wavefront MTL 文件解析obj-file-parser用于 Wavefront OBJ 文件解析webgpu-utils用于简化向 WebGPU 缓冲区传递值WebGPU Offset Computer- 可视化 std140 结构体布局的宝贵工具
场景模型
场景在 Blender 中建模,由重新建模的 Cornell Box 组成。添加了茶壶、Suzanne、梯子和十二面体以展示不同的材质。

参考文献与阅读
- Raytracing in a Weekend - this is where everybody starts with raytracing it seems. I followed Book 1 and 2 and implemented them in C++ before switching to a compute shader approach.
- Intel Path-Tracing Workshop - Raytracing in a Weekend runs on the CPU and does not really explain how to port it to the GPU (where recursion is not allowed). This 2 videos show very well how to do the same task via loops in GLSL. The theory and math presented are also really good.
- Weekend Raytracing with wgpu - Porting "Raytracing in a Weekend" Book 1 to WebGPU. I got the idea to use storage buffers for the frame pixel contents here.
- WebGL Ray Tracer - Path tracer written in WebGL. I studied the code and implemented my model parsing and BVH generation based on it.
- WebGPU Spec
- WGSL Spec
MIT 许可证
Copyright (c) 2024 Georgi Nikolov
特此授予,免费地,向任何获得本软件及相关文档文件(“软件”)副本的人, 授予处理软件的权利,不受限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售 软件副本的权利,并允许向其提供软件的人这样做,但须遵守以下条件:
上述版权声明和本许可声明应包含在 软件的所有副本或重要部分中。
软件按“原样”提供,不提供任何形式的明示或 暗示保证,包括但不限于对适销性、 特定用途适用性和不侵权的保证。在任何情况下, 作者或版权持有人均不对任何索赔、损害或其他 责任负责,无论是基于合同、侵权或其他行为,均因 软件或软件的使用或其他交易而产生。