Home > Experimental, Front End Development, How to, Javascript > Making a 3D Engine in jQuery

Making a 3D Engine in jQuery

September 14th, 2009


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.

 cube 3d

 

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>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</li>
            <li>&#x263A;</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:

  1. Create a camera and initialize it.
  2. Create an Object Holder (Object3D)
  3. Create an Object and put it in the holder.
  4. 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>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</li>
                <li>&#x263A;</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
3d Plane
Wobbling Carousel
wobbling carousel

3D Sphere
3d sphere



Top