This article is the first of series of articles that will hopefully teach
you about 3D math. Each one builds on the previous lesson so you may find
them easiest to understand by reading them in order.
We are going to start code similar to the examples from the article on vertex-buffers
but instead of a bunch of circles we’re going to draw a single F and we’ll use an index buffer to keep the data
smaller.
Let’s work in pixel space instead of clip space, just like the Canvas 2D API
We’ll make an F and we’ll build it from 6 triangles like this
Here’s the data for the F
function createFVertices(){
const vertexData =newFloat32Array([
// left column
0,0,
30,0,
0,150,
30,150,
// top rung
30,0,
100,0,
30,30,
100,30,
// middle rung
30,60,
70,60,
30,90,
70,90,
]);
const indexData =newUint32Array([
0,1,2,2,1,3,// left column
4,5,6,6,5,7,// top run
8,9,10,10,9,11,// middle run
]);
return{
vertexData,
indexData,
numVertices: indexData.length,
};
}
The vertex data above is in pixel space so we need to translate that to clip space.
We can do that by passing the resolution into the shader and doing some math.
Here it is spelled out one step at a time.
structUniforms{
color: vec4f,
resolution: vec2f,
};
structVertex{
@location(0) position: vec2f,
};
structVSOutput{
@builtin(position) position: vec4f,
};
@group(0)@binding(0)var<uniform> uni:Uniforms;
@vertex fn vs(vert:Vertex)->VSOutput{
var vsOut:VSOutput;
let position = vert.position;
// convert the position from pixels to a 0.0 to 1.0 value
You can see we take a vertex position and divide it by the resolution.
This gives us a value that goes from 0 to 1 across the canvas.
We then multiply by 2 to get a value that goes from 0 to 2 across the canvas.
We subtract 1. Now our value is in clip space but it’s flipped because
the clip space goes positive Y up where as canvas 2d goes positive Y down.
So we multiply Y by -1 to flip it. Now we have our needed clip space value
which we can output from the shader.
We’ve only got one attribute so our pipeline looks like this
const pipeline = device.createRenderPipeline({
label:'just 2d position',
layout:'auto',
vertex:{
module,
buffers:[
{
arrayStride:(2)*4,// (2) floats, 4 bytes each
attributes:[
{shaderLocation:0, offset:0, format:'float32x2'},// position
Before we run it lets make the background of the canvas look like
graph paper. We’ll set it’s scale so each grid cell of the graph
paper is 10x10 pixels and every 100x100 pixels we’ll draw a bolder
line.
:root {
--bg-color:#fff;
--line-color-1:#AAA;
--line-color-2:#DDD;
}
@media(prefers-color-scheme: dark){
:root {
--bg-color:#000;
--line-color-1:#666;
--line-color-2:#333;
}
}
canvas {
display: block;/* make the canvas act like a block */
width:100%;/* make the canvas fill its container */
The CSS above should handle both light and dark cases.
All our examples to this point have used an opaque canvas. To make it transparent,
so we can see the background we just setup, we need to make a few changes.
First we need to set the alphaMode when we configure the canvas to 'premultiplied'.
It defaults to 'opaque'.
context.configure({
device,
format: presentationFormat,
alphaMode:'premultiplied',
});
Then we need to clear the canvas to 0, 0, 0, 0 in our GPURenderPassDescriptor.
Because the default clearValue is 0, 0, 0, 0 we can just delete the line that
was setting it to something else.
Notice the F’s size relative to the grid behind it.
The vertex positions of the F data make an F that is 100 pixels
wide and 150 pixels tall and that matches what we displayed.
The F starts at 0,0 and extends right to 100,0 and down to 0,150
Now that we have the basics in place, let’s add translation.
Translation is just the process of moving things so all we need
to do is add translation to our uniforms and add that to our
position
structUniforms{
color: vec4f,
resolution: vec2f,
translation: vec2f,
};
structVertex{
@location(0) position: vec2f,
};
structVSOutput{
@builtin(position) position: vec4f,
};
@group(0)@binding(0)var<uniform> uni:Uniforms;
@vertex fn vs(vert:Vertex)->VSOutput{
var vsOut:VSOutput;
// Add in the translation
let position = vert.position;
let position = vert.position + uni.translation;
// convert the position from pixels to a 0.0 to 1.0 value
Notice it matches our pixel grid. If we set the translation to 200,300 the F
is drawn with its 0,0 top left vertex at 200,300.
This article might have seemed exceedingly simple. We were already using translation
in several examples already though we named it ‘offset’.
This article is part of series. Though it was simple, hopefully its point will make
sense in context as we continue the series.