WebGPU 入门:绘制一个三角形

news2025/1/10 4:03:34

大家好,我是前端西瓜哥。

今天我们来入门 WebGPU,来写一个图形版本的 Hello World,即绘制一个三角形。

WebGPU 是什么?

WebGPU 是一个正在开发中的潜在 Web 标准和 JavaScript API,目标是提供 “现代化的 3D 图形和计算能力”。

简单来说,WebGPU 提供一个更现代的 Web 上的图形渲染标准。

WebGPU 的出现就是为了取代 WebGL 的,因为后者的 API 实在有些过时,无法利用好现代 GPU 的一些高级特性,本身的 API 设计也较难使用。

相比 WebGL,WebGPU 有更好的性能表现,API 更底层更灵活,并支持更高级的现代特性,比如计算着色器。

毫无疑问,WebGPU 是前端图形渲染的未来,值得去学习一下。

像是以性能著称的前端图形库 PixiJS,也开始进行支持 WebGPU 的工作,并在最近发布了预览版本,声称性能将是 WebGL 的 2.5 倍。

不过目前 WebGPU 还不够成熟,仍有许多工作要做,且只有少数浏览器的最新版本直接支持或通过设置开启。

即使之后所有浏览器都支持了,旧版本浏览器还是不支持的,离大范围使用还有相当长的一段路要走。

只能说未来可期

但生产中,我们可以做一个回退机制:如果浏览器支持 WebGPU,我们用 WebGPU 去渲染,如果不支持就回滚到 WebGL。

只要在底层渲染方案上封装一层渲染器 renderer,就像 PixiJS 现在做的事情一样,个人还是比较期待它在性能上的提升的。

绘制三角形

OK,我们开始用 WebGPU 绘制一个三角形。

确保你的浏览器支持 WebGPU,建议用 Chrome,并更新到最新版本。

这里我们创建一个宽高各为 300 的 canvas 元素,用于绘制图形。

<canvas width="300" height="300"></canvas>

初始化 WebGPU 相关的一些对象。

adapter 和 device

创建一个适配器对象 adapter,适配器是一个 GPU 物理硬件设备的抽象。

const adapter = await navigator.gpu.requestAdapter();

requestAdapter() 方法会查看系统上所有可用的 GPU 设备,并选择其中合适的适配器。该方法可以传一些参数,去按条件匹配。比如 { powerPreference: 'low-power' } 表示优先使用低能耗的 GPU。

此外,这个方法返回的是一个 Promise,即它是 异步的,需要用 await 的方式去等待异步的结果。

然后基于 adapter,调用 requestDevice 方法拿到设备对象 device。

device 可以理解为 adapter 的一个会话。做个比喻的话 adapter 是一个公司,device 是一个具体干活的人。

const device = await adapter.requestDevice();

requestDevice() 方法也可以传入配置项,去开启一些高级特性,或是指定一些硬件限制,比如最大纹理尺寸。

配置 canvas

类似 canvas 2d 和 webgl,我们需要通过 canvas 元素拿到上下文。

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('webgpu');

接着是调用 ctx.configure() 方法配置刚刚声明的 device 对象和像素格式。

const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
// 给上下文配置 device 对象和
ctx.configure({
  device,
  format: canvasFormat,
});

navigator.gpu.getPreferredCanvasFormat() 会返回当前环境合适的像素格式的字符串标识,通常是 ‘bgra8unorm’,表示用 8 位无符号整数来表示蓝色、绿色、红色和透明度四个分量。

设置背景色

创建命令编码器 GPUCommandEncoder 实例,它用于编码需要提交给 GPU 的命令。

const encoder = device.createCommandEncoder();

开启一个新的渲染通道(Render Pass),这里清空颜色缓冲区时填充了一个浅蓝色背景。

和 WebGL 一样,使用 RGBA 的格式,每个分量为 0 到 1 的范围,比如 { r: 1, g: 0, b: 0, a: 1 } 表示红色,或者你可以用数组的形式 [1, 0, 0, 1]

const pass = encoder.beginRenderPass({
  // 颜色附件,一个用于存储渲染输出颜色数据的纹理
  colorAttachments: [
    {
      // 要渲染到的目标
      view: ctx.getCurrentTexture().createView(),
      // 渲染前清空颜色缓冲区
      loadOp: 'clear',
      // 清除颜色为浅蓝色,不设置会默认使用黑色
      clearValue: { r: 0.6, g: 0.8, b: 0.9, a: 1 },
      // 渲染结果会被保留在纹理中,后序好绘制到 canvas 上
      storeOp: 'store',
    },
  ],
});

我们先不绘制三角形,看看背景的渲染效果,为此我们提前执行下面代码:

// 这里是绘制三角形的代码,之后会实现

pass.end(); // 完成指令队列的记录
const commandBuffer = encoder.finish(); // 结束编码
device.queue.submit([commandBuffer]); // 提交给 GPU 命令队列

远峰蓝。

创建缓冲区

先说说 WebGPU 的坐标系,它和 WebGL 一样,原点在画布中心,x 轴向右,y 轴向上,取值范围都是 -1 到 1。

声明顶点数据。这些顶点为组成三角形的三个坐标。

const vertices = new Float32Array([
  -0.5, -0.5,
  0.5, -0.5,
  0.5, 0.5,
]);

然后创建顶点缓冲区:

const vertexBuffer = device.createBuffer({
  // 标识,字符串随意写,报错时会通过它定位
  label: 'Triangle Vertices',
  // 缓冲区大小,这里是 24 字节。6 个 4 字节(即 32 位)的浮点数
  size: vertices.byteLength,
  // 标识缓冲区用途(1)用于顶点着色器(2)可以从 CPU 复制数据到缓冲区
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});

label 方便我们定位错误位置:

接着是将顶点数据复制到缓冲区:

device.queue.writeBuffer(vertexBuffer, /* bufferOffset */ 0, vertices);

参数 bufferOffset 表示缓冲区偏移多少字节数的位置写入数据。

读取方式

设置缓冲区的读取方式。

const vertexBufferLayout = {
  // 每组读 8 个字节。一个坐标为两个浮点数(2 * 4字节)
  arrayStride: 2 * 4, 
  attributes: [
    {
      // 指定数据格式,这样 WebGPU 才知道该如何解析,格式为 2 个 32位浮点数
      format: 'float32x2',
      offset: 0, // 从每组的第一个数字开始
      shaderLocation: 0, // 顶点着色器中的位置
    },
  ],
};

attributes 是一个数组,这里我们只有顶点要读,所以只有一个数组元素。如果引入了颜色值并和顶点放在一起,我们就要多声明一个数组元素,并将 offset 指定到颜色的位置。

这个对象此时还没用到,后面设置渲染流水线时会用到。

着色器

声明 WebGPU 的着色器,创建着色器模块(GPUShaderModule)。

WebGPU 使用特有的 WGSL 着色器语言,顶点着色器和片元着色器可以写在一起的。

// 创建着色器模块
const vertexShaderModule = device.createShaderModule({
  label: 'Vertex Shader',
  code: `
    @vertex
    fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f {
      return vec4f(pos, 0, 1);
    }

    @fragment
    fn fragmentMain() -> @location(0) vec4f {
      return vec4f(1, 0, 0, 1);
    }
  `,
});

顶点着色器函数

@vertex 
fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f {
  return vec4f(pos, 0, 1);
}
  • @vertex:装饰器,表示顶点着色器主函数;
  • @location(0):缓冲区读取方式设置的 shaderLocation,这里拿到了两个浮点数
  • vec2f:两个浮点数的向量,同理,vec4f 为 4 浮点数的向量。
  • -> @builtin(position):表示函数的返回值会被设置为内置的顶点位置变量。WebGPU 是利用函数的返回值配合修饰符的方式进行内部变量赋值的。

片元着色器

@fragment
fn fragmentMain() -> @location(0) vec4f {
  return vec4f(1, 0, 0, 1); // 红色
}
  • @fragment 表示片元着色器主函数
  • -> @location(0) 表示将返回的颜色输出到位置为 0 的颜色附件上,简单来说,就是给对应点设置为对应颜色。

渲染流水线

创建渲染流水线,也就是把之前的设置组合起来,用哪个着色器的哪个函数作为入口、如何读取缓冲区等。

const pipeline = device.createRenderPipeline({
  label: 'pipeline', // 标识,定位错误用
  layout: 'auto', // 自动流水线布局
  vertex: {
    module: vertexShaderModule, // 着色器模块
    entryPoint: 'vertexMain', // 入口函数为 vertexMain
    buffers: [vertexBufferLayout], // 读取缓冲区的方式
  },
  fragment: {
    module: vertexShaderModule,
    entryPoint: 'fragmentMain',
    targets: [
      {
        format: canvasFormat, // 输出到 canvas 画布上
      },
    ],
  },
});

将渲染流水线设置到 pass 上。

pass.setPipeline(pipeline);

将缓冲区绑定到管线的第一个顶点缓冲槽(slot)。

pass.setVertexBuffer(0, vertexBuffer);

绘制图元,这里要设置绘制几组,一组是两个点,所以要处以 2。

pass.draw(vertices.length / 2);

然后就是前面讲过的收尾代码。

pass.end(); // 完成指令队列的记录
const commandBuffer = encoder.finish(); // 结束编码
device.queue.submit([commandBuffer]); // 提交给 GPU 命令队列

至此,一个三角形就画好了。

绘制结果

完整代码

线上 demo 演示:

https://codesandbox.io/s/lg4w27?file=/src/index.mjs

完整代码:

const render = async () => {
  const adapter = await navigator.gpu.requestAdapter();
  const device = await adapter.requestDevice();
  const canvas = document.querySelector('canvas');
  const ctx = canvas.getContext('webgpu');
  const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
  ctx.configure({
    device,
    format: canvasFormat,
  });

  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({
    colorAttachments: [
      {
        view: ctx.getCurrentTexture().createView(),
        loadOp: 'clear',
        clearValue: { r: 0.6, g: 0.8, b: 0.9, a: 1 },
        storeOp: 'store',
      },
    ],
  });

  // 创建顶点数据
  // prettier-ignore
  const vertices = new Float32Array([
    -0.5, -0.5,
    0.5, -0.5,
    0.5, 0.5,
  ]);

  // 缓冲区
  const vertexBuffer = device.createBuffer({
    // 标识,字符串随意写,报错时会通过它定位,
    label: 'Triangle Vertices',
    // 缓冲区大小,这里是 24 字节。6 个 4 字节(即 32 位)的浮点数
    size: vertices.byteLength,
    // 标识缓冲区用途(1)用于顶点着色器(2)可以从 CPU 复制数据到缓冲区
    // eslint-disable-next-line no-undef
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });
  // 将顶点数据复制到缓冲区
  device.queue.writeBuffer(vertexBuffer, /* bufferOffset */ 0, vertices);

  // GPU 应该如何读取缓冲区中的数据
  const vertexBufferLayout = {
    arrayStride: 2 * 4, // 每一组的字节数,每组有两个数字(2 * 4字节)
    attributes: [
      {
        format: 'float32x2', // 每个数字是32位浮点数
        offset: 0, // 从每组的第一个数字开始
        shaderLocation: 0, // 顶点着色器中的位置
      },
    ],
  };

  // 着色器用的是 WGSL 着色器语言
  const vertexShaderModule = device.createShaderModule({
    label: 'Vertex Shader',
    code: `
      @vertex
      fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f {
        return vec4f(pos, 0, 1);
      }

      @fragment
      fn fragmentMain() -> @location(0) vec4f {
        return vec4f(1, 0, 0, 1);
      }
    `,
  });

  // 渲染流水线
  const pipeline = device.createRenderPipeline({
    label: 'pipeline',
    layout: 'auto',
    vertex: {
      module: vertexShaderModule,
      entryPoint: 'vertexMain',
      buffers: [vertexBufferLayout],
    },
    fragment: {
      module: vertexShaderModule,
      entryPoint: 'fragmentMain',
      targets: [
        {
          format: canvasFormat,
        },
      ],
    },
  });

  pass.setPipeline(pipeline);
  pass.setVertexBuffer(0, vertexBuffer);
  pass.draw(vertices.length / 2);

  pass.end();
  const commandBuffer = encoder.finish();
  device.queue.submit([commandBuffer]);
};

render();

结尾

本文讲解了如何用 WebGPU 绘制一个三角形。可以看到它和 WebGL 的逻辑有很多共同之处的,都要创建缓冲区、着色器、定义读取方式。

我是前端西瓜哥,欢迎关注我,学习更多前端图形知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1057545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AutoCAD 产品设计:图形单位

本文讲解 AutoCAD 产品的图形单位功能产品设计&#xff0c;没有任何代码实现。 使用的 AutoCAD 为 2020 版本 图形单位是什么&#xff1f; 图形单位是用于设置 一些属性数据应该用什么格式显示 的命令&#xff0c;命令标识为 un&#xff08;units&#xff09;。 举个例子。 …

操作EXCEL计算3万条数据的NDVI并填入

Python操作EXCEL&#xff0c;计算3万条数据的NDVI并填入 问题描述 现在是有构建好了的查找表&#xff0c;不过构建了3万条数据&#xff0c;在excel中手动计算每行的NDVI值太麻烦了&#xff0c;也不会操作。 就试试python吧&#xff0c;毕竟python自动处理大型EXCEL数据很方便…

黑马头条项目环境搭建

注册中心网关配置 spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: truecorsConfigurations:[/**]:allowedHeaders: "*"allowedOrigins: "*"allowedMethods:- GET- POST- DELETE- PUT- OPTIONroutes:# 平台管理- id: useruri: lb://…

51单片机可调幅度频率波形信号发生器( proteus仿真+程序+原理图+报告+讲解视频)

51单片机可调幅度频率信号发生器( proteus仿真程序原理图报告讲解视频&#xff09; 讲解视频1.主要功能&#xff1a;2.仿真3. 程序代码4. 原理图4. 设计报告5. 设计资料内容清单&&下载链接***[资料下载链接](https://docs.qq.com/doc/DS1daV1BKRXZMeE9u)*** 51单片机可…

数据结构——计数与归并非递归

排序算法 前言一、归并的非递归实现二、计数排序三、序算法复杂度及稳定性分析总结 前言 重要的事说三遍&#xff01; 学习&#xff01;学习&#xff01;学习&#xff01; 努力!努力!努力&#xff01; 一、归并的非递归实现 代码实现&#xff1a; void MergeSortNonR(int* a,…

3分钟在移动盘上安装Ubuntu系统和ROS2

目录 原视频准备烧录 一个usb移动固态硬盘可以干什么呢&#xff1f; 可以用移动盘解决电脑存储空间不足的问题&#xff0c;可以用移动盘存储数据&#xff0c;可以用移动盘装其他系统当做双系统来使用&#xff0c;可以在一个移动固态硬盘里装两个甚至更多的系统… 下面&#xf…

《C++ primer plus》精炼(OOP部分)——对象和类(8)

学习是一项持续的投资&#xff0c;永远不会白费——本杰明富兰克林 文章目录 第13章&#xff1a;类继承一个基类和派生类公有继承的逻辑关系&#xff1a;is-a多态公有继承 第13章&#xff1a;类继承 一个基类和派生类 从一个类派生出另一个类时&#xff0c;原始类称为基类&am…

SLAM简要介绍

过去二十年&#xff0c;计算机视觉和机器学习领域都取得了很多进步。这一切都归功于硬件的改进&#xff0c;这使得研究人员和工程师能够设计和训练更复杂和更准确的模型&#xff0c;同时以以前难以想象的规模处理和分析数据。随着硬件的这些改进&#xff0c;未知环境的映射已经…

基于Java的学校固定资产管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

vue 使用 创建二维数组响应数据 渲染 echarts图标

目前我遇到的情况就是用动态的二维数组数据渲染echarts图标&#xff0c;我们从后端收到的接口一般是个一维数组&#xff0c;需要手动构建并且保证响应式。接下来我做了个案例 一、案例总逻辑 1. 先创建一个vue项目 2. 添加 echarts依赖 3. 模拟数据请求&#xff0c;构建二维数组…

支付宝2023年收单外包服务机构评级启动,截止11月15日

9月22日消息&#xff0c;支付宝近日发布公告称&#xff0c;已启动2023年收单外包服务机构评级工作。支付宝表示&#xff0c;收单外包服务机构评级工作是金融监管部门规范引导收单外包服务市场的重要举措&#xff0c;其结果将会向社会公示&#xff0c;直接关系到外包机构的业务开…

QT 之数据库 QSqlQuery CURD 实战

零、参考文档 https://doc.qt.io/archives/qt-6.0/qsqldatabase.html 一、开发环境 Ubuntu 20.04 QT6.0 Microsoft SQL Server 2022 Developer Edition (64-bit) 先修改 /etc/odbc.ini 的数据源配置&#xff0c;指定连接数据库 vdb&#xff0c; sudo vim /etc/odbc.ini[mss…

《发现的乐趣》作者费曼(读书笔记)

目录 一、书简介 二、作者理查德•费曼 费曼式思维 教育与传承 三、个人思考 四、笔记 科学家眼中的花之美 关于偏科 父亲教育我的方式 知道一个概念和真正懂得这个概念有很大区别 我没有义务去成全别人对我的期望 诺贝尔奖——够格吗&#xff1f; 探究世界的游戏规…

基于SpringBoot的商品物品产品众筹平台设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

内网综合扫描工具-fscan的安装和使用

简介 一款内网综合扫描工具&#xff0c;方便一键自动化、全方位漏扫扫描。 支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写公钥、计划任务反弹shell、读取win网卡信息、web指纹识别、web漏洞扫描、netbios探测、域控识别等功能。相当强大&#xff01;&…

JavaSE | 初始Java(九) | 包的使用

包 包是对类、接口等的封装机制的体现&#xff0c;是一种对类或者接口等的很好的组织方式&#xff0c;比如&#xff1a;一个包中的类不想被其他包中的类使用。包还有一个重要的作用&#xff1a;在同一个工程中允许存在相同名称的类&#xff0c;只要处在不同的包中即可。 可以…

【Spring】Spring 创建和使用

Spring 创建和使用 一. 创建 Spring 项目1. 创建⼀个 Maven 项目2. 添加 Spring 框架⽀持3. 添加启动类 二. 存储 Bean 对象1. 创建 Bean2. 将 Bean 注册到容器 三. 获取并使⽤ Bean 对象1. 创建 Spring 上下文2. 获取指定的 Bean 对象3. 使用 Bean Spring 就是⼀个包含了众多⼯…

HDF5文件数据读取

1、HDF5文件说明 HDF 是用于存储和分发科学数据的一种自我描述、多对象文件格式。HDF 是由美国国家超级计算应用中心(NCSA)创建的,以满足不同群体的科学家在不同工程项目领域之需要。HDF 可以表示出科学数据存储和分布的许多必要条件。HDF 被设计为: 自述性:对于一个HDF …

Koa学习4:密码加密、验证登录、颁发token、用户认证

请求体 这里遇到了个问题&#xff0c;ctx.request.body 的值是一个字符串。明明已经使用了koa-body中间件 查了一下原因是&#xff1a; ctx.request.body的值可能是一个对象或一个字符串&#xff0c;取决于请求的Content-Type和请求体的格式。 当使用koa-body中间件时&#x…

专业PDF编辑阅读工具PDF Expert mac中文特点介绍

PDF Expert mac是一款专业的PDF编辑和阅读工具。它可以帮助用户在Mac、iPad和iPhone等设备上查看、注释、编辑、填写和签署PDF文档。 PDF Expert mac软件特点 PDF编辑&#xff1a;PDF Expert提供了丰富的PDF编辑功能&#xff0c;包括添加、删除、移动、旋转、缩放、裁剪等操作…