WebGPU零基础入门

4,514 阅读9分钟

课件:github.com/buglas/webg…

知识点:

  • WebGPU 简介
  • WebGPU 项目搭建
  • WebGPU 绘制三角形

1-WebGPU 简介

1-1-WebGPU是什么

image.png

WebGPU 就是在web 上用JavaScript 控制GPU计算和绘制图形的标准。

WebGPU 是W3C 组织定义的新一代图形API,其定位就是全平台支持。

WebGPU 可以对接不同设备上的现代图形学API,如Direct3D、Vulkan、Metal。

相较于上一代图形API-WebGL,WebGPU更强、更快、更灵活、更现代。

1-2-WebGl 发展史

image-20220513162855571

我们所熟知的WebGL1.0 是在2011年定制的,是OpenGL ES 2.0 在web 上的实现,而OpenGL ES 2.0 又是OpenGL 2.0 在移动端的具体实现。所以,WebGL1.0 对标的是OpenGL 在2004 年的标准。

同样的原理,虽然经过几年的发展,WebGL2.0 出现了,但它对标的实际是OpenGL 在2010 年的标准。

由此可见,WebGL 是在使用一个十几年前的框架体系。而OpenGL 这个标准本身就非常古老了,已经有将近30年的历史。

这也就导致了WebGL 即无法满足现代GPU 设备的框架体系,也无法满足现代高性能计算和渲染需求。

因此,在2015年左右,Apple、Microsoft、Khronos 分别推出了基于全新GPU 框架的Matelasse、Direct3D和Vulkan,这3个API 被大家统称为现代图形学API。

现在,Matelasse、Direct3D和Vulkan这3个API 被大家统称为现代图形学API。

WebGPU 在设计新的Web 端图形学标准的时候,便是参考了这3个现代图形学API,抛弃了OpenGL。

1-3-WebGPU在主流浏览器上的支持状况

截止到现在2022年5月:

  • Chrome,建立了Dawn 开源项目, 使用C++ 让Chrome 从v94开始,对WebGPU 进行适配,仅支持桌面端。
  • FireFox,建立gfx-rs 开源项目,对WebGPU 进行适配,在Nightly Build 版本里支持桌面端和Android 客户端。
  • Safari,在Safari的内核WebKit 中对WebGPU 进行适配,在Technology Preview 版本里支持IOS和Mac端。
  • Edge和国产浏览器,因为使用了谷歌的Chromium 内核,没有独立开发WebGPU的打算,大概率会选择在WebGPU 正式版本公布后,直接用Dawn 适配WebGPU。

1-4-JavaScript是如何操作本地设备的

image-20220518103345770

现代浏览器中的web页面会被独立分配到一个独立的渲染进程中,处于一个相对独立的沙盒环境。

Web 页面本身是没有能力和权利去调动硬件底层的API的。

我们只能通过一系列被规范定义的JavaScript API 跟浏览器进行沟通,让浏览器会通过进程间的通讯,也就是Inter-Process Communication(IPC) 将js 的命令传递给一个独立进程的Native Module。

Native Module 会通过真正的Native API 操作系统底层或者设备的API,比如读取GPS、蓝牙、网络、文件等,操作的结果会通过IPC 返回给JavaScript 进程。

像这种Web端向Native端发起请求并获取结果的IO操作一般都是异步的,大量的异步操作便是WebGPU的特点之一。

在这里我们要知道,真正的图形操作都是在Native 进程中进行的,而Web端只负责向Native 端发起命令,接收结果。

因此,js 进程就没有必要同步的等待Native进程的结束,它可以发送完请求后,先去做点别的,等接收到了结果,再考虑下一步怎么走。

一般浏览器的内核对这种异步操作都会做大量优化,我们可以充分的利用各个线程间的CPU 资源,比如主线程和渲染线程。

1-5-WebGPU 在Chrome 中的工作原理

image-20220518120145411

我们之前说过,Chrome对WebGPU的适配工作,是由Chrome 负责的。

当我们要在JavaScript 中使用WebGPU API 时,就要在Dawn进程中调用本地的Dawn模块,然后通过Dawn 调用操作系统底层的GPU API,比如Windows 的D3D12,IOS和Mac 的Metal,Linux和Android的Vulkan。

WebGPU的API大部分是支持异步的,这类似于js对于本地硬件的IO 操作。

WebGPU 在绘制和渲染图形的时候, 有些API 是有返回结果的,比如对GPU的请求;也有些是没有返回结果的,比如渲染命令。

高性能的GPU在绘图的时候,都是有上万个渲染管道并行的,其每一帧的渲染结果若是都返回给js ,会很耗内存,所以我们直接将其渲染到画布上,而不做存储。

WebGPU在设计的时候,更多的是参考了Metal的API,采取了一个叫commandEncode的设计概念,它允许用户可以在js 中提前写好绘图指令,然后通过Dawn 提交到GPU 中运行。

1-6-WebGPU 工作流程

1.初始化WebGPU

  • 获取GPU实例
  • 配置WebGPU的上下文对象

2.创建GPU 渲染管线

  • 编写shader程序,然后放入渲染管线中
  • 对渲染管线进行初始配置

3.编写绘图指令,并提交给GPU

  • 建立指令编码器
  • 通过指令编码器建立渲染通道
  • 将渲染管线放入渲染通道
  • 将指令编码器中的绘图指令提交给GPU

1-7-WebGPU 中的图形结构

WebGPU 中的图形结构和着色原理跟WebGL 是差不多的,或者说现代图形学API中的图形结构和着色原理都是差不多的。

WebGPU 中的图形结构有点、线、三角面三种,根据绘图逻辑的不同,线和面还可以再细分,这个我们会在后面的课程里详解。

image-20220519223135557

1-8-WebGPU 是着色原理

WebGPU 的着色原理和WebGL 一样,它依旧还是一个光栅引擎,通过顶点着色器定形,然后使用片元着色器逐片元着色。

其基本的着色流程如下:

image-20220519225415776

接下来咱们在代码中演示一下这个流程。

2-WebGPU 项目搭建

2-1-WebGPU 开发准备

浏览器:Google Chrome Canary

编辑器:VSCode

代码构建工具:Vite

开发语言:

  • JavaScript
  • HTML/CSS
  • TypeScript
  • WGSL

系统环境:

  • Node.js
  • npm
  • Git

2-2-WebGPU 开发模板

1.我们可以直接从git 中下载WebGPU 开发模板。

在终端运行以下命令:

# Clone the repo
git clone https://github.com/buglas/webgpu-lesson.git

2.进入项目模板,安装依赖,运行项目

# Go inside the folder
cd webgpu-lesson

# Start installing dependencies
npm install #or yarn

# Run project at localhost:3000
npm run dev #or yarn run dev

3.在readme.md 文件中可以了解项目结构。

├─ 📂 node_modules/ # Dependencies

│ ├─ 📁 @webgpu # WebGPU types for TS

│ └─ 📁 ... # Other dependencies (TypeScript, Vite, etc.)

├─ 📂 src/ # Source files

│ ├─ 📁 shaders # Folder for shader files

│ └─ 📄 *.ts # TS files for each demo

├─ 📂 samples/ # Sample html

│ └─ 📄 *.html # HTML entry for each demo

├─ 📄 .gitignore # Ignore certain files in git repo

├─ 📄 index.html # Entry page

├─ 📄 LICENSE # MIT

├─ 📄 logo.png # Orillusion logo image

├─ 📄 package.json # Node package file

├─ 📄 tsconfig.json # TS configuration file

├─ 📄 vite.config.js # vite configuration file

└─ 📄 readme.md # Read Me!

简单说一下这个项目中比较重要的几个文件。

  • src 中包含了ts 源码和wgsl 着色文件。
  • samples 中包含了所有的HTML 文件,这些HTML文件会作为入口引用文件,其中会引入ts。
  • package.json 中表明项目的基础信息和依赖文件。
{
    "name": "webgpu-lesson",
    "version": "0.1.0",
    "description": "webgpu-lesson",
    "scripts": {
        "dev": "vite",
        "build": "tsc && vite build",
        "preview": "vite preview"
    },
    "devDependencies": {
        "@webgpu/types": "^0.1.15",
        "typescript": "^4.5.4",
        "vite": "^2.8.0"
    },
    "dependencies": {
        "gl-matrix": "^3.4.3"
    }
}

在devDependencies中的@webgpu/types 便是WebGPU在TypeScript 中的官方定义包。

  • tsconfig.json 是ts 配置文件,在types 中需要引入vite和WebGPU 的定义包。
{
    "compilerOptions": {
        "target": "ESNext",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": ["ESNext", "DOM"],
        "moduleResolution": "Node",
        "strict": true,
        "sourceMap": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
        "noEmit": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "types": ["vite/client", "@webgpu/types"]
    },
    "include": ["src"]
}

3-绘制三角形

下面这个三角形就是我们接下来要绘制的。

image-20220519192421905

1.建顶点着色程序。

  • src/shaders/triangle.vert.wgsl
@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);
}

对于着色程序,我们后面会详说,这里我们先知道,顶点着色器中建立了3个顶点,由这3个顶点可以连成一个三角形。

2.建立片元着色程序

  • src/shaders/red.frag.wgsl
@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

上面定义了一个红色的片元颜色,稍后便会将此颜色填充到由顶点着色器定义好的图形中。

3.建立一个ts 文件,引入上面的着色程序。

  • src/triangle.ts
import triangle from "./shaders/triangle.vert.wgsl?raw"
import redFrag from "./shaders/red.frag.wgsl?raw"

4.建立HTML入口文件,引入上面的ts文件。

  • examples/triangle.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Orillusion | Basic Triangle</title>
    <style>
        html,
        body {
            margin: 0;
            width: 100%;
            height: 100%;
            background: #000;
            color: #fff;
            display: flex;
            text-align: center;
            flex-direction: column;
            justify-content: center;
        }
        p{
            font-size: 14px;
            margin:0
        }
        canvas {
           width: 100%;
           height: 100%; 
        }
    </style>
</head>
<body>
    <canvas></canvas>
    <script type="module" src="/src/triangle.ts"></script>
</body>
</html>

接下来在triangle.ts 中用WebGPU 绘图。

5.初始化WebGPU

async function initWebGPU(canvas: HTMLCanvasElement) {
    // 判断当前设备是否支持WebGPU
    if (!navigator.gpu) throw new Error("Not Support WebGPU")
    // 请求Adapter对象,GPU在浏览器中的抽象代理
    const adapter = await navigator.gpu.requestAdapter({
        /* 电源偏好
            high-performance 高性能电源管理
            low-power 节能电源管理模式 
        */
        powerPreference: "high-performance",
    })
    if (!adapter) throw new Error("No Adapter Found")
    //请求GPU设备
    const device = await adapter.requestDevice()
    //获取WebGPU上下文对象
    const context = canvas.getContext("webgpu") as GPUCanvasContext
    //获取浏览器默认的颜色格式
    const format = navigator.gpu.getPreferredCanvasFormat()
    //设备分辨率
    const devicePixelRatio = window.devicePixelRatio || 1
    //canvas尺寸
    const size = {
        width: canvas.clientWidth * devicePixelRatio,
        height: canvas.clientHeight * devicePixelRatio,
    }
  canvas.width = size.width
    canvas.height =size.height
    //配置WebGPU
    context.configure({
        device,
        format,
        // Alpha合成模式,opaque为不透明
        alphaMode: "opaque",
    })

    return { device, context, format, size }
}

6.创建渲染管线

async function initPipeline(
    device: GPUDevice,
    format: GPUTextureFormat
): Promise<GPURenderPipeline> {
    const descriptor: GPURenderPipelineDescriptor = {
        // 顶点着色器
        vertex: {
            // 着色程序
            module: device.createShaderModule({
                code: triangle,
            }),
            // 主函数
            entryPoint: "main",
        },
        // 片元着色器
        fragment: {
            // 着色程序
            module: device.createShaderModule({
                code: redFrag,
            }),
            // 主函数
            entryPoint: "main",
            // 渲染目标
            targets: [
                {
                    // 颜色格式
                    format: format,
                },
            ],
        },
        // 初始配置
        primitive: {
            //绘制独立三角形
            topology: "triangle-list",
        },
        // 渲染管线的布局
        layout: "auto",
    }
    // 返回异步管线
    return await device.createRenderPipelineAsync(descriptor)
}

7.编写绘图指令,并提交给GPU

function draw(
    device: GPUDevice,
    context: GPUCanvasContext,
    pipeline: GPURenderPipeline
) {
    // 创建指令编码器
    const commandEncoder = device.createCommandEncoder()
    // GPU纹理视图
    const view = context.getCurrentTexture().createView()
    // 渲染通道配置数据
    const renderPassDescriptor: GPURenderPassDescriptor = {
        // 颜色附件
        colorAttachments: [
            {
                view: view,
                // 绘图前是否清空view,建议清空clear
                loadOp: "clear", // clear/load
                // 清理画布的颜色
                clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
                //绘制完成后,是否保留颜色信息
                storeOp: "store", // store/discard
            },
        ],
    }
    // 建立渲染通道,类似图层
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    // 传入渲染管线
    passEncoder.setPipeline(pipeline)
    // 绘图,3 个顶点
    passEncoder.draw(3)
    // 结束编码
    passEncoder.end()
    // 结束指令编写,并返回GPU指令缓冲区
    const gpuCommandBuffer = commandEncoder.finish()
    // 向GPU提交绘图指令,所有指令将在提交后执行
    device.queue.submit([gpuCommandBuffer])
}

8.使用WebGPU绘图

async function run() {
    const canvas = document.querySelector("canvas")
    if (!canvas) throw new Error("No Canvas")
    // 初始化WebGPU
    const { device, context, format } = await initWebGPU(canvas)
    // 渲染管道
    const pipeline = await initPipeline(device, format)
    // 绘图
    draw(device, context, pipeline)

    // 自适应窗口尺寸
    window.addEventListener("resize", () => {
    canvas.width=canvas.clientWidth * devicePixelRatio
    canvas.height=canvas.clientHeight * devicePixelRatio
    context.configure({
        device,
        format,
        alphaMode: "opaque",
    })
    draw(device, context, pipeline)
    })
}
run()