webgpu-app/index.js
2026-07-16 15:21:09 -05:00

317 lines
8.3 KiB
JavaScript

if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No WebGPU adapter");
}
const device = await adapter.requestDevice();
const canvas = document.querySelector("canvas");
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0.9, g: 0.5, b: 0.2, a: 1 },
storeOp: "store",
}]
});
renderPass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
const cubeResponse = await fetch("obj/cube.bin");
const cubeObj = await cubeResponse.arrayBuffer();
const cubeView = new DataView(cubeObj);
const indexCount = cubeView.getUint32(0, true);
const indexBufferOffset = cubeView.getUint32(4, true);
const indexBufferSize = cubeView.getUint32(8, true);
const vertexBufferOffset = cubeView.getUint32(12, true);
const vertexBufferSize = cubeView.getUint32(16, true);
console.log(`indexBufferOffset ${indexBufferOffset} indexBufferSize ${indexBufferSize}`);
console.log(`vertexBufferOffset ${vertexBufferOffset} vertexBufferSize ${vertexBufferSize}`);
const cubeIndexCount = indexBufferSize / 2;
const cubeVerticesStride = 36;
const indexVertexBuffer = device.createBuffer({
label: "cell vertices",
size: indexBufferSize + vertexBufferSize,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(indexVertexBuffer, 0, cubeObj, indexBufferOffset, indexBufferSize);
device.queue.writeBuffer(indexVertexBuffer, indexBufferSize, cubeObj, vertexBufferOffset, vertexBufferSize);
const vertexBufferLayout = {
arrayStride: cubeVerticesStride,
attributes: [{
format: "float32x3",
offset: 0,
shaderLocation: 0,
}, {
format: "float32x2",
offset: 12,
shaderLocation: 1,
}, {
format: "float16x4",
offset: 20,
shaderLocation: 2,
}, {
format: "float16x4",
offset: 28,
shaderLocation: 3,
}],
stepMode: "vertex",
};
function getPath(path)
{
return fetch(path).then((response) => {
if (!response.ok) {
throw new Error(`${path}: ${response.status}`)
}
return response.blob()
}).then((blob) => { return blob.text() })
}
const cellShaderModule = device.createShaderModule({
label: "cell shader",
code: await getPath("index.wgsl")
});
const bindGroupLayout = device.createBindGroupLayout({
label: "bind group layout",
entries: [{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "uniform" }
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage" }
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage" }
}]
});
const pipelineLayout = device.createPipelineLayout({
label: "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,
}]
},
primitive: {
topology: 'triangle-list',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
const computeShaderModule = device.createShaderModule({
label: "compute shader",
code: await getPath("compute.wgsl")
});
const computePipeline = device.createComputePipeline({
label: "compute pipeline",
layout: pipelineLayout,
compute: {
module: computeShaderModule,
entryPoint: "computeMain",
},
});
const gridSize = 32;
const workGroupSize = 16;
//const uniformArray2 = new Float32Array([gridSize, gridSize, 1, 1]);
const memory = new WebAssembly.Memory({ initial: 1 });
const uniformArray = new Float32Array(memory.buffer);
uniformArray[0] = gridSize;
uniformArray[1] = gridSize;
uniformArray[2] = 1;
uniformArray[3] = 1;
const uniformArraySize = 4 * 4 + 4 * 4 * 4;
const rotate = await WebAssembly.instantiateStreaming(fetch("rotate.wasm"), {
env: { memory: memory }
});
const uniformBuffer = device.createBuffer({
label: "grid uniform",
size: uniformArraySize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4);
const cellStateArray = new Uint32Array(gridSize * gridSize);
const cellStateStorage = [
device.createBuffer({
label: "cell state 0",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
device.createBuffer({
label: "cell state 1",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
];
for (let i = 0; i < cellStateArray.length; i += 1) {
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
const bindGroups = [
device.createBindGroup({
label: "cell bind group 0",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer },
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] },
}, {
binding: 2,
resource: { buffer: cellStateStorage[1] },
}],
}),
device.createBindGroup({
label: "cell bind group 1",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer },
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] },
}, {
binding: 2,
resource: { buffer: cellStateStorage[0] },
}],
})
];
let tick = 0;
let which = 0;
function createDepthTexture() {
return device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
var depthTexture = undefined;
function render()
{
if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
if (depthTexture !== undefined) {
depthTexture.destroy();
}
depthTexture = createDepthTexture();
}
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroups[which]);
const workgroupCount = Math.ceil((gridSize * gridSize) / workGroupSize);
computePass.dispatchWorkgroups(workgroupCount);
computePass.end();
const aspect = canvas.width / canvas.height;
rotate.instance.exports.rotate(tick * 0.01, aspect, 4 * 4);
tick += 1;
if ((tick % 5) == 0) {
which ^= 1;
}
if (canvas.width > canvas.height) {
uniformArray[2] = aspect;
uniformArray[3] = 1;
} else {
uniformArray[2] = 1;
uniformArray[3] = 1 / aspect;
}
device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4);
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 },
storeOp: "store",
}],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
});
renderPass.setPipeline(cellPipeline);
const vertexOffset = indexBufferSize;
renderPass.setVertexBuffer(0, indexVertexBuffer, vertexOffset, vertexBufferSize);
renderPass.setIndexBuffer(indexVertexBuffer, "uint16", 0, indexBufferSize);
renderPass.setBindGroup(0, bindGroups[which]);
//renderPass.draw(cubeVerticesCount, gridSize * gridSize);
renderPass.drawIndexed(cubeIndexCount, gridSize * gridSize);
//renderPass.drawIndexed(cubeIndexCount);
renderPass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render)
}
requestAnimationFrame(render)