Skip to content

Commit

Permalink
Add a WebGPU example (#116)
Browse files Browse the repository at this point in the history
* Add WebGPU example
* Added more comments to the WebGPU example
* Address feedback from @greggman
  • Loading branch information
toji authored Feb 1, 2024
1 parent 2eb9f5f commit c463f9f
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 0 deletions.
22 changes: 22 additions & 0 deletions examples/webgpu/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bouncy Ball - Comparing Web Animation Techniques - WebGPU</title>
<link rel="stylesheet" href="../shared-styles.css">
<style>
body,
html,
#canvas {
height: 100%;
width: 100%;
margin: 0;
display: block;
}
</style>
</head>
<body>
<canvas id="canvas" width="66" height="236"></canvas>
<script src="script.js" type="module"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions examples/webgpu/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# WebGPU

WebGPU is a JavaScript API for rendering 3D graphics without the use of plug-ins using more modern GPU API patterns, as well as using GPUs for acceleration of non-graphical operations through compute shaders. It is a successor to WebGL.

WebGPU programs consist of control code written in JavaScript and shader code that is written in WebGPU Shading Language (WGSL), a language similar to Rust, and is executed on a computer's graphics processing unit (GPU). WebGPU is designed and maintained by the W3C.

## Project Links

* [The WebGPU API Documentation in MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
156 changes: 156 additions & 0 deletions examples/webgpu/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Requests a WebGPU adapter and create a device from it.
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

// Get a WebGPU context from the canvas and configure it for use with the device.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('webgpu');
context.configure({
device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: 'premultiplied',
});

// Set up the vertex and fragment shaders.
const module = device.createShaderModule({
code: `
// Define the four corners of the square being rendered
var<private> pos = array(
vec2f(-1, -1),
vec2f(1, -1),
vec2f(-1, 1),
vec2f(1, 1),
);
// Define the uniform values that will be read from the uniformBuffer
struct Uniforms {
time: f32,
h: f32,
k: f32,
a: f32,
canvasSize: vec2f,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
// Define the vertex shader outputs/fragment shader inputs
struct VertexOut {
@builtin(position) pos: vec4f,
@location(0) center: vec2f,
@location(1) worldPos: vec2f,
}
@vertex
fn vertexMain(@builtin(vertex_index) i: u32) -> VertexOut {
// Calculate the y position of the ball center
let ypos = (u.a * pow((((u.time + u.h) % (u.h * 2.0)) - u.h), 2.0) + u.k) - u.k/2.0;
var out: VertexOut;
// Scale the ball size appropriately for the canvas
let circleSize = vec2f(100) / u.canvasSize;
out.center = vec2f(0, ypos);
out.worldPos = (pos[i] * circleSize) + out.center;
// Set the vertex positions in Normalized Device Coordinates
out.pos = vec4f(out.worldPos, 0, 1);
return out;
}
@fragment
fn fragmentMain(in: VertexOut) -> @location(0) vec4f {
// Compute the distance from the center of the circle in pixels.
let circleSize = vec2f(100) / u.canvasSize;
let diff = (in.worldPos - in.center) / circleSize;
let d = distance(diff, vec2f(0));
if (d > 0.5) {
discard; // Don't write to any pixels outside the circle.
}
return vec4f(d, d, 0.2 + d, 1); // Color for pixels within circle.
}
`
});

// Create a render pipeline that uses the above shader.
const pipeline = device.createRenderPipeline({
label: 'Bouncing Ball Pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vertexMain',
},
primitive: {
topology: 'triangle-strip',
},
fragment: {
module,
entryPoint: 'fragmentMain',
targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }]
}
});

// Create a buffer for the shader uniforms.
const uniformBuffer = device.createBuffer({
label: 'Uniform Buffer',
size: 8 * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// Calculating variables for animation.
const h = 575;
const k = 2 - (2*(50/canvas.clientHeight));
const a = -4 * k / Math.pow(h * 2, 2);

// Prepare the animation variables to be copied to the uniformBuffer.
const uniformArray = new Float32Array(8);
uniformArray[0] = 0; // time
uniformArray[1] = h;
uniformArray[2] = k;
uniformArray[3] = a;
uniformArray[4] = canvas.clientWidth;
uniformArray[5] = canvas.clientHeight;

// Create a bind group with the uniformBuffer.
const bindGroup = device.createBindGroup({
label: 'Bouncing Ball Bind Group',
layout: pipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}]
});

// The main frame loop.
let start, time;
(function drawFrame(timestamp) {
if (!start) { start = timestamp };
time = timestamp - start;

// Update the variables for the animation on the GPU
uniformArray[0] = time;
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);

// Begin a command encoder and start a render pass
const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
clearValue: [0, 0, 0, 0],
storeOp: 'store',
}]
});

// Draw 4 vertices with the pipeline and bind group created above
renderPass.setPipeline(pipeline);
renderPass.setBindGroup(0, bindGroup);
renderPass.draw(4);

// End the render pass and submit the commands.
renderPass.end();
device.queue.submit([ encoder.finish() ]);

// Queue up the next animation frame.
window.requestAnimationFrame(drawFrame);
})(performance.now());
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ <h1 class="title">Bouncy Ball</h1>
<a href="#video" id="video" class="nav-button button-video">Video</a>
<a href="#matter-js" id="matter-js" class="nav-button button-matterjs">Matter.js</a>
<a href="#webgl" id="webgl" class="nav-button button-webgl">WebGL</a>
<a href="#webgpu" id="webgpu" class="nav-button button-webgpu">WebGPU</a>
<a href="#flash" id="flash" class="nav-button button-flash">Flash</a>
<a href="#popmotion" id="popmotion" class="nav-button button-popmotion">Popmotion</a>
<a href="#lottie" id="lottie" class="nav-button button-lottie">Lottie</a>
Expand Down
1 change: 1 addition & 0 deletions site/css/example-colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
--video: #CFD1AD;
--cssstep: #808080;
--webgl: #AAAAAA;
--webgpu: #8080FF;
--flash: #FF00FF;
--popmotion: #FF1C68;
--lottie: #00E3C7;
Expand Down
1 change: 1 addition & 0 deletions site/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ code a {
.button-video:target { color: var(--video); }
.button-cssstep:target { color: var(--cssstep); }
.button-webgl:target { color: var(--webgl); }
.button-webgpu:target { color: var(--webgpu); }
.button-flash:target { color: var(--flash); }
.button-popmotion:target { color: var(--popmotion); }
.button-lottie:target { color: var(--lottie); }
Expand Down
4 changes: 4 additions & 0 deletions site/js/updatePanes.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ function isCompatible(selectedId) {
if (selectedId === 'p5') {
return Modernizr.webgl;
}
if (selectedId === 'webgpu') {
// WebGPU is still in development on some browsers.
return navigator.gpu;
}
return true;
}

Expand Down

0 comments on commit c463f9f

Please sign in to comment.