webgpu实现画板

575 阅读3分钟

使用自己实现的 webgpu 画板画画的成果

gitee地址,develop分支 gitee.com/yjsdszz/thr…

美丽的脸庞,美丽的头发,好喜欢

8899096ea9740e0cd8838e8bbb004df.png

实现画板

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 抗锯齿后将会使用这个作为渲染最终结果,viewmsaa 结果。如果不开启 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 );

    }

}