一部2024新的WebGPU教程,作者Shi Yan。内容很好,翻译过来与大家共享,内容上会有改动,加上自己的理解。更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加VX:digital_twin123
创建空画布一开始可能看起来比较枯燥,但对于任何涉及 WebGPU 编程的项目来说,它都是一个重要的起点。在本节中,我们将为全书的所有编程练习奠定基础。
环境设置
首先,我们将定义一个基本的 HTML 文件作为我们的基础,该文件仅包含很少的内容:
<html>
<body>
</body>
</html>
为了查看这个文件,我们将设置一个本地 HTTP 服务器,我们可以利用Nodejs中http-server服务来启动。启动之后,我们就会在 Chrome 浏览器中看到我们的 HTML 页面加载。一定要确保使用的是最新版本的 Chrome,因为 WebGPU 是一项相对较新的功能,老版本的浏览器可能不支持。
这里需要注意一下,如果我们不使用 HTTPS 的域提供 WebGPU 页面,Chrome 可能会阻止 WebGPU 运行。之所以存在这种预防措施,是因为 WebGPU 被设计为专门在安全环境中运行。但是,localhost 是一个特殊的域,即使通过 HTTP 访问,也被视为安全上下文,这样就使本地开发变得更加容易。
Linux 用户注意事项:在撰写本文时,WebGPU 是 Linux 上 Chrome 中的一项实验性功能,需要手动启用。要在 Chrome 中启用 WebGPU,请使用以下命令从终端启动 Chrome:
google-chrome --enable-unsafe-webgpu --enable-features=Vulkan,UseSkiaRenderer
环境设置完毕后,我们现在可以开始编写 WebGPU 项目了。如前所述,我们的主要编程任务将围绕定义各种管道和有效管理资源。现在,让我们从基础知识开始,创建一个空白画布,一个没有任何渲染元素的背景。
初始化WebGPU
我们首先将画布标签添加到 HTML 文件中,然后添加一个脚本标签来容纳我们的 JavaScript 代码。我们的首要任务是验证 WebGPU 的可用性。鉴于此功能是新功能,较旧的浏览器可能不支持它,所以在生产环境中,通过显示适当的错误消息来正确处理这种情况至关重要。
<html>
<body>
<canvas id="canvas" width="640" height="480"></canvas>
</body>
<script>
async function webgpu() {
if (!navigator.gpu) {
console.error("WebGPU is not available.");
return;
}
}
webgpu();
</script>
</html>
鉴于许多 WebGPU 函数的异步性质,我们将把代码封装在名为 webgpu()
的异步函数中。首先,我们执行检查以确定 navigator.gpu
是否未定义。此情况适用于不支持 WebGPU 的旧版浏览器。
if (!navigator.gpu) {
console.error("WebGPU is not available.");
showWarning("WebGPU support is not available. A WebGPU capable browser is required to run this sample.");
throw new Error("WebGPU support is not available");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.error("Failed to request Adapter.");
return;
}
接下来,我们继续通过 navigator.gpu
获取适配器,然后通过适配器adapter
获取设备device
。说实话,与 WebGL 相比,这个过程可能显得有些冗长,在 WebGL 中,使用单个句柄(称为 glContext
)足以进行交互。在这里,navigator.gpu
充当 WebGPU 领域的入口点。适配器本质上是实现 WebGPU API 的软件组件的抽象。它与之前介绍的驱动程序的概念相似。但是,考虑到WebGPU本质上是由Web浏览器实现的API,而不是由GPU驱动程序直接提供的,因此适配器可以被设想为浏览器内的WebGPU软件层。在 Chrome 中,适配器由“Dawn”子系统提供。值得注意的是,我们可以使用多个适配器,提供来自不同供应商的不同实现,甚至包括面向调试的虚拟适配器。这些适配器会生成详细的调试日志,但是没有实际的渲染功能。随后,适配器会产生一个设备,它是该适配器的实例。这里可以与 JavaScript 进行类比,其中适配器可以比作一个类,设备可以比作从该类实例化的对象。
该规范强调需要在适配器请求后立即请求设备,因为适配器的有效期有限。虽然在不了解内部工作原理的情况下,适配器失效的内部工作原理仍然有些模糊,但这对于软件开发人员来说并不是一个关键问题。规范中引用了一个适配器失效的例子:拔掉笔记本电脑的电源会导致适配器失效。当笔记本电脑转换到电池模式时,操作系统可能会激活省电措施,使某些 GPU 功能失效。一些笔记本电脑甚至拥有用于不同电源状态的双 GPU,这可能会在它们之间切换时触发类似的失效。根据规范,此行为的其他原因还包括驱动程序更新等。
通常,在请求设备时,我们需要指定一组所需的功能。然后适配器用匹配的设备进行响应。这个过程可以比喻为向类构造函数提供参数。对于本示例,我选择请求默认设备。在接下来的章节中,我将讨论使用功能标志查询设备,并提供更全面的示例。
let device = await adapter.requestDevice();
if (!device) {
console.error("Failed to request Device.");
return;
}
const context = canvas.getContext('webgpu');
const canvasConfig = {
device: device,
format: navigator.gpu.getPreferredCanvasFormat(),
usage:
GPUTextureUsage.RENDER_ATTACHMENT,
alphaMode: 'opaque'
};
context.configure(canvasConfig);
获取设备后,下一步是配置上下文以确保画布已正确设置。这涉及指定颜色格式、透明度首选项和一些其他选项。上下文配置是通过提供画布配置结构来实现的。
format
参数用于指定在画布上渲染结果的像素格式,我们在这里使用默认格式:与画布提供的纹理的“缓冲区使用”有关。在这里,我们指定 RENDER_ATTACHMENT
来表示该画布作为渲染目标。我们将在接下来的章节中解决缓冲区使用的复杂性。最后,alphaMode
参数提供了一个用于调整画布透明度的切换开关。
let colorTexture = context.getCurrentTexture();
let colorTextureView = colorTexture.createView();
let colorAttachment = {
view: colorTextureView,
clearValue: {
r: 1, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store'
};
const renderPassDesc = {
colorAttachments: [colorAttachment]
};
后面,我们的重点将转移到配置渲染通道(render pass)。渲染通道充当指定渲染目标的容器,包含彩色图像和深度图像等元素。打个比方,渲染目标就像我们要在上面绘图的一张纸。但是它和我们刚刚配置的画布有什么不同呢?
如果你以前使用过 Photoshop,那么可以把画布视为包含多个图层的图像文档。每一层都可以比作一个渲染目标。同样,在3D渲染中,我们有时无法使用单个图层完成渲染,因此我们需要多次渲染。每个渲染会话(称为渲染通道Pass)会将结果输出到专用渲染目标。最后,我们将这些结果组合起来并将其显示在画布上。
我们的第一步是从画布上获取纹理。在渲染系统中,此过程通常通过交换链(swap chain,跨多个帧的缓冲区列表)来实现。图形子系统回收这些缓冲区,以消除不断创建缓冲区的需要。因此,在开始渲染之前,我们必须从画布获取可用的缓冲区(纹理)。
接下来,我们生成一个链接到纹理的视图。那么纹理(texture)和纹理视图(texture view)之间的区别是什么呢?与普遍看法不同,纹理不一定是单个图像,它也可以包含多个图像。例如,在 mipmap 的上下文中,每个 mipmap 级别都可以作为单独的图像。mipmap 是同一图像在不同细节级别的金字塔。 mipmap 对于提高纹理贴图采样质量非常有用。我们将在后面的章节中讨论 mipmap。此处的关键点是告诉大家纹理并不等同于图像,在这种情况下,我们需要单个图像(视图)作为渲染目标。
然后我们创建一个 colorAttachment
,它充当渲染通道中的颜色目标。颜色附件可以被认为是保存颜色信息或像素的缓冲区。虽然我们之前将渲染目标比作一张纸,但它通常由多个缓冲区组成,而不仅仅是一个。这些额外的缓冲区充当用于各种目的的暂存空间,并且通常是不可见的,存储不一定代表像素的数据。一个常见的例子是深度缓冲区,用于确定哪些像素最接近观看者,从而实现遮挡等效果。尽管我们可以在此设置中包含深度缓冲区,但我们的简单示例仅旨在使用纯色清除画布,从而不需要深度缓冲区。
我们来分解一下colorAttachment
的参数:
clearValue
表示用于清除此缓冲区的颜色。loadOp
指示渲染通道加载此颜色附件时执行的操作。有两种选择:clear
表示使用默认颜色清除缓冲区,可以避免数据复制。load
表示从缓冲区加载现有数据。
storeOp
规定了保存像素时执行的操作。选项有:discard
表示丢弃生成的像素。虽然这似乎违反直觉,但它具有其他用武之地。例如,我们可能想要更新深度缓冲区而不改变颜色缓冲区。store
表示将生成的像素输出到渲染目标。
commandEncoder = device.createCommandEncoder();
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
在最后阶段,我们创建一个命令(command )并将其提交给 GPU 执行。这个特定的命令很简单:设置视口尺寸以匹配画布的尺寸。由于我们没有绘制任何内容,因此将使用默认的clearValue
(由我们的loadOp
指定)清除渲染目标。
调试方法
在开发过程中,建议使用独特的颜色进行调试,比如我们经常选择红色而不是更传统的黑色或白色,这是因为黑色和白色是许多情况下使用的常见默认颜色。例如,默认网页背景通常是白色的。使用白色作为透明颜色可能会产生误导,可能会掩盖渲染是否实际发生或者画布是否完全丢失。通过选择鲜艳的红色,我们可以确保提供清晰的视觉指示,表明渲染操作确实正在发生。这种方法提供了成功执行的明确信号,从而更容易识别和解决开发过程中可能出现的任何问题。
调试 GPU 代码比调试 CPU 代码更具挑战性。由于 GPU 操作的并行性质,从 GPU 执行生成日志非常复杂。这种复杂性也使得传统的调试方法(例如设置断点和暂停执行)变得不切实际。在这种情况下,颜色成为一种非常宝贵的调试工具。通过将不同的颜色与不同的含义相关联,我们可以增强准确解释结果的能力。随着后续章节的进展,我们将探讨各种示例,展示颜色如何作为 GPU 编程中的基本调试辅助工具。
此外,经验丰富的图形程序员还采用其他策略来增强代码的可读性、可维护性和可调试性:
- 描述性变量命名:图形 API 可能很冗长,整个源代码中似乎都有重复的代码块。使用变量的详细描述性名称有助于有效地识别和导航代码。
- 增量开发:建议从简单开始,逐渐构建复杂性。通常首先渲染纯色对象,然后再添加更复杂的效果。
- 一致的编码模式:在代码中建立并遵循一致的编码模式可以显着提高可读性并减少错误。
- 模块化设计:将复杂的渲染任务分解为更小的、可管理的函数或模块可以使代码更易于理解和维护。
通过采用这些实践,即使面对图形编程带来的困难挑战,开发人员也可以创建更健壮、可读且易于调试的 GPU 代码。
本章的在线编码工具:https://shi-yan.github.io/webgpuunleashed/code/code.html#1_00_empty_canvas