feat: fixed canvas size & enhance WebGPU rendering with grid and time uniforms

This commit is contained in:
Florian Sylvain
2025-04-06 01:45:24 +02:00
parent 10f67b8a11
commit 1b90a6e3fe
4 changed files with 182 additions and 52 deletions
+141 -41
View File
@@ -1,51 +1,60 @@
import "./style.css"
async function init() {
const GRID_SIZE = 32
async function getCanvas(): Promise<HTMLCanvasElement> {
const canvas = document.querySelector<HTMLCanvasElement>("#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<GPUAdapter> {
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<GPUDevice> {
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<GPURenderPipeline> {
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)
})
+40 -4
View File
@@ -1,9 +1,45 @@
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<uniform> 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);
}
-6
View File
@@ -7,9 +7,3 @@ body {
height: 100vh;
background-color: #222;
}
canvas {
width: 512px;
height: 512px;
display: block;
}