Tag Archives: 3d

Visualizing 3D Network Topologies Using Unity

Network topology visualization can be performed using a wide variety of technologies ranging from simple 2D applications to complex 3D applications.  This approach utilizes the Unity (http://unity3d.com/) game engine to develop a network topology visualization in 3D complete with FPS controls in zero gravity and warp to zones.

Demo

Here’s an example of what the end product will look like: View Demo

Screenshots

Unity Topology Screenshot 4 Unity Topology Screenshot 3 Unity Topology Screenshot 2 Unity Topology Screenshot 1

Source Files

The entire Unity project and source files are available for download: Download Unity Topology Source

Layout File Format

Let’s get started, first a data format (or formats) needs to be chosen for loading the layout or topology data.  For my example I have chosen to use GraphML (http://graphml.graphdrawing.org/) being that Unity in C# directly supports XmlDocument.

The GraphML format I am using will support a collection of <node> and <edge> with standard and custom attributes:

<node id="node_1" x="0" y="0" z="0" name="156.145.220.128/25" ... />
<edge id="link_1" source="node_1" target="node_2" ... />

The node defines at the very least the node identifier which will uniquely identify the node in the topology map, the x, y and z which will become our Vector3 coordinates and the name attribute which will become the node label.

The edge defines at the very least the link identifier which will uniquely identify the link in the topology map, the source which identifies the source node of the link and the target which identifies the target node of the link.

Unity Setup

Next let’s define our basic Unity project structure.  Under the Assets folder, create the following subfolders:

  • Data
  • Materials
  • Prefabs
  • Scenes
  • Scripts

Unity - Topology Folders Once we have our basic folder structure setup, let’s create our 2 prefabs we will be using in our project, the Node prefab and the Link prefab.

The Node

Start by creating a new cylinder (GameObject -> Create Other -> Cylinder), size the cylinder down a little to make it look like and adding a Self-Illumin/Diffuse shader.  I made mine blue, however feel free to choose whatever color you wish.  Name the new object “Node”.

Unity - Node

Next add a new script and call it “Node” (Node.cs).  For the script we want to define a basic bit of logic to have our node text always facing the camera and a public string id:

 

using UnityEngine;
using System.Collections;
 
namespace Topology {
 
    public class Node : MonoBehaviour {
 
        public string id;
        public TextMesh nodeText;
 
        void Update () {
            //node text always facing camera
            nodeText.transform.LookAt (Camera.main.transform);
        }
    }
 
}

Add a 3D Text object (GameObject -> Create Other -> 3D Text), move the 3D Text just below the cylinder and move the 3D Text as a child of the Node object.  Next drag the reference into the Node Text (Text Mesh) property.

Unity - Node Properties

Finally drag the “Node” from the Hierarchy window to the “Assets/Prefabs” folder.  Then remove the “Node” object from the Hierarchy view.

The Link

For the link, create an empty game object (GameObject -> Create Empty), name it “Link” and add a new script called “Link” (Link.cs).  Within the script we’ll expose a few public properties such as id, source_id, target_id, etc; and define a LineRenderer which will be used to draw the line connecting the nodes.

 

 

using UnityEngine;
using System.Collections;
 
namespace Topology {
 
    public class Link : MonoBehaviour {
 
        public string id;
        public Node source;
        public Node target;
        public string sourceId;
        public string targetId;
        public string status;
        public bool loaded = false;
 
        private LineRenderer lineRenderer;
 
        void Start () {
            lineRenderer = gameObject.AddComponent<LineRenderer>();
 
            //color link according to status
            Color c;
            if (status == "Up")
                c = Color.gray;
            else
                c = Color.red;
            c.a = 0.5f;
 
            //draw line
            lineRenderer.material = new Material (Shader.Find("Self-Illumin/Diffuse"));
            lineRenderer.material.SetColor ("_Color", c);
            lineRenderer.SetWidth(0.3f, 0.3f);
            lineRenderer.SetVertexCount(2);
            lineRenderer.SetPosition(0, new Vector3(0,0,0));
            lineRenderer.SetPosition(1, new Vector3(1,0,0));
        }
 
        void Update () {
            if(source && target && !loaded){
                //draw links as full duplex, half in each direction
                Vector3 m = (target.transform.position - source.transform.position)/2 + source.transform.position;
                lineRenderer.SetPosition(0, source.transform.position);
                lineRenderer.SetPosition(1, m);
 
                loaded = true;
            }
        }
    }
 
}

Being that this script was a bit larger, I’ll walk through what it does.  First, the public properties; the “id” is the link identifier (ie. “link_1”), the “source” will become the source node reference, the “target” will become the target node, the “sourceId” will hold the id of the source node until the “source” property is populated and the same goes for the “targetId”.  The “status” will hold a value of either “Up” or “Down” and will be used to color the LineRenderer.  Within the “Start()” function, we create a new LineRenderer and color it according to the “status” property value setting its initial line positions to vectors (0,0,0) and (1,0,0) respectively.  The “Update()” method waits until the “source” and “target” properties are populated then sets the LineRenderer end points.  The “loaded” property makes sure this only happens once.

Finally drag the “Link” object from the Hierarchy window to the “Assets/Prefabs” folder.  Then remove the “Link” object from the Hierarchy window.

Controller

Add a new empty game object (GameObject -> Create Empty) and name it “GameController”.  Create a new script called “GameController” (GameController.cs).  This script will be responsible for loading the layout file, creating Nodes and Links and handling general UI updates.

 

using UnityEngine;
using System.Collections;
using System.Xml;
using System.IO;
 
namespace Topology {
 
    public class GameController : MonoBehaviour {
 
        public Node nodePrefab;
        public Link linkPrefab;
 
        private Hashtable nodes;
        private Hashtable links;
        private GUIText statusText;
        private int nodeCount = 0;
        private int linkCount = 0;
        private GUIText nodeCountText;
        private GUIText linkCountText;
 
        //Method for loading the GraphML layout file
        private IEnumerator LoadLayout(){
 
            string sourceFile = Application.dataPath + "/Data/layout.xml";
            statusText.text = "Loading file: " + sourceFile;
 
            //determine which platform to load for
            string xml = null;
            if(Application.isWebPlayer){
                WWW www = new WWW (sourceFile);
                yield return www;
                xml = www.text;
            }
            else{
                StreamReader sr = new StreamReader(sourceFile);
                xml = sr.ReadToEnd();
                sr.Close();
            }
 
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xml);
 
            statusText.text = "Loading Topology";
 
            int scale = 2;
 
            XmlElement root = xmlDoc.FirstChild as XmlElement;
            for(int i=0; i<root.ChildNodes.Count; i++){
                XmlElement xmlGraph = root.ChildNodes[i] as XmlElement;
 
                for(int j=0; j<xmlGraph.ChildNodes.Count; j++){
                    XmlElement xmlNode = xmlGraph.ChildNodes[j] as XmlElement;
 
                    //create nodes
                    if(xmlNode.Name == "node"){
                        float x = float.Parse(xmlNode.Attributes["x"].Value)/scale;
                        float y = float.Parse (xmlNode.Attributes["y"].Value)/scale;
                        float z = float.Parse(xmlNode.Attributes["z"].Value)/scale;
 
                        Node nodeObject = Instantiate(nodePrefab, new Vector3(x,y,z), Quaternion.identity) as Node;
                        nodeObject.nodeText.text = xmlNode.Attributes["name"].Value;
 
                        nodeObject.id = xmlNode.Attributes["id"].Value;
                        nodes.Add(nodeObject.id, nodeObject);
 
                        statusText.text = "Loading Topology: Node " + nodeObject.id;
                        nodeCount++;
                        nodeCountText.text = "Nodes: " + nodeCount;
                    }
 
                    //create links
                    if(xmlNode.Name == "edge"){
                        Link linkObject = Instantiate(linkPrefab, new Vector3(0,0,0), Quaternion.identity) as Link;
                        linkObject.id = xmlNode.Attributes["id"].Value;
                        linkObject.sourceId = xmlNode.Attributes["source"].Value;
                        linkObject.targetId = xmlNode.Attributes["target"].Value;
                        linkObject.status = xmlNode.Attributes["status"].Value;
                        links.Add(linkObject.id, linkObject);
 
                        statusText.text = "Loading Topology: Edge " + linkObject.id;
                        linkCount++;
                        linkCountText.text = "Edges: " + linkCount;
                    }
 
                    //every 100 cycles return control to unity
                    if(j % 100 == 0)
                        yield return true;
                }
            }
 
            //map node edges
            MapLinkNodes();
 
            statusText.text = "";
        }
 
        //Method for mapping links to nodes
        private void MapLinkNodes(){
            foreach(string key in links.Keys){
                Link link = links[key] as Link;
                link.source = nodes[link.sourceId] as Node;
                link.target = nodes[link.targetId] as Node;
            }
        }
 
        void Start () {
            nodes = new Hashtable();
            links = new Hashtable();
 
            //initial stats
            nodeCountText = GameObject.Find("NodeCount").guiText;
            nodeCountText.text = "Nodes: 0";
            linkCountText = GameObject.Find("LinkCount").guiText;
            linkCountText.text = "Edges: 0";
            statusText = GameObject.Find("StatusText").guiText;
            statusText.text = "";
 
            StartCoroutine( LoadLayout() );
        }
 
    }
 
}

So now time for the walk through.  The 2 public properties nodePrefab and linkPrefab define the prefabs to be used when creating nodes and links.  The 2 Hastables, nodes and links define the collection which will hold the live instances of the prefabs “Node” and “Link”.  The GUIText objects will reference the loading status displayed on the screen, the count of nodes and the count of links.  The nodeCount and linkCount hold the actual numeric values for the count of nodes and links.  The “LoadLayout()” method handles the loading of the GraphML file, it loads the xml file into a string, populates an XmlDocument object and iterates over the <node/> and <edge/> elements.  When a <node /> is encountered we instantiate a Node object and populate its members and same goes for <edge /> with respect to populating a Link.  Once all the “Node” and “Link” objects have been created, the “source” and “target” members of the “Link” object are replaced with the living references of “Node” through the call to “MapLinkNodes()”.  The “Start()” method instantiates the nodes and links Hastables, resets the GUIText objects and starts a coroutine call to “LoadLayout()” which allows for returning control back to the Unity platform and preventing UI lock up.

Next, lets’ add the GUIText objects.  Create a GUIText object for LinkCount, NodeCount and StatusText at a minimum.  Next place the GUIText objects in the Game view to their relative positions.

Unity - Game View

Make these GUIText objects a child of GameController and add drag the references to the GameController script references.

Unity - GameController Hierarchy

Unity - GameController

 Camera

Click on the Main Camera object and add a new script “CameraController” (CameraController.cs).  In this script we’ll add basic controls for WASD, Control, Space and Wheel/Scroll controls.

 

using UnityEngine;
using System.Collections;
 
[AddComponentMenu("Camera-Control/Move ZeroG")]
public class CameraControlZeroG : MonoBehaviour {
 
    public float speed = 5f;
    public GUIText movementSpeed;
 
    private Vector3 move = new Vector3();
    private Vector3 cluster1 = new Vector3(1960, 1791, 2726);
    private Vector3 cluster2 = new Vector3(2042, 1579, 4254);
    private Vector3 cluster3 = new Vector3(2692, 81, 2526);
    private Vector3 cluster4 = new Vector3(531, 2317, 3776);
    private Vector3 cluster5 = new Vector3(-587, 2043, 2194);
 
    void Start(){
        //set to first cluster position
        transform.position = cluster1;
    }
 
    void Update () {
        move.x = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
        move.z = Input.GetAxis("Vertical") * speed * Time.deltaTime;
 
        move.y = 0;
        if (Input.GetKey ("space")) {
            move.y = speed * Time.deltaTime;
        }
 
        if (Input.GetKey ("left ctrl")) {
            move.y = -speed * Time.deltaTime;
        }
 
        //adjust speed with mouse wheel
        speed += Input.GetAxis("Mouse ScrollWheel");
        if (speed < 5)
            speed = 5;
 
        movementSpeed.text = "Move Speed: " + speed;
 
        move = transform.TransformDirection(move);
        transform.position += move;
 
        //set warp to cluster controls
        if(Input.GetKey("1")){
            transform.position = cluster1;
        }
 
        if(Input.GetKey("2")){
            transform.position = cluster2;
        }
 
        if(Input.GetKey("3")){
            transform.position = cluster3;
        }
 
        if(Input.GetKey("4")){
            transform.position = cluster4;
        }
 
        if(Input.GetKey("5")){
            transform.position = cluster5;
        }
    }
}

Walking through the above code, we define 2 public properties, speed which will hold the speed of movement through the zero gravity environment and GUIText movementSpeed which displays our current movement speed.  Next we define a Vector3 for move which will be used as the movement vector.  The following cluster[1-5] members define areas of the Topology which we’ll allow the camera to quickly jump to, feel free to replace these with your own coordinates.  The “Start()” function places our camera at the first cluster position (the largest cluster).  The “Update()” function first updates the move vector to move the camera in a direction.  The “space” check will change our “y” movement upward.  The “left ctrl” changes our “y” movement downward.  The “Mouse ScrollWheel” changes our overall movement speed, then we display the speed in the GUIText.  The final modifications are made to the camera’s transform position.  The GetKey() calls for 1 through 5 warp our camera to the cluster[1-5] positions.

We’ll also want to import a standard asset from the Unity “Character Controller” set.  Click Assets -> Import Package -> Character Controller, deselect everything, then online select “MouseLook.cs”.

Unity - Import Asset

 

This will import the “MouseLook.cs” script which we’ll need to add to the Main Camera (Add Component -> Camera-Control -> Mouse Look).

Putting It All Together

Lastly, build, deploy and check it out.  One thing to keep in mind is that the layout.xml in the Data directory (/Assets/Data/layout.xml) does not get deployed with the build.  You will need to manually copy this file out to your build/deploy directory.  Hopefully you won’t have any errors and will be able to view the 3D topology in all of its beauty and splendor.  Have fun!

Developing A 3D JavaScript Engine

Many developers have really pushed the limits of JavaScript development. Most advancements have been geared toward business applications, but what about the fun stuff. What about doing it just for the challenge or perhaps, for gaming.

So let’s develope a 3D JavaScript engine. Let’s start with the most basic structure we’re going to need, a vector.

var Vector = {
}//end Vector 
 
Vector.multiply = function(v1, v2){
    return {
        x: (v1.x*v2.x),
        y: (v1.y*v2.y),
        z: (v1.z*v2.z)
    }
}//end multiply() 
 
Vector.divide = function(v1, v2){
    return {
        x: (v1.x/v2.x),
        y: (v1.y/v2.y),
        z: (v1.z/v2.z)
    }
}//end divide() 
 
Vector.subtract = function(v1, v2){
    return{
        x: (v1.x-v2.x),
        y: (v1.y-v2.y),
        z: (v1.z-v2.z)
    }
}//end subtract() 
 
Vector.add = function(v1, v2){
    return{
        x: (v1.x+v2.x),
        y: (v1.y+v2.y),
        z: (v1.z+v2.z)
    }
}//end add()

As you can see, this is a simple vector object with a few added mothods for performing basic vector based math.

The next thing we are going to need now that we have a vector is a polygon class, which is pretty much just a collection of vectors.

//Polygon class
function Polygon(vectors){
    this.vectors = new Array();
    for(var i=0; i<vectors.length; i++)
        this.vectors[i] = vectors[i].clone();
}//end Polygon() 
 
//isVisible
Polygon.isVisible = function(xpoints, ypoints){ 
 
    var p1x = xpoints[1];
    var p1y = ypoints[1]; 
 
    var v1x = xpoints[2]-p1x;
    var v1y = ypoints[2]-p1y; 
 
    var v2x = xpoints[0]-p1x;
    var v2y = ypoints[0]-p1y; 
 
    a = v1x*v2y-v2x*v1y; 
 
    return a<0;
}//end isVisible()

This is truly a simple polygon class with only a single method for determining if the face is visible by way of counter clockwise vector rendering.

Now we are going to need to paint faces on our polygons, so let’s create a face class.

//Face class
function Face(color){
    this.points = new Array();
    this.color = color;
    if(!this.color)
        this.color = "#AFAFAF"; 
 
    for(var i=1; i<Face.arguments.length; i++)
        this.points[i-1] = Face.arguments[i];
}//end Face()

Well that was difficult, wasn’t it? This class contains an array of points, the face color (with a default color) and an open ended argument section that allows for an unlimited amount of points to be added.

Well now that we have the face, we need a model object. This basically pulls all the previous parts together in a renderable form.

//Model class
function Model(polygon, faces, vector){
    this.polygon = polygon.clone();
    this.faces = faces;
    this.vector = vector; 
 
    //rotate
    this.rotate = function(vec){ 
 
        for(var i=0; i<this.polygon.vectors.length; i++){
            var tempVector = this.polygon.vectors[i]; 
 
            var cos = Math.cos(vec.z);
            var sin = Math.sin(vec.z); 
 
            var zx = (tempVector.x*cos) - (tempVector.y*sin) - tempVector.x;
            var zy = (tempVector.x*sin) + (tempVector.y*cos) - tempVector.y; 
 
            var dist = tempVector.x + zx;
            cos = Math.cos(vec.y);
            sin = Math.sin(vec.y); 
 
            var yx = (dist*cos) - (tempVector.z*sin) - dist;
            var yz = (dist*sin) + (tempVector.z*cos) - tempVector.z; 
 
            dist = tempVector.y + zy;
            var dist2 = tempVector.z + yz;
            cos = Math.cos(vec.x);
            sin = Math.sin(vec.x); 
 
            var xy = (dist*cos) - (dist2*sin) - dist;
            var xz = (dist*sin) + (dist2*cos) - (tempVector.z + yz); 
 
            this.polygon.vectors[i].x += (yx+zx);
            this.polygon.vectors[i].y += (zy+xy);
            this.polygon.vectors[i].z += (xz+yz);
        }//end for 
 
    }//end rotate() 
 
}//end Model()

So, the model object has a polygon object, a list of faces, and a position vector. No too difficult, I also added a rotate method just for fun. One thing that might stand out a little if you look closely is the clone method of the polygon object, don’t worry about this just yet, I’ll explain that one later.

Now we are ready to render an object in space, or are we. We need a way to view the object, so let’s create a camera object.

//Camera class
function Camera(vector, angle, focalDistance){
    this.vector = vector;
    this.angle = angle;
    this.focalDistance = focalDistance;
    if(!this.focalDistance)
        this.focalDistance = 1;
}//end Camera()

Ok, so now we have a camera object that contains a vector or position in space, the angle of the camera, and a focaldistance member (defaulted to 1).

So now comes the slightly difficult part, we’re going to need a few predefined matrixes for the world and a scaling matrix.

var WCMatrix = {
    x1: 1.0, x2: 0.0, x3: 0.0,
    y1: 0.0, y2: 1.0, y3: 0.0,
    zx: 0.0, zy: 0.0, zz: 1.0
} 
 
var ScaleMatrix = {
    x1: 1.0, x2: 0.0, x3: 0.0,
    y1: 0.0, y2: 1.0, y3: 0.0,
    zx: 0.0, zy: 0.0, zz: 1.0
}

So, the WCMatrix or world matrix just simply defines the world coordinate system. The ScaleMatrix defines a matrix to scale objects by.

Now to discuss the clone method of the prototype object. This method simply creates a clone of the prototype object, which is needed to create a copy instead of passing a reference. Otherwise we’d be passing the reference to a single polygon object around, which we don’t want. We want to be able to define a polygon and reuse it’s characteristics.

Object.prototype.clone = function(){
    var b = new Object();
    for(i in this)
        b[i] = this[i];
    return b;
}

Another method we will add is a generic clone function, responsible for…you guessed it, cloning simple objects.

function Clone(obj){
    for(i in obj)
        this[i] = obj[i]; 
 
    return this;
}//end Clone()

Now we’re heading down the final road and more than likely the most difficult to understand. In order for us to render objects on a 2D plane from 3D coordinates, we need a simple rendering engine. So that’s what we’ll make.

function Engine(){ 
 
    var models = new Array();
    var g = null;
    var _camera = new Camera( {x:0,y:0,z:300}, {x:0,y:0,z:0} ); 
 
    this.wireframe = true;
    this.filled = true;
    var fps = 0;
    var trackFPS = false; 
 
    this.addModel = function(model){
        models.push(model);
    }//end addModel() 
 
    this.getFPS = function(){
        return fps;
    }//end getFPS() 
 
    this.enableFPS = function(b){
        trackFPS = b;
    }//end enableFPS() 
 
    this.setGraphics = function(graphics){
        g = graphics;
    }//end setGraphics() 
 
    this.setCamera = function(cam){
        _camera = camera;
    }//end setCamera() 
 
    var screenXA = new Array();
    var screenYA = new Array(); 
 
    var st = 0;
    var frame = 0; 
 
    var compareModels = function(m1, m2){
        return m2.vector.z-m1.vector.z;
    }//end compareModels() 
 
    this.render = function(){
        if(trackFPS && frame == 0)
            st = new Date().getTime(); 
 
        var wfX = new Array();
        var wfY = new Array();
        var wfI = 0; 
 
        var scale = 0.003; 
 
        //sort models
        models.sort(compareModels); 
 
        for(var j=0; j<models.length; j++){
            var model = models[j]; 
 
            for(var i=0; i<model.polygon.vectors.length; i++){     
 
                //transform to World Coordinates
                var vec = Vector.transformWCS(model.polygon.vectors[i]); 
 
                //move vector to model's position vector
                vec = Vector.add(model.vector, vec); 
 
                vec = Vector.transformPerspective(vec, _camera); 
 
                var x = vec.x;
                var y = vec.y; 
 
                if(g.toString() == "SVG"){
                    x += 100;
                    y += 100;
                }//end if 
 
                screenXA[i] = x;
                screenYA[i] = y;
            }//end for 
 
            for(var i=0; i<model.faces.length; i++){
                var face = model.faces[i]; 
 
                var paX = new Array();
                var paY = new Array(); 
 
                for(var n=0; n<face.points.length; n++){
                    paX[n] = screenXA[face.points[n]];
                    paY[n] = screenYA[face.points[n]];
                }//end for 
 
                if(Polygon.isVisible(paX, paY) && this.filled){
                    var color = face.color; 
 
                    g.setColor(color);
                    g.fillPolygon(paX, paY);
                }//end if 
 
                if(this.wireframe && this.filled){
                    wfX[wfI] = paX;
                    wfY[wfI++] = paY;
                }//end if 
 
                if(this.wireframe && !this.filled){
                    g.setColor("#000000");
                    g.drawPolygon(paX, paY);
                }//end if 
 
            }//end for 
 
        }//end for 
 
        if(this.wireframe && this.filled){
            g.setColor("#000000");
            for(var i=0; i<wfI; i++){
                g.drawPolygon(wfX[i], wfY[i]);
            }//end for
        }//end if 
 
        g.paint(); 
 
        //fps tracking
        if(trackFPS){
            frame++;
            var et = new Date().getTime();
            var timediff = (et-st)/1000;
            if(timediff >= 1){
                fps = frame;
                frame = 0;
            }//end if
        }//end if 
 
    }//end render() 
 
}//end Engine()

Ok, I know, this one’s a little complex. But I’ll break it down piece by piece so that it will hopefully be a little simpler to understand.

First let’s start with the basic properties. The engine has a collection of models to render, a graphics object called g, and a default camera.

Next is the wireframe and filled properties for rendering a wireframe of the object, or that actual filled object.

After that we have a fps or frames per second counter and a way to turn that tracking on or off.

The first method is pretty self explanitory, it adds a model to the model array.

The next method returns the current fps, just in case you want to display it.

The enableFPS method determines whether or not to track the fps.

The setGraphics method is used to set a graphics renderer which we will discuss a little later.

The setCamera method, well I supposed it sets the camera.

Now for the nitty gritty. First we create an array of x and y coordinates for rendering the 3D coordinates once they have been translated to 2D. The st and frame variables are used in tracking fps, not too important really. The compareModels method is a poor mans way of determining which model should be rendered in front of the other one. Then finally the guts of the whole operation, the render method.

So the first real operation here is sorting the faces. We sort so we know what to display and in what order.

Next is the model loop, we loop through each model and transform it’s polygon coordinates to world coordinates.

Next we move that model to it’s position vector, where it is supposed to sit in the world.

After that, we transform the coordinates to the camera’s perspective, and we remember the x and y for rendering later. Now I know the SVG check is like, what the hell is that, but I’ll explain later. Let’s just say for now Mozilla and IE don’t play well when trying to render VML or SVG graphics.

Once we’re done with the models coordinates we need to render the models faces. So we iterate each of the model’s faces, create a new x and y array and transform the coordinates to the relative position of the model’s position in 3D space.

After that we check to see if the polygon is visible and filled, then we paint that face. If it’s a wireframe, we just draw a line connecting the 2 points and don’t worry if the face is visible. The reason we check the polygon face for visibility on solid objects is that we don’t want to paint a face that is facing away from the camera.

The very next thing we do is actually render the wireframe, and I do this later just in case you wanted to see it on top of the solid faces.

The last part simple does the fps tracking.

Now that our engine is complete, let’s put it all together. When it’s all said and done, you should have something that looks like the following.

//3d.js 
 
var WCMatrix = {
    x1: 1.0, x2: 0.0, x3: 0.0,
    y1: 0.0, y2: 1.0, y3: 0.0,
    zx: 0.0, zy: 0.0, zz: 1.0
} 
 
var ScaleMatrix = {
    x1: 1.0, x2: 0.0, x3: 0.0,
    y1: 0.0, y2: 1.0, y3: 0.0,
    zx: 0.0, zy: 0.0, zz: 1.0
} 
 
Object.prototype.clone = function(){
    var b = new Object();
    for(i in this)
        b[i] = this[i];
    return b;
} 
 
function Clone(obj){
    for(i in obj)
        this[i] = obj[i]; 
 
    return this;
}//end Clone() 
 
var Vector = {
}//end Vector 
 
Vector.multiply = function(v1, v2){
    return {
        x: (v1.x*v2.x),
        y: (v1.y*v2.y),
        z: (v1.z*v2.z)
    }
}//end multiply() 
 
Vector.divide = function(v1, v2){
    return {
        x: (v1.x/v2.x),
        y: (v1.y/v2.y),
        z: (v1.z/v2.z)
    }
}//end divide() 
 
Vector.subtract = function(v1, v2){
    return{
        x: (v1.x-v2.x),
        y: (v1.y-v2.y),
        z: (v1.z-v2.z)
    }
}//end subtract() 
 
Vector.add = function(v1, v2){
    return{
        x: (v1.x+v2.x),
        y: (v1.y+v2.y),
        z: (v1.z+v2.z)
    }
}//end add() 
 
Vector.transformWCS = function(v1){
    return{
        x: (v1.x * WCMatrix.x1 + v1.y * WCMatrix.x2 + v1.z * WCMatrix.x3),
        y: (v1.x * WCMatrix.y1 + v1.y * WCMatrix.y2 + v1.z * WCMatrix.y3),
        z: (v1.x * WCMatrix.z1 + v1.y * WCMatrix.z2 + v1.z * WCMatrix.z3)
    }
}//end transformWCS() 
 
Vector.transformPerspective = function(vec, cam){
    //if(!Vertex->Aligned.z)
    //Vertex->Aligned.z=1;
    //Vertex->Screen.x = FOCAL_DISTANCE * Vertex->Aligned.x / Vertex->Aligned.z + XOrigin;
    //Vertex->Screen.y = FOCAL_DISTANCE * Vertex->Aligned.y / Vertex->Aligned.z + YOrigin; 
 
    var z = cam.z;
    if(!z)
        z = 1; 
 
    return {
        x: (cam.focalDistance * vec.x / z),
        y: (cam.focalDistance * vec.y / z),
        z: z
    }
} 
 
//Polygon class
function Polygon(vectors){
    this.vectors = new Array();
    for(var i=0; i<vectors.length; i++)
        this.vectors[i] = vectors[i].clone();
}//end Polygon() 
 
//isVisible
Polygon.isVisible = function(xpoints, ypoints){ 
 
    var p1x = xpoints[1];
    var p1y = ypoints[1]; 
 
    var v1x = xpoints[2]-p1x;
    var v1y = ypoints[2]-p1y; 
 
    var v2x = xpoints[0]-p1x;
    var v2y = ypoints[0]-p1y; 
 
    a = v1x*v2y-v2x*v1y; 
 
    return a<0;
}//end isVisible() 
 
//Face class
function Face(color){
    this.points = new Array();
    this.color = color;
    if(!this.color)
        this.color = "#AFAFAF"; 
 
    for(var i=1; i<Face.arguments.length; i++)
        this.points[i-1] = Face.arguments[i];
}//end Face() 
 
//Model class
function Model(polygon, faces, vector){
    this.polygon = polygon.clone();
    this.faces = faces;
    this.vector = vector; 
 
    //rotate
    this.rotate = function(vec){ 
 
        for(var i=0; i<this.polygon.vectors.length; i++){
            var tempVector = this.polygon.vectors[i]; 
 
            var cos = Math.cos(vec.z);
            var sin = Math.sin(vec.z); 
 
            var zx = (tempVector.x*cos) - (tempVector.y*sin) - tempVector.x;
            var zy = (tempVector.x*sin) + (tempVector.y*cos) - tempVector.y; 
 
            var dist = tempVector.x + zx;
            cos = Math.cos(vec.y);
            sin = Math.sin(vec.y); 
 
            var yx = (dist*cos) - (tempVector.z*sin) - dist;
            var yz = (dist*sin) + (tempVector.z*cos) - tempVector.z; 
 
            dist = tempVector.y + zy;
            var dist2 = tempVector.z + yz;
            cos = Math.cos(vec.x);
            sin = Math.sin(vec.x); 
 
            var xy = (dist*cos) - (dist2*sin) - dist;
            var xz = (dist*sin) + (dist2*cos) - (tempVector.z + yz); 
 
            this.polygon.vectors[i].x += (yx+zx);
            this.polygon.vectors[i].y += (zy+xy);
            this.polygon.vectors[i].z += (xz+yz);
        }//end for 
 
    }//end rotate() 
 
}//end Model() 
 
//Camera class
function Camera(vector, angle, focalDistance){
    this.vector = vector;
    this.angle = angle;
    this.focalDistance = focalDistance;
    if(!this.focalDistance)
        this.focalDistance = 1;
}//end Camera() 
 
function Engine(){ 
 
    var models = new Array();
    var g = null;
    var _camera = new Camera( {x:0,y:0,z:300}, {x:0,y:0,z:0} ); 
 
    this.wireframe = true;
    this.filled = true;
    var fps = 0;
    var trackFPS = false; 
 
    this.addModel = function(model){
        models.push(model);
    }//end addModel() 
 
    this.getFPS = function(){
        return fps;
    }//end getFPS() 
 
    this.enableFPS = function(b){
        trackFPS = b;
    }//end enableFPS() 
 
    this.setGraphics = function(graphics){
        g = graphics;
    }//end setGraphics() 
 
    this.setCamera = function(cam){
        _camera = camera;
    }//end setCamera() 
 
    var screenXA = new Array();
    var screenYA = new Array(); 
 
    var st = 0;
    var frame = 0; 
 
    var compareModels = function(m1, m2){
        return m2.vector.z-m1.vector.z;
    }//end compareModels() 
 
    this.render = function(){
        if(trackFPS && frame == 0)
            st = new Date().getTime(); 
 
        var wfX = new Array();
        var wfY = new Array();
        var wfI = 0; 
 
        var scale = 0.003; 
 
        //sort models
        models.sort(compareModels); 
 
        for(var j=0; j<models.length; j++){
            var model = models[j]; 
 
            for(var i=0; i<model.polygon.vectors.length; i++){ 
 
                //transform to World Coordinates
                var vec = Vector.transformWCS(model.polygon.vectors[i]); 
 
                //move vector to model's position vector
                vec = Vector.add(model.vector, vec); 
 
                vec = Vector.transformPerspective(vec, _camera); 
 
                var x = vec.x;
                var y = vec.y; 
 
                if(g.toString() == "SVG"){
                    x += 100;
                    y += 100;
                }//end if 
 
                screenXA[i] = x;
                screenYA[i] = y;
            }//end for 
 
            for(var i=0; i<model.faces.length; i++){
                var face = model.faces[i]; 
 
                var paX = new Array();
                var paY = new Array(); 
 
                for(var n=0; n<face.points.length; n++){
                    paX[n] = screenXA[face.points[n]];
                    paY[n] = screenYA[face.points[n]];
                }//end for 
 
                if(Polygon.isVisible(paX, paY) && this.filled){
                    var color = face.color; 
 
                    g.setColor(color);
                    g.fillPolygon(paX, paY);
                }//end if 
 
                if(this.wireframe && this.filled){
                    wfX[wfI] = paX;
                    wfY[wfI++] = paY;
                }//end if 
 
                if(this.wireframe && !this.filled){
                    g.setColor("#000000");
                    g.drawPolygon(paX, paY);
                }//end if 
 
            }//end for 
 
        }//end for 
 
        if(this.wireframe && this.filled){
            g.setColor("#000000");
            for(var i=0; i<wfI; i++){
                g.drawPolygon(wfX[i], wfY[i]);
            }//end for
        }//end if 
 
        g.paint(); 
 
        //fps tracking
        if(trackFPS){
            frame++;
            var et = new Date().getTime();
            var timediff = (et-st)/1000;
            if(timediff >= 1){
                fps = frame;
                frame = 0;
            }//end if
        }//end if 
 
    }//end render() 
 
}//end Engine()

Next, let’s actually use it.

To make this cross browser for Mozilla and IE, I create 2 pages. One called 3d.xhtml for Mozilla and one called 3d.html for IE. The reason being that Mozilla needs an XHTML compliant page to render SVG, IE doesn’t.

So now lets do the check and redirect.

var g = null; 
 
if(document.all){
    g = new VML("canvas");
    window.status = "Rendering in VML";
    document.title = window.status;
}
else{
    if(window.XML && document.location.href.indexOf("xhtml") < 0)
        document.location = "3d.xhtml";
    else if(window.XML){
        g = new SVG("canvas");
        window.status = "Rendering in SVG";
        document.title = window.status;
    }
    else{
        g = new jsGraphics("canvas");
        window.status = "Rendering in Zorn";
        document.title = window.status;
    }//end if
}//end if

What I’ve actually done above is create my graphics object for use in my engine and check what browser we are using and that we are on the right page for rendering. If it’s IE then we create a new VML renderer. If it’s Mozilla we redirect to 3d.xhtml and using the exact same code, create a new SVG renderer. If it’s neither of those, I create a graphics object using the Zorn Graphics Library available at http://www.walterzorn.com . The Zorn library is the slowest but will work no matter what.

Next let’s create our own camera and some 3d objects.

var camera = new Camera( {x:300,y:0,z:0}, {x:0.1,y:0,z:0} ); 
 
var i=80;
var rectVectors = new Array(
    {x:-i,y:i,z:i*2},
    {x:-i,y:i,z:-i*2},
    {x:i,y:i,z:-i*2},
    {x:i,y:i,z:i*2},
    {x:-i,y:-i,z:i*2},
    {x:-i,y:-i,z:-i*2},
    {x:i,y:-i,z:-i*2},
    {x:i,y:-i,z:i*2}
); 
 
var squareVectors = new Array(
    {x:-i,y:i,z:i},
    {x:-i,y:i,z:-i},
    {x:i,y:i,z:-i},
    {x:i,y:i,z:i},
    {x:-i,y:-i,z:i},
    {x:-i,y:-i,z:-i},
    {x:i,y:-i,z:-i},
    {x:i,y:-i,z:i}
); 
 
var triVectors = new Array(
    {x:0,y:0,z:-i},
    {x:-i,y:-i,z:i},
    {x:-i,y:i,z:i},
    {x:i,y:i,z:i},
    {x:i,y:-i,z:i}
); 
 
var faces = new Array(
    new Face("#0000FF", 0,3,2,1,0),
    new Face("#0000EE", 0,1,5,4,0),
    new Face("#0000DD", 3,0,4,7,3),
    new Face("#0000CC", 2,3,7,6,2),
    new Face("#0000BB", 1,2,6,5,1),
    new Face("#0000AA", 4,5,6,7,4)
); 
 
var faces2 = new Array(
    new Face("#00FF00", 0,3,2,1,0),
    new Face("#00EE00", 0,1,5,4,0),
    new Face("#00DD00", 3,0,4,7,3),
    new Face("#00CC00", 2,3,7,6,2),
    new Face("#00BB00", 1,2,6,5,1),
    new Face("#00AA00", 4,5,6,7,4)
); 
 
var triFaces = new Array(
    new Face("#FF0000", 0,1,2,0),
    new Face("#EE0000", 0,2,3,0),
    new Face("#DD0000", 0,3,4,0),
    new Face("#CC0000", 0,4,1,0),
    new Face("#BB0000", 1,4,3,2,1)
);

So all we’ve done here is create a camera to use with our engine, a set of object vectors for a square, triange, and rectangle and create some corresponding faces with colors.

Now lets create the models.

var cube = new Model(new Polygon(squareVectors), faces, {x:0,y:300,z:400});
var cube2 = new Model(new Polygon(rectVectors), faces2, {x:0,y:0,z:200});
var tri = new Model(new Polygon(triVectors), triFaces, {x:0,y:100,z:100});

Simple enough right, we create a new cube model from the squre vector, add its faces, and create the model’s vertex point, or where it will sit in 3D space. We do this for all three objects respectively.

Next I create an update method that simply will rotate the objects and fire update again later, basically an update thread.

function Update(){ 
 
    cube.rotate( {x:0.05,y:0.05,z:0.05} );
    cube2.rotate( {x:0.0,y:0.05,z:0.05} );
    tri.rotate( {x:0.05, y:0.05, z:0.0} ); 
 
    setTimeout("Update()", 1);
}//end Update()

All that’s being done here is a simple rotation with the rotation vector being passed in. For example, the cube.rotate call is going to rotate the object along the x axis +0, the y axis +.05 and the z axis +.05 for every update call.

Now let’s initialize the engine.

var engine = new Engine();
engine.setCamera(camera);
engine.setGraphics(g);
engine.wireframe = false;
engine.enableFPS(true); 
 
engine.addModel(cube);
engine.addModel(cube2);
engine.addModel(tri);

We create a new engine object, set the camera, set our graphics renderer, add our models, and now we’re ready to start rendering 3D objects.

So let’s create the render method.

function Render(){
    document.getElementById("debug").innerHTML = engine.getFPS() + " Frames Per Second";
    document.getElementById("canvas").innerHTML = ""; 
 
    g.clear();
    engine.render(); 
 
    setTimeout("Render()", 1);
}//end Render()

This is basically the rendering thread, it clears the graphics renderer, then paints, then repeats.

Last but not least the begin method and a few user input methods.

function Begin(){
        Render();
        Update();
}//end Begin() 
 
setTimeout("Begin()", 1000); 
 
function setFilled(b){
    engine.filled = b;
}//end setFilled() 
 
function setWireFrame(b){
    engine.wireframe = b;
}//end setWireFrame()

The begin method launches both the rendering thread and the update thread. The setTimeout method calls the begin method 1 second after the page is loaded, just to make sure everything has had time to get parsed. The setFilled sets the filled property of the engine and the setWireFrame sets the wireframe property of the engine.

The last thing to do is to create a renderer for SVG and VML. I created each one modeled after the Zorn renderer.

These three files are available for download below:
VML Renderer http://www.godlikemouse.com/script/VML.js
SVG Renderer http://www.godlikemouse.com/script/SVG.js
Zorn Renderer http://www.godlikemouse.com/script/wz_jsgraphics.js

When you start looking through these files, you’ll see that all I’ve done is make the SVG and VML drawing accessible by the exact same calls as would be made to the Zorn library.

Now the entire IE document.

<html xmlns:v="urn:schemas-microsoft-com:vml"> 
 
<head>
<script language="javascript" src="http://www.godlikemouse.com/script/3d.js"></script>
<script language="javascript" src="http://www.godlikemouse.com/script/VML.js"></script>
<script language="javascript" src="http://www.godlikemouse.com/script/wz_jsgraphics.js"></script>
</head> 
 
<style> 
 
v:* {
behavior: url(#default#VML);
} 
 
</style> 
 
<body> 
 
<div id="canvas" style="position:absolute;left:100px;top:100px;width:400px;height:400px;"></div> 
 
<div style="position:absolute;top:500px;">
<input type="checkbox" onclick="setWireFrame(this.checked);"> Show Wireframes
<input type="checkbox" onclick="setFilled(this.checked);" checked> Show Filled
</div> 
 
<div style="position:absolute;top:600px;" id="debug"></div> 
 
</body> 
 
<script language="javascript" src="script/3d_demo.js">
</script> 
 
</html>

As you can see the set up a VML namespace needed in IE to render VML, then I include the engine code 3d.js, the VML renderer VML.js and the Zorn library wz_jsgraphics.js . Next I setup the style to all for the behavior of VML to be applied. Next I create a div section to use as a canvas to draw on, then I setup some user fields allow them to turn on or off the wireframe and filled properties of the engine. Then I have a debug div form displaying fps, followed by the demonstration 3d_demo.js where we actually created some 3D objects and initialize the engine.

Now for the Mozilla document.

<html xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/1999/xhtml"> 
 
<head>
<script language="javascript" src="http://www.godlikemouse.com/script/3d.js"></script>
<script language="javascript" src="http://www.godlikemouse.com/script/SVG.js"></script>
</head> 
 
<body style="margin:0;"> 
 
<div id="canvas" style="width:400px;height:400px;"/> 
 
<div style="position:absolute;top:500px;">
<input type="checkbox" onclick="setWireFrame(this.checked);" /> Show Wireframes
<input type="checkbox" onclick="setFilled(this.checked);" checked="true" /> Show Filled
</div> 
 
<div style="position:absolute;top:600px;" id="debug"></div> 
 
</body> 
 
<script language="javascript" src="script/3d_demo.js">
</script> 
 
</html>

This is very similar to the IE document, except that now we create an SVG namespace necessary for using SVG in Mozilla, and the XHTML namespace. Next I include the 3d.js engine file and the SVG.js renderer. Just like the IE document I create a div section as a canvas to draw on, followed by some user fields to set filled and wireframe on and off. Then the debug div for fps and include of the 3d_demo.js file just like the IE document.

Now we’re done, check out the entire demo in all its glory at:


http://www.godlikemouse.com/3d.html

Thanks for stopping by, and I hope I explained everything clear enough.