From 1b90a6e3fe147af0e2d50f9ed102838248e7887e Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Sun, 6 Apr 2025 01:45:24 +0200 Subject: [PATCH] feat: fixed canvas size & enhance WebGPU rendering with grid and time uniforms --- index.html | 2 +- src/main.ts | 182 ++++++++++++++++++++++++++++++++---------- src/shaders/cell.wgsl | 44 +++++++++- src/style.css | 6 -- 4 files changed, 182 insertions(+), 52 deletions(-) diff --git a/index.html b/index.html index b13b58b..b7e5d24 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ Whatever WebGPU - + diff --git a/src/main.ts b/src/main.ts index 257e79d..f6920eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,51 +1,60 @@ import "./style.css" -async function init() { +const GRID_SIZE = 32 + +async function getCanvas(): Promise { const canvas = document.querySelector("#canvas") - if (!canvas) { - throw new Error("Canvas element not found") - } - if (!navigator.gpu) { - throw new Error("WebGPU not supported on this browser.") - } + if (!canvas) throw new Error("Canvas element not found") + return canvas +} +async function getAdapter(): Promise { + if (!navigator.gpu) throw new Error("WebGPU not supported on this browser.") const adapter = await navigator.gpu.requestAdapter() - if (!adapter) { - throw new Error("No appropriate GPUAdapter found.") - } + if (!adapter) throw new Error("No appropriate GPUAdapter found.") + return adapter +} +async function getDevice(adapter: GPUAdapter): Promise { + return await adapter.requestDevice() +} + +function configureContext( + canvas: HTMLCanvasElement, + device: GPUDevice, +): GPUCanvasContext { const context = canvas.getContext("webgpu") - if (!context) { - throw new Error("Failed to get WebGPU context.") - } - - const device = await adapter.requestDevice() + if (!context) throw new Error("Failed to get WebGPU context.") const canvasFormat = navigator.gpu.getPreferredCanvasFormat() context.configure({ device, format: canvasFormat }) + return context +} - const encoder = device.createCommandEncoder() - - const vertexBufferLayout = { +function createVertexBufferLayout(): GPUVertexBufferLayout { + return { arrayStride: 8, attributes: [ - { - format: "float32x2" as GPUVertexFormat, - offset: 0, - shaderLocation: 0, - }, + { format: "float32x2" as GPUVertexFormat, offset: 0, shaderLocation: 0 }, ], } +} + +async function createCellPipeline( + device: GPUDevice, + canvasFormat: GPUTextureFormat, +): Promise { const cellShaderModule = device.createShaderModule({ label: "Cell shader", code: (await import("./shaders/cell.wgsl?raw")).default, }) - const cellPipeline = device.createRenderPipeline({ + + return device.createRenderPipeline({ label: "Cell pipeline", layout: "auto", vertex: { module: cellShaderModule, entryPoint: "vertexMain", - buffers: [vertexBufferLayout], + buffers: [createVertexBufferLayout()], }, fragment: { module: cellShaderModule, @@ -53,7 +62,44 @@ async function init() { targets: [{ format: canvasFormat }], }, }) +} +function createGridUniformBuffer(device: GPUDevice): GPUBuffer { + const gridUniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]) + const gridUniformBuffer = device.createBuffer({ + label: "Grid Uniforms", + size: gridUniformArray.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + device.queue.writeBuffer(gridUniformBuffer, 0, gridUniformArray) + return gridUniformBuffer +} + +function createTimeBuffer(device: GPUDevice): GPUBuffer { + return device.createBuffer({ + label: "Time Uniform", + size: 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) +} + +function createBindGroup( + device: GPUDevice, + cellPipeline: GPURenderPipeline, + gridUniformBuffer: GPUBuffer, + timeBuffer: GPUBuffer, +): GPUBindGroup { + return device.createBindGroup({ + label: "Cell renderer bind group", + layout: cellPipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: gridUniformBuffer } }, + { binding: 1, resource: { buffer: timeBuffer } }, + ], + }) +} + +function createVertexBuffer(device: GPUDevice): GPUBuffer { const vertices = new Float32Array([ -0.8, -0.8, 0.8, -0.8, 0.8, 0.8, -0.8, -0.8, 0.8, 0.8, -0.8, 0.8, ]) @@ -63,24 +109,78 @@ async function init() { usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }) device.queue.writeBuffer(vertexBuffer, 0, vertices) + return vertexBuffer +} - const pass = encoder.beginRenderPass({ - colorAttachments: [ - { - view: context.getCurrentTexture().createView(), - loadOp: "clear", - clearValue: [0.0, 0.0, 0.4, 1.0], - storeOp: "store", - }, - ], - }) - pass.setPipeline(cellPipeline) - pass.setVertexBuffer(0, vertexBuffer) - pass.draw(vertices.length / 2) - pass.end() - device.queue.submit([encoder.finish()]) +class Renderer { + constructor( + private device: GPUDevice, + private context: GPUCanvasContext, + private cellPipeline: GPURenderPipeline, + private vertexBuffer: GPUBuffer, + private bindGroup: GPUBindGroup, + private timeBuffer: GPUBuffer, + private vertices: Float32Array, + ) {} + + public render(timeMs: number): void { + const time = timeMs / 1000 + this.device.queue.writeBuffer(this.timeBuffer, 0, new Float32Array([time])) + + const encoder = this.device.createCommandEncoder() + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: this.context.getCurrentTexture().createView(), + loadOp: "clear", + clearValue: [0.0, 0.0, 0.4, 1.0], + storeOp: "store", + }, + ], + }) + + pass.setPipeline(this.cellPipeline) + pass.setVertexBuffer(0, this.vertexBuffer) + pass.setBindGroup(0, this.bindGroup) + pass.draw(this.vertices.length / 2, GRID_SIZE * GRID_SIZE) + pass.end() + + this.device.queue.submit([encoder.finish()]) + requestAnimationFrame((time) => this.render(time)) + } +} + +async function init() { + const device = await getDevice(await getAdapter()) + const context = configureContext(await getCanvas(), device) + const canvasFormat = navigator.gpu.getPreferredCanvasFormat() + + const cellPipeline = await createCellPipeline(device, canvasFormat) + const gridUniformBuffer = createGridUniformBuffer(device) + const timeBuffer = createTimeBuffer(device) + const bindGroup = createBindGroup( + device, + cellPipeline, + gridUniformBuffer, + timeBuffer, + ) + const vertexBuffer = createVertexBuffer(device) + const vertices = new Float32Array([ + -0.8, -0.8, 0.8, -0.8, 0.8, 0.8, -0.8, -0.8, 0.8, 0.8, -0.8, 0.8, + ]) + + const renderer = new Renderer( + device, + context, + cellPipeline, + vertexBuffer, + bindGroup, + timeBuffer, + vertices, + ) + requestAnimationFrame((time) => renderer.render(time)) } init().catch((error) => { - console.error("Failed to initialize the application:", error) + console.error("Error initializing WebGPU:", error) }) diff --git a/src/shaders/cell.wgsl b/src/shaders/cell.wgsl index de62aba..54a6be6 100644 --- a/src/shaders/cell.wgsl +++ b/src/shaders/cell.wgsl @@ -1,9 +1,45 @@ +@group(0) @binding(0) var grid: vec2f; +@group(0) @binding(1) var time: f32; + +struct VertexInput { + @location(0) pos: vec2f, + @builtin(instance_index) instance: u32, +}; + +struct VertexOutput { + @builtin(position) pos: vec4f, + @location(0) cell: vec2f, +}; + @vertex -fn vertexMain(@location(0) pos: vec2f) -> @builtin(position) vec4f { - return vec4f(pos, 0, 1); +fn vertexMain(input: VertexInput) -> VertexOutput { + let i = f32(input.instance); + let cell = vec2f(i % grid.x, floor(i / grid.x)); + let cellOffset = cell / grid * 2.0; + let gridPos = (input.pos + 1.0) / grid - 1.0 + cellOffset; + + var output: VertexOutput; + output.pos = vec4f(gridPos, 0.0, 1.0); + output.cell = cell; + return output; +} + +fn hueToRGB(hue: f32) -> vec3f { + let r = 0.5 + 0.5 * sin(6.28318 * (hue + 0.0)); + let g = 0.5 + 0.5 * sin(6.28318 * (hue + 0.33)); + let b = 0.5 + 0.5 * sin(6.28318 * (hue + 0.66)); + return vec3f(r, g, b); } @fragment -fn fragmentMain() -> @location(0) vec4f { - return vec4f(1, 0, 0, 1); +fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { + let center = grid * 0.5; + let delta = input.cell - center; + let angle = atan2(delta.y, delta.x); + let normalizedAngle = (angle / (2.0 * 3.14159)) + 0.5; + + let speed = 0.3; + let hue = fract(normalizedAngle + time * speed); + let color = hueToRGB(hue); + return vec4f(color, 1.0); } diff --git a/src/style.css b/src/style.css index e83c9a1..d352790 100644 --- a/src/style.css +++ b/src/style.css @@ -7,9 +7,3 @@ body { height: 100vh; background-color: #222; } - -canvas { - width: 512px; - height: 512px; - display: block; -}