One of my idol is my grandmother. She was a very great mathematics and chemistry teacher when she was active at the school. She taught to me the love of numbers. I remember we were solving geometric examples, equations or some fun math problems in our free time. And I miss it sometimes. So today I decided to write a little 3D engine.

Don't be scared, I show you how simple it is. Let's start with 2D:

You need 2 coordinates in 2D. X and Y. Pretty simple. That defines you a vector. In 3D you have the same - but with 3 coordinates. Of course you cannot represent 3D on a 2D canvas just as it is. We need a little trick. We need to change the 2 base coordinates (X and Y) according to the Z value. So we mimic the effect of 3 dimension. Imagine your eye is in 0:0. Grab a pencil, hold in a way that the tip is couple of centimeters above your eye and move horizontally back and forth - keep your eye on the tip:

The closer you bring the higher you look. But it's not linear, it's exponential:

And that's it. You need to adjust the original X:Y value by the power of the distance.

Let's see how to do it in JavaScript and Canvas. We need the usual barebone HTML:

<!doctype html> <html> <head> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> <script type="text/javascript"></script> </head> <body onload="onLoad()"> <canvas id="screen" width="800" height="600"></canvas> <pre>Use: up/down, left/right, s/w.</pre> </body> </html>

Let's initialize the canvas:

var ctx, canvas; function onLoad() { canvas = document.getElementById('screen'); if (!canvas.getContext) { return; } ctx = canvas.getContext('2d'); }

For the exponential function (distorsion) we need to find a proper base that makes a fine illusion of 3D:

var _3d_scale_base = 1.6;

To make point handling handy we should create a dedicated object constructor:

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

And finally the magic function that created the 3D adjustment - 2 coords from 3:

var scale = 100; function get3DCoordsPoint(point) { return { x: scale * point.x * Math.pow(_3d_scale_base, point.z), y: scale * point.y * Math.pow(_3d_scale_base, point.z) }; }

Here the scale variable is just a small extra - so we transform our world to look nice on a normal size window (~ 800 x 600). And really that's it. A 3D engine. Now we can create some points and put it on the canvas. Let it be the Hello World of 3D - a cube:

var world = []; world.push(new Point(-1, 1, 1)); world.push(new Point(-1, -1, 1)); world.push(new Point(1, -1, 1)); world.push(new Point(1, 1, 1)); world.push(new Point(1, 1, -1)); world.push(new Point(1, -1, -1)); world.push(new Point(-1, -1, -1)); world.push(new Point(-1, 1, -1));

And finally the render method:

function render() { canvas.width = canvas.width; var w = jQuery('#screen').width(); var h = jQuery('#screen').height(); var center_x = w >> 1; var center_y = h >> 1; for (var idx in world) { var xy = get3DCoordsPoint(world[idx]); ctx.strokeRect((center_x + xy.x), (center_y + xy.y), 4, 4); } }

Now you think it's so lame, huh? :) All right. Let's do some extra math :) Rotation! Yay! Let's rotate though the X and Y axis:

var rotation_x = 0; var rotation_y = 0;

Event handlers with jQuery:

jQuery('body').keydown(function(event){ switch (event.keyCode) { // Rotation X. case 37: rotation_x -= 0.1; break; case 39: rotation_x += 0.1; break; // Rotation Y. case 38: rotation_y -= 0.1; break; case 40: rotation_y += 0.1; break; } });

Now we have the angle would like to apply let's stop for a second and think about rotation. When you rotate you have a base point where you rotate around. Let's make it easy and use our total zero point - so no offset transformation required. We will use traditional rotation matrices:

I hope you remember how to multiply matrices. Let's add it to the Point object prototypes:

Point.prototype.rotateX = function(deg) { return new Point( this.x, this.y * Math.cos(deg) - this.z * Math.sin(deg), this.y * Math.sin(deg) + this.z * Math.cos(deg) ); }; Point.prototype.rotateY = function(deg) { return new Point( this.x * Math.cos(deg) + this.z * Math.sin(deg), this.y, -1 * this.x * Math.sin(deg) + this.z * Math.cos(deg) ); };

So we have the angles, we have the event handlers and the transformation matrix - let's really turn those point around - a little adjustment before the render:

var _p = world[idx]; var _p_tx = _p.rotateX(rotation_x); var _p_txy = _p_tx.rotateY(rotation_y); var xy = get3DCoordsPoint(_p_txy); ctx.strokeRect((center_x + xy.x), (center_y + xy.y), 4, 4);

And of course we have to make render continuous:

setInterval(render, 10);

I hope you believe it wasn't rocket science. You can find an online version of this code (have a few extras) or the source on GitHub.

---

Tomorrow shaders and particle systems. Just kidding :)

Peter

This comment has been removed by the author.

ReplyDeleteNekem a contol gombok fel vannak cserélve, a fel/le gombbal forog jobbra/balra de amúgy nagyon jó ... :)

ReplyDeleteNemrég vizsgáztam grafikából, és valahogy nem láttam a tárgyban a lehetőségeket de a Bézier görbés bejegyzés után, meg ezután, kicsit másképp tekintek a témára ... :)

Szia Gergo, nagyon szepen koszonom. Grafika szaraz volt valoban, viszont volt Flash oran es ott lehetett tomenytelen sok jatekot irni (http://itarato.uw.hu/index.php?p=flash).

ReplyDeleteSzeretnek irni a fuggvenyekrol kulon hogy mennyire sokat lehet oket hasznalni. Csak kell meg gyujtogetni hozza.

Enjoyed your post, thank you. Here's another great example of the topic (in 317 bytes): http://jsfiddle.net/hakim/6YVEH/911/

ReplyDeleteHi Erno, thanks :) It's pretty 'whoadude' :)

ReplyDeletehello

ReplyDeletei would like to learn the 3d math better, can you please explain the get3DCoordsPoint,how it works and why espacialy that _3d_scale_base, what dose it represent? why is it the pwoer on the z?

and wht dont you use somthing with deviations? like:

xp = x * scale / (z + scale) yp = y * scale / (z + scale)

i tried playing with it the equation with deviation dosnt work.

how can i prove a 3d to 2d equation (or learn to understand it intuitively).

thank you for your tutrial

Hi there,

DeleteI'm not sure what is your question exactly. The get3DCoordsPoint() function takes a point that has 3 coordinates and creates a 2d coordinate by adjusting the X and Y values according to the Z value. _3d_scale_base is used to scale the distorsion of the depth. If you hit W or S keys on the demo page you can see that it changes the depth. If you want to see what's happening in the background set up a breakpoint in the JS inspection tool (Firebug or Chrome inspector) and follow the variables.

I don't see your point about the deviation. What do you want to achieve with that?

Peter