别再说你不会WebGPU了

9,683 阅读5分钟

前言

2023年了,还是抛弃webGL吧,学一学新时代的webGPU,本文将几分钟让你快速掌握核心原理、流程、以及快速开发WebGPU。

WebGPU

image.png

为什么要用WebGPU

webgpu是面向现代的web处理gpu的图形API,由于之前的webgl的2.0对应的opengl的是2010版本,gpu显卡的迅猛发展让厂商开始慢慢抛弃openGL的老旧存在缺陷的api,为了适应新的时代发展,才有了最新的webgpu的发展,底层抛弃了opengl,D3D12(D3D12 [Direct3D 12]是Microsoft开发的图形API)、Metal(Apple)、Vulkan(Khronos Group开发的跨平台的图形API)所实现。在编译阶段处理大量的cpu的损耗。

device.queue.submit([commandEncoder.finish()]);

同时在webgpu可以通过多线程处理,在web worker中可以单独引入device的队列,submit你的指令集。

WebGPU 和 WebGL 区别

  1. 上下文区分,首先webGPU的上下文是区分多个,而不是webgl只有一个上下文。每块单独负责一个区域的工作, 所以你再也不用像之前返回的webgl的gl上下文,从这个gl中处理所有工作。
  2. 性能好,与现有用于浏览器图形加速的WebGL相比,WebGPU将底层接口从老旧的OpenGL升级到了最新的Direct3D 12、Vulkan和Metal,所以这也使得它既拥有了比过去高得多的执行效率。
  3. 更高效的api,webgl开发绘制多个图形,你要做循环大量的初始gl定义,而webgpu可以保存之前buffer设置等
  4. ......

渲染流程

  • GPUDevice创建资源,例如纹理和缓冲区。
  • GPUCommandEncoder允许对单个命令进行编码,包括渲染和计算过程。
  • 完成后,它变成GPUCommandBuffer对象,可以提交给GPUQueueGPU 执行。
  • 可以将渲染结果呈现给 HTML 画布

上下文

之前我们获取webgl的上下文使用canvas.getContext('webgl'),那么同样在webGPU如何获取上下文呢?

// 获取 webgpu 上下文
const context = canvas.getContext('webgpu') as GPUCanvasContext;

初始化

  const adapter = await navigator.gpu.requestAdapter(); // 物理设备
  const device = await adapter.requestDevice(); // 逻辑设备
  const context = canvas.getContext('webgpu') as GPUCanvasContext;

  // 移动端dpr适配
  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',  
  });

在context.configure为alphaMode指定字符串 ‘opaque’。alphaMode设置的是 Canvas 和 HTML 元素背景的混合方式。如果设置为’opaque’,则用 WebGPU 绘图内容完全覆盖。也可以为alphaMode 设置为 ‘premultiplied’ (相当于alpha预乘),在这种情况下,作为 WebGPU 绘图的结果,如果画布像素的 alpha 小于 1,则该像素将是画布和 HTML 元素背景混合的颜色。

兼容性

image.png

image.png

所以你要开发WebGPU,你的浏览器必须使用edge或chrome,并升级到113版本以上的开发者版本

插件

image.png

vscode提供wgsl文件更好的高亮支持的插件

绘制一个三角形

import { makeSample, SampleInit } from '../../components/SampleLayout';

import triangleVertWGSL from '../../shaders/triangle.vert.wgsl';
import redFragWGSL from '../../shaders/red.frag.wgsl';

const init: SampleInit = async ({ canvas, pageState }) => {
  const adapter = await navigator.gpu.requestAdapter();
  const device = await adapter.requestDevice();

  if (!pageState.active) return;
  const context = canvas.getContext('webgpu') as GPUCanvasContext;

  // 移动端dpr适配
  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',
  });

  // 渲染管线
  const pipeline = device.createRenderPipeline({
    layout: 'auto', // 控制gpu如何执行渲染管线
    vertex: {
      module: device.createShaderModule({
        code: triangleVertWGSL,
      }),
      entryPoint: 'main',
    },
    fragment: {
      module: device.createShaderModule({
        code: redFragWGSL,
      }),
      entryPoint: 'main',
      targets: [
        {
          format: presentationFormat,
        },
      ],
    },
    // 绘制图元的结构配置
    primitive: {
      topology: 'triangle-list', // 定义的基本图元类型
    },
  });

  // 绘制
  function frame() {
    if (!pageState.active) return;

    const commandEncoder = device.createCommandEncoder();
    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',
        },
      ],
    };

    // 这里可以理解为webgl的program 程序对象
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
    passEncoder.setPipeline(pipeline);
    passEncoder.draw(3, 1, 0, 0);
    passEncoder.end();

    // 由 GPUCommandEncoder 编码的命令使用 GPUCommandEncoder.finish() 方法重新编码为 GPUCommandBuffer。然后,命令缓冲区通过 submit() 调用传递到队列中,准备由 GPU 处理。
    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
};

/**
 * 渲染入口,将init、以及其他web页面配置传入渲染页面
 */
const HelloTriangle: () => JSX.Element = () => {
  /**
   * nextjs在运行时编译配置存在的全局变量
   *
   * __filename 文件路径
   *
   * __dirname 目录路径
   *
   * __filename.substring(__dirname.length + 1) 文件名
   */
  return makeSample({
    name: 'Hello Triangle',
    description: 'Shows rendering a basic triangle.',
    init,
    sources: [
      {
        name: __filename.substring(__dirname.length + 1),
        contents: __SOURCE__,
      },
      {
        name: '../../shaders/triangle.vert.wgsl',
        contents: triangleVertWGSL,
        editable: true,
      },
      {
        name: '../../shaders/red.frag.wgsl',
        contents: redFragWGSL,
        editable: true,
      },
    ],
    filename: __filename,
  });
};

export default HelloTriangle;

这里用的是webgpu-samples的源码,是一个不错入门的开源项目,该项目用nextjs构建并同构渲染,服务端渲染页面、客户端监听路由传参请求服务端更新渲染页面。

简单的执行流程:

  1. 将device挂载到webgpu上下文,初始化gpu
  2. 开始写入我的着色器的执行方式,将着色器源码等传入生成渲染管线
  3. 生成程序对象,传入渲染管线并定义buffer绘制方式
  4. requestAnimationFrame绘制帧

image.png

CommandEncoder 编码器

    // ...
    const commandEncoder = device.createCommandEncoder();
    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);
    // 一个打包指令要绘制3个点,0偏移缓冲区,0偏移打包指令的实例
    passEncoder.draw(3, 1, 0, 0);
    // 保存
    passEncoder.end();
    
    // 发送到gpu
    device.queue.submit([commandEncoder.finish()]);
    // ...

其实就是将缓冲区的指令打包给gpu,类似一个webgl的程序对象。所以为什么更高效,因为gl配置都可以缓存。

着色器

image.png

这里注解对应是顶点着色器和图元着色器, @builtin注入对应的全局变量,这里之前定义了draw绘制3个点,所以这里会返回3个点, -> 符号代表返回对应的全局位置信息。vec4 代表4维向量,每个向量存储浮点数32位。 在@fragment这里@location(0)对应fragment.targets数组中的第0个格式。

总结

在webgpu在未来替代webgl的可能性是很大的,相比之前的webgl更方便了我们的开发,并提供了极大的灵活性,对于复杂图形渲染带来了更好的性能提升。在Babylon已经完全支持WebGPU的支持,同时Threejs也逐步在支持。卷一卷还是有必要的。

附录

  1. 为什么我的WebGL开发这么丝滑 🌊
  2. 女朋友想学webGL修图,安排!
  3. 卷不动?学学简单的webGL矩阵算法
  4. 立体感十足的数据可视化:我的WebGL 3D环状图制作分享

本文正在参加「金石计划」