MAIN

EN

Spinning spatial cross

Volumetric figures • Rotation matrix • Experimental model
16.01.2023

We are writing an algorithm for rotating a three-dimensional figure around its center along all three axes at once. In the previous example, we rotated cube in space — in this example, there are a lot of cubes, the algorithm will be almost the same, and we will use the same formulas. For clarity, let’s take two variants of a symmetrical volumetric figure in two types of projections — spatial cross and cross-cube — we consider the difference between them.

Testing the experimental interface: Volumetric tetris.

Spatial cross #

Parallel projection

Canvas for displaying computations results

Perspective projection

Canvas for displaying computations results

Cross-cube #

Parallel projection

Canvas for displaying computations results

Perspective projection

Canvas for displaying computations results

Parallel projection — all cubes are the same size.

Perspective projection — the cubes look shrinking in the distance.

Experimental model #

Slightly complicated version from the previous example — now there are a lot of cubes. In addition to the previous settings there can be changed: variant of the figure — spatial cross or cross-cube, face sorting direction — straight perspective or reverse perspective and transparency of the cube walls.

Canvas for displaying computations results

Rotation around axes:
Center onto observer screen:
150
150
125
Remoteness of projection center:
300
Transparency of cubes:
20%
Variant of the figure:
Linear perspective:

Algorithm description #

We prepare a three-dimensional matrix of zeros and ones, where one means a cube in a certain place of the figure. Then we bypass this matrix and fill in the array of cubes with the corresponding coordinates of the vertices. After that, we start the rotation along all three axes at once. At each step, we bypass the array of cubes and get projections of their faces. Then we sort the array of faces by remoteness from the projection center, bypass this array and throw away the same pairs from it — these are the adjacent walls between neighboring cubes inside the figure. After that we draw with a translucent color the cube faces — first the distant, and then the near ones, so that the distant faces can be seen through the near ones.

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 objects according to templates and draw their projections on the plane.

'use strict';
// matrices-templates for cubes
const shape1 = [ // spatial cross
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
const shape2 = [ // cross-cube
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
// cube size, number of cubes in a row, indent
const size = 40, row = 5, gap = 50;
// arrays for cubes
const cubes1 = [], cubes2 = [];
// bypass the matrices, fill the arrays with cubes
for (let x=0; x<row; x++)
  for (let y=0; y<row; y++)
    for (let z=0; z<row; z++) {
      if (shape1[x][y][z]==1)
        cubes1.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
      if (shape2[x][y][z]==1)
        cubes2.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
    }
// 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,125);
// rotation angle in degrees
const deg = {x:1,y:1,z:1};
// we'll draw two pictures for each figure
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const canvas3 = document.getElementById('canvas3');
const canvas4 = document.getElementById('canvas4');
// image refresh
function repaint() {
  // spatial cross
  processFigure(cubes1,canvas1,canvas2);
  // cross-cube
  processFigure(cubes2,canvas3,canvas4);
}
// rotate the figure and get projections
function processFigure(cubes,cnv1,cnv2) {
  // arrays of projections of faces of cubes
  let parallel = [], perspective = [];
  // rotate the cubes and get projections
  for (let cube of cubes) {
    cube.rotate(deg, t0);
    parallel = parallel.concat(cube.projection('parallel',tv,d));
    perspective = perspective.concat(cube.projection('perspective',tv,d));
  }
  // we do not draw adjacent walls between neighboring cubes
  noAdjacent(parallel);
  noAdjacent(perspective);
  // sort the faces of different cubes by remoteness and inside one cube by tilt
  parallel.sort((a,b)=>Math.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
  // sort the faces by remoteness from the projection center
  perspective.sort((a,b)=>b.dist-a.dist);
  // draw parallel projection
  drawFigure(cnv1, parallel);
  // draw perspective projection
  drawFigure(cnv2, perspective);
}
// do not draw adjacent walls between neighboring cubes
function noAdjacent(array) {
  // sort the faces by remoteness
  array.sort((a,b) => b.dist-a.dist);
  // remove the adjacent walls between cubes
  for (let i=0, j=1; i<array.length-1; j=++i+1)
    while (j<array.length && Cube.pEquidistant(array[i],array[j]))
      if (Cube.pAdjacent(array[i],array[j])) {
        array.splice(j,1);
        array.splice(i,1);
        i--; j=array.length;
      } else j++;
}
// draw a figure by points from an array
function drawFigure(canvas, proj, alpha=0.8) {
  const context = canvas.getContext('2d');
  // 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 = 1.9;
    context.lineJoin = 'round';
    context.fillStyle = 'rgba(200,230,201,'+alpha+')';
    context.strokeStyle = 'rgba(102,187,106,'+(0.2+alpha)+')';
    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

Upward