Wednesday, 23 January 2013

Damn simple 3D tutorial



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

7 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Nekem a contol gombok fel vannak cserélve, a fel/le gombbal forog jobbra/balra de amúgy nagyon jó ... :)
    Nemré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 ... :)

    ReplyDelete
  3. 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).
    Szeretnek irni a fuggvenyekrol kulon hogy mennyire sokat lehet oket hasznalni. Csak kell meg gyujtogetni hozza.

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

    ReplyDelete
  5. Hi Erno, thanks :) It's pretty 'whoadude' :)

    ReplyDelete
  6. hello

    i 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

    ReplyDelete
    Replies
    1. Hi there,

      I'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

      Delete