前言
第一篇文章,介绍了webGPU的环境、工具、例子以及一些知识的补充。
应该都会在想,数据是写死的,怎么灵活地处理数据?那么,这篇文章,我简单分享一下,js如何与GPU进行数据交换。
shader内存 & 渲染管线
from Orillusion; 补充:orillusion翻译的文章也挺好的:WebGPU、wgsl
简单解析一下上图:
- 最上面一行是GPU内存数据信息
- 中间一行:是render pipeline渲染过程,WebGPU仅提供
Vertex Shader
和Fragment Shader
给我们,其他的是WebGPU自行完成。如果需要处理数据,那么就需要在这两个阶段处理完 - 最后一行:是render pipeline处理过程。实际上,上一篇文章有描述:
primitive: {topology: 'triangle-list'}
可以设置第二阶段的组合方式(Primitive Assmbly)
例子
例子效果:
顶点插槽
我们来简单来梳理一下,js通过顶点插槽,怎么实现数据传递。
device.createBuffer
,创建GPU buffer- 在pipeline
vertex.buffers
,设置GPU buffer在vertex shader里的引用方式 device.queue.writeBuffer
,将js内存中的数据,写入GPU bufferpassEncoder.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;
}
解析:
- 由于
fragment
要接受来自vertex
的数据,结构是VertexObj
,所以,将两个文件合并为一个,分别修改fn的名称,同时,也要修改js代码中pipeline的entryPoint
。 @fragment
接收来自vertex的数据,返回里面的color
@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的shaderLocation
和passEncoder.setVertexBuffer
的第一个参数是要一样的;前面是声明在shader哪里可以获取到数据,后面的是设置数据到哪里。
资源绑定
类似顶点插槽,但是,相比顶点插槽,资源绑定,类似于js的全局变量,我们看一下大致过程:
以下代码采用顶点插槽代码进行改动,便于对比
device.createBuffer
,创建GPU bufferdevice.createBindGroup
,设置数据,并将pipeline binding到当前groupdevice.queue.writeBuffer
,将js内存中的数据,写入GPU bufferpassEncoder.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的过程,相同的道理,其他数据(图片、视频等)也是可以传递的。本文到此结束,分享的东西并不多,实现起来也容易多了,感兴趣的同学可以是直接试试效果。