mandelbrot

This commit is contained in:
nora 2024-10-30 21:30:30 +01:00
parent 1a07df64ca
commit c6fd528af8
3 changed files with 150 additions and 80 deletions

View file

@ -18,7 +18,7 @@
</head> </head>
<body> <body>
<div id="unsupported-browser"> <div id="unsupported-browser">
<p class="bold">You are using an unsupported browser.</p> <p class="bold">You are using an unsupported browser or platform.</p>
<p> <p>
WebGPU is still experimental and does not have wide browser support. WebGPU is still experimental and does not have wide browser support.
</p> </p>
@ -31,14 +31,23 @@
the time of writing. Firefox may have one at the time of reading, in the time of writing. Firefox may have one at the time of reading, in
which case I recommend it instead. which case I recommend it instead.
</p> </p>
<p>Also check out the <a href="https://developer.chrome.com/docs/web-platform/webgpu/troubleshooting-tips">Chrome Dev guide</a> when using Chrome</p> <p>
Also check out the
<a
href="https://developer.chrome.com/docs/web-platform/webgpu/troubleshooting-tips"
>Chrome Dev guide</a
>
when using Chrome
</p>
</div> </div>
<noscript> <noscript>
This website uses WebGPU, which requires JavaScript to operate. This website uses WebGPU, which requires JavaScript to operate.
</noscript> </noscript>
<div id="error" class="hidden error"></div> <div id="error" class="hidden error"></div>
<button id="render-me">Render Me</button> <div>
<canvas id="result" height="800" width="1200"></canvas>
</div>
<script type="module" src="./index.js"></script> <script type="module" src="./index.js"></script>
</body> </body>

152
index.js
View file

@ -44,78 +44,104 @@ async function init() {
code: shader, code: shader,
}); });
document.getElementById("render-me").addEventListener("click", doStuff); // document.getElementById("render-me").addEventListener("click", doStuff);
}
async function doStuff() { const canvas = document.getElementById("result");
const output = device.createBuffer({ const context = canvas.getContext("webgpu");
size: 1000, context.configure({
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, device: device,
}); format: navigator.gpu.getPreferredCanvasFormat(),
const stagingBuffer = device.createBuffer({ alphaMode: "premultiplied",
size: 1000,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
}); });
const vertices = new Float32Array([
// Triangle 1 (Blue)
-1,
-1,
1,
-1,
1,
1,
const bindGroupLayout = device.createBindGroupLayout({ -1, // Triangle 2 (Red)
entries: [ -1,
{ 1,
binding: 0, 1,
visibility: GPUShaderStage.COMPUTE, -1,
buffer: { 1,
type: "storage", ]);
const vertexBuffer = device.createBuffer({
label: "Vertices",
size: vertices.byteLength, // make it big enough to store vertices in
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const GRID_SIZE = 4;
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 vertexBuffers = [
{
attributes: [
{
shaderLocation: 0,
offset: 0,
format: "float32x2",
}, },
}, ],
], arrayStride: 8,
}); stepMode: "vertex",
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: output,
},
},
],
});
const computePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
}),
compute: {
module: shaderModule,
entryPoint: "main",
}, },
}); ];
const pipelineDescriptor = {
label: "Render Pipeline",
vertex: {
module: shaderModule,
entryPoint: "vertex_main",
buffers: vertexBuffers,
},
fragment: {
module: shaderModule,
entryPoint: "fragment_main",
targets: [
{
format: navigator.gpu.getPreferredCanvasFormat(),
},
],
},
primitive: {
topology: "triangle-list",
},
layout: "auto",
};
const renderPipeline = device.createRenderPipeline(pipelineDescriptor);
const commandEncoder = device.createCommandEncoder(); const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass(); const clearColor = { r: 0.0, g: 0.5, b: 1.0, a: 1.0 };
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup); const renderPassDescriptor = {
passEncoder.dispatchWorkgroups(Math.ceil(1000 / 64)); colorAttachments: [
{
clearValue: clearColor,
loadOp: "clear",
storeOp: "store",
view: context.getCurrentTexture().createView(),
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(renderPipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.draw(vertices.length / 2);
passEncoder.end(); passEncoder.end();
commandEncoder.copyBufferToBuffer(
output,
0, // source offset
stagingBuffer,
0, // destination offest
1000
);
device.queue.submit([commandEncoder.finish()]); device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(
GPUMapMode.READ,
0, // offset
1000 // length
);
const copyArrayBuffer = stagingBuffer.getMappedRange(0, 1000);
const data = copyArrayBuffer.slice();
stagingBuffer.unmap();
console.log(new Float32Array(data));
} }
init(); init();

View file

@ -1,19 +1,54 @@
@group(0) @binding(0) const CANVAS: vec2f = vec2f(1200, 800);
var<storage, read_write> output: array<f32>;
@compute @workgroup_size(64) @vertex
fn main( fn vertex_main(@location(0) pos: vec2f) ->
@builtin(global_invocation_id) @builtin(position) vec4f {
global_id : vec3u, return vec4f(pos, 0, 1);
}
@builtin(local_invocation_id) @fragment
local_id : vec3u, fn fragment_main(
) { @builtin(position) pos: vec4f
// Avoid accessing the buffer out of bounds ) -> @location(0) vec4f {
if (global_id.x >= 1000) { let step_size = vec2f(3.0, 2.0) / CANVAS;
return; let center = vec2f(-0.75, 0.0);
let offset = vec2f(
center.x - CANVAS.x / 2 * step_size.x,
-(center.y - CANVAS.y / 2.0 * step_size.y) - 2.0,
);
let sample_pos = offset + (step_size * vec2f(pos.x, pos.y));
let value = mandelbrot_pixel(sample_pos);
let multiplier = 1.0 - (value * value) / f32(ITERATIONS);
let i = 255.0 * multiplier;
return vec4f(i, i, i, 1);
}
const ITERATIONS = 50000;
const THRESHOLD: f32 = 100;
fn mandelbrot_pixel(position: vec2f) -> f32 {
var n = vec2f(0, 0);
let c = position;
n = n + c;
var passed_threshold_after: i32 = ITERATIONS;
for (var i = 0; i < ITERATIONS; i += 1) {
n = cmul(n, n) + c;
if n.x > THRESHOLD || n.y > THRESHOLD && passed_threshold_after == ITERATIONS {
passed_threshold_after = i;
}
} }
output[global_id.x] = return f32(passed_threshold_after);
f32(local_id.x);
} }
fn cmul(a: vec2f, b: vec2f) -> vec2f {
return vec2f(
a.x * b.x - a.y * b.y,
a.x * b.y + a.y * b.x,
);
}