Day 12 — 3D pixel particles

The Christmas season is pretty much over, but before you take the tree down, and return your baubles to the musty box in the cupboard, we have one final 12 days of CreativeJS post for you.

And it’s a goodie! Today, I’m going to explain a magic formula that can figure out where a 3D point would be on your 2D canvas. Once we know that we can make some 3D pixel particles!

A 3D point has an x, y and z position, where z is how far away it is from you. If a point is at 0,0,0 then we are going to render it right in the middle of the screen. Let’s call our 3D co-ordinates x3d, y3d, and z3d.

The first part of the formula tells us how big or small things should appear at a given z distance :

var scale = fov/(z3d+fov);

Where fov is a notional field of view, in other words, how zoomed-in or wide-angled our virtual lens is1.

Now we know how big things are at that distance, we just multiply our 3D x and y by that scale factor to get our 2D x and y.

var x2d = x3d * scale; 
var y2d = y3d * scale;

If you think about it, this kinda makes sense. Let’s say our particle is 5 pixels over to the right, and it moves towards us. As it gets nearer, the particle moves further to the right in the 2D space. We’re multiplying our x3d (which is 5) by a growing scale factor to get our x2d. And our particle will move further right as it comes towards us.

There’s just one more thing though – this formula assumes that the 2D co-ordinate system also has its 0,0 point in the middle of the screen. So we need to add half the width and height of our canvas to the result to offset this.

var x2d = (x3d * scale) + halfWidth; 
var y2d = (y3d * scale) + halfHeight;

Get it? Great! You should also watch out what happens if points go behind the camera, because if you try to render them they’ll be all inverted. We’re not worrying about that in our simple code here, but if you wanted to implement that then just check to see if z is less than –fov and if it is, don’t draw it.

And that’s all there is to it. Who’d have thought it’d take so few lines of code to make a simple 3D pixel renderer?

1It’s not an actual field of view as an angle, it’s just an arbitrary value that the formula requires. I usually use around 150 – 250, but you can experiment to see what results you get.

So now if we combine it with Paul King’s day 3 post about setting pixels in a canvas, we can now colour the relevant 2D pixel for the 3D point.

So let’s make a whole bunch of 3D points! :

var pixels = []; 
for(var x = -250; x<250; x+=5) { 
	for(var z = -250; z<250; z+=5) { 
		var pixel = {x:x, y:40, z:z};
		pixels.push(pixel); 
	}
}

You can see here we’re creating a horizontal grid of 3D point objects with x, y and z properties. Here’s what it looks like when we render it :

Now let’s look at our render function, called 30 times a second :

function render() { 
 
	// clear the canvas
	context.clearRect(0,0,width, height); 
	// and get the imagedata out of it
	var imagedata = context.getImageData(0, 0, canvas.width, canvas.height);
 
	// iterate through every point in the array
	var i=pixels.length; 
	while(i--){
		var pixel = pixels[i]; 
 
		// here's the 3D to 2D formula, first work out 
		// scale for that pixel's z position (distance from 
		// camera)
		var scale = fov/(fov+pixel.z); 
 
		// and multiply our 3D x and y to get our
		// 2D x and y. Add halfWidth and halfHeight
		// so that our 2D origin is in the middle of 
		// the screen.
		var x2d = (pixel.x * scale) + halfWidth; 
		var y2d = (pixel.y * scale) + halfHeight; 
 
		// and set that 2D pixel to be green
		setPixel(imagedata, x2d, y2d, 0, 255, 60); 
 
		// add 1 to the z position to bring it a little 
		// closer to the camera each frame
		pixel.z-=1;
 
		// if it's now too close to the camera, 
		// move it to the back
		if(pixel.z<-fov) pixel.z += (fov*2);
 
	}
 
	// and write all those pixel values into the canvas
	context.putImageData(imagedata,0,0); 
 
}

So we’re iterating through each 3D point object, converting it into 2D and setting the pixel for that 2D position. Then we’re moving each point forward towards the camera by decrementing z, and once it gets too close we’re moving it to the back.

Play with it here on JSFiddle.

How about this version where we create 3D pixels in undulating waves using Math.sin(…), and we have now declared a custom Pixel3D object in which to store our 3D x, y and z.

Or this version that has pixels in a big doughnut shape (or torus if you want to sound extra posh). Note that we’re now rotating every point around the origin relative to the mouse position – using the newly added rotateX and rotateY functions in our Pixel3D object. And instead of just setting a pixel colour, we’re adding the colour values onto what’s already there (this is known as additive blending and it causes the particles on top of each other to burn out to white).

And finally, here’s a strange attractor, copied from Paul’s example from a while back. This version has a trails effect, and also a pulsating colour for each pixel based on a sine wave.

So clearly there are a few techniques in these examples that I haven’t explained in great detail, mainly to do with setting up 3D points and rotating them. But at least once you have x, y and z values for 3D points, you now know how to draw them in 2D. And for me that was the largest stumbling block to getting started in 3D.

I hope you enjoy your new 3D explorations, and I hope you’ve enjoyed the 12 days of CreativeJS! We’d love to hear what you think, so please leave a comment.