VecJS has reach r1, so maybe it’s time for a little introduction on what it is and how to use it.
First, let’s begin by clearing out what vecJS is not. It’s not another 3D engine, there’s already plenty of them out there. vecJS is a vector maths library, and a fast one at that. You can use it to build your own engine, or to do anything you want as long as it involves 3D maths…
There are 4 type of objects that you can use:
- V3
- A regular 3 dimensional vector, holds the x, y, and z values, for all your simple vector needs.
- V4
- A “4 dimensional” vector, holds x, y, z values plus w, you can use it to do weird stuff with homogenous coordinates that a V3 won’t let you do.
- M44
- A 4×4 matrix used to do affine transformation, mainly used for projection. If you only need rotation, translation and scaling, you can use the faster M34 below
- M34
- A 4×4 matrix stored with the last row implied as [0, 0, 0, 1]. This is to avoid generally unneeded work, skipping part of the homogeneous coordinates calculations and the homogeneous divide. Good stuff.
- Q
- A quaternion. Allows you to do strange things that usually involves rotations. Look at Martin Baker’s EuclideanSpace if you have no idea what I’m talking about.
In this introduction, we’ll make the fugly triangles below rotate, with a camera that we can move around. We’ll only be using V3, M34 and M44; V4 and Q will have to wait for something a bit more advanced than hello world.

So let’s begin by the page structure. We’ll go for a really simple one with only a canvas tag in the body:
<!DOCTYPE html>
<html>
<head></head>
<body>
<canvas id="canvas" width="500" height="500" style="border: 1px solid #000;"></canvas>
<script src="vec.dbg.js"></script>
<script>
/* This is where we'll be writing our script... */
</script>
</body>
</html>
As you can see, nothing fancy; the canvas, a link to vecJS (we use the debug version here, see this post to know what this means) and the main script tag.
The first thing we do in the script is grab the canvas, get its context, and cache some dimensions we’ll be using later:
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
canvasHalfX = canvas.width / 2,
canvasHalfY = canvas.height / 2,
Then we declare the mesh object: vertices, faces and a color for each face, so that we know which is which when we draw them later.
$V.V3() is shorthand for new vecJS.V3(), you can use whichever you want, or mix them like crazy and end up with stuff like new $V.V3() if you want; your call.
Just remember to always pass arrays. Always. If you don’t, you’ll either get errors with the debug version, or silent fails that’ll drive you mad with the release one.
Btw, since we won’t be z-sorting our faces, we take the easy way out and make them semi-transparent. Since there won’t ever be more than 2 faces overlapping, they will always look “good” regardless.
mesh = {
vertices: [
$V.V3([ 1, 0, 1]) // 0
,$V.V3([ 1, 0, -1]) // 1
,$V.V3([-1, 0, 1]) // 2
,$V.V3([-1, 0, -1]) // 3
,$V.V3([ 0, 1, 0]) // 4
,$V.V3([ 0, -1, 0]) // 5
],
faces: [
[2, 0, 4]
,[2, 0, 5]
,[3, 1, 4]
,[3, 1, 5]
],
colors: [
'rgba(255, 0, 0, .5)'
,'rgba(0, 255, 0, .5)'
,'rgba(0, 0, 255, .5)'
,'rgba(255, 255, 0, .5)'
],
objectMatrix: $V.M34().identity(),
rotation: 0
},
For good measure, we throw in the mesh transformation matrix and it’s current rotation angle (it only rotates around y, so one number is enough). Actually, we don’t really need to store the transformation matrix with the object, but javascript doesn’t like you when you create a new instance of an object. So we only create it once and just update it in our loop. Note that since there is no projection involved at this stage, it’s enough to have an M34 here.
Oh, and the mesh structure has nothing to to with vecJS, it’s just convenient to have everything in the same place.
So now, on with the camera:
camera = {
fov: 70,
aspect: canvas.width / canvas.height,
pos: $V.V3([2.2, .5, 2]),
at: $V.V3([0, 0, 0]),
viewMatrix: $V.M44(),
projectionMatrix: $V.M44()
},
projectionMatrix = $V.M44();
viewMatrix is the current position of the camera and where it is looking at. We’ll use it later to transform the vertices from world space to camera space. projectionMatrix is used to transform from camera space to screen coordinates.
The last variable is used to combine the view matrix and the projection matrix, and transform our vertices from world space to screen space with one calculation.
Before we begin with the fun stuff, let’s just create some buffers (again, we don’t want to create new instances all the time in the inner loop, so we create them upfront), then tell the canvas that the origin of our coordinate system is at the center, (not in the corner as it is by default) and that we want our strokes to be solid black. We also declare a function to draw a triangle on the canvas from our vertices/faces structure (hi boilerplate!).
// Init the mesh buffers
mesh.worldVertices = [];
mesh.screenVertices = [];
for (vl = mesh.vertices.length; vl; vl--) {
mesh.worldVertices.push($V.V3());
mesh.screenVertices.push($V.V3());
}
// Put (0,0) at the center of the canvas
ctx.setTransform(1, 0, 0, 1, canvasHalfX + .5, canvasHalfY + .5);
// Set the default stroke style
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
// A function to draw a single triangle on the canvas
function drawTriangle(vertices, face) {
ctx.beginPath();
ctx.moveTo(vertices[face[0]].v[0], vertices[face[0]].v[1]);
ctx.lineTo(vertices[face[1]].v[0], vertices[face[1]].v[1]);
ctx.lineTo(vertices[face[2]].v[0], vertices[face[2]].v[1]);
ctx.lineTo(vertices[face[0]].v[0], vertices[face[0]].v[1]);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
Ok, so the first thing we’re gonna need is to create the screen projection matrix for the camera:
// Setup the perspective projection matrix
camera.projectionMatrix.perspectiveFov(camera.fov, camera.aspect, 1, 10);
We create a perspective projection, with a field of view of 70 degrees (as declared in our camera object) and the aspect ration of the canvas (width/height).
We are now ready to write our inner loop. We begin by updating the mesh rotation and then call ou render function (which we’ll write next). To make the rotation smoother, we don’t set the new value directly, but rather ease it in gradually with each frame.
function loop() {
// Schedule the next loop call
setTimeout(loop, 1000/60);
mesh.rotation += (targetRotation - mesh.rotation) * 0.05;
mesh.objectMatrix.setRotateY(mesh.rotation);
render();
}
Now that we’re done with that, we’re ready for the big one, the render() function:
// The render function refreshing the canvas
function render() {
var v, vi, vl,
f, fi, fl;
// Update the camera view matrix from its current position
camera.viewMatrix.lookAt(camera.pos.v, camera.at.v, [0, 1, 0]);
// multiply the projection and the view matrices to get the screen projection matrix
projectionMatrix.assignMul(camera.projectionMatrix.m, camera.viewMatrix.m);
for (vi=0, vl = mesh.vertices.length; vi < vl; vi++) {
// Calculate the world coordinates of the mesh vertices
mesh.worldVertices[vi]
.set(mesh.vertices[vi].v)
.mulM(mesh.objectMatrix.m);
// Project the mesh vertices on the screen
mesh.screenVertices[vi]
.set(mesh.worldVertices[vi].v)
.mulCoord(projectionMatrix.m);
mesh.screenVertices[vi].v[0] *= canvasHalfX;
mesh.screenVertices[vi].v[1] *= canvasHalfY;
}
// Finally, clear the canvas and draw the mesh faces
ctx.clearRect(-canvasHalfX, -canvasHalfY, canvas.width, canvas.height);
for (fi=0, fl = mesh.faces.length; fi < fl; fi++) {
f = mesh.faces[fi];
ctx.fillStyle = mesh.colors[fi];
drawTriangle(mesh.screenVertices, f);
}
}
Ok, let's take that one again (almost) line by line.
We begin by updating the camera view matrix with its current position and where it's looking at. We won't bank the camera so we use an up vector pointing... "up" (really!).
camera.viewMatrix.lookAt(camera.pos.v, camera.at.v, [0, 1, 0]);
Every vector we'll multiply with that matrix will be converted from the world coordinates to a coordinate system where the camera is at the origin.
We then take that matrix and multiply it with the screen projection matrix:
projectionMatrix.assignMul(camera.projectionMatrix.m, camera.viewMatrix.m);
Now we have matrix that can convert directly from world to screen coordinates.
We're done with the camera; let's transform the mesh vertices. For each vertex, we begin by applying the transformation matrix we created before calling render():
// Calculate the world coordinates of the mesh vertices
mesh.worldVertices[vi]
.set(mesh.vertices[vi].v)
.mulM(mesh.objectMatrix.m);
We then take those coordinates and project them to the screen:
// Project the mesh vertices on the screen
mesh.screenVertices[vi]
.set(mesh.worldVertices[vi].v)
.mulCoord(projectionMatrix.m);
mesh.screenVertices[vi].v[0] *= canvasHalfX;
mesh.screenVertices[vi].v[1] *= canvasHalfY;
}
mulCoord is the equivalent of multiplying a V4 with an M44, and the dividing the x, y and x components by w. It' just that you can do it with a V3 instead, and in that case, you would have to divide by w for the projection to work anyway.
Since the screen coordinates are expressed in the [-1,1] range, we also have to scale x and y to the size of the canvas once they're projected.
If you're not interested in having the mesh world coordinates lying around, you can combine the two operations and get rid of the worldVertices buffer by writing instead:
mesh.screenVertices[vi]
.set(mesh.vertices[vi].v)
.mulM(mesh.objectMatrix.m)
.mulCoord(projectionMatrix.m);
mesh.screenVertices[vi].v[0] *= canvasHalfX;
mesh.screenVertices[vi].v[1] *= canvasHalfY;
As a side-note, I realize now that having an assignMulM would be a nice addition. Maybe in R2 :)
After that, we have everything we need, so we just clear the canvas and draw our triangles, and we're done!
I haven't written anything about how the camera position and the mesh rotation are actually updated, it's not really relevant here since it's only binding browser events and changing values in the callbacks, but if you really want to see how it's done, just have a look at the source code of the...
/X
One thing I noticed while writing tests for vecJS, is that
- It’s easy to forget to use arrays and pass the wrong type of parameter.
- I do it quite often.
- It’s really hard to find where I did it, since it fails silently.
First, let me explain why the functions don’t check if the parameters came as an array or as a list of variables.
Usually, you know pretty well what’s in your variables, so checking in the function if you just passed your arguments as an array or as several parameters actually seems a bit stupid, and really it’s time consuming; for that reason, vecJS accepts only arrays. On the good side, you can either pass an array directly, like this:
var v = $V.V3([1, 2, 3]);
v.cross([4, 5, 6]);
or you can pass the content of another vecJS object:
var v1 = $V.V3([1, 2, 3]),
v2 = $V.V3([4, 5, 6]);
v1.cross(v2.v);
So, all is well, except for the fact that, as mentioned, it’s easy to forget the []s and pass something completely wrong. You won’t get an exception or even an error in the console, but at the end of the line, all your vectors and arrays will be filled with Nan or 0s. Here is an example:
var v1 = $V.V3([1, 2, 3]);
v1.cross(4, 5, 6);
You would assume that v1 contains the cross product of [1, 2, 3] and [4, 5, 6] (which is [-3, 6, -3] btw), but if you check, you’ll see that it’s actually [Nan, Nan, Nan].
While it might be phonetically funny, and easy to spot here, it’s a real pain when you do a lot of stuff. So, in the end, not funny.
To make this easier to spot, I added som “debug code” that check that you really do pass an array (it even check that you have the right array length, so that you don’t mix up your objects), and throws an exception if you don’t. Now you just have to click on the error in the console, and see where you made your mistake.
Since those are the kind of mistakes that are usually the result of typos and only happen when you’re writing the code, but are gone once you’re done, the tests are only present in the vec.dbg.js version of the files. The build process filter them out in the regular and minified versions (thanks sed), and they won’t be there stealing your CPU doing something that is not necessary.
So once you’re sure about your code, just switch vec.dbg.js to vec.min.js, and you’re done! :D
/X