{
let device;
let adapter;
try {
adapter = await navigator.gpu?.requestAdapter();
device = await adapter?.requestDevice();
} catch (e) {
return 'WebGPU not supported'
}
const shaders = `
struct Uniforms {
color: vec4f,
aspect: vec2f,
mouse: vec2f,
resolution: vec2f,
// https://gpuweb.github.io/gpuweb/#limits
padding: vec2f
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
let pos = array(
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2( 1.0, 1.0),
vec2(-1.0, -1.0),
vec2(-1.0, 1.0),
vec2( 1.0, 1.0)
);
return vec4f(pos[vertexIndex] * uniforms.aspect, 0.0, 1.0);
}
@fragment
fn fs() -> @location(0) vec4f {
var uv = uniforms.mouse / uniforms.resolution;
uv.y = 1.0 - uv.y;
return vec4(uv, 0, 1);
}
`;
const shaderModule = device.createShaderModule({
code: shaders,
})
// ==================================================
// configure canvas context
// ==================================================
const canvas = DOM.canvas(width, 512);
const context = canvas.getContext('webgpu');
context.configure({
device: device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: 'premultiplied',
})
// ==================================================
// create render pipeline
// ==================================================
// config for render pipeline stages
const renderPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs',
},
fragment: {
module: shaderModule,
entryPoint: 'fs',
targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }],
},
primitive: {
topology: 'triangle-list',
},
});
// ==================================================
// uniforms
// ==================================================
const uniformBufferSize =
4 * 4 + // color is 4 32bit floats (4 bytes each)
2 * 4 + // aspect is 2 32bit floats (4 bytes each)
2 * 4 + // mouse is 2 32bit floats (4 bytes each)
2 * 4 + // resolution is 2 32bit floats (4 bytes each)
2 * 4; // padding: https://gpuweb.github.io/gpuweb/#limits
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// create a typedarray to hold the values for the uniforms in JavaScript
const uniformValues = new Float32Array(uniformBufferSize / 4);
// set uniforms. second arg is the offset (sum of all prev component sizes)
uniformValues.set([0, 1, 0, 1], 0); // set color (offset = 0)
uniformValues.set([0.5 / (canvas.width / canvas.height), 0.5], 4); // set aspect (offset = 0 + vec4)
uniformValues.set([0, 0], 6); // set mouse (offset = 0 + vec4 + vec2)
uniformValues.set([canvas.width, canvas.height], 8); // set resolution (offset = 0 + vec4 + vec2 + vec2)
// bind buffer to shader `@binding(0)`
const uniformsBindGroup = device.createBindGroup({
layout: renderPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: uniformBuffer }},
],
});
// ==================================================
// render pass
// ==================================================
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: [0, 0.5, 1, 1],
loadOp: 'clear',
storeOp: 'store',
view: undefined // filled out during render
},
],
};
const render = (uniforms = {}) => {
if (uniforms.mouse) {
uniformValues.set(uniforms.mouse, 6)
} else if (uniforms.color) {
uniformValues.set(uniforms.color, 0); // set color
}
const w = canvas.width;
const h = canvas.height;
const aspect = w / h;
uniformValues.set([0.5 / aspect, 0.5], 4); // set aspect
// copy the values from JavaScript to the GPU
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(renderPipeline);
passEncoder.setBindGroup(0, uniformsBindGroup);
passEncoder.draw(6);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
}
canvas.addEventListener('pointermove', e => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
render({ mouse: [x | 0, y | 0] })
})
render()
return canvas;
}