WebGPU
WebGPU 是一种新的网络图形 API,它遵循现代计算机图形 API的架构,例如Vulkan、DirectX 12和Metal。Web 图形 API 范式的这种转变允许用户利用原生图形 API 带来的相同好处:由于能够让 GPU 保持忙碌的工作,更快的应用程序,更少的图形驱动程序特定的错误,以及新功能的潜力。将来实施。
WebGPU 可以说是 Web 上所有渲染 API 中最复杂的,尽管这一成本被 API 提供的性能提升和对未来支持的保证所抵消。这篇文章旨在揭开 API 的神秘面纱,让我们更容易拼凑出如何编写使用它的 Web 应用程序。
⚠️注意:这篇博文基于 2022 年 9 月 5 日的 WebGPU API,如果有任何变化,请在此处或Twitter 上告诉我,我会立即更新。
我已经准备了一个Github 存储库,其中包含您入门所需的一切。我们将逐步介绍如何使用TypeScript编写 WebGPU Hello Triangle应用程序。
查看我在WebGL上的另一篇文章,了解如何使用较旧但广泛支持的 Web 图形 API 编写图形应用程序。
设置
广泛的平台支持 WebGPU:
- Google Chrome:WebGPU 在 Chrome 中作为原始试用版提供。目前 Android 版 Chrome Canary 不支持 WebGPU。
- Mozilla Firefox:您必须使用 Beta 或 Nightly 频道。Firefox Nightly for Android 支持 WebGPU,尽管它们的实现似乎缺少一些功能。
- Microsoft Edge:WebGPU 目前可通过他们的金丝雀版本获得,但功能上与 Chrome 相同。
- Apple Safari:Safari 团队正在致力于在桌面上支持 WebGPU,但在移动方面没有任何消息。
拥有具有 WebGPU 功能的浏览器后,请安装以下内容:
- Chrome 或任何基于 Chromium 的浏览器的 Canary 版本(例如 或 ,并访问
about:flags
以启用unsafe-webgpu
. - 吉特
- 节点.js
- 文本编辑器,例如Visual Studio Code。
然后在任何终端中键入以下内容,例如VS Code 的 Integrated Terminal。
# 🐑 Clone the repo
git clone https://github.com/alaingalvan/webgpu-seed
# 💿 go inside the folder
cd webgpu-seed
# 🔨 Start building the project
npm start
有关 Node.js、包等的更多详细信息,请参阅有关设计 Web 库和应用程序的博客文章。
项目布局
随着您的项目变得越来越复杂,您将需要分离文件并将您的应用程序组织成更类似于游戏或渲染器的东西,查看这篇关于游戏引擎架构的文章和这篇关于实时渲染器架构的文章以了解更多详细信息。
├─ 📂 node_modules/ # 👶 Dependencies
│ ├─ 📁 gl-matrix # ➕ Linear Algebra
│ └─ 📁 ... # 🕚 Other Dependencies (TypeScript, Webpack, etc.)
├─ 📂 src/ # 🌟 Source Files
│ ├─ 📄 renderer.ts # 🔺 Triangle Renderer
│ └─ 📄 main.ts # 🏁 Application Main
├─ 📄 .gitignore # 👁️ Ignore Certain Files in Git Repo
├─ 📄 package.json # 📦 Node Package File
├─ 📄 license.md # ⚖️ Your License (Unlicense)
└─ 📃readme.md # 📖 Read Me!
依赖项
- gl-matrix - 一个 JavaScript 库,允许用户编写
glsl
JavaScript 代码,具有向量、矩阵等类型。虽然在此示例中未使用,但它对于编程更高级的主题(如相机矩阵)非常有用。 - TypeScript - 带有类型的 JavaScript,通过即时自动完成和类型检查使 Web 应用程序编程变得更加容易。
- Webpack - 一种 JavaScript 编译工具,用于构建缩小输出并更快地测试我们的应用程序。
概述
在此应用程序中,我们需要执行以下操作:
- 初始化 API - 检查是否
navigator.gpu
存在,如果存在,请求 aGPUAdapter
,然后请求 aGPUDevice
,并获取该设备的 defaultGPUQueue
。 - 设置框架背衬- 创建
GPUCanvasContext
并配置它以接收GPUTexture
当前框架以及您可能需要的任何其他附件(例如深度模板纹理等)。为这些纹理创建GPUTextureView
s。 - 初始化资源- 创建您的 Vertex 和 Index
GPUBuffer
s,将您的 WebGPU 着色语言 (WGSL) 着色器加载为GPUShaderModule
s,GPURenderPipeline
通过描述图形管道的每个阶段来创建您的着色器。GPUCommandEncoder
最后,使用您打算运行的渲染通道构建您的,然后GPURenderPassEncoder
使用您打算为该渲染通道执行的所有绘制调用。 - 渲染-
GPUCommandEncoder
通过调用提交您的.finish()
,并将其提交给您的GPUQueue
. 通过调用刷新画布上下文requestAnimationFrame
。 - 销毁- 使用完 API 后销毁所有数据结构。
下面将解释可以在Github 存储库中找到的片段,省略了某些部分,并且成员变量 ( this.memberVariable
) 声明为内联而没有this.
前缀,因此它们的类型更易于查看,并且此处的示例可以单独运行。
初始化 API
入口点
要访问 WebGPU API,您需要查看 global 中是否存在gpu
对象navigator
。
// 🏭 Entry to WebGPU
const entry: GPU = navigator.gpu;
if (!entry) {
throw new Error('WebGPU is not supported on this browser.');
}
适配器
适配器描述给定 GPU的物理属性,例如其名称、扩展和设备限制。
// ✋ Declare adapter handle
let adapter: GPUAdapter = null;
// 🙏 Inside an async function...
// 🔌 Physical Device Adapter
adapter = await entry.requestAdapter();
设备
设备是您访问WebGPU API核心的方式,并允许您创建所需的数据结构。
// ✋ Declare device handle
let device: GPUDevice = null;
// 🙏 Inside an async function...
// 💻 Logical Device
device = await adapter.requestDevice();
队列
队列允许您将工作异步发送到 GPU 。在撰写本文时,您只能从给定的GPUDevice
.
// ✋ Declare queue handle
let queue: GPUQueue = null;
// 📦 Queue
queue = device.queue;
框架背衬
画布上下文
为了查看您正在绘制的内容,您需要一个HTMLCanvasElement
和 从该画布设置一个画布上下文。Canvas Context 管理一系列纹理,您将使用这些纹理将最终渲染输出呈现给<canvas>
元素。
// ✋ Declare context handle
const context: GPUCanvasContext = null;
// ⚪ Create Context
context = canvas.getContext('webgpu');
// ⛓️ Configure Context
const canvasConfig: GPUCanvasConfiguration = {
device: this.device,
format: 'bgra8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
alphaMode: 'opaque'
};
context.configure(canvasConfig);
帧缓冲附件
在执行渲染系统的不同通道时,您需要写入输出纹理,无论是用于深度测试或阴影的深度纹理,还是延迟渲染器各个方面的附件,例如视图空间法线、PBR 反射率/粗糙度等.
帧缓冲区附件是对纹理视图的引用,稍后您将在我们编写渲染逻辑时看到。
// ✋ Declare attachment handles
let depthTexture: GPUTexture = null;
let depthTextureView: GPUTextureView = null;
// 🤔 Create Depth Backing
const depthTextureDesc: GPUTextureDescriptor = {
size: [canvas.width, canvas.height, 1],
dimension: '2d',
format: 'depth24plus-stencil8',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
};
depthTexture = device.createTexture(depthTextureDesc);
depthTextureView = depthTexture.createView();
// ✋ Declare canvas context image handles
let colorTexture: GPUTexture = null;
let colorTextureView: GPUTextureView = null;
colorTexture = context.getCurrentTexture();
colorTextureView = colorTexture.createView();
初始化资源
顶点和索引缓冲区
缓冲区是一个数据数组,例如网格的位置数据、颜色数据、索引数据等。当使用基于光栅的图形管道渲染三角形时,您需要 1 个或多个顶点数据缓冲区(通常称为Vertex缓冲区对象或VBO s),以及与您打算绘制的每个三角形顶点相对应的索引的 1 个缓冲区(也称为索引缓冲区对象或IBO)。
// 📈 Position Vertex Buffer Data
const positions = new Float32Array([
1.0, -1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0, 0.0
]);
// 🎨 Color Vertex Buffer Data
const colors = new Float32Array([
1.0,
0.0,
0.0, // 🔴
0.0,
1.0,
0.0, // 🟢
0.0,
0.0,
1.0 // 🔵
]);
// 📇 Index Buffer Data
const indices = new Uint16Array([0, 1, 2]);
// ✋ Declare buffer handles
let positionBuffer: GPUBuffer = null;
let colorBuffer: GPUBuffer = null;
let indexBuffer: GPUBuffer = null;
// 👋 Helper function for creating GPUBuffer(s) out of Typed Arrays
const createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
// 📏 Align to 4 bytes (thanks @chrimsonite)
let desc = {
size: (arr.byteLength + 3) & ~3,
usage,
mappedAtCreation: true
};
let buffer = device.createBuffer(desc);
const writeArray =
arr instanceof Uint16Array
? new Uint16Array(buffer.getMappedRange())
: new Float32Array(buffer.getMappedRange());
writeArray.set(arr);
buffer.unmap();
return buffer;
};
positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
着色器
WebGPU 带来了一种新的着色器语言:WebGPU 着色语言 (WGSL):
从其他着色语言转换为 WGSL 既简单又直接。该语言类似于金属着色语言 (MSL)、Rust 和 HLSL 等其他着色语言,具有 C++ 风格的装饰器,如@location(0)
Rust 风格的函数:
这是顶点着色器源:
struct VSOut {
@builtin(position) Position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@stage(vertex)
fn main(@location(0) inPos: vec3<f32>,
@location(1) inColor: vec3<f32>) -> VSOut {
var vsOut: VSOut;
vsOut.Position = vec4<f32>(inPos, 1.0);
vsOut.color = inColor;
return vsOut;
}
这是片段着色器源:
@stage(fragment)
fn main(@location(0) inColor: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(inColor, 1.0);
}
着色器模块
Shader Module是纯文本 WGSL 文件,在执行给定管道时在 GPU 上执行。
// 📄 Import or declare in line your WGSL code:
import vertShaderCode from './shaders/triangle.vert.wgsl';
import fragShaderCode from './shaders/triangle.frag.wgsl';
// ✋ Declare shader module handles
let vertModule: GPUShaderModule = null;
let fragModule: GPUShaderModule = null;
const vsmDesc = { code: vertShaderCode };
vertModule = device.createShaderModule(vsmDesc);
const fsmDesc = { code: fragShaderCode };
fragModule = device.createShaderModule(fsmDesc);
统一缓冲区
您经常需要将数据直接提供给着色器模块,为此您需要指定统一。为了在着色器中创建统一缓冲区,请在 main 函数之前声明以下内容:
struct UBO {
modelViewProj: mat4x4<f32>,
primaryColor: vec4<f32>,
accentColor: vec4<f32>
};
@group(0) @binding(0)
var<uniform> uniforms: UBO;
// ❗ Then in your Vertex Shader's main file,
// replace the 4th to last line with:
vsOut.Position = uniforms.modelViewProj * vec4<f32>(inPos, 1.0);
然后在您的 JavaScript 代码中,创建一个统一的缓冲区,就像使用索引/顶点缓冲区一样。
您将需要使用gl-matrix 之类的库,以便更好地管理线性代数计算,例如矩阵乘法。
// 👔 Uniform Data
const uniformData = new Float32Array([
// ♟️ ModelViewProjection Matrix (Identity)
1.0, 0.0, 0.0, 0.0
0.0, 1.0, 0.0, 0.0
0.0, 0.0, 1.0, 0.0
0.0, 0.0, 0.0, 1.0
// 🔴 Primary Color
0.9, 0.1, 0.3, 1.0
// 🟣 Accent Color
0.8, 0.2, 0.8, 1.0
]);
// ✋ Declare buffer handles
let uniformBuffer: GPUBuffer = null;
uniformBuffer = createBuffer(uniformData, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
管道布局
一旦你有了制服,你可以创建一个管道布局来描述执行图形管道时制服的位置。
您在这里有 2 个选项,您可以让 WebGPU 为您创建管道布局,或者从已经创建的管道中获取它:
let bindGroupLayout: GPUBindGroupLayout = null;
let uniformBindGroup: GPUBindGroup = null;
// 👨🔧 Create your graphics pipeline...
// 🧙♂️ Then get your implicit pipeline layout:
bindGroupLayout = pipeline.getBindGroupLayout(0);
// 🗄️ Bind Group
// ✍ This would be used when *encoding commands*
uniformBindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer
}
}
]
});
或者,如果您事先知道布局,您可以自己描述并在管道创建过程中使用它:
// ✋ Declare handles
let uniformBindGroupLayout: GPUBindGroupLayout = null;
let uniformBindGroup: GPUBindGroup = null;
let pipelineLayout: GPUPipelineLayout = null;
// 📁 Bind Group Layout
uniformBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {}
}
]
});
// 🗄️ Bind Group
// ✍ This would be used when *encoding commands*
uniformBindGroup = device.createBindGroup({
layout: uniformBindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer
}
}
]
});
// 🗂️ Pipeline Layout
// 👩🔧 This would be used as a member of a GPUPipelineDescriptor when *creating a pipeline*
const pipelineLayoutDesc = { bindGroupLayouts: [uniformBindGroupLayout] };
const layout = device.createPipelineLayout(pipelineLayoutDesc);
编码命令时,您可以将此制服与setBindGroup
:
// ✍ Later when you're encoding commands:
passEncoder.setBindGroup(0, uniformBindGroup);
图形管道
图形管道描述了要输入到基于光栅的图形管道执行中的所有数据。这包括:
- 🔣输入组件- 每个顶点是什么样的?哪些属性在哪里,它们如何在内存中对齐?
- 🖍️着色器模块- 执行此图形管道时您将使用哪些着色器模块?
- ✏️深度/模板状态- 你应该执行深度测试吗?如果是这样,您应该使用什么函数来测试深度?
- 🍥混合状态- 颜色应该如何在先前写入的颜色和当前颜色之间混合?
- 🔺光栅化- 光栅化器在执行此图形管道时表现如何?它会剔除面孔吗?人脸应该剔除哪个方向?
- 💾 Uniform Data - 你的着色器应该期待什么样的统一数据?在 WebGPU 中,这是通过描述Pipeline Layout来完成的。
WebGPU 具有图形管道状态的智能默认值,因此大多数时候您甚至不需要设置它的一部分:
// ✋ Declare pipeline handle
let pipeline: GPURenderPipeline = null;
// ⚗️ Graphics Pipeline
// 🔣 Input Assembly
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0, // @location(0)
offset: 0,
format: 'float32x3'
};
const colorAttribDesc: GPUVertexAttribute = {
shaderLocation: 1, // @location(1)
offset: 0,
format: 'float32x3'
};
const positionBufferDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const colorBufferDesc: GPUVertexBufferLayout = {
attributes: [colorAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
// 🌑 Depth
const depthStencil: GPUDepthStencilState = {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8'
};
// 🦄 Uniform Data
const pipelineLayoutDesc = { bindGroupLayouts: [] };
const layout = device.createPipelineLayout(pipelineLayoutDesc);
// 🎭 Shader Stages
const vertex: GPUVertexState = {
module: vertModule,
entryPoint: 'main',
buffers: [positionBufferDesc, colorBufferDesc]
};
// 🌀 Color/Blend State
const colorState: GPUColorTargetState = {
format: 'bgra8unorm'
};
const fragment: GPUFragmentState = {
module: fragModule,
entryPoint: 'main',
targets: [colorState]
};
// 🟨 Rasterization
const primitive: GPUPrimitiveState = {
frontFace: 'cw',
cullMode: 'none',
topology: 'triangle-list'
};
const pipelineDesc: GPURenderPipelineDescriptor = {
layout,
vertex,
fragment,
primitive,
depthStencil
};
pipeline = device.createRenderPipeline(pipelineDesc);
命令编码器
Command Encoder对您打算在Render Pass Encoders组中执行的所有绘制命令进行编码。完成对命令的编码后,您将收到一个可以提交到队列的命令缓冲区。
从这个意义上说,命令缓冲区类似于回调,一旦提交到队列,就会在 GPU 上执行绘制函数。
// ✋ Declare command handles
let commandEncoder: GPUCommandEncoder = null;
let passEncoder: GPURenderPassEncoder = null;
// ✍️ Write commands to send to the GPU
const encodeCommands = () => {
let colorAttachment: GPURenderPassColorAttachment = {
view: this.colorTextureView,
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store'
};
const depthAttachment: GPURenderPassDepthStencilAttachment = {
view: this.depthTextureView,
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store'
};
const renderPassDesc: GPURenderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthAttachment
};
commandEncoder = device.createCommandEncoder();
// 🖌️ Encode drawing commands
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setPipeline(pipeline);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setScissorRect(0, 0, canvas.width, canvas.height);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, colorBuffer);
passEncoder.setIndexBuffer(indexBuffer, 'uint16');
passEncoder.drawIndexed(3);
passEncoder.endPass();
queue.submit([commandEncoder.finish()]);
};
使成为
在 WebGPU 中渲染是一件简单的事情,只需更新您打算更新的任何制服,从您的上下文中获取下一个附件,提交您的命令编码器以执行,然后使用requestAnimationFrame
回调再次完成所有这些操作。
const render = () => {
// ⏭ Acquire next image from context
colorTexture = context.getCurrentTexture();
colorTextureView = colorTexture.createView();
// 📦 Write and submit commands to queue
encodeCommands();
// ➿ Refresh canvas
requestAnimationFrame(render);
};
结论
WebGPU 可能比其他图形 API 更难,但它是一个更符合现代显卡设计的API,因此,它不仅应该带来更快的应用程序,而且应该让应用程序持续更长时间。
我没有在这篇文章中介绍一些内容,因为它们超出了本文的范围,例如:
- 矩阵,无论是用于相机还是用于转换场景中的对象。gl-matrix是那里的宝贵资源。
- 图形管线每个可能状态的详细概述。WebGPU 类型定义在那里非常有用。
- 混合模式,直观地看到这一点会很有帮助,Anders Riggelsen 在这里写了一个工具来查看 OpenGL 的混合模式行为。
- 计算管道,如果您想尝试,请查看规范或下面的一些示例。
- 加载纹理,这可能有点涉及,下面的例子很好地介绍了这一点。
其他资源
这里有一些关于 WebGPU 的文章/项目,没有特别的顺序:
- William Usher ( @_wusher ) 的文章:从 0 到使用 WebGPU 的 glTF。
- Dzmitry Malyshau写了一篇与这篇文章类似的文章,介绍了 Mozilla FireFox 中的 WebGPU。
- Warren Moore ( @warrenm ) 写了一篇文章来帮助人们从 Metal API 过渡到WebGPU。
- Brandon Jones ( @Tojiro ) 写了一篇文章,描述了如何在 WebGPU 中编写 GLTF 渲染器。
- Learn WGPU是使用 Rust 编写 WebGPU 应用程序的介绍。
还有一些开源项目,包括:
- Austin Eng的WebGPU 示例
- Tarek Sherif ( @tsherif ) 的WebGPU 示例
- @RedCamel15的RedGPU ,为 WebGPU 编写的一系列示例。
- Three.js的WebGPU源码
- BabylonJS 的 WebGPU 源码
- WebGPU 的类型定义
- WebGPU 的一致性测试
- Dawn - WebGPU 的 C++ 实现,用于为 Chromium 的 WebGPU 实现提供动力。Carl Woffenden 发布了一个带有 WebGPU 和 Dawn 的 Hello Triangle 示例。
WebGPU 和 WebGPU Shading Language 的规范也值得一看:
您可以在此处的GitHub 存储库中找到这篇文章的所有源代码。
源博客地址:https://alain.xyz/blog/raw-webgpu