经过上一篇的实践,我们已经能够在画布上使用 webgpu 绘制一个简单的三角形,但是这个图形过于“平面”,不够“3D”。接下来我们就一起来看看如何使用 uniform 的 BindingGroup 来更新 MVP 矩阵,实现一个动态旋转的 3D 动画。
由于项目初始化、绘制等操作代码相对固定,我们提取出一个WebGPURenderEngin
类来做封装,完成 webgpu 的初始化工作。同时再创建一个WebGPURenderPipeline
类来封装 renderpipeline 的相关功能。
WebGPURenderEngin
这个类作为主渲染“引擎”,让我们姑且这样叫它,虽然它功能还比较弱鸡,最多算是个 helper。主要有两个方法,一个是init()
用来初始化整个 webgpu 的相关全局内容(GPUAdapter、GPUDevice、Canvas、Context、SwapChain 等等);另一个主要的方法就是draw()
用来执行绘制操作。
WebGPURenderPipeline
由于 renderpipeline 的内容很复杂,设置项比较多,所以我们抽出一个类,专门处理 renderpipeline。这个类的构造函数是constructor(engine: WebGPURenderEngin, vs: string, fs: string)
一个 engine,一个顶点着色器代码,一个片元着色器代码。
这个类还包含了一系列的工具方法帮助我们快速设置 attribute、index、uniform 等信息。分别是addAttribute()
、setIndex()
、addUniformBuffer()
等。
因此我们只需要创建一个 pipeline 类的实例,然后设置属性,设置索引,设置 uniform,然后调用 engine 的 draw 方法就可以实现简单的绘制,是不是感觉简练很多。
提炼后的代码结构如下所示:
// 初始化方法
const init = async () => {
// 初始化引擎 注意这里是个promise
const engineReady = await renderEngine.init();
if (engineReady) {
// 创建pipeline
pipline = new WebGPURenderPipeline(renderEngine, vs, fs);
// 设置顶点
pipline.addAttribute(positions);
// 设置颜色
pipline.addAttribute(colors);
// 设置索引
pipline.setIndex(indices);
// 设置mvp矩阵内容
pipline.addUniformBuffer(matrixArray);
// 生成pipeline
pipline.generatePipline();
// 开启主渲染循环
render();
}
};
// 渲染循环
const render = () => {
// 获取mvp的矩阵内容
let buffer = pipline.getUniformEntryByBinding(0).resource.buffer as GPUBuffer;
// 更新物体的model旋转矩阵buffer
pipline.updateBuffer(buffer, 0, getRotateMatrix());
// 调用绘制
renderEngine.draw();
requestAnimationFrame(render);
};
ok,再看完了大致的结构后,让我们来看看这些代码背后的黑魔法吧。由于上篇,我们已经讲解了如何使用流程化的代码创建 attribute 以及 index 索引,我们就不再重复这部分的内容,直接看这篇文章的重点,就是 uniform 的使用以及 GPUBuffer 的更新。
在 sheader 中,我们加入 uniform 的内容,我们使用了一个 uniform 的 bindgroup,binding 值为 0,内容为一个 projection matrix 投影矩阵,一个 modelview matrix 矩阵。在计算顶点位置的时候我们带上矩阵运算。
#version 450
layout(set = 0, binding = 0) uniform Uniforms {
mat4 uProjectionMatrix;
mat4 uModelViewMatrix;
};
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec4 aColor;
layout(location = 0) out vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
vColor = aColor;
}`
首先,我们需要在引擎(WebGPURenderEngin
)的绘制方法中加入 renderpass 对 bindinggroup 的设置
this.renderPassEncoder.setBindGroup(0, currentPipeline.uniformBindGroup);
其次我们看看WebGPURenderPipeline
中我们如何设置 bindgroup 信息。当我们设置设置 uniformbuffer 的时候,会将一个 ubo 对象加入到 uniform 数组中,然后在 pipeline 的generateUniforms()
方法中生成 BindingGroup 以及 BindGroupLayout。
BindingGroup 其实就是一组 uniform 的集合,里面包含了当前绘制 command 的所有 uniform 信息,包括 uniformbuffer、texture、sampler 等具体的对象。在一个 renderpass 中我们其实可以设置多次 BindingGroup 信息,然后分别 draw, 这样来大多多次绘制切换的目的。
BindGroupLayout 是 BindingGroup 内容的结构描述信息,用来描述 BindingGroup 中包含哪些内容,每个 uniform 项目的 binding 值是几等。
这个示例中我们先调用了 pipeline 的addUniformBuffer(matrixArray)
方法,将一个包含 projection 以及 modelview 的矩阵内容传入 uniformbuffer。
const buffer = this.createBuffer(typedArray, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
this.addUniformEntry({
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
type: "uniform-buffer",
resource: {
buffer: buffer,
},
});
然后在生成 BindingGroup 以及 BindGroupLayout
const bindGroupLayoutDes: any = { entries: [] };
const bindGroupEntries: Array<any> = [];
this.uniformEntries.forEach((entry, key) => {
bindGroupLayoutDes.entries.push({
binding: entry.binding,
visibility: entry.visibility,
type: entry.type,
});
bindGroupEntries.push({
binding: entry.binding,
resource: entry.resource,
});
});
this.uniformBindGroupLayout = this.engin.device.createBindGroupLayout(bindGroupLayoutDes);
this.uniformBindGroup = this.engin.device.createBindGroup({
layout: this.uniformBindGroupLayout,
// @ts-ignore
entries: bindGroupEntries,
});
生成好 BindingGroup 以及 BindGroupLayout 之后,我们只要在 renderpipeline 中设置 layout 的时候加入生成好的 BindGroupLayout 就可以了。
this.layout = this.engin.device.createPipelineLayout({
bindGroupLayouts: [this.uniformBindGroupLayout],
});
之前的 demo 中,我们没有 uniform 值,这里的 bindGroupLayouts 是个空数组,即没有 bindingGroup 内容。
在完成了设置之后,我们如何载每次更新的时候重新更新 uniform 的值呢。这里我们在 renderpipeline 中加入一个工具方法updateBuffer(to: GPUBuffer, offset: number, fromTypedArray: Float32Array | Uint16Array | Uint32Array)
,用来更新 buffer 的值,这里不仅可以更新 uniform 的 buffer,而是任意只要类型是 GPUBuffer 的对象。
updateBuffer(to: GPUBuffer, offset: number, fromTypedArray: Float32Array | Uint16Array | Uint32Array) {
// @ts-ignore
this.engin.device.defaultQueue.writeBuffer(to, offset, fromTypedArray, 0, fromTypedArray.byteLength);
}
对于 buffer 的更新,网上大多教程使用了 setData()或者调用 commandEncoder 的copyBufferToBuffer
方法执行一次 commandbuffer。笔者感觉这样的方法并不是十分的好用,于是参考了官方文档的说明,使用了最新的 api device.defaultQueue.writeBuffer
方法,直接写入 buffer,感觉简单直接了很多。
在页面上我们使用 gl-matrix 库进行矩阵运算,然后更新 buffer
// 渲染循环
const render = () => {
// 更新物体的model旋转矩阵buffer
pipline.updateBuffer(buffer, 0, getRotateMatrix());
// 调用绘制
renderEngine.draw();
};
const getRotateMatrix = () => {
mat4.fromRotation(modelMatrix, 0.005 * new Date().getTime(), vec3.fromValues(0, 1, 0));
mat4.mul(mvMatrix, viewMatrix, modelMatrix);
matrixArray.set(projectionMtrix);
matrixArray.set(mvMatrix, 16);
return matrixArray;
};
至此,我们就完成了一个简单的使用 uniform 来旋转 model 的动画效果。