First Steps
Web configuration
We need to know that WebGPU is under development.
Because of that we are going to install Chrome Canary and activate some flags.
To activate go to chrome://flags/ and search the flags.
#enable-unsafe-webgpu & #enable-webgpu-developer-features.
Basic concepts
Buffers
The buffers are memory stores initialized by the GPU which will allow us to save the information of our vertices, indices, and uniforms.
JavaScript
We will need to know the basics of html and javascript to be able to use this graphical API.
Shaders
A graphic shader are precompiled functions that allow us to make color and/or position transformations. of the vertices that will later be painted on the screen.
Pipelines
A pipeline is a configuration variable that allows us to pass information from graphics processing. In our case we will send you the shader and the rendering settings.
Uniforms
Uniforms are special variables that we will pass to our shader.
Bindings
The bindings is just what its name indicates, we bind a buffer to the shader, this implies that the shader will be able to read that buffer and make use of it.
Script Instance
We need to create the init and frame function to make the graphic loop.
It should be noted that we are going to work within this function.
// Create grafic Loop
/////////////////////////////////////////////////////////////////////////
const init = async => {
// Constants Inicializations (vars that doesn't need the frame)
function frame() {
// Graphic logic
requestAnimationFrame(frame); // This is to update the frame sequently
}
requestAnimationFrame(frame); // This is to update the frame sequently
}
init();
/////////////////////////////////////////////////////////////////////////
Demand Resources from GPU
To request the resources from the GPU we will use the navigator.gpu object.
// Init
/////////////////////////////////////////////////////////////////////////
const gpu = navigator.gpu;
const adapter = await gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById("my_canvas");
const context = canvas.getContext("webgpu");
/////////////////////////////////////////////////////////////////////////
Create Swap Chain
The swap chain is needed to make a canvas to draw in the web page.
Once created, it will be added to the previously created context.
// Swap Chain
/////////////////////////////////////////////////////////////////////////
const presentationFormat = gpu.getPreferredCanvasFormat();
context.configure({
device, // Create link between GPU and canvas.
format: presentationFormat,
alphaMode: "opaque"
});
/////////////////////////////////////////////////////////////////////////
Creaate the Shaders
The shader is the most important part of this code, because if you don't write, the GPU isn't going to know what
to do so it can received trash.
I decide to make only one. But you can make 2 intead.
I use the following shader with packed vertex and some uniforms.
We need to remember that the frament shader only return a rgba color that means a Vec4
// Define Basic Shader
/////////////////////////////////////////////////////////////////////////
const shaderModule = device.createShaderModule({
code: `
@group(0) @binding(0) var viewProjection : mat4x4;
@group(0) @binding(1) var model : mat4x4;
struct VertexOut {
@builtin(position) position : vec4,
@location(0) color : vec4,
};
@vertex
fn vertex_main(@location(0) position: vec4,
@location(1) color: vec4) -> VertexOut
{
var output : VertexOut;
output.position = viewProjection * model * position;
output.color = color;
return output;
}
@fragment
fn fragment_main(fragData: VertexOut) -> @location(0) vec4
{
return fragData.color;
}
`,
});
/////////////////////////////////////////////////////////////////////////
Make the vertex & Index array
To make it, you need to know if you are going to do in triangle-strip or triangle-list ( I recommend you the
list ).
Also you need a basic knowled about make a object mesh.
// SetUp Vertex (position (vec4), color(vec4))
/////////////////////////////////////////////////////////////////////////
// Pack them all into one array
// Order (x, y, z, w) / (xn, yn, zn)
const vertices = new Float32Array([
// Vertex 0
-0.5, -0.5, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
// Vertex 1
0.5, -0.5, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0,
// Vertex 2
0.5, 0.5, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0,
// Vertex 3
-0.5, 0.5, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0
]);
/////////////////////////////////////////////////////////////////////////
// SetUp Indexes
/////////////////////////////////////////////////////////////////////////
let indices = new Uint16Array([
0, 1, 2,
0, 2, 3
]);
/////////////////////////////////////////////////////////////////////////
Make the buffers
Once the array of indices and vertices have been created, it is necessary to request memory from the graph.
For this we will use the function device.createBuffer({Configs});
The following example is from the index buffer.
// Index Buffers
/////////////////////////////////////////////////////////////////////////
const indexBuffer = device.createBuffer({
size: indices.byteLength,
usage: GPUBufferUsage.INDEX,
mappedAtCreation: true,
});
new Uint16Array(indexBuffer.getMappedRange()).set(indices);
indexBuffer.unmap();
/////////////////////////////////////////////////////////////////////////
Vertex Configuration
Before pass the vertex to the GPU we need to create a var telling the GPU what type of vertex we are going to
use.
As in my case I have a packed vertex made up of vec4 pos and vec4 color, I will configure it as follows.
Remember to put it in the same order as you make the vertex array.
// Vertex Descriptor
/////////////////////////////////////////////////////////////////////////
const vertexBuffersDescriptors = [
{
attributes: [
{
shaderLocation: 0,
offset: 0,
format: "float32x4",
},
{
shaderLocation: 1,
offset: 16,
format: "float32x4",
},
],
arrayStride: 32,
stepMode: "vertex",
},
];
/////////////////////////////////////////////////////////////////////////
Create the Pipeline
Create the pipeline is relatively easy. You need to tell where is the vertex & fragment shader and the triangle
draw configuration.
Of course for the vertex shader you need to pass the Vertex configuration.
// Create render pipeline
/////////////////////////////////////////////////////////////////////////
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vertex_main",
buffers: vertexBuffersDescriptors,
},
fragment: {
module: shaderModule,
entryPoint: "fragment_main",
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: "triangle-list",
indexFormat: "uint16",
// Add if you want any type of cull
// frontFace: "cw",
// cullMode: "back",
},
});
/////////////////////////////////////////////////////////////////////////
Binding uniforms
From now on, we'll do everything inside the frame function.
For the uniforms I'm going to pass the viewProjection & model Matrix.
To do it you need to create the uniforms, reserve memory and pass to one entry of the group.
// Pass the camera & model uniforms
/////////////////////////////////////////////////////////////////////////
const bindGroupLayout = pipeline.getBindGroupLayout(0);
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: viewProjectionBuffer,
offset: 0,
size: viewProjectionMatrix.byteLength
}
},
{
binding: 1,
resource: {
buffer: modelBuffer,
offset: 0,
size: modelMatrix.byteLength
}
}
]
});
/////////////////////////////////////////////////////////////////////////
Frame Render
The frame render is the config used to set the update init canvas.
// Create render pass descriptor
/////////////////////////////////////////////////////////////////////////
const renderPassDescriptor = {
colorAttachments: [
{
loadOp: "clear", // Clear image on each load
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
storeOp: "store", // Write result to the view
},
],
};
// Get the latest swap chain image, set as our output color attachment image.
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
/////////////////////////////////////////////////////////////////////////
Beggin to draw
// Begin to draw
/////////////////////////////////////////////////////////////////////////
// Create command encoder to record rendering commands
const commandEncoder = device.createCommandEncoder();
// Pass render pass descriptor to get back a GPURenderPassEncorder
// So that we can record rendering commands
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
/////////////////////////////////////////////////////////////////////////
Draw
Here you set the vertex you are going to draw and the index to make the triangles.
Also, you need to pass the uniforms.
if you have a triangle-strip instead of triangle-list, you need to change the last 3 lines with
passEncoder.draw(nVertices);
// Draw
/////////////////////////////////////////////////////////////////////////
// Configure the pass encoder
passEncoder.setPipeline(pipeline);
// Remove if yo don't have
passEncoder.setBindGroup(0, bindGroup);
// Draw the triangle
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.setIndexBuffer(indexBuffer, "uint16");
passEncoder.drawIndexed(indices.length);
/////////////////////////////////////////////////////////////////////////
End draw
// End draw
/////////////////////////////////////////////////////////////////////////
// End the render pass
passEncoder.end();
// Get command buffer to submit to GPU, by calling commandEncoder.finish()
device.queue.submit([commandEncoder.finish()]);
/////////////////////////////////////////////////////////////////////////
Quad Example
This example is a GPU procces that makes the canvas.
Full Example code. To use it make sure you have the gl-Matrix library.