Why Should I read this?

Not that you really should, go out and play if you want. The thing is that if, like me, you don't think you'll be grasping the concepts of WebGL any soon and don't want to use a library that takes a huge amount of time just to download and that is already too complex for what you need, just want more flexibility or simply you're too lazy to get the proper setup done for a demo you're writing just for fun ( high five :D ), but would still like to make cool 3d stuff, then this simple post is for you ;)

So, what do I have to do to get a 3d perspective effect?

I'm not going to go into much maths stuff, and I'll just show you a very hacky way of doing those simple effects. Basically, what happens in perspective is that stuff that is closer to you gets bigger, and stuff that is away from you gets gradually smaller and seems to be going into a vanishing point. How do we do that? Let's say we have a point with x,y,z properties and you're in the 0,0,0 of your world. The first thing you need to know is that the z pretty much specifies your perspective distance, and you need to scale the point inversely proportional to that (1/x for example). Another thing we need is the focalLength, which just specifies how much is the scale conditioned by the z. So let's do that!

  var focalLength = 250, // usually anywhere between 200 and 300 is good
    point = { x: 100, y: 20, z: 1000 },
    scale = focalLength / ( focalLength + point.z );

You're almost done for a single point! Let's say you now wanted to draw a ball on the 2d canvas with the coordinates of the point object. Of course your main concern is to get convert a virtual x,y,z to a screen x,y and a scale for the radius, right? That is pretty easy, you just need to define a vanishPoint with x,y properties in relation to the 2d screen, and scale everything based on that! First let's assume we got a fullscreen canvas with w and h indicating width and height of it, aswell as a ctx for the 2d context of the canvas.

  var vanishPoint = { x: w / 2, y: h / 2 },
    screenBallPoint = { 
            x: vanishPoint.x + point.x * scale,
            y: vanishPoint.y + point.y * scale
    },
    radius = scale;

ctx.beginPath();
ctx.arc( screenBallPoint.x, screenBallPoint.y, radius, 0, Math.PI*2 );

Boom! This is so little code, but it's a working example of hacky 3d code. Of course there's a lot wrong with it: if you get the ball behind you you'll still see it in reverse for example. Just try to play around with the coordinates ( and maybe the focalLength ), I'm sure you can already do something awesome with just those few things! Before explaining a few more basic concepts, I'm just going to put this code into a few functions:

  function Point( x, y, z ){
    this.x = x;
    this.y = y;
    this.z = z;
    this.screen = {};
}
Point.prototype.dimensionize = function(){

    var scale = fl / ( fl + this.z ); // fl = focalLength

    this.screen.x = vp.x + this.x * scale; // vp = vanishPoint
    this.screen.y = vp.y + this.y * scale;
    this.screen.s = scale;
}

Now of course you can make a lot of the balls, change their values dinamically ( like by adding velocity, make them bounce and similar ) and generally see how they behave. Also try giving them different colours and see what happens.

Some weird stuff is going on...

That's pretty normal. The main thing you may notice is that if the balls go behind you they act pretty strangely. Easy fix: if the scale is negative just don't display them. This is very inaccurate, but it works pretty well.

If you did add different colors to your balls, you may also have noticed that some balls in the back will show above some in front. Of course you don't want that, so the easy solution is to sort the array of the balls so that the balls with the highest z get drawn first, so that they'll be behind anything else. Of course this is extremely easy to do in good ol' JS!

  // this is in your loop function.
    // first update the balls' position
    balls.sort( function( a, b ){ return b.z - a.z } );// now sort them
    // and dimensionize + draw them

Of course you may use any sorting method you want, but the standard one used by JS is pretty efficient ( mainly because it's executed by the lower level interpreter, and that makes it very fast ). If don't get how it works the rules are very simple: the sort will iterate through the array comparing every pair of items in the array it needs to compare and passes them through a function. If the returned value is negative, it switches the element's order, otherwise it keeps them the way they are. This is extremely handy in many other occasions ;)

Want to draw shapes other than circles? Guess what! Just dimensionize the vertices of the shapes and fill them with standard 2d canvas functions! Now getting the array sorted will not be enough though. But it will still work pretty well if you don't have screnes with intersecting shapes or a few other special cases where you can't just draw a shape once ( like this one: http://opengl.datenwolf.net/gltut/html/Positioning/ThreeTriangleOverlap.svg ).

A few more things

Anyway, here are a few other things you may want to consider for small optimizations and such:

With triangles, either choose a center point and sort them by that, but if you want to be a bit more accurate, use the point with the lowest z. It doesn't always work, but it generally does a good job.

Remember not to draw the shape if either the center point you've chosen (or all of the vertices)'s screen x and y values are out of the canvas' boundaries! You just won't see those, might aswell avoid extra graphics computation.

You may want to allow the player to move around a bit. In that case, remember that the view is always centered at the 0,0,0 of the world, so you need to move the world according to where you are. The rules are pretty simple: translate the point by the negative of the player's position, then rotate it by the player's rotation, and only then do all of your perspective transforms. Here's how I'd do it:

  Point.prototype.dimensionize = function(){

  var x = player.x - this.x,
      y = player.y - this.y,
      z = player.z - this.z,
      x1 = x;

  x = x * player.cos - z * player.sin;
  z = z * player.cos + x1 * player.sin;

    z -= opts.fl;

  var scale = fl / ( fl + z );

  this.screen.x = vp.x + x * scale;
  this.screen.y = vp.y + y * scale;
  this.screen.z = z;
  this.screen.s = scale;
}

Don't worry about the strange trigonometry function, just remember to calculate player.sin and player.cos at the beginning of the loop as Math.sin( player.rot ) and Math.cos( player.rot ). You also need to keep in mind the relative z just to that the array sorting can be done according to the player's position.

If you want to get deeper into how 3d should really be done, try googling triangle backculling, z-buffer and other stuff like light calculation on triangle or anything you may need. It's also pretty relevant to learn about matrices to get some more elegantly written code.

That's it from me! I just started with 3d aswell, but if you have any questions feel free to reach me in the comments or on twitter (@MateiCopot)! Also check out some of my other stuff on codepen, it would mean a lot to me ;)

This is just another demo showing a few of the flaws and optimizations