WebGPU 基础 (WebGPU Fundamentals)

21 阅读20分钟

WebGPU 基础 (WebGPU Fundamentals)

本文将尝试向你教授 WebGPU 的最基本基础知识。

在阅读本文之前,默认你已经了解 JavaScript。本文将广泛使用数组映射(mapping arrays)、解构赋值(destructuring assignment)、展开运算符(spreading values)、async/await、es6 模块等概念。如果你还不了解 JavaScript 并想学习它,请参阅 JavaScript.info、Eloquent JavaScript 和/或 CodeCademy。

如果你已经了解 WebGL,请阅读这篇文章

WebGPU 是一个允许你执行 2 项基本操作的 API:

  1. 在纹理(textures)上绘制三角形/点/线
  2. 在 GPU 上运行计算(computations)

仅此而已!

除此之外,关于 WebGPU 的一切都取决于你。这就像学习 JavaScript、Rust 或 C++ 等计算机语言一样。首先你学习基础知识,然后由你创造性地利用这些基础知识来解决你的问题。

WebGPU 是一个极低级别的 API。虽然你可以制作一些简单的示例,但对于许多应用来说,它可能需要大量的代码和严的数据组织。例如,支持 WebGPU 的 three.js 包含约 550k 字节的压缩 JavaScript,而这仅仅是其基础库。这还不包括加载器(loaders)、控制器(controls)、后处理(post-processing)和许多其他功能。同样,TensorFlow 的核心加上 WebGPU 后端约为 600k 字节的压缩 JavaScript,且不包括对各种可选功能的持。

重点是,如果你只是想在屏幕上显示某些东西,最好选择一个能提供大量代码的库,因为如果你自己动手,就必须编写这些代码。

另一方面,也许你有自定义的使用场景,或者你想修改现有库,或者你只是好奇它是如何工作的。在这些情况下,请继续阅读!

入门 (Getting Started)

很难决定从哪里开始。在某种程度上,WebGPU 是一个非常简单的系统。它所做的只是在 GPU 上运行 3 种类型的函数:顶点着色器(Vertex Shaders)、片元着色器(Fragment Shaders)和计算着色器(Compute Shaders)。

顶点着色器计算顶点。着色器返回顶点位置。对于顶点着色器函数返回的每组 3 个顶点,都会在这 3 个位置之间绘制一个三角形。[1]

片元着色器计算颜色。[2] 当绘制三角形时,对于要绘制的每个像素,GPU 都会调用你的片元着色器。片元着色器随后返回一种颜色。

计算着色器更通用。它实际上只是一个你调用的函数,并说“执行这个函数 N 次”。GPU 在每次调用函数时都会传递迭代次数,因此你可以使用该数字在每次迭代中执行独特的操作。

如果你仔细观察,可以认为这些函数类似于传递给 array.forEacharray.map 的函数。你在 GPU 上运行的函数就是函数,就像 JavaScript 函数一样。不同之处在于它们运行在 GPU 上,因此为了运行它们,你需要将希望它们访问的所有数据以缓冲区(buffers)和纹理(textures)的形式复制到 GPU,并且它们只能输出到这些缓冲区和纹理。你需要在函数中指定函数将查找数据的绑定(bindings)或位置(locations)。而且,在 JavaScript 中,你需要将持有数据的缓冲区和纹理绑定到这些绑定或位置。完成这些操作后,你告诉 GPU 执行该函数。

关于这张图需要注意的地方:

  • 有一个 管线(Pipeline) 。它包含了 GPU 将运行的顶点着色器和片元着色器。你也可以拥有包含计算着色器的管线。
  • 着色器通过 绑定组(Bind Groups) 间接引用资源(缓冲区、纹理、采样器)。
  • 管线定义了通过内部状态间接引用缓冲区的属性(Attributes)。
  • 属性从缓冲区中提取数据并将其输入到顶点着色器中。
  • 顶点着色器可能会将数据输入到片元着色器中。
  • 片元着色器通过渲染通道描述(render pass description)间接写入纹理。

要在 GPU 上执行着色器,你需要创建所有这些资源并设置这些状态。资源的创建相对直接。有趣的一点是,大多数 WebGPU 资源在创建后不能更改。你可以更改它们的内容,但不能更改它们的大小、用法、格式等。如果你想更改这些内容,你需要创建一个新资源并销毁旧资源。

某些状态是通过创建并执行 命令缓冲区(command buffers) 来设置的。命令缓冲区正如其名,它们是命令的缓冲区。你创建 命令编码器(command encoders) 。编码器将命令编码到命令缓冲区中。然后你完成编码器,它会返回创建的命令缓冲区。接着你可以提交该命令缓冲区,让 WebGPU 执行这些命令。

以下是编码命令缓冲区的一些伪代码,以及生成的命令缓冲区的表示:

JavaScript

encoder = device.createCommandEncoder()
// 绘制某些东西
{
  pass = encoder.beginRenderPass(...)
  pass.setPipeline(...)
  pass.setVertexBuffer(0, …)
  pass.setVertexBuffer(1, …)
  pass.setIndexBuffer(...)
  pass.setBindGroup(0, …)
  pass.setBindGroup(1, …)
  pass.draw(...)
  pass.end()
}
// 绘制其他东西
{
  pass = encoder.beginRenderPass(...)
  pass.setPipeline(...)
  pass.setVertexBuffer(0, …)
  pass.setBindGroup(0, …)
  pass.draw(...)
  pass.end()
}
// 计算某些东西
{
  pass = encoder.beginComputePass(...)
  pass.setBindGroup(0, …)
  pass.setPipeline(...)
  pass.dispatchWorkgroups(...)
  pass.end();
}
commandBuffer = encoder.finish();

一旦创建了命令缓冲区,你就可以提交它来执行:

JavaScript

device.queue.submit([commandBuffer]);

前面显示的“WebGPU 设置简化图”表示命令缓冲区中单个绘制命令的状态。执行命令将设置内部状态,然后绘制命令将告诉 GPU 执行顶点着色器(并间接执行片元着色器)。dispatchWorkgroup 命令将告诉 GPU 执行计算着色器。

我希望这能让你对需要设置的状态有一些心理映射。如上所述,WebGPU 可以做 2 件基本事情:

  1. 在纹理上绘制三角形/点/线
  2. 在 GPU 上运行计算

我们将详细讲解执行这两件事的小示例。其他文章将展示向这些事物提供数据的各种方法。请注意,这将非常基础。我们需要建立这些基础。稍后我们将展示如何使用它们来执行人们通常使用 GPU 执行的操作,如 2D 图形、3D 图形等。

在纹理上绘制三角形 (Drawing triangles to textures)

WebGPU 可以将三角形绘制到纹理上。就本文而言,纹理是像素的 2D 矩形。[3] <canvas> 元素代表网页上的一个纹理。在 WebGPU 中,我们可以向画布请求一个纹理,然后渲染到该纹理。

为了使用 WebGPU 绘制三角形,我们必须提供 2 个“着色器”。再次强调,着色器是运行在 GPU 上的函数。这两个着色器是:

  1. 顶点着色器 (Vertex Shaders) :计算用于绘制三角形/线/点的顶点位置的函数。
  2. 片元着色器 (Fragment Shaders) :计算在绘制三角形/线/点时,要绘制/光栅化的每个像素的颜色(或其他数据)的函数。

让我们从一个非常小的 WebGPU 程序开始画一个三角形。

我们需要一个画布来显示我们的三角形:

<canvas></canvas>

然后我们需要一个 <script> 标签来存放我们的 JavaScript:

<canvas></canvas>
<script type="module">
  ... javascript goes here ...
</script>

下面的所有 JavaScript 都将放在这个 script 标签内。

WebGPU 是一个异步 API,因此在异步函数中使用它是最简单的。我们首先请求一个适配器(adapter),然后从适配器请求一个设备(device)。

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();
  if (!device) {
    fail('need a browser that supports WebGPU');
    return;
  }
}
main();

上面的代码相当直白。首先,我们使用 ?. 可选链操作符请求适配器,这样如果 navigator.gpu 不存在,适配器将是 undefined。如果它存在,我们将调用 requestAdapter。它异步返回结果,所以我们需要 await。适配器代表特定的 GPU。某些设备有多个 GPU。

从适配器中,我们请求设备,同样使用 ?.,这样如果适配器恰好是 undefined,设备也将是 undefined。如果设备未设置,可能是用户使用了旧浏览器。

接下来,我们查找画布并为其创建 webgpu 上下文。这将让我们获得一个要渲染到的纹理。该纹理将用于在网页中显示画布。

// 从画布获取 WebGPU 上下文并进行配置
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
  device,
  format: presentationFormat,
});

同样,上面的代码非常直白。我们从画布获取 "webgpu" 上下文。我们询问系统首选的画布格式是什么。这将是 "rgba8unorm" 或 "bgra8unorm"。它是什么并不重要,但查询它会使用户的系统运行得更快。

我们将该格式作为 format 通过调用 configure 传递到 webgpu 画布上下文中。我们还传入了 device,这将此画布与我们刚刚创建的设备关联起来。

接下来,我们创建一个着色器模块。着色器模块包含一个或多个着色器函数。在我们的例子中,我们将创建一个顶点着色器函数和一个片元着色器函数。

const module = device.createShaderModule({
  label: 'our hardcoded red triangle shaders',
  code: /* wgsl */ `
    @vertex
    fn vs(
      @builtin(vertex_index) vertexIndex : u32
    ) -> @builtin(position) vec4f {
      let pos = array(
        vec2f( 0.0,  0.5),  // 顶部中心
        vec2f(-0.5, -0.5),  // 左下角
        vec2f( 0.5, -0.5)   // 右下角
      );
      return vec4f(pos[vertexIndex], 0.0, 1.0);
    }

    @fragment
    fn fs() -> @location(0) vec4f {
      return vec4f(1.0, 0.0, 0.0, 1.0);
    }
  `,
});

着色器是用一种称为 WebGPU 着色语言 (WGSL) 的语言编写的,通常发音为 wig-sil。WGSL 是一种强类型语言,我们将在另一篇文章中尝试更详细地介绍。目前,我希望通过一些解释,你可以推断出一些基础知识。

注意:在本网站中,存储 WGSL 的字符串前面都有 /* wgsl */ 注释。这是一种约定,旨在帮助文本编辑器尝试对 WGSL 进行语法高亮和/或提供智能提示。

上面我们看到一个名为 vs 的函数使用了 @vertex 属性声明。这指定它为一个顶点着色器函数。

@vertex
fn vs(
  @builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
  ...

它接受一个我们命名为 vertexIndex 的参数。vertexIndex 是一个 u32,意思是 32 位无符号整数。它从名为 vertex_index 的内置变量(builtin)中获取值。vertex_index 就像一个迭代次数,类似于 JavaScript 的 Array.map(function(value, index) { ... }) 中的 index。如果我们通过调用 draw 告诉 GPU 执行此函数 10 次,第一次 vertex_index 将为 0,第二次为 1,第三次为 2,依此类推。[4]

我们的 vs 函数声明返回一个 vec4f,它是四个 32 位浮点值的向量。可以把它看作一个包含 4 个值的数组或一个具有 4 个属性的对象,如 {x: 0, y: 0, z: 0, w: 0}。此返回值将被分配给 position 内置变量。在“三角形列表”(triangle-list)模式下,顶点着色器每执行 3 次,就会连接我们返回的 3 个位置值绘制一个三角形。

WebGPU 中的位置需要返回到 裁剪空间(clip space) 中,其中 X 从左侧的 -1.0 到右侧的 +1.0,Y 从底部的 -1.0 到顶部的 +1.0。无论我们要绘制的纹理大小如何,这都是正确的。

vs 函数声明了一个由 3 个 vec2f 组成的数组。每个 vec2f 由两个 32 位浮点值组成。

let pos = array(
  vec2f( 0.0,  0.5),  // 顶部中心
  vec2f(-0.5, -0.5),  // 左下角
  vec2f( 0.5, -0.5)   // 右下角
);

最后,它使用 vertexIndex 从数组中返回 3 个值之一。由于该函数要求返回类型为 4 个浮点值,且由于 posvec2f 数组,因此代码为剩余的 2 个值提供了 0.0 和 1.0。

return vec4f(pos[vertexIndex], 0.0, 1.0);

请注意,对于在 2D 中绘制内容,我们通常只需要位置的 x 和 y 值。z 值用于深度测试(depth testing),将在正交投影文章中提到。w 值用于透视除法(perspective divide),将在透视投影文章中提到。目前,将 z 设置为 0.0,将 w 设置为 1.0 是我们绘制三角形所需的。

着色器模块还声明了一个名为 fs 的函数,该函数使用 @fragment 属性声明,使其成为片元着色器函数。

@fragment
fn fs() -> @location(0) vec4f {

此函数不接受任何参数,并在 location(0) 返回一个 vec4f。这意味着它将写入第一个渲染目标。稍后我们将使第一个渲染目标成为我们的画布纹理。

return vec4f(1, 0, 0, 1);

代码返回 1, 0, 0, 1,即红色。WebGPU 中的颜色通常指定为 0.0 到 1.0 的浮点值,其中上述 4 个值分别对应红、绿、蓝和阿尔法(alpha)。

当 GPU 对三角形进行光栅化(用像素绘制)时,它将调用片元着色器以找出每个像素的颜色。在我们的例子中,我们只是返回红色。

还需要注意的一点是 label。几乎每个 WebGPU 对象都可以带有一个标签。标签完全是可选的,但最好给它们贴上标签。当发生错误时,大多数 WebGPU 错误会显示引发错误的对象的标签。在包含 100 个着色器模块、100 个管线、100 个缓冲区的程序中,如果没有标签,你可能会收到类似“着色器模块发生错误”的错误,这将需要大量工作才能找出具体是哪一个。如果给它们贴上标签,你会得到类似“着色器模块 '我们的硬编码红色三角形着色器' 发生错误”的错误,这更具描述性。

现在我们有了着色器模块,接下来需要创建一个渲染管线。

const pipeline = device.createRenderPipeline({
  label: 'our hardcoded red triangle pipeline',
  layout: 'auto',
  vertex: {
    module,
    entryPoint: 'vs',
  },
  fragment: {
    module,
    entryPoint: 'fs',
    targets: [{ format: presentationFormat }],
  },
});

在这种情况下,没有太多要设置的。我们将 layout 设置为 'auto',这意味着我们希望 WebGPU 从着色器中派生数据的布局。不过我们没有使用任何数据。

然后我们告诉渲染管线为顶点着色器使用着色器模块中的 vs 函数,为片元着色器使用 fs 函数。此外,我们告诉它第一个渲染目标的格式。“渲染目标”意味着我们将要渲染到的纹理。当我们创建管线时,我们必须指定最终将使用此管线进行渲染的纹理的格式。

targets 数组的元素 0 对应于我们在片元着色器返回值中指定的 location 0。稍后,我们将把该目标设置为画布的纹理。

一个快捷方式是,对于每个着色阶段 vertexfragment,如果对应类型只有一个函数,则无需指定 entryPoint。WebGPU 将使用与着色阶段匹配的唯一函数。因此我们可以简化上面的代码。

接下来,我们准备一个 GPURenderPassDescriptor,它描述了我们要绘制到哪些纹理以及如何使用它们。

const renderPassDescriptor = {
  label: 'our basic canvas renderPass',
  colorAttachments: [
    {
      // view: <- 渲染时填充
      clearValue: [0.3, 0.3, 0.3, 1],
      loadOp: 'clear',
      storeOp: 'store',
    },
  ],
};

GPURenderPassDescriptor 有一个 colorAttachments 数组,其中列出了我们要渲染到的纹理以及如何处理它们。我们将稍后填充实际要渲染到的纹理。目前,我们设置了一个半深灰色的清除值,以及 loadOpstoreOploadOp: 'clear' 指定在绘制之前将纹理清除为清除值。另一个选项是 'load',这意味着将纹理的现有内容加载到 GPU 中,以便我们可以绘制在已有内容之上。storeOp: 'store' 意味着存储我们绘制的结果。我们也可以传递 'discard',这将丢弃我们绘制的内容。我们将在另一篇文章中讨论为什么要这样做。

现在是渲染的时候了。

function render() {
  // 从画布上下文获取当前纹理
  // 并将其设置为我们要渲染到的纹理。
  renderPassDescriptor.colorAttachments[0].view =
      context.getCurrentTexture().createView();

  // 创建一个命令编码器来开始编码命令
  const encoder = device.createCommandEncoder({ label: 'our encoder' });

  // 开启渲染通道来运行着色器
  const pass = encoder.beginRenderPass(renderPassDescriptor);
  pass.setPipeline(pipeline);
  pass.draw(3);  // 调用顶点着色器 3 次
  pass.end();

  // 完成编码并提交命令
  const commandBuffer = encoder.finish();
  device.queue.submit([commandBuffer]);
}

render();

首先,我们通过调用 context.getCurrentTexture().createView() 获取画布的当前纹理视图。通过调用 context.getCurrentTexture(),我们正在获取一个将显示在网页画布中的纹理。我们还调用 createView。你可以从纹理的一部分创建视图,但在没有任何参数的情况下,它将返回最常见的默认视图。我们需要设置 colorAttachments[0].view

接下来,我们创建一个命令编码器。然后通过调用 encoder.beginRenderPass(renderPassDescriptor) 创建一个渲染通道(render pass)。渲染通道会执行我们的渲染命令。我们在 renderPassDescriptor 中传入了颜色附件,因此它将开始通过清除纹理进行渲染。

我们设置管线,然后调用 draw。由于我们将 3 传递给 draw,我们的顶点着色器将被调用 3 次,vertex_index 将分别为 0、1 和 2。由于我们的顶点着色器在每次执行时返回不同的位置,因此每组 3 个位置将产生一个三角形。

最后,我们结束通道,完成编码器以获得命令缓冲区,并提交命令缓冲区。

运行该程序,我们得到一个三角形。

[Triangle Demo Placeholder]

GPU 计算 (Running computations on the GPU)

接下来让我们看看如何利用 GPU 进行计算。

我们将使用一个简单的例子:取一些数字并将它们翻倍。

首先,我们需要一个计算着色器。

const module = device.createShaderModule({
  label: 'doubling compute shader',
  code: /* wgsl */ `
    @group(0) @binding(0) var<storage, read_write> data: array<f32>;

    @compute @workgroup_size(1)
    fn main(@builtin(global_invocation_id) id: vec3u) {
      data[id.x] = data[id.x] * 2.0;
    }
  `,
});

在这个着色器中,我们声明了一个名为 data 的变量。

@group(0) @binding(0) var<storage, read_write> data: array<f32>;

它被赋予了 @group(0)@binding(0)。它被声明为 var<storage, read_write>,这意味着它将被存储在缓冲区中,并且它是可读写的。它被定义为 array<f32>,即 32 位浮点数的数组。

然后我们定义了函数 main

@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) id: vec3u) {

我们赋予它 @compute 属性,使其成为计算着色器。我们还赋予它 @workgroup_size(1) 属性,我们将在另一篇文章中讨论它的含义。

它接受一个参数 idid 是一个 vec3u 类型,由三个 32 位无符号整数组成。它通过内置变量 global_invocation_id 获取它的值。如果你仔细观察,你可以认为这就像我们在上面谈到的 vertex_index。如果我们告诉 GPU 运行此函数 10 次,那么在第一次运行中 id.x 将为 0,第二次为 1,第三次为 2,依此类推。

代码本身非常简单:

data[id.x] = data[id.x] * 2.0;

它使用 id.x 索引到我们的数组中,并将值乘以 2。

现在我们有了着色器,我们需要创建一个计算管线。

const pipeline = device.createComputePipeline({
  label: 'doubling compute pipeline',
  layout: 'auto',
  compute: {
    module,
    entryPoint: 'main',
  },
});

正如我们之前所做的,我们将 layout 设置为 'auto'

接下来,我们需要一些数据。

const input = new Float32Array([1, 3, 5]);

由于数据是在 JavaScript 端(CPU 端),我们需要在 GPU 端创建一个缓冲区,并将数据从 JavaScript 复制到 GPU 缓冲区。

// 在 GPU 上创建一个缓冲区来保存数据
const workBuffer = device.createBuffer({
  label: 'work buffer',
  size: input.byteLength,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
// 将数据复制到缓冲区
device.queue.writeBuffer(workBuffer, 0, input);

通过传递 GPUBufferUsage.STORAGE,我们说希望该缓冲区可被用作存储。这使其与着色器中的 var<storage,...> 兼容。此外,我们希望能够将数据复制到此缓冲区,因此我们包含了 GPUBufferUsage.COPY_DST 标志。最后,我们希望能够从缓冲区复制数据,因此我们包含了 GPUBufferUsage.COPY_SRC

请注意,你无法直接从 JavaScript 读取 WebGPU 缓冲区的内容。相反,你必须对其进行“映射”(map),这是另一种向 WebGPU 请求访问缓冲区的方式,因为缓冲区可能正在使用中,并且它可能仅存在于 GPU 上。

可以映射到 JavaScript 的 WebGPU 缓冲区不能用于太多其他用途。换句话说,我们不能直接映射上面创建的缓冲区,如果我们尝试添加标志使其可映射,我们将收到一个错误,因为它与用法 STORAGE 不兼容。

因此,为了看到计算结果,我们需要另一个缓冲区。运行计算后,我们将把上面的缓冲区复制到这个结果缓冲区中,并设置其标志以便我们可以对其进行映射。

// 在 GPU 上创建一个缓冲区来获取结果的副本
const resultBuffer = device.createBuffer({
  label: 'result buffer',
  size: input.byteLength,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});

MAP_READ 意味着我们希望能够映射此缓冲区以读取数据。

为了告诉我们的着色器我们要处理的缓冲区,我们需要创建一个绑定组(bindGroup)。

// 设置一个绑定组来告诉着色器使用哪个
// 缓冲区进行计算
const bindGroup = device.createBindGroup({
  label: 'bindGroup for work buffer',
  layout: pipeline.getBindGroupLayout(0),
  entries: [
    { binding: 0, resource: { buffer: workBuffer } },
  ],
});

我们从管线获取绑定组的布局。然后设置绑定组条目。pipeline.getBindGroupLayout(0) 中的 0 对应着色器中的 @group(0)。条目中的 {binding: 0 ... 对应着色器中的 @group(0) @binding(0)

现在我们可以开始编码命令。

// 编码执行计算的命令
const encoder = device.createCommandEncoder({
  label: 'doubling encoder',
});
const pass = encoder.beginComputePass({
  label: 'doubling compute pass',
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(input.length);
pass.end();

我们创建一个命令编码器。开始一个计算通道。设置管线,然后设置绑定组。这里,pass.setBindGroup(0, bindGroup) 中的 0 对应着色器中的 @group(0)。然后我们调用 dispatchWorkgroups,在这种情况下,我们传入 input.length(即 3),告诉 WebGPU 运行计算着色器 3 次。然后结束通道。

这是执行 dispatchWorkgroups 时的情况。

计算完成后,我们要求 WebGPU 从 workBuffer 复制到 resultBuffer

// 编码将结果复制到可映射缓冲区的命令。
encoder.copyBufferToBuffer(workBuffer, 0, resultBuffer, 0, resultBuffer.size);

现在我们可以完成编码器以获得命令缓冲区,然后提交该命令缓冲区。

// 完成编码并提交命令
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);

然后我们映射结果缓冲区并获取数据的副本。

// 读取结果
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange());
console.log('input', input);
console.log('result', result);
resultBuffer.unmap();

要映射结果缓冲区,我们调用 mapAsync 并必须等待它完成。映射后,我们可以调用 resultBuffer.getMappedRange(),在不带参数的情况下它将返回整个缓冲区的 ArrayBuffer。我们将其放入 Float32Array 类型数组视图中,然后查看这些值。一个重要的细节是,getMappedRange 返回的 ArrayBuffer 仅在调用 unmap 之前有效。在 unmap 之后,它的长度将被设置为 0,其数据将不再可访问。

运行该程序,我们可以看到我们得到了结果,所有的数字都翻倍了。

[Compute Demo Placeholder]

我们将在其他文章中介绍如何真正使用计算着色器。目前,希望你已经对 WebGPU 的作用有了初步的了解。除此之外的一切都取决于你!将 WebGPU 视为类似于其他编程语言。它提供了一些基本功能,其余的留给你的创造力。

使 WebGPU 编程特别的是,这些函数(顶点着色器、片元着色器和计算着色器)运行在你的 GPU 上。GPU 可能拥有超过 10,000 个处理器,这意味着它们可以并行进行 10,000 次以上的计算,这可能比你的 CPU 并行计算能力高出 3 个或更多数量级。

(后略:关于画布调整大小等细节)