Making a 3D Engine in jQuery
In the previous post “3D tag cloud” I received quite a few requests for different shapes besides the ring.
The problem is how the tag cloud was created it doesn’t lend itself to different shapes very well. So
this post will show how to create a 3D engine in jQuery / JavaScript for those more exciting shapes.
What We’re Building
We’re going to create a 3D engine that will allow us to create shapes simply by creating an array of points. This engine will have a Camera, a Scene and an Object. It’ll work pretty well the same as in real life. The closer the camera is to the object the larger it gets. This won’t be a complete rendering engine, as this won’t render multiple objects at this point.
Display Object
The Display Object is more or less a way to give each object a set of properties and methods. Say I wanted to create a cube, it would first have to inherit the DisplayObject3D class for it to work. It gives the object the tools it needs to get the job done. Kind of like a tool box for a handy man.
Here is the JavaScript Class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | var DisplayObject3D = function(){ return this; }; DisplayObject3D.prototype._x = 0; DisplayObject3D.prototype._y = 0; //Create 3d Points DisplayObject3D.prototype.make3DPoint = function(x,y,z) { var point = {}; point.x = x; point.y = y; point.z = z; return point; }; //Create 2d Points DisplayObject3D.prototype.make2DPoint = function(x,y, depth, scaleFactor){ var point = {}; point.x = x; point.y = y; point.depth = depth; point.scaleFactor = scaleFactor; return point; }; //Holds the container DisplayObject3D.prototype.container = undefined; //Holds an array of 3d points. DisplayObject3D.prototype.pointsArray = []; // Set the container and create place holders if // there is no <ul> in the container DisplayObject3D.prototype.init = function (container){ this.container = $(container); this.containerId = this.container.attr("id"); //if there isn't a ul than it creates a list of +'s if ($(container+":has(ul)").length === 0){ for (i=0; i < this.pointsArray.length; i++){ this.container.append('<b id="item'+i+'">+</b>'); } } }; |
If this seems a little foreign to you, you might want to check out the post Object Oriented Programming with JavaScript.
Make 3d/2d Points
These functions basically create objects, one meant for 3d (x,y,z) and one meant for 2d(x,y,depth). These will come in handy when the scene is rendered.
Initialization
The init function assigns a variable to the container passed in. The container is where ever you want the 3d object to display. There is also a part of this function that checks to see if the container has an unordered list. If it doesn’t it just creates a bunch of +’s Really these lines aren’t necessary, I just put it in so I don’t always have to create a unordered list to see if my object is rendering.
Camera
I’m guessing from the name you can probably figure out what this class does. It’s just like a real life camera. It basically creates a point of reference.
Here is the Camera Class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var Camera3D = function (){}; //The x,y,z of the camera Camera3D.prototype.x = 0; Camera3D.prototype.y = 0; Camera3D.prototype.z = 500; //Determines the zoom Camera3D.prototype.focalLength = 1000; //Figure out how large the object should be in //reference to the camera. Camera3D.prototype.scaleRatio = function(item){ return this.focalLength/(this.focalLength + item.z - this.z); }; //Initialize the camera with values. Camera3D.prototype.init = function (x,y,z,focalLength){ this.x = x; this.y = y; this.z = z; this.focalLength = focalLength; }; |
The focal length basically determines how large the object is going to look in reference to how close it is from the camera. Think of it like a zoom lens. You can zoom in, or zoom out.
The scaleRatio does the magic. It figures out how large the item should be in reference to the camera.
Object 3D
This class isn’t overly important. I basically made this class so I can add more than one object in the future. Right now the engine can only deal with one object at a time. Object3D is a glorified array. Anytime you add an item to the array, it runs the init function on that item.
Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // class creates an array of objects to // be rendered, and initializes them. var Object3D = function (container){ this.container = $(container); }; Object3D.prototype.objects = []; //Add object to the list of objects. Object3D.prototype.addChild = function (object3D){ this.objects.push(object3D); object3D.init(this.container); return object3D; }; |
Scene
This is where all the complicated stuff happens. I won’t get into great detail with the mathematics. High school math was boring enough
I will give you a high overview of what the code does though, so at least you have an understanding of what it does.
Here is the Scene Class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | var Scene3D = function (){}; Scene3D.prototype.sceneItems = []; //Adds objects to the list of items to be rendered. Scene3D.prototype.addToScene = function (object){ this.sceneItems.push(object); }; //Converts a 3d point into a 2d point. Scene3D.prototype.Transform3DPointsTo2DPoints = function(points, axisRotations,camera){ var TransformedPointsArray = []; var sx = Math.sin(axisRotations.x); var cx = Math.cos(axisRotations.x); var sy = Math.sin(axisRotations.y); var cy = Math.cos(axisRotations.y); var sz = Math.sin(axisRotations.z); var cz = Math.cos(axisRotations.z); var x,y,z, xy,xz, yx,yz, zx,zy, scaleFactor; var i = points.length; while (i--){ x = points[i].x; y = points[i].y; z = points[i].z; // rotation around x xy = cx*y - sx*z; xz = sx*y + cx*z; // rotation around y yz = cy*xz - sy*x; yx = sy*xz + cy*x; // rotation around z zx = cz*yx - sz*xy; zy = sz*yx + cz*xy; scaleFactor = camera.focalLength/(camera.focalLength + yz); x = zx*scaleFactor; y = zy*scaleFactor; z = yz; var displayObject = new DisplayObject3D(); TransformedPointsArray[i] = displayObject.make2DPoint(x, y, -z, scaleFactor); } return TransformedPointsArray; }; //Takes the converted 2d and applies the appropriate CSS. Scene3D.prototype.renderCamera = function (camera){ // Loop through all objects in the scene. for(var i = 0 ; i< this.sceneItems.length; i++){ var obj = this.sceneItems[i].objects[0]; //transform the points in the object to 2d points. var screenPoints = this.Transform3DPointsTo2DPoints(obj.pointsArray, axisRotation, camera); //does the container have a ul inside of it. var hasList = (document.getElementById(obj.containerId).getElementsByTagName("ul").length > 0); //Cycle through each point in the object. for (k=0; k < obj.pointsArray.length; k++){ var currItem = null; //if the container has a list then select the lis if (hasList){ currItem = document.getElementById(obj.containerId).getElementsByTagName("ul")[0].getElementsByTagName("li")[k]; }else{ //otherwise select whatever is there. currItem = document.getElementById(obj.containerId).getElementsByTagName("*")[k]; } //If there are items to render then... if(currItem){ currItem._x = screenPoints[k].x; currItem._y = screenPoints[k].y; currItem.scale = screenPoints[k].scaleFactor; //Render the CSS. currItem.style.position = "absolute"; currItem.style.top = currItem._y+'px'; currItem.style.left = currItem._x+'px'; currItem.style.fontSize = 100*currItem.scale+'%'; $(currItem).css({opacity:(currItem.scale-.5)}); } } } }; //Center for rotation var axisRotation = new DisplayObject3D().make3DPoint(0,0,0); |
Add To Scene
First thing you’ll see is the addToScene function. This basically populates the array that is used to render the scene. If you object is in this array it will get rendered, if it isn’t….it missed the bus.
Transform 3D point to 2D
Next is the Transform3DPointsTo2DPoints function. This is probably the most important part of the engine. What it does is it takes a 3D point in space (x,y,z) and through a little magic converts it into a 2D point that can be displayed on screen.
Render Camera
The renderCamera function doesn’t do anything too complicated. It’s sole purpose is to display the items on screen. It does this by looping through the objects in the scene, and passes each one into the Transform3DPointsTo2DPoints function. The function will return a 2D point that can be plotted using CSS. You might have noticed I’m using JavaScript for the CSS and selecting of the DOM instead of jQuery. Initially I used jQuery but found the browser was crawling, so I went back to the old fashion way.
The Cube
I’ve put the Cube class in a separate file so you can import objects as you need them instead of having to load ones you aren’t using.
The class isn’t overly complicated. It’s creating an array and placing all the points on the cube into that array. It uses the make3DPoint function from the DisplayObject3D class to create each point.
Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | var Cube = function (size){ //if the size is not set then give a default if (size === undefined){ size = 10; } //Create a 3d point for every point on the cube. this.pointsArray = [ //make3dpoint is a function inherited from //DisplayObject3D this.make3DPoint(-size,-size,-size), this.make3DPoint(size,-size,-size), this.make3DPoint(size,-size,size), this.make3DPoint(-size,-size,size), this.make3DPoint(-size,size,-size), this.make3DPoint(size,size,-size), this.make3DPoint(size,size,size), this.make3DPoint(-size,size,size), this.make3DPoint(0,size,-size), this.make3DPoint(size,size,0), this.make3DPoint(0,size,size), this.make3DPoint(-size,size,0), this.make3DPoint(0,-size,-size), this.make3DPoint(size,-size,0), this.make3DPoint(0,-size,size), this.make3DPoint(-size,-size,0), this.make3DPoint(-size,0,-size), this.make3DPoint(size,0,-size), this.make3DPoint(size,0,size), this.make3DPoint(-size,0,size) ]; }; //Inherit DisplayObject3d methods and properties Cube.prototype = new DisplayObject3D(); |
Using the 3D Engine
There are a few steps that need to be done in order for this to work. First we’ll need some HTML. I’ll just create a div with an id and place a <ul> full of smiley faces. If you wanted you can leave the div blank and it will automatically create a bunch of +’s for you.
Here is the HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <body> <div id="item"> <!-- List of smiley faces --> <ul> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> </ul> </div> </body> <script type="text/javascript" src="jquery-1.3.2.min.js"></script> <script src="3DEngine.js" type="text/javascript" charset="utf-8"></script> <!-- Each 3d object is a seperate js so you only have to import what you need. --> <script src="Cube.js" type="text/javascript" charset="utf-8"></script> |
Next comes the jQuery. There are a couple of steps you need to do:
- Create a camera and initialize it.
- Create an Object Holder (Object3D)
- Create an Object and put it in the holder.
- Create a Scene and put the holder in it.
Here is the jQuery:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | $(document).ready(function() { //Create a camera var camera = new Camera3D(); //initialize it. camera.init(0,0,0,300); //Create an object holder. var item = new Object3D($("#item")); //add a new cube. item.addChild(new Cube(100)); //Create a scene var scene = new Scene3D(); //Place the object Holder in the scene. scene.addToScene(item); //Animates the cube. var animateIt = function(){ //rotates on the y and x axis. axisRotation.y += .01 axisRotation.x -= .01 //Render the scene. scene.renderCamera(camera); }; setInterval(animateIt, 20); }); |
I didn’t get a chance to make it interact with the mouse. You can do that just by adjusting the axisRotation x, y and z values when the mouse moves. Perhaps I’ll do this in my next post.
That’s All For Now
Stay tuned for the next couple of posts. I plan to make a couple different shapes for this. If you feel frisky by all means try and create your own shapes. Just create an array with a bunch of 3D Points.
There are still a couple of kinks to work out, but for now here is all the code together.
3DEngine.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | /* * DisplayObject3D ---------------------------------------------- */ var DisplayObject3D = function(){ return this; }; DisplayObject3D.prototype._x = 0; DisplayObject3D.prototype._y = 0; //Create 3d Points DisplayObject3D.prototype.make3DPoint = function(x,y,z) { var point = {}; point.x = x; point.y = y; point.z = z; return point; }; //Create 2d Points DisplayObject3D.prototype.make2DPoint = function(x,y, depth, scaleFactor){ var point = {}; point.x = x; point.y = y; point.depth = depth; point.scaleFactor = scaleFactor; return point; }; DisplayObject3D.prototype.container = undefined; DisplayObject3D.prototype.pointsArray = []; DisplayObject3D.prototype.init = function (container){ this.container = $(container); this.containerId = this.container.attr("id"); //if there isn't a ul than it creates a list of +'s if ($(container+":has(ul)").length === 0){ for (i=0; i < this.pointsArray.length; i++){ this.container.append('<b id="item'+i+'">+</b>'); } } }; /* * DisplayObject3D End ---------------------------------------------- */ /* * Camera3D ---------------------------------------------- */ var Camera3D = function (){}; Camera3D.prototype.x = 0; Camera3D.prototype.y = 0; Camera3D.prototype.z = 500; Camera3D.prototype.focalLength = 1000; Camera3D.prototype.scaleRatio = function(item){ return this.focalLength/(this.focalLength + item.z - this.z); }; Camera3D.prototype.init = function (x,y,z,focalLength){ this.x = x; this.y = y; this.z = z; this.focalLength = focalLength; }; /* * Camera3D End ---------------------------------------------- */ /* * Object3D ---------------------------------------------- */ var Object3D = function (container){ this.container = $(container); }; Object3D.prototype.objects = []; Object3D.prototype.addChild = function (object3D){ this.objects.push(object3D); object3D.init(this.container); return object3D; }; /* * Object3D End ---------------------------------------------- */ /* * Scene3D ---------------------------------------------- */ var Scene3D = function (){}; Scene3D.prototype.sceneItems = []; Scene3D.prototype.addToScene = function (object){ this.sceneItems.push(object); }; Scene3D.prototype.Transform3DPointsTo2DPoints = function(points, axisRotations,camera){ var TransformedPointsArray = []; var sx = Math.sin(axisRotations.x); var cx = Math.cos(axisRotations.x); var sy = Math.sin(axisRotations.y); var cy = Math.cos(axisRotations.y); var sz = Math.sin(axisRotations.z); var cz = Math.cos(axisRotations.z); var x,y,z, xy,xz, yx,yz, zx,zy, scaleFactor; var i = points.length; while (i--){ x = points[i].x; y = points[i].y; z = points[i].z; // rotation around x xy = cx*y - sx*z; xz = sx*y + cx*z; // rotation around y yz = cy*xz - sy*x; yx = sy*xz + cy*x; // rotation around z zx = cz*yx - sz*xy; zy = sz*yx + cz*xy; scaleFactor = camera.focalLength/(camera.focalLength + yz); x = zx*scaleFactor; y = zy*scaleFactor; z = yz; var displayObject = new DisplayObject3D(); TransformedPointsArray[i] = displayObject.make2DPoint(x, y, -z, scaleFactor); } return TransformedPointsArray; }; Scene3D.prototype.renderCamera = function (camera){ for(var i = 0 ; i< this.sceneItems.length; i++){ var obj = this.sceneItems[i].objects[0]; var screenPoints = this.Transform3DPointsTo2DPoints(obj.pointsArray, axisRotation, camera); var hasList = (document.getElementById(obj.containerId).getElementsByTagName("ul").length > 0); for (k=0; k < obj.pointsArray.length; k++){ var currItem = null; if (hasList){ currItem = document.getElementById(obj.containerId).getElementsByTagName("ul")[0].getElementsByTagName("li")[k]; }else{ currItem = document.getElementById(obj.containerId).getElementsByTagName("*")[k]; } if(currItem){ currItem._x = screenPoints[k].x; currItem._y = screenPoints[k].y; currItem.scale = screenPoints[k].scaleFactor; currItem.style.position = "absolute"; currItem.style.top = currItem._y+'px'; currItem.style.left = currItem._x+'px'; currItem.style.fontSize = 100*currItem.scale+'%'; $(currItem).css({opacity:(currItem.scale-.5)}); } } } }; /* * Scene3D End ---------------------------------------------- */ //Center for rotation var axisRotation = new DisplayObject3D().make3DPoint(0,0,0); |
Cube.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | var Cube = function (size){ if (size === undefined){ size = 10; } this.pointsArray = [ this.make3DPoint(-size,-size,-size), this.make3DPoint(size,-size,-size), this.make3DPoint(size,-size,size), this.make3DPoint(-size,-size,size), this.make3DPoint(-size,size,-size), this.make3DPoint(size,size,-size), this.make3DPoint(size,size,size), this.make3DPoint(-size,size,size), this.make3DPoint(0,size,-size), this.make3DPoint(size,size,0), this.make3DPoint(0,size,size), this.make3DPoint(-size,size,0), this.make3DPoint(0,-size,-size), this.make3DPoint(size,-size,0), this.make3DPoint(0,-size,size), this.make3DPoint(-size,-size,0), this.make3DPoint(-size,0,-size), this.make3DPoint(size,0,-size), this.make3DPoint(size,0,size), this.make3DPoint(-size,0,size) ]; }; Cube.prototype = new DisplayObject3D(); |
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | <html> <head> <title>Untitled Page</title> <style type="text/css" media="screen"> #item{ width:100px; height:100px; margin:0 auto; top:300px; position: relative; } ul{ list-style-type: none; } body{ background-color: #111; color: #69c; font-family: Arial, "MS Trebuchet", sans-serif; font-weight: bold; font-size:2em; } </style> </head> <body> <div id="item"> <ul> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> <li>☺</li> </ul> </div> </body> <script type="text/javascript" src="jquery-1.3.2.min.js"></script> <script src="3DEngine.js" type="text/javascript" charset="utf-8"></script> <script src="Cube.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> //<![CDATA[ $(document).ready(function() { var camera = new Camera3D(); camera.init(0,0,0,300); var item = new Object3D($("#item")); item.addChild(new Cube(100)); var scene = new Scene3D(); scene.addToScene(item); var animateIt = function(){ axisRotation.y += .01 axisRotation.x -= .01 scene.renderCamera(camera); }; setInterval(animateIt, 20); }); //]]> </script> </html> |
Related
3D Plane
|
Wobbling Carousel
|
3D Sphere
|




















Wow.
I absolutely CANNOT wait for an excuse to use this! Bookmarked!
Thanks
really nice jquery plugin nice work
Impressive!
Impressive o_o
Nice =), I’m waiting a long time for this made in js…the performance ist mostly the problem…I love the wordpress-cumulus plugin and would like to have it without being forced to use flash =)…maybe some day!
The idea is very very cool, I like it.
But doing it in the DOM and using styles… please…
Use the canvas, I beg of you! I do like what, 20 elements and I need a Pentium i7 quad core to keep up?
can u help me making 3d??
Hello I just search the google and I found out your site. Nice post I am very worth reading it. Thanks!