WebGPU 编码与原理(1):绘制三角形
2023年4月7日,谷歌宣布在 Chrome 用户可在 113 Beta 版本中,启用全新的 WebGPU 图形 API,支持硬件图形加速。
本系列是学习记录,尚不能称之为教程(因此可能在代码的实现上、原理的阐述上等都可能存在不合适、不严谨或错误)。该系列希望通过代码撰写以及解读代码的含义,尝试阐述 WebGPU 中相关的图形学效果的实现方法、原理。如有遗漏、错误,还请指正与赐教。
一、绘制三角形
一、gpu
GPU 是 WebGPU 的入口。它具有以下方法和属性:
requestAdapter(options)
从用户代理请求适配器。用户代理选择是否返回适配器,如果是,则根据提供的选项进行选择。
getPreferredCanvasFormat
返回用于显示8位深度的最佳 GPUTextureFormat,该系统上的标准动态范围内容。只能返回 “rgba8unorm” 或 “bgra8unorm” 。
返回值可以作为 format 传递给 GPUCanvasContext 上的 configure()调用,以确保相关的 canvas 能够有效地显示其内容。
注意:没有显示在屏幕上的 canvas 可能会也可能不会从使用这种格式中受益。
wgslLanguageFeatures
Adapters (适配器)可能在任何时候失效(“过期”)。当系统状态发生任何可能影响 requestAdapter() 调用结果的更改时,用户代理应该使以前返回的所有适配器过期。
例如:
-
添加/移除物理 Adapter(适配器)(通过插入/拔出,驱动程序更新,挂起恢复等)
-
系统的电源配置已经更改(笔记本电脑被拔下,电源设置更改,等等)
注意:
用户代理可能会经常选择使适配器过期,即使没有系统状态更改(例如,在适配器创建后的几秒或几分钟)。这有助于混淆真实的系统状态变化,并使开发人员更清楚地意识到在调用requestDevice() 之前总是有必要再次调用 requestAdapter() 。如果应用程序遇到这种情况,标准的设备丢失恢复处理应该允许它进行恢复。
const adapter = await navigator.gpu.requestAdapter();
GPU 对象分别通过 Navigator 和 WorkerNavigator 接口在 Window 和 DedicatedWorkerGlobalScope 上下文中可用,并通过 navigator.gpu 公开。
二、GPUAdapter
WebGPU 在计算机的适配器上实现图形计算和渲染工作。适配器包括物理显示适配器(GPU)或软件渲染器(CPU),每个应用程序可以通过 GPUAdapter 同时使用多个适配器。
GPUAdapter 封装了一个 adapter(适配器),并描述了它的功能(特性和限制)。
要获得一个 GPUAdapter,请使用 requestAdapter()。
WebGPU “适配器”(GPUAdapter
)是一个对象,用于标识系统上的特定 WebGPU 实现(例如,集成或离散 GPU 上的硬件加速实现,或软件实现)。 同一页面上的两个不同的 “GPUAdapter” 对象可以指代同一个底层实现,或指两个不同的底层实现(例如集成和离散 GPU)。页面可见的适配器集由用户代理决定。
(Adapter:<物理设备>允许您查询重要的设备特定详细信息,例如内存大小和功能支持。)
WebGPU “设备”(GPUDevice
)表示与 WebGPU 适配器的逻辑连接。 之所以称为“设备”,是因为它抽象了底层实现(例如视频卡)并封装了单个连接:拥有设备的代码可以充当适配器的唯一用户。作为这种封装的一部分,设备是从它创建的所有 WebGPU 对象(纹理等)的根所有者,只要设备丢失或损坏,就可以(内部)释放这些对象。 单个网页上的多个组件可以各自拥有自己的 WebGPU 设备。所有 WebGPU 的使用都是通过 WebGPU 设备或从它创建的对象完成的。 从这个意义上说,它服务于“WebGLRenderingContext”目的的一个子集; 然而,与 WebGLRenderingContext
不同的是,它不与画布对象相关联,并且大多数命令是通过“子”对象发出的。
(Device:<逻辑设备>使您可以访问 API的核心内部功能,例如创建纹理、缓冲区、队列、管道等图形数据结构。这种类型的数据结构在所有现代图形 API 中大部分都是相同的,具有非常他们之间几乎没有什么变化。
Vulkan和DirectX 12通过设备创建内存数据结构来提供对内存的控制。)
获得适配器对象后,通过adapter.requestDevice() 获取 GPUDevice设备对象,从而创建缓存(Buffer)、纹理(Texture)、渲染管线(Pipeline)、着色器模块(Shader Module)等,对适配器中的资源进行操作。
const device = await adapter.requestDevice();
三、GPUCanvasContext 和 GPUCanvasConfiguration
利用 GPUCanvasContext 将 WebGPU 的 context 与画布关联,配置 context 的 device、format 等,绘制结果显示在画布 canvas 上。不同于 ebGL,WebGPU 可以将绘制结果直接输出。
const context = canvas.getContext("webgpu");
if (!context) return;
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: "premultiplied"
});
四、createRenderPipeline
渲染管线是通过设备对象的 createRenderPipeline
方法来创建的。
创建渲染管线需要一个 GPURenderPipelineDescriptor
类型的对象作为参数。
在WebGPU中,渲染管线GPURenderPipeline 由 GPUDevice 创建,在 GPURenderPass 上进行设定。渲染管线接受一个 GPURenderPipelineDescriptor 类型 的参数,包括顶点着色器 vertex 和片元着色器 fragment。
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: device.createShaderModule({
code: triangleVertWGSL
}),
entryPoint: "main"
},
fragment: {
module: device.createShaderModule({
code: redFragWGSL
}),
entryPoint: "main",
targets: [
{
format: presentationFormat
}
]
},
primitive: {
topology: "triangle-list"
}
});
五、createCommandEncoder
在GPU中创建一个指令编码器GPUCommandEncoder,将指令写入GPU的指令缓冲区,执行指令、操控GPU显存或开发调试,最后调用 commandEncoder.finish()命令结束编码器。然后使用 commandEncoder.beginRenderPass() 函数开启一个渲染通道,这个函数接受一个GPURenderPassDescriptor 类型的参数作为渲染通道选项,其中序列 colorAttachments 用于储存图像信息。
function frame() {
const commandEncoder = device.createCommandEncoder();
if(!context) return
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView,
clearValue: {
r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: "clear",
storeOp: "store"
}
]
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(frame);
}
六、WGSL Shader
red.frag.wgsl:
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4(1.0, 0.0, 0.0, 1.0);
}
上面的代码中的:@fragment 表示一种注释,表明接下来的代码应用于 片元阶段
triangle.vert.wgsl:
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
上面的代码中的:@vertex 表示一种注释,表明接下来的代码应用于 顶点阶段
在 WGSL 中的关键字大致可以分为以下 3 种:
- 表明某种类型的关键字,例如 f16、mat2x3、vec2、vec3 等
- 一些语法对应的关键字,例如 let、if、for、flase、return 等
- 一些保留的关键字,例如 Buffer、Self、as、new、void、yield 等
WGSL 中是不存在隐式转换的
例如数字 1 表示为一个整数,而数字 1.0 则表示为一个浮点数,因此 1 和 1.0 是不相同的两种数值。
七、绘制矩形
基于以上三角形的 demo,修改 WGSL 的 Vertex,来绘制五角形。
三角形的 Vertex 顶点分别为:[0,0.5],[-0.5,-0.5],[0.5,-0.5]
同理,矩形的顶点可以为:
实现:
1、修改 vert.wgsl 中的顶点,矩形虽然是四个顶点,但绘制是需要五个顶点,第一个和最后一个相同,实现闭合。
2、绘制方式修改为 triangle-strip
3、draw 方法第一个参数修改为 5
效果:
八、绘制带孔洞的多边形(矩形)
1、带孔洞的矩形 需要明晰外多边形坐标与内多边形坐标,这里多边形都用矩形。
2、知道坐标后,需要陈列出多边形顶点:需要明晰绘制关系、闭合关系,比如这里虽然带孔洞的矩形可以分别用两个矩形,共8个顶点表示:[0.0,0,3],[0.0,0.0],[0.3,0.0],[0.3,0.3] 和 [0.1,0.2],[0.1,0.1],[0.2,0.1],[0.2,0.2]。
但是由于 WebGPU 的绘制方式 triangle-strip,所以这里的顶点使用了 19 个:
3、绘制结果如下:
4、绘制流程大致和下面视频的演示一致:
这里是使用部分梯形和三角形的方式,实现的带孔洞的绘制。其思想和 earcut 这种多边形三角剖分库多少有点相似之处。
代码仓库地址:TianWebGPU
参考链接: