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 — 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.
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.
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.
Rotation along Y
axis.
Rotation along Z
axis.
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.
Perspective projection.
Distance from the point to the projection center.
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.
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.
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 {
// 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 {
// 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