Rotate a cube to be isometric

Viewed 2210

I'm following this rotating cube tutorial and I'm trying to rotate the cube to an isometric perspective (45 degrees, 30 degrees).

The problem is, I think, is that the rotateY and rotateX functions alter the original values such that the two red dots in the middle of the cube (visually) don't overlap. (If that makes any sense)

How can I rotate the cube on it's X and Y axis at the same time so the functions don't effect each other?

const canvas = document.getElementById('stage');
    canvas.width = canvas.parentElement.clientWidth
    canvas.height = canvas.parentElement.clientHeight
    const context = canvas.getContext('2d');
    context.translate(200,200)

    var node0 = [-100, -100, -100];
    var node1 = [-100, -100,  100];
    var node2 = [-100,  100, -100];
    var node3 = [-100,  100,  100];
    var node4 = [ 100, -100, -100];
    var node5 = [ 100, -100,  100];
    var node6 = [ 100,  100, -100];
    var node7 = [ 100,  100,  100];
    var nodes = [node0, node1, node2, node3, node4, node5, node6, node7];

    var edge0  = [0, 1];
    var edge1  = [1, 3];
    var edge2  = [3, 2];
    var edge3  = [2, 0];
    var edge4  = [4, 5];
    var edge5  = [5, 7];
    var edge6  = [7, 6];
    var edge7  = [6, 4];
    var edge8  = [0, 4];
    var edge9  = [1, 5];
    var edge10 = [2, 6];
    var edge11 = [3, 7];
    var edges = [edge0, edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8, edge9, edge10, edge11];

    var draw = function(){

      for (var e=0; e<edges.length; e++){
        var n0 = edges[e][0]
        var n1 = edges[e][1]
        var node0 = nodes[n0];
        var node1 = nodes[n1];
        
        context.beginPath();
        context.moveTo(node0[0],node0[1]);
        context.lineTo(node1[0],node1[1]);
        context.stroke();
      }

      //draw nodes
      for (var n=0; n<nodes.length; n++){
        var node = nodes[n];
        context.beginPath();
        context.arc(node[0], node[1], 3, 0, 2 * Math.PI, false);
        context.fillStyle = 'red';
        context.fill();
      }
    }


    var rotateZ3D = function(theta){
      var sin_t = Math.sin(theta);
      var cos_t = Math.cos(theta);
      for (var n=0; n< nodes.length; n++){
        var node = nodes[n];
        var x = node[0];
        var y = node[1];
        node[0] = x * cos_t - y * sin_t;
        node[1] = y * cos_t + x * sin_t;
      };
    };

    var rotateY3D = function(theta){
      var sin_t = Math.sin(theta);
      var cos_t = Math.cos(theta);

      for (var n=0; n<nodes.length; n++){
        var node = nodes[n];
        var x = node[0];
        var z = node[2];
        node[0] = x * cos_t - z * sin_t;
        node[2] = z * cos_t + x * sin_t;
      }
    };

    var rotateX3D = function(theta){
      var sin_t = Math.sin(theta);
      var cos_t = Math.cos(theta);

      for (var n = 0; n< nodes.length; n++){
        var node = nodes[n];
        var y = node[1];
        var z = node[2];
        
        node[1] = y * cos_t - z * sin_t;
        node[2] = z * cos_t + y * sin_t;
      }
    }

    rotateY3D(Math.PI/4);
    rotateX3D(Math.PI/6);


    draw();
#stage {
  background-color: cyan;
 }
<canvas id="stage" height='500px' width='500px'></canvas>

Edit: I should have included a picture to further explain what I'm trying to achieve. I have a room picture that is isometric (45°,30°) and I'm overlaying it with a canvas so that I can draw the cube on it. As you can see it's slightly off, and I think its the effect of two compounding rotations since each function alters the original node coordinates.

enter image description here

2 Answers

You want projection not rotation

Your problem is that you are trying to apply a projection but using a transformation matrix to do it.

The transformation matrix will keep the box true to its original shape, with each axis at 90 deg to the others.

You want to have one axis at 45deg and the other at 30deg. You can not do that with rotations alone.

Projection matrix

The basic 3 by 4 matrix represents 4 3D vectors. These vectors are the direction and scale of the x,y,z axis in 3D space and the 4th vector is the origin.

The projection matrix removes the z part converting coordinates to 2D space. The z part of each axis is 0.

As the isometric projection is parallel we can just create a matrix that sets the 2D axis directions on the canvas.

The axis

The xAxis at 45 deg

const xAxis = Math.PI * ( 1 /  4);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis), 0);

The yAxis at 120 deg

const yAxis = Math.PI * ( 4 / 6);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);

And also the z axis which is up the page

iso.z.set(0,-1,0);

The transformation

Then we just multiply each vertex coord by the appropriate axis

// m is the matrix (iso)
// a is vertex in
// b is vertex out
// m.o is origin (not used in this example
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
//    ^^^^^^^^^^^   ^^^^^^^^^^^   ^^^^^^^^^^^  
//    move x dist   move y dist   move z dist
//    along x axis  along y axis  along y axis
//     45deg          120deg        Up -90deg

An example of above code

I have laid out a very basic Matrix in the snippet for reference.

The snippet creates 3D object using your approx layout.

The transform needs a second object for the result

I also added a projectIso that takes the directions of x,y,z axis and the scale of the x,y,z axis and creates a projection matrix as outlined above.

So thus the above is done with

const mat = Mat().projectIso(
    Math.PI * ( 1 / 4), 
    Math.PI * ( 4 / 6),
    Math.PI * ( 3 / 2)  // up
); // scales default to 1

const ctx = canvas.getContext('2d');

var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;

const V = (x,y,z) => ({x,y,z,set(x,y,z){this.x = x;this.y = y; this.z = z}});
const Mat = () => ( {
   x : V(1,0,0),
   y : V(0,1,0),
   z : V(0,0,1),
   o : V(0,0,0), // origin
   ident(){
      const m = this;
      m.x.set(1,0,0);
      m.y.set(0,1,0);
      m.z.set(0,0,1);
      m.o.set(0,0,0);
      return m;
   },
   rotX(r) {
      const m = this.ident();      
      m.y.set(0, Math.cos(r), Math.sin(r));
      m.z.set(0, -Math.sin(r), Math.cos(r));
      return m;      
   },
   rotY(r) {
      const m = this.ident();      
      m.x.set(Math.cos(r), 0, Math.sin(r));
      m.z.set(-Math.sin(r), 0, Math.cos(r));
      return m;      
   },      
   rotZ(r) {
      const m = this.ident();      
      m.x.set(Math.cos(r), Math.sin(r), 0);
      m.y.set(-Math.sin(r), Math.cos(r), 0);
      return m;      
   },    
   projectIso(xAxis, yAxis, zAxis, xScale = 1, yScale = 1, zScale = 1) {
      const m = this.ident();      
      iso.x.set(Math.cos(xAxis) * xScale, Math.sin(xAxis) * xScale, 0);
      iso.y.set(Math.cos(yAxis) * yScale, Math.sin(yAxis) * yScale, 0);
      iso.z.set(Math.cos(zAxis) * zScale, Math.sin(zAxis) * zScale, 0);
      return m;
   },
   transform(obj, result){
      const m = this;
      const na = obj.nodes;
      const nb = result.nodes;
      var i = 0;
      while(i < na.length){
         const a = na[i];
         const b = nb[i++];
         b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
         b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
         b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
      }
      return result;
   }
});

// create a box
const Box = (size = 35) =>( {
  nodes: [
    V(-size, -size, -size),
    V(-size, -size, size),
    V(-size, size, -size),
    V(-size, size, size),
    V(size, -size, -size),
    V(size, -size, size),
    V(size, size, -size),
    V(size, size, size),
  ],
  edges: [[0, 1],[1, 3],[3, 2],[2, 0],[4, 5],[5, 7],[7, 6],[6, 4],[0, 4],[1, 5],[2, 6],[3, 7]],
});

// draws a obj that has nodes, and edges

function draw(obj) {
    ctx.fillStyle = 'red';
  const edges =  obj.edges;
  const nodes =  obj.nodes;
  var i = 0;
  ctx.beginPath();
  while(i < edges.length){
    var edge = edges[i++];
    ctx.moveTo(nodes[edge[0]].x, nodes[edge[0]].y);
    ctx.lineTo(nodes[edge[1]].x, nodes[edge[1]].y);
    
  }
  ctx.stroke();    
  i = 0;
  ctx.beginPath();
  while(i < nodes.length){
    const x = nodes[i].x;
    const y = nodes[i++].y;
    ctx.moveTo(x+3,y);
    ctx.arc(x,y, 3, 0, 2 * Math.PI, false);
  }
  ctx.fill();
}

// create boxes (box1 is the projected result)
var box = Box();
var box1 = Box();
var box2 = Box();

// create the projection matrix
var iso = Mat();
// angles for X, and Y axis
const xAxis = Math.PI * ( 1 / 4);
const yAxis = Math.PI * ( 4 / 6);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis),0);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
// the direction of Z
iso.z.set(0, -1, 0);

// center rendering
    
ctx.setTransform(1,0,0,1,cw* 0.5,ch);

// transform and render
draw(iso.transform(box,box1));

iso.projectIso(Math.PI * ( 1 / 6), Math.PI * ( 5 / 6), -Math.PI * ( 1 / 2))
ctx.setTransform(1,0,0,1,cw* 1,ch);
draw(iso.transform(box,box1));

iso.rotY(Math.PI / 4);
iso.transform(box,box1);
iso.rotX(Math.atan(1/Math.SQRT2));
iso.transform(box1,box2);
ctx.setTransform(1,0,0,1,cw* 1.5,ch);
draw(box2);
<canvas id="canvas" height='200' width='500'></canvas>

I think the issue may be that the rotation about the x-axis of the room is not 30°. In isometric images there is often an angle of 30° between the sides of a cube and the horizontal. But in order to get this horizontal angle, the rotation around the x-axis should be about 35° (atan(1/sqrt(2))). See the overview in the Wikipedia article.

Having said that, sometimes in computer graphics, the angle between the sides of a cube and the horizontal is about 27° (atan(0.5)), since this produces neater rastered lines on a computer screen. In that case, the rotation around the x-axis is 30°. Check out this article for a lot more information about the different types of projection.

Related