WebGPU入门(一)--认识 WebGPU

2023-05-17  本文已影响0人  晨钟暮鼓_7bc3

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 功能。

3. WebGPU架构原理

image.png

WebGPU 使用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 目前能够处理两种类型的管线:

image.png

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>

参考

上一篇 下一篇

猜你喜欢

热点阅读