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