The demonstration can be viewed and played with here.

Edit: Version 2.1 here has faces and back-face culling however, I decided to go with a different model format *.vtx because the models are triangles and the format is simpler.

The project can be forked here on GitHub.

I was playing the classic 1980s game Battlezone using Mame last night and thought it would be fun to make the game in Javascript. It's true I could simply use three.js or Unity or something like that...but for me it's more about being able to build something and how it works than the actual game itself. I had some years ago experimented with 3D in the browser with..."some"...success. I was able to import the model and display it but, my lack of understanding of what was actually happening at the time meant the models were often broken and I was unable to rotate them or really do anything with them for that matter. My abilities have expanded a great deal since then and so it was time to revisit this project.

I originally chose Anim8or because it was a program and format I was familiar with and the file format was in plain text.

The current version only imports the Anim8or format amd only imports the model data without the Materials or UV data. .The project was really just so I could get the import and use of basic wire-frame models into the browser for the Battlezone game but I might expand it to allow filled faces and maybe even textures eventually. Maybe I will add more model formats.

If I do the back-end will be in Node.js or python. ( I have been working in PHP for 10 years now and it's time to expand into something else to be competitive ).

I built the program using Wes Mantooth to render the graphics. I didn't need to but, as I continue creating Battlezone it will be helpful to have a library handy.

const W = 800, H = 800 window.onload = function() { 'use strict'; $w.gamespeed = 80; let i = $w.add_object_single ( 1, Cube,{ w:W, h:H }, document.getElementById('target'), W,H ); $w.loop(true,i); } /** * Cube * * @param {Object} * @returns {Void} * */ var Cube = function(o){ this.i = o.i this.focalLength = W; this.zOrigin = 10; // If no model uploaded the tetra is shown by default this.pointsArray = [ this.make3DPoint(9.9853,9.9853,9.9853), this.make3DPoint(9.9853,9.9853,-9.9853), this.make3DPoint(0.14684,-9.9853,0), this.make3DPoint(-9.9853,9.9853,9.9853), this.make3DPoint(-9.9853,9.9853,-9.9853) ]; this.facesArray = [ [0,3,2], [1,2,4], [0,2,1], [3,4,2], [0,1,4,3] ]; this.cubeAxisRotations = this.make3DPoint(0,0,0); } /** * loop * @returns {Void} * */ Cube.prototype.loop = function() { //this.drawPoints(this.rotateCube(0.1,0.1)); this.drawLines(this.rotateCube(0.1,0.1),this.boxmodel); } /** * drawPoints * @param {Array} * @returns {Void} * */ Cube.prototype.drawPoints = function(screenPoints){ let l = this.pointsArray.length; for (let i=0; i<l; i++){ $w.canvas.circle(this.i,screenPoints[i].x + (W/2),screenPoints[i].y + (H/2),3 * screenPoints[i].scaleRatio,'#000000',1); } } /** * drawLines * @param {Array} * @returns {Void} * */ Cube.prototype.drawLines = function(screenPoints){ let l = this.facesArray.length,poly = []; let prev, fl = 0; for (let i=0; i<l; i++){ if (0 == fl) { fl = this.facesArray[0].length; } for(let j=0; j<fl; j++){ if (j==0) { prev = this.facesArray[i][j]; }else{ if (undefined !== screenPoints[prev] && undefined !== screenPoints[this.facesArray[i][j]]) { $w.canvas.line(this.i,screenPoints[prev].x+(W/2),screenPoints[prev].y+(H/2),screenPoints[this.facesArray[i][j]].x+(W/2),screenPoints[this.facesArray[i][j]].y+(H/2)); } prev = this.facesArray[i][j]; } } } } /** * make3DPoint * @param {Float} * @param {Float} * @param {Float} * @returns {Object} * */ Cube.prototype.make3DPoint = function(x,y,z){ let point = { x:x, y:y, z:z } return point; } /** * make2DPoint * @param {Float} * @param {Float} * @param {Number} * @param {Number} * @returns {Object} * */ Cube.prototype.make2DPoint = function(x, y, depth, scaleRatio){ let point = { x:x, y:y, depth:depth, scaleRatio:scaleRatio } return point; } /** * Transform3DPointsTo2DPoints * @param {Float} * @param {Number} * @returns {Array} * */ Cube.prototype.Transform3DPointsTo2DPoints = function(points, axisRotations){ // the array to hold transformed 2D points - the 3D points // from the point array which are here rotated and scaled // to generate a point as it would appear on the screen let TransformedPointsArray = []; // Math calcs for angles - sin and cos for each (trig) // this will be the only time sin or cos is used for the // entire portion of calculating all rotations let sx = Math.sin(axisRotations.x); let cx = Math.cos(axisRotations.x); let sy = Math.sin(axisRotations.y); let cy = Math.cos(axisRotations.y); let sz = Math.sin(axisRotations.z) * this.zOrigin; let cz = Math.cos(axisRotations.z) * this.zOrigin; // a couple of letiables to be used in the looping // of all the points in the transform process let x,y,z, xy,xz, yx,yz, zx,zy, scaleFactor; // 3... 2... 1... loop! // loop through all the points in your object/scene/space // whatever - those points passed - so each is transformed let i = points.length; while (i--){ // apply Math to making transformations // based on rotations // assign letiables for the current x, y and z let x = points[i].x; let y = points[i].y; let z = points[i].z; // perform the rotations around each axis // rotation around x let xy = cx*y - sx*z; let xz = sx*y + cx*z; // rotation around y let yz = cy*xz - sy*x; let yx = sy*xz + cy*x; // rotation around z let zx = cz*yx - sz*xy; let zy = sz*yx + cz*xy; // now determine perspective scaling factor // yz was the last calculated z value so its the // final value for z depth let scaleRatio = this.focalLength/(this.focalLength + yz); // assign the new x, y and z (the last z calculated) x = zx*scaleRatio; y = zy*scaleRatio; z = yz; // create transformed 2D point with the calculated values // adding it to the array holding all 2D points TransformedPointsArray[i] = this.make2DPoint(x, y, -z, scaleRatio); } // after looping return the array of points as they // exist after the rotation and scaling return TransformedPointsArray; } /** * rotateCube * @param {Float} * @param {Float} * @returns {Array} * */ Cube.prototype.rotateCube = function(x,y){ // automatically rotate model for demo //this.cubeAxisRotations.x += x; this.cubeAxisRotations.y += y; // return this.Transform3DPointsTo2DPoints(this.pointsArray, this.cubeAxisRotations); } // --------- // Begin control functions // --------- /** * rotateModel * @param {Event} * @returns {Void} * */ function rotateModel(event) { $w.objects.Cube[0].cubeAxisRotations.y = (-event.screenX) / 400; $w.objects.Cube[0].cubeAxisRotations.x = event.screenY / 400; } /** * setZoom * @param {Number} * @returns {Void} * */ function setZoom(z) { $w.objects.Cube[0].zOrigin = z; }