MAIN EN typewriter

older-tomato

Spinning cube in space

Linear perspective • Rotation matrix • Experimental model 11.01.2023

We consider the difference between parallel and perspective projection. Both are widely used in practice for various purposes. In the previous example, we rotated square on plane — we pass into three-dimensional space. Now, to display the rotation of a three-dimensional object on the screen plane, we first need to create a mathematical model of a three-dimensional object, rotate it by an angle, draw a projection from it and display already the projection on the screen. For clarity, we will use the cartesian coordinate system.

Complicated model, many cubes: Spinning spatial cross.

Parallel projection

Canvas for displaying computations results

Perspective projection

Canvas for displaying computations results

Parallel projection — the projection center is infinitely distant from the plane of the observer screen, dimensions of the objects look the same.

Perspective projection — parallel lines converge in the center of the perspective, objects appear to shrink in the distance.

Experimental model #

Cube size 200, canvas size 300, origin of coordinates is in the upper left corner. The center of the figure is in the middle of the canvas. The X axis is directed to the right, the Y axis is directed downwards, the Z axis is directed to the distance. The rotation is performed sequentially around all three axes: first around the X axis, then around the Y axis and then around the Z axis. Model settings can be controlled, for example, you can switch off redundant rotation around the axes and move the central point of the projection onto the observer screen.

Canvas for displaying computations results

Rotation around axes:
Center onto observer screen:
150
150
60
Remoteness of projection center:
300

Point rotation in space #

We calculate the new coordinates of the point using the formulas of the rotation matrix for three-dimensional space. We rotate the point t relative to the point t0 — we get the point t'.

Rotation along X axis.

&x'=x,&\\&y'=y_0+(y-y_0)cos\varphi-(z-z_0)sin\varphi,&\\&z'=z_0+(y-y_0)sin\varphi+(z-z_0)cos\varphi.&\\

Rotation along Y axis.

&x'=x_0+(x-x_0)cos\varphi-(z-z_0)sin\varphi,&\\&y'=y,&\\&z'=z_0+(x-x_0)sin\varphi+(z-z_0)cos\varphi.&\\

Rotation along Z axis.

&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi,&\\&z'=z.&\\

Point projection #

Experimental formulas with the possibility of shifting the projection center d0 on the observer screen tv. We map the point of space t to the plane of the screen — we get the point t'.

Parallel projection.

&x'=x,&\\&y'=y+(y_v-z)/4.&\\

Perspective projection.

&x'=x_v+d_0\cdot(x-x_v)/(z-z_v+d_0),&\\&y'=y_v+d_0\cdot(y-y_v)/(z-z_v+d_0).&\\

Distance from the point to the projection center.

d(t,d_0)=\sqrt{(x-x_v)^2+(y-y_v)^2+(z-z_v+d_0)^2}.

Face sorting #

When creating a cube, we set the vertices of each face clockwise. When obtaining a projection, we substitute three consecutive vertices into the equation of a line, to determine the tilt of the face and its remoteness from the projection plane.

Equation of the line, that passes through two points.

{(x-x_1)\over(y-y_1)}={(x_2-x_1)\over(y_2-y_1)}.

Algorithm description #

First, we bypass the vertices of the cube and rotate them by an angle relative to the center point. Then we bypass the faces of the cube and get projections of the vertices included in them. After that, we sort the projections of the faces by remoteness. Then we draw projections on the plane — we link the points with lines. We draw with an almost transparent color first the far faces and atop them the near ones, so that the far faces can be seen through the near ones.

At each step of displaying the figure, we repeat the sorting of the faces by remoteness, since with a change in the angle of rotation, the coordinates shift, and the near faces become far.

Implementation in JavaScript #

The Point class of the three-dimensional space contains methods for rotations by an angle and for obtaining projections onto a plane. When obtaining projections, the distance from the point to the projection center is calculated. Point also contains a static method to compare two projections of points.

class Point
class Point {
  // point coordinates
  constructor(x,y,z) {
    this.x=x;
    this.y=y;
    this.z=z;
  }
  // rotate this point by an angle (deg) along
  // axes (x,y,z) relative to the point (t0)
  rotate(deg, t0) {
    // functions to obtain sine and cosine of angle in radians
    const sin = (deg) => Math.sin((Math.PI/180)*deg);
    const cos = (deg) => Math.cos((Math.PI/180)*deg);
    // calculate new coordinates of point using the formulas
    // of the rotation matrix for three-dimensional space
    let x,y,z;
    // rotation along 'x' axis
    y = t0.y+(this.y-t0.y)*cos(deg.x)-(this.z-t0.z)*sin(deg.x);
    z = t0.z+(this.y-t0.y)*sin(deg.x)+(this.z-t0.z)*cos(deg.x);
    this.y=y; this.z=z;
    // rotation along 'y' axis
    x = t0.x+(this.x-t0.x)*cos(deg.y)-(this.z-t0.z)*sin(deg.y);
    z = t0.z+(this.x-t0.x)*sin(deg.y)+(this.z-t0.z)*cos(deg.y);
    this.x=x; this.z=z;
    // rotation along 'z' axis
    x = t0.x+(this.x-t0.x)*cos(deg.z)-(this.y-t0.y)*sin(deg.z);
    y = t0.y+(this.x-t0.x)*sin(deg.z)+(this.y-t0.y)*cos(deg.z);
    this.x=x; this.y=y;
  }
  // get a projection of (type) from a distance (d)
  // onto the plane of the observer screen (tv)
  projection(type, tv, d) {
    let proj = {};
    // obtain a projection using experimental formulas
    switch (type) {
      case 'parallel': {
        proj.x = this.x;
        proj.y = this.y+(tv.y-this.z)/4;
        break;
      }
      case 'perspective': {
        proj.x = tv.x+d*(this.x-tv.x)/(this.z-tv.z+d);
        proj.y = tv.y+d*(this.y-tv.y)/(this.z-tv.z+d);
        break;
      }
    }
    // calculate distance to projection center
    proj.dist = Math.sqrt((this.x-tv.x)*(this.x-tv.x)
        +(this.y-tv.y)*(this.y-tv.y)
        +(this.z-tv.z+d)*(this.z-tv.z+d));
    return proj;
  }
  // compare two projections of points (p1,p2),
  // coordinates (x,y) should match
  static pEquals(p1, p2) {
    return Math.abs(p1.x-p2.x)<0.0001
        && Math.abs(p1.y-p2.y)<0.0001;
  }
};

The Cube class contains a collection of vertices of the Point class and an array of faces. Each face is an array of 4 vertices, coming from the same point and going clockwise. The Cube contains methods for rotating all vertices by an angle and for obtaining projections of all faces onto a plane. When obtaining projections, the tilt of the face is calculated — this is the remoteness from the projection plane. The cube also contains two static methods for comparing two face projections: for defining the equidistant faces from the projection center and adjacent walls between neighboring cubes.

class Cube
class Cube {
  // left upper near coordinate and size
  constructor(x,y,z,size) {
    // right lower distant coordinate
    let xs=x+size,ys=y+size,zs=z+size;
    let v={ // vertices
      t000: new Point(x,y,z),    // top
      t001: new Point(x,y,zs),   // top
      t010: new Point(x,ys,z),   // bottom
      t011: new Point(x,ys,zs),  // bottom
      t100: new Point(xs,y,z),   // top
      t101: new Point(xs,y,zs),  // top
      t110: new Point(xs,ys,z),  // bottom
      t111: new Point(xs,ys,zs)};// bottom
    this.vertices=v;
    this.faces=[ // faces
      [v.t000,v.t100,v.t110,v.t010], // front
      [v.t000,v.t010,v.t011,v.t001], // left
      [v.t000,v.t001,v.t101,v.t100], // upper
      [v.t001,v.t011,v.t111,v.t101], // rear
      [v.t100,v.t101,v.t111,v.t110], // right
      [v.t010,v.t110,v.t111,v.t011]];// lower
  }
  // rotate vertices of the cube by an angle (deg)
  // along axes (x,y,z) relative to the point (t0)
  rotate(deg, t0) {
    for (let vertex in this.vertices)
      this.vertices[vertex].rotate(deg, t0);
  }
  // get projections of (type) from a distance (d)
  // onto the plane of the observer screen (tv)
  projection(type, tv, d) {
    let proj = [];
    for (let face of this.faces) {
      // face projection, array of vertices
      let p = [];
      // cumulative remoteness of vertices
      p.dist = 0;
      // bypass the vertices of the face
      for (let vertex of face) {
        // obtain the projections of the vertices
        let proj = vertex.projection(type, tv, d);
        // accumulate the remoteness of vertices
        p.dist+=proj.dist;
        // add to array of vertices
        p.push(proj);
      }
      // calculate face tilt, remoteness from the projection plane
      p.clock = ((p[1].x-p[0].x)*(p[2].y-p[0].y)
                -(p[1].y-p[0].y)*(p[2].x-p[0].x))<0;
      proj.push(p);
    }
    return proj;
  }
  // compare two projections of faces (f1,f2), vertices
  // should be equidistant from the center of projection
  static pEquidistant(f1, f2) {
    return Math.abs(f1.dist-f2.dist)<0.0001;
  }
  // compare two projections of faces (f1,f2), coordinates
  // of points along the main diagonal (p0,p2) should match
  static pAdjacent(f1, f2) {
    return Point.pEquals(f1[0],f2[0])
        && Point.pEquals(f1[2],f2[2]);
  }
};

Create an object and draw two projections on the plane.

'use strict';
// we will draw two pictures at once, there will be
// one object, and there will be many projections
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
// create an object
const cube = new Cube(50,50,50,200);
// figure center, we'll perform a rotation around it
const t0 = new Point(150,150,150);
// remoteness of the projection center
const d = 300;
// observer screen position
const tv = new Point(150,150,80);
// rotation angle in degrees
const deg = {x:0,y:1,z:0};
// figure rotation and image refresh
function repaint() {
  cube.rotate(deg, t0);
  // draw parallel projection
  drawFigure(canvas1, cube.projection('parallel', tv));
  // draw perspective projection
  drawFigure(canvas2, cube.projection('perspective', tv, d));
}
// draw a figure by points from an array
function drawFigure(canvas, proj) {
  let context = canvas.getContext('2d');
  // sort the faces by their tilt
  proj.sort((a,b) => b.clock-a.clock);
  // clear the entire canvas
  context.clearRect(0, 0, canvas.width, canvas.height);
  // bypass the array of cube faces
  for (let i = 0; i < proj.length; i++) {
    // bypass the array of points and link them with lines
    context.beginPath();
    for (let j = 0; j < proj[i].length; j++) {
      if (j == 0) {
        context.moveTo(proj[i][j].x, proj[i][j].y);
      } else {
        context.lineTo(proj[i][j].x, proj[i][j].y);
      }
    }
    context.closePath();
    // draw the face of the cube along with the edges
    context.lineWidth = 2.2;
    context.lineJoin = 'round';
    context.fillStyle = '#fff9';
    context.strokeStyle = '#222';
    context.fill();
    context.stroke();
  }
}
// after loading the page, set the image refresh rate at 20 Hz
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));

© Golovin G.G., Code with comments, translation from Russian, 2023