WebGPU入门学习(1)-- 基本概念和绘制三角形

252 阅读5分钟

WebGPU

WebGPU是WebAssembly的一部分,它允许在Web浏览器中使用底层GPU功能。

  • WebGPU的目标是提供一种跨平台、高性能的图形和计算解决方案,可以在Web应用程序中使用。
  • WebGPU的设计受到了现代GPU架构的启发,它提供了一组底层的API,允许开发者直接控制GPU的操作,从而实现更高效、更灵活的GPU计算。
  • WebGPU可以用于多种应用场景,包括游戏、虚拟现实、数据可视化、机器学习等。它是Web技术发展的一个重要方向,可以为Web应用程序带来更强大的图形和计算能力。

通用模型

image.png

WebGPU的架构包括以下几个部分:
  • GPU设备:负责执行渲染和计算任务,通过向GPU内存中分配和管理内存来处理数据;
  • GPU内存:GPU专用的内存,用于存储渲染和计算所需的数据;
  • GPU适配器:连接GPU设备和GPU内存的接口,负责管理GPU设备的配置和数据传输;
  • GPU命令缓冲区:用于记录GPU命令队列中的绘制和计算任务,并在GPU设备上执行这些任务;
  • WebGPU API:提供给Web开发者的API接口,用于创建、管理和销毁GPU资源,以及提交渲染和计算任务。

渲染流程

image.png

WebGPU的渲染流程包括以下几个步骤:

  1. 顶点着色器:顶点序列;
  2. 图元装配:将顶点着色器输出的图元组装成最终的几何图形;
  3. 光栅化:将几何图形转换为像素,并计算每个像素的颜色值;
  4. 片段着色器:计算每个像素的颜色、深度和模板值;
  5. 深度测试和模板测试:确定像素是否可见,并处理遮挡关系;
  6. 混合和颜色输出:将多个像素的颜色值进行混合,并输出到屏幕上。

绘制一个三角形

简单了解完理论知识,接下来开始实践,绘制一个三角形。可以分成简单的三个步骤:

  1. 获取WebGPU上下文
  2. 构建渲染管线
  • 顶点处理阶段:在该阶段中,顶点着色器(vertex shader)接受GPU输入的位置数据并使用像旋转、平移或透视等特定的效果将顶点在3D空间中定位,然后,这些顶点会被组装成基本的渲染图元,例如三角形等,然后通过GPU进行光栅化,计算出每个顶点应该覆盖在canvas上的哪些像素。
  • 片元处理阶段:发生的是像素着色。
  1. 录制绘制命令

获取webGPU上下文

async function initWebGPU(canvas:HTMLCanvasElement){
    if(!navigator.gpu){
        throw new Error("不支持webgpu");
    }
    const adapter = await navigator.gpu.requestAdapter();
    if(!adapter){
        throw new Error("获取不到adapter")
    }
    const device = await adapter.requestDevice();
    if(!device){
        throw new Error('获取不到device')
    }
    const format = navigator.gpu.getPreferredCanvasFormat();
    const context = canvas.getContext('webgpu');
    if(!context){
        throw new Error('获取不到context')
    }
    context.configure({
        device,
        format
    })
    return {device,format,context}
 }

构建渲染管线

先简单了解下坐标系和着色器语言

坐标系:在webGPU中,坐标系原点在画布中心,x轴从左向右,y轴从下到上,z轴比较特殊,垂直画布,朝向屏幕内

image.png

着色器语言:WGSL。WGSL着色器语言具有以下特点:

  • 高效的计算模型:WGSL采用了更高效的计算模型,可以在GPU上进行高效的并行处理。这使得开发者可以利用GPU的强大计算能力,以实现高性能的图形渲染。
  • 统一的标准:WGSL是WebGPU API的一部分,它提供了一套统一的标准,使得开发者可以更容易地编写跨浏览器的图形应用程序。
  • 高级语言特性:WGSL着色器语言具有高级语言特性,例如变量作用域、类型推导、函数等。这些特性使得开发者可以更方便地编写着色器代码,减少了编写和维护着色器代码的工作量。
  • 可扩展性:WGSL着色器语言支持自定义扩展,这使得开发者可以针对特定的图形应用进行优化,以满足不同的需求。

顶点着色器

  • 定义三角形顶点位置
const vertexCode = `
@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 vertex = {
       module: device.createShaderModule({
            code: vertexCode,
        }),
        entryPoint: 'main', // 指定入口函数名
 }

片元着色器

  • 定义颜色
const fragCode = `
@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}`
  • 创建片元着色器
const  fragment = {
        module:  device.createShaderModule({
            code: fragCode
        }),
        entryPoint: 'main',
        targets: [{
          format
        }]
}
  • 创建渲染管线
const pipeline = await device.createRenderPipelineAsync({
      vertex,
      fragment,
      layout: 'auto'
})

录制绘制命令

    const commandEncoder = device.createCommandEncoder()
    const view = context.getCurrentTexture().createView()
    const renderPassDescriptor: GPURenderPassDescriptor = {
      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)
    passEncoder.draw(3)
    passEncoder.end()
    device.queue.submit([commandEncoder.finish()])

渲染结果

image.png

完整代码

const vertexCode = `
@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 fragCode = `
@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
`
class Triangle {
  async draw() {
    // 初始化
    const { device, format, context } = await this.init()
    // 创建渲染管线
    const pipeline = await this.createPipeline(device, format);

    // 开始绘制
    const commandEncoder = device.createCommandEncoder()
    const view = context.getCurrentTexture().createView()
    const renderPassDescriptor: GPURenderPassDescriptor = {
      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)
    passEncoder.draw(3)
    passEncoder.end()
    device.queue.submit([commandEncoder.finish()])
  }
  async createPipeline(device: GPUDevice, format: GPUTextureFormat) {
    // 顶点
    const vertexModule = device.createShaderModule({
      code: vertexCode,
    })
    // 片元
    const fragModule = device.createShaderModule({
      code: fragCode
    })

    const pipeline = await device.createRenderPipelineAsync({
      vertex: {
        module: vertexModule,
        entryPoint: 'main',
      },
      fragment: {
        module: fragModule,
        entryPoint: 'main',
        targets: [{
          format
        }]
      },
      layout: 'auto'
    })
    return pipeline;
  }
  async init() {
    if (!navigator.gpu) {
      throw new Error('不支持wegpug')
    }
    const adapter = await navigator.gpu.requestAdapter();
    if (!adapter) {
      throw new Error('获取不到adapter')
    }
    const device = await adapter.requestDevice();
    if (!device) {
      throw new Error('获取不到device')
    }
    const format = navigator.gpu.getPreferredCanvasFormat();

    const canvas = document.getElementById('canvas') as HTMLCanvasElement;
    if (!canvas) {
      throw new Error('获取不到canvas')
    }
    const devicePixelRatio = window.devicePixelRatio || 1
    canvas.width = canvas.clientWidth * devicePixelRatio
    canvas.height = canvas.clientHeight * devicePixelRatio
    const context = canvas.getContext('webgpu') as GPUCanvasContext
    context.configure({
      device, format
    })
    return { adapter, device, format, context }
  }
}