前面我们绘制了二维的一个正方形,现在我们来绘制一个立方体,进一步了解WebGPU 的基本使用。
数据准备
开始之前我们需要主备一些数据,包括顶点数据,顶点着色器,片元着色器,以及透视投影、旋转、视图等矩阵。
// 顶点着色器,片元着色器
import { basicVert, positionFrag } from './shaders/rotatingCube.js'
//顶点数据,uv坐标数据
import * as cube from './util/cube.js'
//透视投影、旋转、视图等功能
import { getMvpMatrix } from './util/math.js'
初始化
初始化WebGPU,与前面一样,用之前封装的initWebGPU函数来初始化。
async function initWebGPU(canvas) {
if (!navigator.gpu)
throw new Error('Not Support WebGPU')
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
})
if (!adapter)
throw new Error('No Adapter Found')
const device = await adapter.requestDevice()
const context = canvas.getContext('webgpu')
const format = navigator.gpu.getPreferredCanvasFormat()
const size = { width: canvas.width, height: canvas.height }
context.configure({
device: device,
format: format,
})
return { device, context, format, size }
}
初始化渲染管线
渲染管线,包括顶点着色器,片元着色器,顶点数据,顶点格式,渲染模式,颜色格式,深度格式,以及视图矩阵等。在下面代码中,有详细说明。
// 管线组装
async function initPipeline(device, format, size) {
const pipeline = await device.createRenderPipelineAsync({
label: 'Basic Pipline',
layout: 'auto',// 渲染管线的布局
vertex: {
module: device.createShaderModule({
code: basicVert,
}),
entryPoint: 'main',
buffers: [{// 这里的 buffers 属性就是缓冲区集合,其中一个元素对应一个缓冲对象
arrayStride: 5 * 4,// 一个顶点长度 以字节为单位
attributes: [
{
shaderLocation: 0,// 遍历索引,这里的索引值就对应的是着色器语言中 @location(0) 的数字
offset: 0,// 偏移
format: 'float32x3',// 参数格式
},
{
shaderLocation: 1,// 这里的索引值就对应的是着色器语言中 @location(1) 的数字
offset: 3 * 4,
format: 'float32x2',
}
]
}]
},
fragment: {
module: device.createShaderModule({
code: positionFrag,
}),
entryPoint: 'main',
targets: [
{
format: format
}
]
},
primitive: {//用于描述图元的状态
topology: 'triangle-list',
cullMode: 'back',//剔除背面
frontFace: 'ccw'
},
//depthWriteEnabled指定是否写入深度缓冲区。depthCompare指定如何比较深度测试。format指定深度缓冲区格式。
depthStencil: {
depthWriteEnabled: true,// 开启深度测试
depthCompare: 'less',// 设置比较函数为 less
format: 'depth24plus',// 设置depth为24bit
}
})
//深度缓冲区
const depthTexture = device.createTexture({
size, format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
})
const depthView = depthTexture.createView()//深度视图
//创建 VBO,顶点数据缓冲区
// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据
// 建立顶点缓冲区
// 它就是一块存储空间,先让 js 把顶点数据放进去,然后再让着色器从里面读取
// 至于为什么 js 不通过一个简单的方法直接把顶点数据传递给着色器,这是因为 js 和 着色器用的是两种不一样的语言
// 它们无法直接对话,因此需要一个缓冲地带,也就是缓冲区对象
const vertexBuffer = device.createBuffer({
label: 'GPUBuffer store vertex',
size: cube.vertex.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(vertexBuffer, 0, cube.vertex)
//mvp矩阵缓冲区
// COPY_DST 通常就意味着有数据会复制到此 GPUBuffer 上,这种 GPUBuffer 可以通过 queue.writeBuffer 方法写入数据
const mvpBuffer = device.createBuffer({
label: 'GPUBuffer store 4x4 matrix',
size: 4 * 4 * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})
//mvp矩阵缓冲区,绑定uniform
// 通过打组,可以很方便地将某种条件下的一组 uniform 资源分别传入着色器进行 WebGPU 渲染编程。
// GPUBindGroup 的最大作用,就是隔离不相关的 uniform,把相关的资源摆在一块。
const uniformGroup = device.createBindGroup({
label: 'Uniform Group with Matrix',
layout: pipeline.getBindGroupLayout(0),// 指定绑定组的布局对象,渲染管线的布局
entries: [
{
binding: 0,
resource: {
buffer: mvpBuffer
}
}
]
})
return { pipeline, vertexBuffer, mvpBuffer, uniformGroup, depthTexture, depthView }
}
渲染
渲染函数,这里主要就是把数据写入缓冲区,然后把缓冲区绑定到管线上,然后调用 draw 方法进行渲染。
// 创建一个i名为 commandEncoder 的指令编码器,用来复制显存
// 我们不能直接操作command buffer,需要创建command encoder,使用它将多个commands(如render pass的draw)设置到一个command buffer中,然后执行submit,把command buffer提交到gpu driver的队列中。
// command buffer有
// creation, recording,ready,executing,done五种状态。
// 根据该文档,结合代码来分析command buffer的操作流程:
// const commandEncoder = device.createCommandEncoder()这个语句:创建command encoder时,应该是创建了command buffer,它的状态为creation;
// const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)这个语句:开始render pass(webgpu还支持compute pass,不过这里没用到),command buffer的状态变为recording;
// passEncoder.setPipeline(pipeline) 这个语句:将“设置pipeline”、“绘制”的commands设置到command buffer中;
// passEncoder.end() 这个语句:(可以设置下一个pass,如compute pass,不过这里只用了一个pass);
// commandEncoder.finish() 这个语句:将command buffer的状态变为ready;
// device.queue.submit 这个语句:command buffer状态变为executing,被提交到gpu driver的队列中,不能再在cpu端被操作;
// 如果提交成功,gpu会决定在某个时间处理它。
function draw(device, context, pipelineObj) {
const commandEncoder = device.createCommandEncoder()
const renderPassDescriptor = {
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
// loadOp和storeOp决定渲染前和渲染后怎样处理attachment中的数据。
loadOp: 'clear',// load 的意思是渲染前保留attachment中的数据,clear 意思是渲染前清除
storeOp: 'store'// 如果为“store”,意思是渲染后保存被渲染的内容到内存中,后面可以被读取;如果为“clear”,意思是渲染后清空内容。
}
],
depthStencilAttachment: {
view: pipelineObj.depthView,
// 在深度测试时,gpu会将fragment的z值(范围为[0.0-1.0])与这里设置的depthClearValue值(这里为1.0)比较。其中使用depthCompare定义的函数(这里为less,意思是所有z值大于等于1.0的fragment会被剔除)进行比较。
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
}
}
// 建立渲染通道,类似图层
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
// 传入渲染管线
passEncoder.setPipeline(pipelineObj.pipeline)
// VBO 要通过 passEncoder 的 setVertexBuffer 方法写入数据
// 有时还要配合 GPUBufferUsage.INDEX 即 索引缓存 来使用
// 通道编码器中指定坐标缓存、颜色缓存
// 写入顶点缓冲区
passEncoder.setVertexBuffer(0, pipelineObj.vertexBuffer)
// draw 之前设置绑定的组
passEncoder.setBindGroup(0, pipelineObj.uniformGroup)
// 绘图:指定绘制的顶点个数
passEncoder.draw(cube.vertexCount)
passEncoder.end()
// 提交写好的复制功能的命令
// commandEncoder.finish(): 结束指令编写,并返回 GPU 指令缓冲区
// device.queue.submit:向 GPU 提交绘图指令,所有指令将在提交后执行
device.queue.submit([commandEncoder.finish()])
}
运行
async function run() {
const canvas = document.querySelector('canvas')
if (!canvas)
throw new Error('No Canvas')
const { device, context, format, size } = await initWebGPU(canvas)
const pipelineObj = await initPipeline(device, format, size)
// 模型、投影、视图矩阵初始数据
let aspect = size.width / size.height
const position = { x: 0, y: 0, z: -5 }
const scale = { x: 1, y: 1, z: 1 }
const rotation = { x: 0, y: 0, z: 0 }
// 帧循环
function frame() {
// 按时间旋转,并更新变换矩阵
const now = Date.now() / 1000
rotation.x = Math.sin(now)
rotation.y = Math.cos(now)
//矩阵处理方法
const mvpMatrix = getMvpMatrix(aspect, position, rotation, scale)
// 写入:从 CPU 到 GPU
// 将颜色数据/旋转数据 写入到缓冲区对象
device.queue.writeBuffer(
pipelineObj.mvpBuffer,// 传给谁
0,
mvpMatrix.buffer// 传递 ArrayBuffer
// mvpMatrix.byteOffset, // 从哪里开始
// mvpMatrix.byteLength // 取多长
)
// 开始绘制
draw(device, context, pipelineObj)
// requestAnimationFrame(frame)
}
frame()
}
run()
立方体数据
const vertex = new Float32Array([
// float3 点坐标, float2 uv
// face1
+1, -1, +1, 1, 1,
-1, -1, +1, 0, 1,
-1, -1, -1, 0, 0,
+1, -1, -1, 1, 0,
+1, -1, +1, 1, 1,
-1, -1, -1, 0, 0,
// face2
]);
const vertexCount = 36
export { vertex, vertexCount }
WGSL 的一些介绍
入口点
WGSL 没有强制使用固定的 main() 函数作为入口点(Entry Point),它通过 @vertex、@fragment、@compute 三个着色器阶段(Shader State)标记提供了足够的灵活性让开发人员能更好的组织着色器代码。你可以给入口点取任意函数名,只要不重名,还能将所有阶段(甚至是不同着色器的同一个阶段)的代码组织同一个文件中
Group 与 Binding 属性
WGSL 中每个资源都使用了 @group(X) 和 @binding(X) 属性标记,例如 @group(0) @binding(0) var params: Uniforms。 params 它表示的是 Uniform buffer 对应于哪个绑定组中的哪个绑定槽。这与 GLSL 中的 layout(set = X, binding = X) 布局标记类似。WGSL 的属性非常明晰,描述了着色器阶段到结构的精确二进制布局的所有内容。
export const basicVert = `@binding(0) @group(0) var<uniform> mvpMatrix : mat4x4<f32>;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>
};
@vertex
fn main(
@location(0) position : vec4<f32>,
@location(1) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
output.Position = mvpMatrix * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
`
export const positionFrag = `@fragment
fn main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
return fragPosition;
}
`
mvp 矩阵
import { mat4, vec3 } from 'gl-matrix'
// 从给定的比例、位置、旋转角度、缩放值返回mvp矩阵
function getMvpMatrix(
aspect,
position,
rotation,
scale
) {
// 模型视图矩阵
const modelViewMatrix = getModelViewMatrix(position, rotation, scale)
// 投影矩阵
const projectionMatrix = getProjectionMatrix(aspect)
// mvp 矩阵
const mvpMatrix = mat4.create()
mat4.multiply(mvpMatrix, projectionMatrix, modelViewMatrix)
return mvpMatrix
}
function getModelViewMatrix(
position = { x: 0, y: 0, z: 0 },
rotation = { x: 0, y: 0, z: 0 },
scale = { x: 1, y: 1, z: 1 }
) {
const modelViewMatrix = mat4.create()
mat4.translate(modelViewMatrix, modelViewMatrix, vec3.fromValues(position.x, position.y, position.z))
// rotate
mat4.rotateX(modelViewMatrix, modelViewMatrix, rotation.x)
mat4.rotateY(modelViewMatrix, modelViewMatrix, rotation.y)
mat4.rotateZ(modelViewMatrix, modelViewMatrix, rotation.z)
// scale
mat4.scale(modelViewMatrix, modelViewMatrix, vec3.fromValues(scale.x, scale.y, scale.z))
return modelViewMatrix
}
const center = vec3.fromValues(0, 0, 0)
const up = vec3.fromValues(0, 1, 0)
function getProjectionMatrix(
aspect,
fov = 60 / 180 * Math.PI,
near = 0.1,
far = 100.0,
position = { x: 0, y: 0, z: 0 }
) {
// create cameraview
const cameraView = mat4.create()
const eye = vec3.fromValues(position.x, position.y, position.z)
mat4.translate(cameraView, cameraView, eye)
mat4.lookAt(cameraView, eye, center, up)
// get a perspective Matrix
const projectionMatrix = mat4.create()
mat4.perspective(projectionMatrix, fov, aspect, near, far)
mat4.multiply(projectionMatrix, projectionMatrix, cameraView)
return projectionMatrix
}
export { getMvpMatrix, getModelViewMatrix, getProjectionMatrix }