Triangle mesh for 3D objects in HTML5

Tutorials

Today’s lesson is a bridge between two-dimensional graphics in html5 and truly three-dimensional (using WebGL). Today I will show how to draw three-dimensional objects using a polygonal mesh. A polygon mesh or unstructured grid is a collection of vertices, edges and faces that defines the shape of a polyhedral object in 3D computer graphics and solid modeling. The faces usually consist of triangles, quadrilaterals or other simple convex polygons, since this simplifies rendering, but may also be composed of more general concave polygons, or polygons with holes.

In order to understand what it is about, I recommend to read the basis described in wikipedia.
To demonstrate, we have prepared simple three-dimensional objects – a cube and multi-dimensional sphere (with a variable number of faces).

Live Demo

If you are ready – let’s start!


Step 1. HTML

As usual (for all canvas-based demos) we have a very basic html markup (with a single canvas object inside):

01 <html lang="en" >
02     <head>
03         <meta charset="utf-8" />
04         <meta name="author" content="Script Tutorials" />
05         <title>Triangle mesh for 3D objects in HTML5 | Script Tutorials</title>
06         <!-- add styles -->
07         <link href="css/main.css" rel="stylesheet" type="text/css" />
08         <!-- add script -->
09         <script src="js/meshes.js"></script>
10         <script src="js/transform.js"></script>
11         <script>
12             //var obj = new cube();
13             //var obj = new sphere(6);
14             var obj = new sphere(16);
15         </script>
16         <script src="js/main.js"></script>
17     </head>
18     <body>
19         <div class="container">
20             <canvas id="scene" height="500" width="700" tabindex="1"></canvas>
21             <div class="hint">Please use Up / Down keys to change opacity</div>
22         </div>
23     </body>
24 </html>

I extracted a generated object initialization here, look:

1 <script>
2     //var obj = new cube();
3     //var obj = new sphere(6);
4     var obj = new sphere(16);
5 </script>

It means that if we need to display a cube – you have to uncomment the first one line, if you’d like to display a sphere with 6 faces – select the second variant.

Step 2. JS

There are three JS files (main.js, meshes.js and transform.js), we will publish two of them, third one (transform.js) contains only math-related functions (to rotate, scale, translate and project objects). It will be available in our package. So, let’s review the code of the first javascript:

js/meshes.js

001 // get random color
002 function getRandomColor() {
003     var letters = '0123456789ABCDEF'.split('');
004     var color = '#';
005     for (var i = 0; i < 6; i++ ) {
006         color += letters[Math.round(Math.random() * 15)];
007     }
008     return color;
009 }
010 // prepare object
011 function prepareObject(o) {
012     o.colors = new Array();
013     // prepare normals
014     o.normals = new Array();
015     for (var i = 0; i < o.faces.length; i++) {
016         o.normals[i] = [0, 0, 0];
017         o.colors[i] = getRandomColor();
018     }
019     // prepare centers: calculate max positions
020     o.center = [0, 0, 0];
021     for (var i = 0; i < o.points.length; i++) {
022         o.center[0] += o.points[i][0];
023         o.center[1] += o.points[i][1];
024         o.center[2] += o.points[i][2];
025     }
026     // prepare distances
027     o.distances = new Array();
028     for (var i = 1; i < o.points.length; i++) {
029         o.distances[i] = 0;
030     }
031     // calculate average center positions
032     o.points_number = o.points.length;
033     o.center[0] = o.center[0] / (o.points_number - 1);
034     o.center[1] = o.center[1] / (o.points_number - 1);
035     o.center[2] = o.center[2] / (o.points_number - 1);
036     o.faces_number = o.faces.length;
037     o.axis_x = [1, 0, 0];
038     o.axis_y = [0, 1, 0];
039     o.axis_z = [0, 0, 1];
040 }
041 // Cube object
042 function cube() {
043     // prepare points and faces for cube
044     this.points=[
045         [0,0,0],
046         [100,0,0],
047         [100,100,0],
048         [0,100,0],
049         [0,0,100],
050         [100,0,100],
051         [100,100,100],
052         [0,100,100],
053         [50,50,100],
054         [50,50,0],
055     ];
056     this.faces=[
057         [0,4,5],
058         [0,5,1],
059         [1,5,6],
060         [1,6,2],
061         [2,6,7],
062         [2,7,3],
063         [3,7,4],
064         [3,4,0],
065         [8,5,4],
066         [8,6,5],
067         [8,7,6],
068         [8,4,7],
069         [9,5,4],
070         [9,6,5],
071         [9,7,6],
072         [9,4,7],
073     ];
074     prepareObject(this);
075 }
076 // Sphere object
077 function sphere(n) {
078     var delta_angle = 2 * Math.PI / n;
079     // prepare vertices (points) of sphere
080     var vertices = [];
081     for (var j = 0; j < n / 2 - 1; j++) {
082         for (var i = 0; i < n; i++) {
083             vertices[j * n + i] = [];
084             vertices[j * n + i][0] = 100 * Math.sin((j + 1) * delta_angle) * Math.cos(i * delta_angle);
085             vertices[j * n + i][1] = 100 * Math.cos((j + 1) * delta_angle);
086             vertices[j * n + i][2] = 100 * Math.sin((j + 1) * delta_angle) * Math.sin(i * delta_angle);
087         }
088     }
089     vertices[(n / 2 - 1) * n] = [];
090     vertices[(n / 2 - 1) * n + 1] = [];
091     vertices[(n / 2 - 1) * n][0] = 0;
092     vertices[(n / 2 - 1) * n][1] =  100;
093     vertices[(n / 2 - 1) * n][2] =  0;
094     vertices[(n / 2 - 1) * n + 1][0] = 0;
095     vertices[(n / 2 - 1) * n + 1][1] = -100;
096     vertices[(n / 2 - 1) * n + 1][2] = 0;
097     this.points = vertices;
098     // prepare faces
099     var faces = [];
100     for (var j = 0; j < n / 2 - 2; j++) {
101         for (var i = 0; i < n - 1; i++) {
102             faces[j * 2 * n + i] = [];
103             faces[j * 2 * n + i + n] = [];
104             faces[j * 2 * n + i][0] = j * n + i;
105             faces[j * 2 * n + i][1] = j * n + i + 1;
106             faces[j * 2 * n + i][2] = (j + 1) * n + i + 1;
107             faces[j * 2 * n + i + n][0] = j * n + i;
108             faces[j * 2 * n + i + n][1] = (j + 1) * n + i + 1;
109             faces[j * 2 * n + i + n][2] = (j + 1) * n + i;
110         }
111         faces[j * 2 * n + n - 1] = [];
112         faces[2 * n * (j + 1) - 1] = [];
113         faces[j * 2 * n + n - 1  ][0] = (j + 1) * n - 1;
114         faces[j * 2 * n + n - 1  ][1] = (j + 1) * n;
115         faces[j * 2 * n + n - 1  ][2] = j * n;
116         faces[2 * n * (j + 1) - 1][0] = (j + 1) * n - 1;
117         faces[2 * n * (j + 1) - 1][1] = j * n + n;
118         faces[2 * n * (j + 1) - 1][2] = (j + 2) * n - 1;
119     }
120     for (var i = 0; i < n - 1; i++) {
121         faces[n * (n - 4) + i] = [];
122         faces[n * (n - 3) + i] = [];
123         faces[n * (n - 4) + i][0] = (n / 2 - 1) * n;
124         faces[n * (n - 4) + i][1] = i;
125         faces[n * (n - 4) + i][2] = i + 1;
126         faces[n * (n - 3) + i][0] = (n / 2 - 1) * n + 1;
127         faces[n * (n - 3) + i][1] = (n / 2 - 2) * n + i + 1;
128         faces[n * (n - 3) + i][2] = (n / 2 - 2) * n + i;
129     }
130     faces[n * (n - 3) - 1] = [];
131     faces[n * (n - 2) - 1] = [];
132     faces[n * (n - 3) - 1][0] = (n / 2 - 1) * n;
133     faces[n * (n - 3) - 1][1] = n - 1;
134     faces[n * (n - 3) - 1][2] = 0;
135     faces[n * (n - 2) - 1][0] = (n / 2 - 1) * n + 1;
136     faces[n * (n - 2) - 1][1] = (n / 2 - 2) * n;
137     faces[n * (n - 2) - 1][2] = (n / 2 - 2) * n + n - 1;
138     this.faces=faces;
139     prepareObject(this);
140 }

In the most beginning, we should prepare all points and faces of our object. There are 2 functions: cube (which generates initial arrays for a simple cube object) and sphere (to generate sphere). As you see – it is much more difficult to calculate all points and faces for multi-dimensional sphere. Once we get all these points and surfaces we have to calculate other params (like normals, distances, absolute center and three axis).

js/main.js

001 // inner variables
002 var canvas, ctx;
003 var vAlpha = 0.5;
004 var vShiftX = vShiftY = 0;
005 var distance = -700;
006 var vMouseSens = 0.05;
007 var iHalfX, iHalfY;
008 // initialization
009 function sceneInit() {
010     // prepare canvas and context objects
011     canvas = document.getElementById('scene');
012     ctx = canvas.getContext('2d');
013     iHalfX = canvas.width / 2;
014     iHalfY = canvas.height / 2;
015     // initial scale and translate
016     scaleObj([3, 3, 3], obj);
017     translateObj([-obj.center[0], -obj.center[1], -obj.center[2]],obj);
018     translateObj([0, 0, -1000], obj);
019     // attach event handlers
020     document.onkeydown = handleKeydown;
021     canvas.onmousemove = handleMousemove;
022     // main scene loop
023     setInterval(drawScene, 25);
024 }
025 // onKeyDown event handler
026 function handleKeydown(e) {
027     kCode = ((e.which) || (e.keyCode));
028     switch (kCode) {
029         case 38: vAlpha = (vAlpha <= 0.9) ? (vAlpha + 0.1) : vAlpha; break// Up key
030         case 40: vAlpha = (vAlpha >= 0.2) ? (vAlpha - 0.1) : vAlpha; break// Down key
031     }
032 }
033 // onMouseMove event handler
034 function handleMousemove(e) {
035     var x = e.pageX - canvas.offsetLeft;
036     var y = e.pageY - canvas.offsetTop;
037     if ((x > 0) && (x < canvas.width) && (y > 0) && (y < canvas.height)) {
038         vShiftY = vMouseSens * (x - iHalfX) / iHalfX;
039         vShiftX = vMouseSens * (y - iHalfY) / iHalfY;
040     }
041 }
042 // draw main scene function
043 function drawScene() {
044     // clear canvas
045     ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
046     // set fill color, stroke color, line width and global alpha
047     ctx.strokeStyle = 'rgb(0,0,0)';
048     ctx.lineWidth = 0.5;
049     ctx.globalAlpha= vAlpha;
050     // vertical and horizontal rotate
051     var vP1x = getRotationPar([0, 0, -1000], [1, 0, 0], vShiftX);
052     var vP2x = getRotationPar([0, 0, 0], [1, 0, 0], vShiftX);
053     var vP1y = getRotationPar([0, 0, -1000], [0, 1, 0], vShiftY);
054     var vP2y = getRotationPar([0, 0, 0], [0, 1, 0], vShiftY);
055     rotateObj(vP1x, vP2x, obj);
056     rotateObj(vP1y, vP2y, obj);
057     // recalculate distances
058     for (var i = 0; i < obj.points_number; i++) {
059         obj.distances[i] = Math.pow(obj.points[i][0],2) + Math.pow(obj.points[i][1],2) + Math.pow(obj.points[i][2], 2);
060     }
061     // prepare array with face triangles (with calculation of max distance for every face)
062     var iCnt = 0;
063     var aFaceTriangles = new Array();
064     for (var i = 0; i < obj.faces_number; i++) {
065         var max = obj.distances[obj.faces[i][0]];
066         for (var f = 1; f < obj.faces[i].length; f++) {
067             if (obj.distances[obj.faces[i][f]] > max)
068                 max = obj.distances[obj.faces[i][f]];
069         }
070         aFaceTriangles[iCnt++] = {faceVertex:obj.faces[i], faceColor:obj.colors[i], distance:max};
071     }
072     aFaceTriangles.sort(sortByDistance);
073     // prepare array with projected points
074     var aPrjPoints = new Array();
075     for (var i = 0; i < obj.points.length; i++) {
076         aPrjPoints[i] = project(distance, obj.points[i], iHalfX, iHalfY);
077     }
078     // draw an object (surfaces)
079     for (var i = 0; i < iCnt; i++) {
080         ctx.fillStyle = aFaceTriangles[i].faceColor;
081         // begin path
082         ctx.beginPath();
083         // face vertex index
084         var iFaceVertex = aFaceTriangles[i].faceVertex;
085         // move to initial position
086         ctx.moveTo(aPrjPoints[iFaceVertex[0]][0], aPrjPoints[iFaceVertex[0]][1]);
087         // and draw three lines (to build a triangle)
088         for (var z = 1; z < aFaceTriangles[i].faceVertex.length; z++) {
089             ctx.lineTo(aPrjPoints[iFaceVertex[z]][0], aPrjPoints[iFaceVertex[z]][1]);
090         }
091         // close path, strole and fill a triangle
092         ctx.closePath();
093         ctx.stroke();
094         ctx.fill();
095     }
096 }
097 // sort function
098 function sortByDistance(x, y) {
099     return (y.distance - x.distance);
100 }
101 // initialization
102 if (window.attachEvent) {
103     window.attachEvent('onload', sceneInit);
104 else {
105     if (window.onload) {
106         var curronload = window.onload;
107         var newonload = function() {
108             curronload();
109             sceneInit();
110         };
111         window.onload = newonload;
112     else {
113         window.onload = sceneInit;
114     }
115 }

Well, it’s the time to back to our main page functionality. As soon as the page is loaded, we do main initialization (sceneInit function). We create canvas and context objects, then we perform initial scale and translate of our object which we created in the most beginning (cube or sphere). Then we attach onkeydown and onmousemove event handlers and set timer to draw our main scene (drawScene function). Don’t forget that we can change globalAlpha param with clicking Up/Down buttons.


Live Demo

[sociallocker]

download in package

[/sociallocker]


Conclusion

That’s all for today, we have just finished building the basic triangle mesh objects at canvas. See you next time, good luck!

Rate article