3.动态数据:顶点插槽&资源绑定

前言

第一篇文章,介绍了webGPU的环境、工具、例子以及一些知识的补充。

应该都会在想,数据是写死的,怎么灵活地处理数据?那么,这篇文章,我简单分享一下,js如何与GPU进行数据交换。

shader内存 & 渲染管线

from Orillusion; 补充:orillusion翻译的文章也挺好的:WebGPUwgsl

简单解析一下上图:

  1. 最上面一行是GPU内存数据信息
  2. 中间一行:是render pipeline渲染过程,WebGPU仅提供Vertex ShaderFragment Shader给我们,其他的是WebGPU自行完成。如果需要处理数据,那么就需要在这两个阶段处理完
  3. 最后一行:是render pipeline处理过程。实际上,上一篇文章有描述:primitive: {topology: 'triangle-list'}可以设置第二阶段的组合方式(Primitive Assmbly)

例子

例子效果:

顶点插槽

我们来简单来梳理一下,js通过顶点插槽,怎么实现数据传递。

  1. device.createBuffer,创建GPU buffer
  2. 在pipelinevertex.buffers,设置GPU buffer在vertex shader里的引用方式
  3. device.queue.writeBuffer,将js内存中的数据,写入GPU buffer
  4. passEncoder.setVertexBuffer,将GPU buffer的数据关联到当前commandEncoder中

经历以上步骤,数据才能同步,类似于js中的参数传递

那么,我们用js代码的作用域,模拟一下上面的过程(不能完全模拟,但,是类似的思想):

js 复制代码
const pc: any = {
  js: {
    data: { a: 12 }, // js中的数据
    createBuffer() {
      // 步骤1: 在gpu内存声明空间(大小、权限)
      pc.gpu.buffer = null
    },
    vertexAttr() {
      // 步骤2: 告诉shader,从@location(x)获取数据
      pc.shader.attr = '0'
    },
    writeBuffer() {
      // 步骤3: 将js的数据拷贝到gpu内存中
      pc.gpu.buffer = JSON.parse(JSON.stringify(pc.js.data))
    },
    setVertexBuffer() {
      // 步骤4: gpu内存中的数据关联
      pc.shader.attr['0'] = pc.gpu.buffer
    }
  },
  gpu: {},
  shader: {
    attr: {}
  }
}

顶点插槽代码

那么,我们看看,实际应该怎么写,以下代码是上一篇文章的代码调整和修改,不熟悉其他相关知识的,建议先查看上一篇代码文章

shader

js 复制代码
struct VertexObj { // 结构
  @builtin(position) position : vec4<f32>, // @builtin(position)内置指令 输出顶点坐标位置
  @location(0) color : vec4<f32>
}

@vertex
fn vertex_main( // 由于同一个文件,不能相同fn名称,故修改
  @builtin(vertex_index) VertexIndex : u32, // @builtin(vertex_index) 内置指令 当前绘制的顶点索引
  @location(0) color: vec4<f32> // @location(0): pipeline.vertex.buffers.attributes.shaderLocation
) -> VertexObj {
  var pos = array<vec2<f32>, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5),
  );

  var vertexOutput: VertexObj;
  vertexOutput.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
  vertexOutput.color = color;

  return vertexOutput; 
}

@fragment
fn fragment_main(fragData: VertexObj) -> @location(0) vec4<f32> {
  return fragData.color;
}

解析:

  1. 由于fragment要接受来自vertex的数据,结构是VertexObj,所以,将两个文件合并为一个,分别修改fn的名称,同时,也要修改js代码中pipeline的entryPoint
  2. @fragment接收来自vertex的数据,返回里面的color
  3. @vertex接收内置指令@builtin(vertex_index),返回对应的数据,这里的顶点数据也能抽离到js中,不抽离是方便对比前一篇文章代码的差异;@location参数,接收来自js传递的数据,直接赋值给结构体@builtin(position)(内置指令,表示当前节点的输出位置,位置需要齐次坐标数据)

js改动

创建GPU buffer,申请大小和权限

js 复制代码
const vertexBuffer = device.createBuffer({ // 创建一个buffer
    size: 48, // 设置创建与传入数据大小一样的buffer,也可以写成: 4 * 4 * 3(4个字节 * rgba * 3个点)
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST // 申请GPU的权限,如果没有申请,则无权限操作
})

pipeline设置数据在shader中的获取属性

js 复制代码
    ......
    layout: 'auto',
    vertex: {
      module: device.createShaderModule({ code: wgsl }),
      entryPoint: 'vertex_main',
      buffers: [{ // 设置vertex中获取到的数据结构,以下配置,表示在vertex shader中可以通过 @location(0) 获取传入的数据
        attributes: [{
          shaderLocation: 0, // 对应vertex中的@location(0)。注意:vertex和fragment的@location(0)并不是一个意思
          offset: 0, // 数据偏移量
          format: 'float32x4'
        }],
        arrayStride: 16 // 说明每个片段的大小,4个字节 * 4位数:rgba
      }]
    },
    fragment: {
   ......

将js内存的数据设置到GPU内存里

js 复制代码
// 颜色是float类型,占4个字节,每一个行的数据表示color:rgba
const colorArray = new Float32Array([ // 接收的数据类型是f32,所以,我们的数据也得是f32的
  [1, 0, 0, 1],
  [0, 1, 0, 1],
  [0, 0, 1, 1]
])
// 将buffer写入queue(可以理解为shader gpu的内存),但是,还没有关联对应的pipeline
device.queue.writeBuffer(vertexBuffer, 0, colorArray, 0, colorArray.length)

将GPU内存的数据关联到具体的pipeline

js 复制代码
......
passEncoder.setPipeline(pipeline)
// 将shader gpu中vertex buffer的数据关联到pipeline,pipeline进行渲染
passEncoder.setVertexBuffer(0, vertexBuffer)
passEncoder.draw(3)
passEncoder.end()
......

通过以上代码,我们在shader的@vertex代码中就能接收参数@location(0)的数据了,注意@location(0)的数字0,在pipeline的shaderLocationpassEncoder.setVertexBuffer的第一个参数是要一样的;前面是声明在shader哪里可以获取到数据,后面的是设置数据到哪里

资源绑定

类似顶点插槽,但是,相比顶点插槽,资源绑定,类似于js的全局变量,我们看一下大致过程:

以下代码采用顶点插槽代码进行改动,便于对比

  1. device.createBuffer,创建GPU buffer
  2. device.createBindGroup,设置数据,并将pipeline binding到当前group
  3. device.queue.writeBuffer,将js内存中的数据,写入GPU buffer
  4. passEncoder.setBindGroup,将group空间关联到当前commandEncoder中

那么,同样的,我们用js代码的作用域,模拟一下上面的过程(不能完全模拟,但,是类似的思想):

js 复制代码
const pc: any = {
  js: {
    data: { a: 12 }, // js中的数据
    createRenderPipelineAsync() { // 先创建pipeline
      pc.gpu.pipeline = []
    },
    createBuffer() {
      // 步骤1: 在gpu内存声明空间(大小、权限)
      pc.gpu.buffer = null
    },
    createBindGroup() {
      // 步骤2: 创建group、关联pipeline
      pc.gpu.group = []
      pc.gpu.group[0] = {}

      pc.gpu.group.binding = []
      pc.gpu.group.binding[0] = pc.gpu.buffer

      // pipeline包含:vertex和fragment,所以shader能直接获取数据
      pc.gpu.pipeline[0] = pc.gpu.group.binding[0]
    },
    writeBuffer() {
      // 步骤3: 将js的数据拷贝到gpu内存中
      pc.gpu.buffer = JSON.parse(JSON.stringify(pc.js.data))
    }
  },
  gpu: {},
  shader: {
    // @group(0) @binding(0) 获取到数据
  }
}

资源绑定代码

shader

js 复制代码
@group(0) @binding(0) var<uniform> colorArray : array<vec4<f32>, 3>; // 在这里获取数据

struct VertexObj { // 结构
  @builtin(position) position : vec4<f32>, // @builtin(position)内置指令 输出顶点坐标位置
  @location(0) color : vec4<f32>
}

@vertex
fn vertex_main( // 由于同一个文件,不能相同fn名称,故修改
  @builtin(vertex_index) VertexIndex : u32, // @builtin(vertex_index) 内置指令 当前绘制的顶点索引
) -> VertexObj {
  var pos = array<vec2<f32>, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5),
  );

  var vertexOutput: VertexObj;
  vertexOutput.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
  vertexOutput.color = colorArray[VertexIndex];

  return vertexOutput; 
}

@fragment
fn fragment_main(fragData: VertexObj) -> @location(0) vec4<f32> {
  return fragData.color;
}

解析: @group(0) @binding(0)获取到数据,根据顶点的位置,返回对应的颜色,改动不多。

js改动

创建GPU buffer,申请大小和权限

js 复制代码
  const colorBuffer = device.createBuffer({ // 创建一个buffer
    size: 48, // 设置创建与传入数据大小一样的buffer,也可以写成: 4 * 4(4个字节 * rgba * 3)
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST // 申请GPU的权限,如果没有申请,则无权限操作
  })

创建group关联资源和pipeline

js 复制代码
......
const pipeline = await device.createRenderPipelineAsync(...)
......
const colorGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{
  binding: 0,
  resource: {
    buffer: colorBuffer
  }
}]
})
......

将js内存的数据设置到GPU内存里

js 复制代码
// 颜色是float类型,占4个字节,每一个行的数据表示color:rgba
const colorArray = new Float32Array([ // 接收的数据类型是f32,所以,我们的数据也得是f32的
  [1, 0, 0, 1],
  [0, 1, 0, 1],
  [0, 0, 1, 1]
])
// 将buffer写入queue(可以理解为shader gpu的内存),但是,还没有关联对应的pipeline
device.queue.writeBuffer(vertexBuffer, 0, colorArray, 0, colorArray.length)

设置group在Encoder的位置,并且关联group

js 复制代码
......
passEncoder.setPipeline(pipeline)
// 设置group在当前pipeline的位置:0,并将内存区域设置进去,shader中@group(0) 就可以获取group了
passEncoder.setBindGroup(0, colorGroup)
passEncoder.draw(3)
passEncoder.end()
......

通过以上代码,在shader中,我们就能通过@group(0) @binding(0)获取数据了,@group(0)是来自代码:passEncoder.setBindGroup(0, colorGroup)@binding(0)则是来自代码:device.createBindGroup()关联的数据。

结尾

经由上面两种形式,我们可以实现js数据到GPU的过程,相同的道理,其他数据(图片、视频等)也是可以传递的。本文到此结束,分享的东西并不多,实现起来也容易多了,感兴趣的同学可以是直接试试效果。

相关推荐
学习使我快乐0122 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199522 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   6 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   6 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web6 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery