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的过程,相同的道理,其他数据(图片、视频等)也是可以传递的。本文到此结束,分享的东西并不多,实现起来也容易多了,感兴趣的同学可以是直接试试效果。

相关推荐
L耀早睡7 分钟前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer21 分钟前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿27 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年1 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net
爱分享的程序员2 小时前
全栈项目搭建指南:Nuxt.js + Node.js + MongoDB
前端
隐含3 小时前
webpack打包,把png,jpg等文件按照在src目录结构下的存储方式打包出来。解决同一命名的图片资源在打包之后,重复命名的图片就剩下一个图片了。
前端·webpack·node.js
lightYouUp3 小时前
windows系统中下载好node无法使用npm
前端·npm·node.js
Dontla3 小时前
npm cross-env工具包介绍(跨平台环境变量设置工具)
前端·npm·node.js