1. WebGpu的前辈WebGL
要说起webGPU就不得不提起一下他的前辈webgl.这样才能更好知道WebGPU 取代 WebGL 为什么是大势所趋。
说到 WebGL,就不得不说说 OpenGL。在早期的个人电脑中,使用最广泛的 3D 图形渲染技术是 Direct3D 和 OpenGL。Direct3D 是微软 DirectX 技术的一部分,主要用于 Windows 平台。 OpenGL 作为一种开源的跨平台技术,赢得了众多开发者的青睐。
后来一个特殊的版本——OpenGL ES,它专为嵌入式计算机、智能手机、家用游戏机和其他设备而设计。它从 OpenGL 中删除了许多旧的和无用的功能,同时添加了新功能。例如去掉了矩形等多余的多边形,只保留点、线、三角形等基本图形。这使它在保持轻巧的同时仍然足够强大以渲染精美的 3D 图形。
而 WebGL 是从 OpenGL ES 派生出来的,它专注于 Web 的 3D 图形渲染。
下图展示了它们之间的关系:
image.png
WebGL 历史
image.png
从上图可以看出,WebGL 已经很老了。不仅因为它存在已久,还因为它的标准是从 OpenGL 继承而来的。OpenGL 的设计理念可以追溯到 1992 年,而这些古老的理念其实与今天 GPU 的工作原理非常不符。
对于浏览器开发者来说,需要适配 GPU 的不同特性,这给他们带来了很多不便。虽然这些对于上层开发人员来说是看不到的。
从上图可以看出,2014 年苹果发布了 Metal。 Steve Jobs 是 OpenGL ES 的支持者,他认为这是行业的未来。所以当时苹果设备上的游戏都依赖 OpenGL ES(比如愤怒的小鸟,水果忍者),都是很经典的游戏。
但乔布斯去世后,苹果放弃了 OpenGL ES,开发了新的图形框架 Metal。
微软在 2015 年也发布了自己的 D3D12【Direct3D 12】图形框架。紧随其后的是 Khronos Group,图形界的国际组织,类似于前端圈子里的 W3C、TC39。而 WebGL 是它的标准。甚至也逐渐淡化了 WebGL,转而支持现在的 Vulkan。
迄今为止,Metal、D3D12 [Direct3D 12] 和 Vulkan 并列为现代三大图形框架。这些框架充分释放了 GPU 的可编程能力,让开发者可以最大限度的自由控制 GPU。
同样重要的是要注意,当今的主流操作系统不再支持 OpenGL 作为主要支持。这意味着今天编写的每一行 WebGL 代码都有 90% 的机会不被 OpenGL 绘制。它在 Windows 计算机上使用 DirectX 绘制,在 Mac 计算机上使用 Metal 绘制。
可见 OpenGL 已经过期了。但这并不意味着它会消失。继续在嵌入式、科学研究等特殊领域发挥作用。
WebGL 也是如此,大量的适配工作使得推进困难重重,于是推出了 WebGPU。
2. WebGPU介绍
WebGPU 是由 W3C GPU for the Web 社区组所发布的规范,目标是允许网页代码以高性能且安全可靠的方式访问 GPU 功能。
- WebGPU API 使 web 开发人员能够使用底层系统的 GPU(图形处理器)进行高性能计算并绘制可在浏览器中渲染的复杂图形。
- WebGPU 是 WebGL 的继任者,为现代 GPU 提供更好的兼容、支持更通用的 GPU 计算、更快的操作以及能够访问到更高级的 GPU 特性。
- 封装了现代图形API(Dx12、Vulkan、Metal),提供给Web 3D程序员,为 Web释放了更多的GPU 硬件的功能。
3. WebGPU架构原理
image.pngWebGPU 使用Adapter来实现从操作系统的本机图形 API 到 WebGPU 的转换层。对于使用者是通过LogicalDevice来实现对GPU的抽象,由于浏览器是可以运行多个 Web 应用程序的单一 OS 级应用程序,因此需要多路复用,以便每个 Web 应用程序感觉就像它拥有对 GPU 的唯一控制权。这就是逻辑设备抽象的作用。
4、着色器
使用过WebGL的都知道,我们要告诉GPU需要绘制什么是通过着色器和片段着色器,需要你将数据缓冲区上传到 GPU,并告诉它如何将该数据解释为一系列三角形。每个顶点占据该数据缓冲区的一块,描述该顶点在 3D 空间中的位置,但可能还包括颜色、纹理 ID、法线和其他内容等辅助数据。列表中的每个顶点都由 GPU 在顶点阶段处理,在每个顶点上运行顶点着色器,这将应用平移、旋转或透视变形。
着色器: “着色器”这个词曾经让我感到困惑,因为你可以做的不仅仅是着色。但在过去(即 1980 年代后期!),这个术语是恰当的:它是在 GPU 上运行的一小段代码,用于决定每个像素应该是什么颜色,这样你就可以对正在渲染的对象进行着色,实现灯光和阴影的错觉。如今,着色器泛指在 GPU 上运行的任何程序。
GPU 现在对三角形进行光栅化,这意味着 GPU 会计算出每个三角形在屏幕上覆盖的像素。然后每个像素由片段着色器处理,它可以访问像素坐标,也可以访问辅助数据来决定该像素应该是哪种颜色。如果使用得当,可以使用此过程创建令人惊叹的 3D 图形。
这种将数据传递到顶点着色器,然后到片段着色器,然后将其直接输出到屏幕上的系统称为管道,在 WebGPU 中,你必须明确定义管道。
5. 管线
管线(pipeline)是一个逻辑结构,其包含在你完成程序工作的可编程阶段。WebGPU 目前能够处理两种类型的管线:
-
渲染管线用于渲染图形,通常渲染到
<canvas>
元素中,但它也可以在画面之外的地方渲染图形。它有两个主要阶段:- 顶点着色阶段:在该阶段中,顶点着色器(vertex shader)接受 GPU 输入的位置数据并使用像旋转、平移或透视等特定的效果将顶点在 3D 空间中定位。然后,这些顶点会被组装成基本的渲染图元,例如三角形等,然后通过 GPU 进行光栅化,计算出每个顶点应该覆盖在 canvas 上的哪些像素。
- 片元着色阶段:在该阶段中,片元着色器(fragment shader)计算由顶点着色器生成的基本图元所覆盖的每个像素的颜色。这些计算通常使用输入,如图像(以纹理的方式)提供表面细节以及虚拟化光源的位置和颜色。
-
计算管线用于通用计算。计算管线包含单独的计算阶段,在该阶段中,计算着色器(compute shader)接受通用的数据,在指定数量的工作组之间并行处理数据,然后将结果返回到一个或者多个缓冲区。这些缓冲区可以包含任意类型的数据。
6.命令编码器
命令编码器(mmand encoder),的意思是说你可以把你想让 GPU 执行所有的命令都压到命令编码器里面,让命令编码器把所有的这些命令编在一起,然后存成命令缓存,然后再发送到 GPU 里面去执行。这样设计的好处就是把数据编码这种cpu擅长的事情交给cpu处理,这样可以减少和GPU的通信开销。提高效率。
7.WebGPU作流程和代码实践
有了上面这些知识点,就可以来看看WebGPU的一个工作流程。
image.png根据这个流程,我们就可以上手来写一个简单的demo,通过编码来体会它的工作机制。直接上代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas></canvas>
<script type="module">
const triangleVert = `
@vertex
fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
`;
const redFrag = `
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
`;
// initialize webgpu device & config canvas context
async function initWebGPU(canvas) {
if (!navigator.gpu) throw new Error("Not Support WebGPU");
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
// powerPreference: 'low-power'
});
if (!adapter) throw new Error("No Adapter Found");
const device = await adapter.requestDevice({
requiredFeatures: ["texture-compression-bc"],
requiredLimits: {
maxStorageBufferBindingSize:
adapter.limits.maxStorageBufferBindingSize,
},
});
// 获取canvas
const context = canvas.getContext("webgpu");
// 获取浏览器默认的颜色格式
const format = navigator.gpu.getPreferredCanvasFormat();
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const size = { width: canvas.width, height: canvas.height };
context.configure({
// json specific format when key and value are the same
device,
format,
// prevent chrome warning
alphaMode: "opaque",
});
return { device, context, format, size };
}
// create a simple pipiline
async function initPipeline(device, format) {
const descriptor = {
layout: "auto",
vertex: {
module: device.createShaderModule({
code: triangleVert,
}),
entryPoint: "main",
},
primitive: {
topology: "triangle-list", // try point-list, line-list, line-strip, triangle-strip?
},
fragment: {
module: device.createShaderModule({
code: redFrag,
}),
entryPoint: "main",
targets: [
{
format: format,
},
],
},
};
return await device.createRenderPipelineAsync(descriptor);
}
// create & submit device commands
function draw(device, context, pipeline) {
const commandEncoder = device.createCommandEncoder();
const view = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [
{
view: view,
clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
loadOp: "clear", // clear/load
storeOp: "store", // store/discard
},
],
};
const passEncoder =
commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
// 3 vertex form a triangle
passEncoder.draw(3);
passEncoder.end();
// webgpu run in a separate process, all the commands will be executed after submit
device.queue.submit([commandEncoder.finish()]);
}
async function run() {
const canvas = document.querySelector("canvas");
if (!canvas) throw new Error("No Canvas");
const { device, context, format } = await initWebGPU(canvas);
const pipeline = await initPipeline(device, format);
// start draw
draw(device, context, pipeline);
// re-configure context on resize
window.addEventListener("resize", () => {
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
// don't need to recall context.configure() after v104
draw(device, context, pipeline);
});
}
run();
</script>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
background: #000;
color: #fff;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
}
canvas {
width: 100%;
height: 100%;
}
</style>
</body>
</html>
参考
- [1] WebGPU深入探索
- [2] WebGPU API
- [3] webgpu入门
网友评论