WebGPU物理渲染实现

最近,我花了相当多的时间在 WebGPU 中使用 IBL(基于图像的照明)编写 PBR(基于物理的渲染)渲染器。 PBR 本身并没有什么新奇之处。 这是一项自 2014 年以来就存在的技术,所有现代图形引擎都使用它。 但我还没有看到任何在 WebGPU ThreeJS 中执行此操作的教程,其他人已经实现了它,但阅读这些源代码并不容易。

我很想创建一个完整的 PBR 指南,并提供适当的理论解释,但互联网上已经有很多相关材料,我需要花费几个月的时间才能重新创建它。 我决定只写下与目前互联网上可用的内容相比我必须找出的缺失部分。 因此,对于我们为什么要做这个问题,请参考互联网上可用的丰富资源。 我将回答有关 WebGPU 的问题。

top:金属表面 bottom:介电表面(塑料)
要记住一件事:我不确定自己在做什么。 我对使用 GPU API 有了很好的掌握,但我对 WebGPU 很陌生,也不是计算机图形编程方面的专家,所以我所做的一些事情可能并没有真正意义。 但这似乎有效!

我这里的所有工作都是基于优秀的 Learn OpenGL 教程。 我已经使用兰伯特漫反射、GGX 高光和 Fresnel Schlick 近似实现了基本的 PBR 着色。 我使用预过滤的环境贴图和辐照度贴图实现了 IBL。

1、什么是PBR?

基于物理的渲染(PBR: Physically Based Rendering)是计算机图形学中最现代的渲染方法。 自 2010 年代以来,它一直是一个研究课题,并且自 2010 年代左右开始变得足够有效,可用于实时游戏引擎。 关于它的两本著名出版物是 Brian Karis 的《虚幻引擎 4 中的真实着色》和 Sébastien Lagarde 和 Charles de Rousiers 的《Moving Frostbite to PBR》

基本思想是使用光的精确物理特性了,这与之前非常粗略的近似不同。 为了实现实时性能,仍然需要近似和缓存大量计算,但结果比以前更加真实。

PBR 将材料分为两类:金属和电介质。 金属是像镜子一样反射光的材料,电介质是像玻璃一样反射光的材料。 不同之处在于,金属具有大量可以移动并反射光的自由电子,而电介质则没有。 最重要的结果是金属具有镜面反射,而电介质则没有。

2、什么是 IBL?

基于图像的照明(IBL: Image Based Lighting)是一种允许使用环境贴图来照亮场景的技术。 它近似于从所有周围反射并来自各个方向的光,以稍微照亮场景。 它经常用作 PBR 的一部分。

3、什么是WebGPU?

简而言之,WebGPU是 WebGL 的继承者。 它反映了计算机图形 API 的演变,并遵循 Vulkan、Metal 和 DirectX 12 的方法及其较低级别和更明确的设计。

如果你对图形 API 的历史感到好奇,并想了解更多 WebGPU 的诞生方式以及其中采取的妥协措施,请查看这篇精彩的文章:我想谈谈 WebGPU

4、面临的挑战

有几件事对于刚接触 WebGPU 的人来说可能并不明显,对我来说当然也不明显。 当从 OpenGL 翻译代码时,我遇到了以下问题:

  • 解析 HDR 纹理 – 与 WebGPU 无关,但我以前从未这样做过,事实证明确实存在与 WebGPU 相关的问题。
  • 渲染立方体贴图 - 立方体贴图在 WebGL 中有些特定,因此如何在 WebGPU 中执行它们并不简单。
  • 渲染到立方体贴图 - 在 WebGL/OpenGL 中我们将使用帧缓冲区。 相当于什么?
  • 渲染到 mip 级别并读取特定的 mip 级别 - 要么有一个特定的 API,要么我就注定失败(剧透:API 就在那里)。

5、解析 HDR 纹理

为了解析 HDR 纹理,我使用了 Marcin Ignac 的简单但优秀的 parse-hdr 库。 我只需要处理一个问题:我的平台拒绝处理 rgba32float,但parse-hdr库依赖于 Float32Array。

我发现库 @petamoriken/float16 实现了 Float16Array,并且我成功地使用了它。所以我只是修补了parse-hdr库,然后就可以开始了。

6、渲染立方体贴图

我使用了 WebGPU 示例中的 Cubemap。 那里没有什么特别令人惊讶的。

7、渲染到立方体贴图

我忘了是如何找到它的,但诀窍是在创建视图时传递 baseArrayLayerarrayLayerCount

除此之外,它需要与相应的 OpenGL 解决方案类似的数学运算,例如准备 6 个视图矩阵,一个用于立方体贴图的每个面。

const passEncoder = commandEncoder.beginRenderPass({
  colorAttachments: [
    {
      view: cubemapTexture.createView({
        baseArrayLayer: i,
        arrayLayerCount: 1,
      }),
      loadOp: "load",
      storeOp: "store",
    },
  ],
  // ...
});

8、渲染到 mip 级别

我使用了 WebGPU 基础教程,特别是在 GPU 上生成 mip 的指南。 诀窍是仅指定与之前的 baseMipLevelmipLevelCount 不同的值。

const passEncoder = commandEncoder.beginRenderPass({
  colorAttachments: [
    {
      view: prefilterTexture.createView({
        baseArrayLayer: i,
        arrayLayerCount: 1,
        baseMipLevel: mip,
        mipLevelCount: 1,
      }),
      clearValue: [0.3, 0.3, 0.3, 1],
      loadOp: "load",
      storeOp: "store",
    },
  ],
  // ...
});

事实证明,可以使用 textureSampleLevel函数读取特定的mip级别。 这是最令人担忧的部分,因为我知道如果它不能直接在 API 中使用,就没有任何办法解决这个问题。 幸运的是,WebGPU 现在是一个成熟且严肃的 API,可以处理类似的事情。

let prefilteredColor = textureSampleLevel(
  prefilterMap, // texture
  ourSampler, // sampler
  r, // texture coordinate
  roughness * MAX_REFLECTION_LOD, // mip level
).rgb;

9、结束语

点击这里查看完整源代码。

关于渲染技术、优化、妥协等还有很多要补充的,但我不是谈论这些的最佳人选。


原文链接:PBR in WebGPU: Implementation Details

BimAnt翻译整理,转载请标明出处