使用自己实现的 webgpu 画板画画的成果
gitee地址,develop分支 gitee.com/yjsdszz/thr…
美丽的脸庞,美丽的头发,好喜欢
实现画板
1. wgsl着色器
非常简单的一段 wgsl
着色器,只需要顶点数据
和颜色数据
struct MyVSInput {
@location(0) position : vec4f,
};
@binding(0) @group(0) var<storage, read> color : vec3f;
@vertex
fn myVSMain(v : MyVSInput) -> @builtin(position) vec4f {
return v.position;
}
@fragment
fn myFSMain() -> @location(0) vec4f {
return vec4f(color, 1);
}
2. 请求 gpu 设备,配置 canvas
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.querySelector( 'canvas' );
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const context = canvas.getContext( 'webgpu' );
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure( {
device,
format: presentationFormat,
alphaMode: 'premultiplied',
} );
3. 创建渲染管线,配置 msaa 抗锯齿
const sampleCount = 4;
const module = device.createShaderModule( { code: ShaderCode } );
const pipeline = device.createRenderPipeline( {
layout: 'auto',
vertex: {
module,
buffers: [
{
arrayStride: 2 * 4,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2'
},
],
},
],
},
fragment: {
module,
targets: [
{ format: presentationFormat },
],
},
primitive: {
topology: "line-strip",
},
multisample: {
count: sampleCount
}
} );
4. 绘制逻辑
4.1 鼠标左键摁下,设置可绘制状态为真,创建一个GPU顶点缓冲区,一个GPU颜色缓冲区
pointsPerBuffer
定义了每个缓冲区可存储的顶点数据,如果连续绘制的点位过长,将会动态进行扩容。
objects
存储了每一次的绘制信息(包括顶点缓冲区和颜色缓冲区),用于后续的撤销操作和恢复操作
const pointsPerBuffer = 256;
function createObject ( color: number[] ) {
const buffer = device.createBuffer( {
size: pointsPerBuffer * 2 * 4,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
} );
const points = [];
const colorBuffer = device.createBuffer( {
size: 4 * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
} );
new Float32Array( colorBuffer.getMappedRange() ).set( color );
colorBuffer.unmap();
const object: ObjectType = {
buffer,
points,
color: colorBuffer,
count: 0
};
objects.push( object );
}
4.2 鼠标移动时候,获取鼠标位置信息,向GPU缓冲区写入数据(颜色和坐标)
object
鼠标左键摁下的时候创建的对象,包含顶点缓冲区和颜色缓冲区和顶点数据和顶点数量。
const addPoint = ( x: number, y: number ) => {
const object = objects[ objects.length - 1 ];
const points = object.points;
const count = object.count
if ( count > 0 && count % pointsPerBuffer === 0 ) {
// 缓冲区存储上限,释放这个缓冲区,创建新的缓冲区,新缓冲区的大小每次增加 pointsPerBuffer * 2 * 4
object.buffer.destroy();
object.buffer = device.createBuffer( {
size: object.buffer.size + pointsPerBuffer * 2 * 4,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
} );
new Float32Array( object.buffer.getMappedRange() ).set( points );
object.buffer.unmap();
}
// 向缓冲区的特定位置写入新增的顶点数据
device.queue.writeBuffer( object.buffer, count * 2 * 4, new Float32Array( [ x, y ] ) );
points.push( x, y );
object.count++;
};
4.3 鼠标左键弹起,设置可绘制状态为假。
5. 创建GPU渲染管线
const sampleCount = 4;
const module = device.createShaderModule( { code: ShaderCode } );
const pipeline = device.createRenderPipeline( {
layout: 'auto',
vertex: {
module,
buffers: [
{
arrayStride: 2 * 4,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2'
},
],
},
],
},
fragment: {
module,
targets: [
{ format: presentationFormat },
],
},
primitive: {
topology: "line-strip",
},
multisample: {
count: sampleCount
}
} );
6. 渲染
renderPassDescriptor
渲染通道描述对象,此处描述颜色附着(拓展:还有深度模板附着depthStencilAttachment
)
resolveTarget
在开启 msaa
抗锯齿后将会使用这个作为渲染最终结果,view
为 msaa
结果。如果不开启 msaa
, view
为渲染结果,resolveTarget = undefined
。
bindingGroup
定义了一组要在组中绑定在一起的资源,以及如何在着色器阶段使用这些资源。
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: undefined, // Assigned later
resolveTarget: undefined,
clearValue: [ 1, 1, 1, 1.0 ],
loadOp: 'clear',
storeOp: 'store',
},
],
};
function render () {
// 创建 msaa texture , 在编码结束需要释放内存,否则项目马上就崩溃
const texture = device.createTexture( {
size: [ canvas.width, canvas.height ],
format: presentationFormat,
sampleCount: sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT
} );
renderPassDescriptor.colorAttachments[ 0 ].view = texture.createView();
const canvasTexture = context.getCurrentTexture();
renderPassDescriptor.colorAttachments[ 0 ].resolveTarget = canvasTexture.createView();
// renderPassDescriptor.colorAttachments[ 0 ].view = context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass( renderPassDescriptor );
pass.setPipeline( pipeline );
// 遍历所有渲染对象(此处为绘制的多条线)
objects.forEach( ( object ) => {
// 创建绑定组
const bindingGroup = device.createBindGroup( {
layout: pipeline.getBindGroupLayout( 0 ),
entries: [
{
binding: 0,
resource: {
buffer: object.color
}
}
]
} );
pass.setBindGroup( 0, bindingGroup );
pass.setVertexBuffer( 0, object.buffer );
pass.draw( object.count );
} );
pass.end();
device.queue.submit( [ encoder.finish() ] );
// 销毁 msaa texture ,避免浏览器崩溃
texture.destroy();
requestAnimationFrame( render );
}
requestAnimationFrame( render );
7. 撤销和恢复
const history: ObjectType[] = [];
/** 撤销操作 */
function undo () {
const object = objects.pop();
if ( object ) {
history.push( object );
}
}
/** 恢复操作 */
function redo () {
const object = history.pop();
if ( object ) {
objects.push( object );
}
}