Last active
December 25, 2023 05:16
-
-
Save include-yy/5c41d0c9734769938d58ed2720f5430f to your computer and use it in GitHub Desktop.
first webgpu app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>First WebGPU APP</title> | |
</head> | |
<body> | |
<canvas width="720" height="720"></canvas> | |
</body> | |
<script src="./index.js"></script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const GRID_SIZE = 32 | |
const WORKGROUP_SIZE = 8 | |
const UPDATE_INTERVAL = 10 | |
let step = 0 | |
async function me() { | |
const canvas = document.querySelector("canvas") | |
if (!navigator.gpu) { | |
throw new Error("webGPU not support!") | |
} | |
const adapter = await navigator.gpu.requestAdapter() | |
if (!adapter) { | |
throw new Error("no GPUAdapter found") | |
} | |
const device = await adapter.requestDevice() | |
const context = canvas.getContext("webgpu") | |
const canvasFormat = navigator.gpu.getPreferredCanvasFormat() | |
context.configure({ | |
device: device, | |
format: canvasFormat, | |
}) | |
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]) | |
const uniformBuffer = device.createBuffer({ | |
label: "Grid Uniforms", | |
size: uniformArray.byteLength, | |
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, | |
}) | |
device.queue.writeBuffer(uniformBuffer, 0, uniformArray) | |
const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE) | |
const cellStateStorage = [ | |
device.createBuffer({ | |
label: "Cell State", | |
size: cellStateArray.byteLength, | |
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | |
}), | |
device.createBuffer({ | |
label: "Cell State", | |
size: cellStateArray.byteLength, | |
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | |
}) | |
] | |
for (let i = 0; i < cellStateArray.length; i += 3) { | |
cellStateArray[i] = Math.random() > 0.3 ? 1 : 0 | |
} | |
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray) | |
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray) | |
const vertices = new Float32Array([ | |
-0.8, -0.8, 0.8, -0.8, 0.8, 0.8, // triangle 1 | |
-0.8, -0.8, 0.8, 0.8, -0.8, 0.8, // triangle 2 | |
]) | |
const vertexBuffer = device.createBuffer({ | |
label: "Cell vertices", | |
size: vertices.byteLength, | |
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, | |
}) | |
device.queue.writeBuffer(vertexBuffer, 0, vertices) | |
const vertexBufferLayout = { | |
arrayStride: 8, | |
attributes: [{ | |
format: 'float32x2', | |
offset: 0, | |
shaderLocation: 0, | |
}] | |
} | |
const cellShaderModule = device.createShaderModule({ | |
label: 'Cell shader', | |
code: ` | |
struct VertexInput { | |
@location(0) pos: vec2f, | |
@builtin(instance_index) instance: u32 | |
}; | |
struct VertexOutput { | |
@builtin(position) pos: vec4f, | |
@location(0) cell: vec2f | |
}; | |
@group(0) @binding(0) var<uniform> grid: vec2f; | |
@group(0) @binding(1) var<storage> cellState: array<u32>; | |
@vertex | |
fn vertexMain(input : VertexInput) -> VertexOutput { | |
let i = f32(input.instance); | |
let cell = vec2f(i % grid.x, floor(i / grid.x)); | |
let state = f32(cellState[input.instance]); | |
let cellOffset = cell / grid * 2.0; | |
let gridPos = (input.pos * state + 1.0) / grid - 1.0 + cellOffset; | |
var output: VertexOutput; | |
output.pos = vec4f(gridPos, 0, 1); | |
output.cell = cell; | |
return output; | |
} | |
@fragment | |
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { | |
let c = input.cell / grid; | |
return vec4f(c, 1-c.x, 1); | |
} | |
` | |
}) | |
const simulationShaderModule = device.createShaderModule({ | |
label: "Game of Life simulation shader", | |
code: ` | |
@group(0) @binding(0) var<uniform> grid: vec2f; | |
@group(0) @binding(1) var<storage> cellStateIn: array<u32>; | |
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>; | |
fn cellIndex(cell: vec2u) -> u32 { | |
return (cell.y % u32(grid.y)) * u32(grid.x) + | |
(cell.x % u32(grid.x)); | |
} | |
fn callActive(x: u32, y: u32) -> u32 { | |
return cellStateIn[cellIndex(vec2(x, y))]; | |
} | |
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}) | |
fn computeMain(@builtin(global_invocation_id) cell: vec3u) { | |
let activeNegihbors = callActive(cell.x + 1, cell.y + 1) + | |
callActive(cell.x + 1, cell.y) + | |
callActive(cell.x + 1, cell.y - 1) + | |
callActive(cell.x, cell.y - 1) + | |
callActive(cell.x - 1, cell.y - 1) + | |
callActive(cell.x - 1, cell.y) + | |
callActive(cell.x - 1, cell.y + 1) + | |
callActive(cell.x, cell.y + 1); | |
let i = cellIndex(cell.xy); | |
switch activeNegihbors { | |
case 2: { | |
cellStateOut[i] = cellStateIn[i]; | |
} | |
case 3: { | |
cellStateOut[i] = 1; | |
} | |
default: { | |
cellStateOut[i] = 0; | |
} | |
} | |
} | |
` | |
}) | |
const bindGroupLayout = device.createBindGroupLayout({ | |
label: "Cell Bind Group Layout", | |
entries: [{ | |
binding: 0, | |
visibility: GPUShaderStage.VERTEX | |
| GPUShaderStage.COMPUTE | |
| GPUShaderStage.FRAGMENT, | |
buffer: {} | |
}, { | |
binding: 1, | |
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE, | |
buffer: {type: "read-only-storage"} | |
}, { | |
binding: 2, | |
visibility: GPUShaderStage.COMPUTE, | |
buffer: {type: "storage"} | |
}] | |
}) | |
const bindGroups = [ | |
device.createBindGroup({ | |
label: "Cell renderer bind group A", | |
layout: bindGroupLayout, | |
entries: [{ | |
binding: 0, | |
resource: {buffer: uniformBuffer} | |
}, { | |
binding: 1, | |
resource: {buffer: cellStateStorage[0]} | |
}, { | |
binding: 2, | |
resource: {buffer: cellStateStorage[1]} | |
}] | |
}), | |
device.createBindGroup({ | |
label: "Cell renderer bind group B", | |
layout: bindGroupLayout, | |
entries: [{ | |
binding: 0, | |
resource: {buffer: uniformBuffer} | |
}, { | |
binding: 1, | |
resource: {buffer: cellStateStorage[1]} | |
}, { | |
binding: 2, | |
resource: {buffer: cellStateStorage[0]} | |
}] | |
}) | |
] | |
const pipelineLayout = device.createPipelineLayout({ | |
label: "Cell pipeline Layout", | |
bindGroupLayouts: [ bindGroupLayout ] | |
}) | |
const cellPipeline = device.createRenderPipeline({ | |
label: "Cell pipeline", | |
layout: pipelineLayout, | |
vertex: { | |
module: cellShaderModule, | |
entryPoint: "vertexMain", | |
buffers: [vertexBufferLayout] | |
}, | |
fragment: { | |
module: cellShaderModule, | |
entryPoint: "fragmentMain", | |
targets: [{ | |
format: canvasFormat | |
}] | |
} | |
}) | |
const simulationPipeline = device.createComputePipeline({ | |
label: "Simulation pipeline", | |
layout: pipelineLayout, | |
compute: { | |
module: simulationShaderModule, | |
entryPoint: "computeMain", | |
} | |
}) | |
const updateGrid = () => { | |
const encoder = device.createCommandEncoder() | |
const computePass = encoder.beginComputePass() | |
computePass.setPipeline(simulationPipeline) | |
computePass.setBindGroup(0, bindGroups[step % 2]) | |
const workgroupCount = Math.ceil(GRID_SIZE / WORKGROUP_SIZE) | |
computePass.dispatchWorkgroups(workgroupCount, workgroupCount) | |
computePass.end() | |
step++ | |
const pass = encoder.beginRenderPass({ | |
colorAttachments: [{ | |
view: context.getCurrentTexture().createView(), | |
loadOp: "clear", | |
clearValue: [0, 0, 0, 1.0], | |
storeOp: "store", | |
}] | |
}) | |
pass.setPipeline(cellPipeline) | |
pass.setBindGroup(0, bindGroups[step%2]) | |
pass.setVertexBuffer(0, vertexBuffer) | |
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE) | |
pass.end() | |
device.queue.submit([encoder.finish()]) | |
} | |
setInterval(updateGrid, UPDATE_INTERVAL) | |
console.log(vertices.length / 2) | |
console.log(vertices.length) | |
} | |
me() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment