Skip to content

Instantly share code, notes, and snippets.

@xem
Last active June 19, 2019 05:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xem/6cd197015d030b521836df9dfb562d82 to your computer and use it in GitHub Desktop.
Save xem/6cd197015d030b521836df9dfb562d82 to your computer and use it in GitHub Desktop.
wip projection article

3D projection on a 2D canvas (wip blog post)


A very short article about a very specific topic: draw 3D points on a 2D canvas!
It's not very complex, but it puzzled me for years, until C4ntelope made this handy introduction based on his own intuition and computations, and Román Cortés helped me complete these explanations and simplify the maths and the code.
(This article is based on that Twitter thread).


Ingredients
- Camera position: cx, cy, cz.
- Camera rotation: yaw, pitch, roll.
- Simulated distance between the eye and the screen: perspective
- Canvas half-width and half-height: w = canvas.width/2, h = canvas.height/2.
- Points in 3D space, with coordinates [x, y, z].
- The points' size (or radius) in the "real world": realSize.


How to draw each point
Of course, you can't just draw every point at the coordinates [X,Y] of your 2D canvas. You need a way to represent its depth, with perspective, in other terms: do a projection.

- Take the point's coordinates: x,y,z.
- Perform camera rotations using Euler's equations:

rotate = (a, b, angle) => [
  Math.cos(angle) * a - Math.sin(angle) * b,
  Math.sin(angle) * a + Math.cos(angle) * b
];
[x,z] = rotate(x,z,yaw);
[y,z] = rotate(y,z,pitch);
[x,y] = rotate(x,y,roll);


- add the camera offset to the coordinates:

x -= cx;
y -= cy;
z -= cz;


- Scale the point according to its depth:

size = realSize / z * perspective;


- If Z > 0, place the point on the canvas:

if(Z > 0){
  X = w + x / z * perspective;
  Y = h + y / z * perspective;
  c.arc(x, y, size, 0, 2 * Math.PI);
  c.fill()
}

>>> CODEPEN DEMO <<<



Z-ordering

Let's go a bit further and notice that the points are drawn in the order in which they are declared, which can be problematic in case of overlap. Here's the previous demo with bigger points and colors:

The solution is to order all the points by decreasing depth before projecting and drawing them:

// Loop on all the points (like before)
{
  // (...)
  
  // Camera rotation (like before)
  
  // Camera offset (like before)
  
  // Set point color
  // (...)

  // Store the point in an array
  points.push({x, y, z, color});
}

// Order the points by depth
points.sort((a,b) => b.z - a.z);

// Draw the points (like before)
for(i in points){
  x = points[i].x;
  y = points[i].y;
  z = points[i].z;
  color = points[i].color;
  size = realSize / z * perspective;
  if(Z > 0){
    // (...)
  }
}

>>> CODEPEN DEMO <<<



Animation

You can embed this code in an animation and make the camera and/or the points move at each frame.

setInterval(
  () => {
  
    // Reset canvas
    a.width = a.width;
    
    // Increase every camera angle (for example)
    pitch += .01;
    yaw += .01;
    roll += .01;</b>
  
    // (everything like before)
  
  }
  , 16
);

>>> CODEPEN DEMO <<<



Polygons

It's easy to link 3D points and make polygons, outlined and/or filled with a color.
But it's not super easy to z-order these polygons correctly.
I found an approach that seems to work, at least for "simple" scenes (for example, a cube made of 6 faces):
- Make an array containing all your faces, along with their center, vertices and color (rgba colors are possible!).
- Apply the camera rotation and offset to each face's center and vertices.
- Order the faces by decreasing center depth.
- Draw the faces from the furthest to the nearest by linking its vertices and filling the shape with the right color.

Basically:

cube = [
{center:[-1,0,0],vertices:[[-1,-1,-1],[-1,-1,1],[-1,1,1],[-1,1,-1]],color:"hsla(50, 50%,50%,.5)"},
{center:[1,0,0], vertices:[[1,-1,-1],[1,-1,1],[1,1,1],[1,1,-1]],    color:"hsla(100,50%,50%,.5)"},
{center:[0,-1,0],vertices:[[-1,-1,-1],[-1,-1,1],[1,-1,1],[1,-1,-1]],color:"hsla(150,50%,50%,.5)"},
{center:[0,1,0], vertices:[[-1,1,-1],[-1,1,1],[1,1,1],[1,1,-1]],    color:"hsla(200,50%,50%,.5)"},
{center:[0,0,-1],vertices:[[-1,-1,-1],[-1,1,-1],[1,1,-1],[1,-1,-1]],color:"hsla(250,50%,50%,.5)"},
{center:[0,0,1], vertices:[[-1,-1,1],[-1,1,1],[1,1,1],[1,-1,1]],    color:"hsla(300,50%,50%,.5)"},
];
centers = [];
for(i in cube){
  [x, y, z] = cube[i].center;
  // (project)
  centers.push({x, y, z});
}
centers.sort((a,b) => b.z - a.z);
// (draw)

>>> CODEPEN DEMO <<<

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment