Adventures in the 3D World using CSS and JavaScript

January 17, 2012

EDIT: Google Plus doesn't allow you to embed public videos in external sites. Added links to the posts instead.

In case you haven't heard, as i expect none of you to do, i've been keeping myself pretty busy by working on an easy-to-use, minimalistic CSS-Powered 3D Framework called "Tracy" (Three-Dee + CSS + Crazy, don't tell me it doesn't make perfect sense).

I had experimented with 3D CSS before, but i really wanted to see if i'd be able to make a Wolfestein clone using this stuff as all the elements to make one were certainly there.

As i was just testing the feasibility of this approach, my first attempt was quite naive; I decided to manually hand-code and position all the elements in a stylesheet, put them inside a larger "container" and then grab a reference to that container via javascript to move it around.
After a couple of hours (i spent most of the time modelling the scene) i had a working demo with decent performance, you can check out a video by clicking here.

The first problem that i found was that using big surfaces would severely impact the overall performance of the game. This means that if you need to cover a vast area with a big surface it's preferable to use many "small" 2D planes. Although a bit unintuitive, another thing that i found was that you should implement occlusion culling yourself, even at the expense of executing many reflow events (don't just "hide" them, remove those elements completely from the DOM tree).

While i was still figuring out what worked and what didn't, i came across Keith Clark's impressive FPS demo using a similar approach to what i was doing.

Clark's project plus my own attempts were inspirational enough to convince myself that this approach showed some promise and so it was worth investing a bit more of my time, so i decided to start building a framework around it.

Unlike "conventional" and "real" 3D frameworks where you can define a geometry in 3D space by specifying the coordinates of all the vertices, i needed a way to turn 6 2D planes into a cube. I decided to create a new class called "Group" that would serve as a container to other elements (then i could just move the group around, instead of having to move around all the elements independently). Then i just created a "Rect3D" class extended the "Group".

I did the same for cylinders, too. You can see a video of the current state of Tracy at the time below by clicking here.

However, when i wanted to create a cone instead of a cylinder i ran into a bit of a problem:

Rect and Triangle

How do you turn a rectangle into a triangle using CSS?

Short answer is: You can't. I should also mention that at the time i was using DIVs to make the 2D Planes.

This is when i decided to think outside the box for a bit, and came up with the idea of using CANVAS elements instead of DIVs to make the shapes. I decided to create an object called "Custom2DPolygon" that would allow me to specify an array containing X, Y coordinates. Then, depending on the coordinates, i'd figure out the size of the plane. The property "needsRenderUpdate" would also make sure that the contents are rendered on demand, instead of all the time.

if (this.needsRenderUpdate) {
    var cnv = document.createElement('canvas'),
    	c = cnv.getContext('2d');

    // Calculate the width/height of the canvas
    var minX = 0,
        minY = 0,
        maxX = 0,
        maxY = 0;
    
	// Some coordinates may be negative
    for (var i = 0; i < this.vertices.length; i += 2) {
        minX = (this.vertices[i] < minX) ? this.vertices[i] : minX;
        maxX = (this.vertices[i] > maxX) ? this.vertices[i] : maxX;
        minY = (this.vertices[i + 1] < minY) ? this.vertices[i + 1] : minY;
        maxY = (this.vertices[i + 1] > maxY) ? this.vertices[i + 1] : maxY;
    }
    
    cnv.width = (maxX + this.borderWidth * 2) + (minX * -1);
    cnv.height = (maxY + this.borderWidth * 2) + (minY * -1);

    this.width = cnv.width;
    this.height = cnv.height;

    c.fillStyle = this.backgroundColor.toString();

    if (this.borderWidth > 0) {
        c.lineWidth = this.borderWidth;
        c.strokeStyle = this.borderColor.toString();
    }

    c.translate(((minX < (this.borderWidth * -1)) ? (minX * -1) + this.borderWidth : minX), 
                ((minY < (this.borderWidth * -1)) ? (minY * -1) + this.borderWidth : minY));
    
    c.beginPath();

    for (var i = 0; i < this.vertices.length; i += 2) {
        c.lineTo(this.vertices[i],
                 this.vertices[i + 1]);
    }

    c.closePath(); 
    c.fill();

    if (this.borderWidth > 0) {
        c.stroke(); 
    }

    this.texture = cnv.toDataURL();

    this.needsRenderUpdate = false;
}

Another optimization tip that i learned the hard way back when i was using DOM to make games was that you should avoid using document.createElement() and instead handle absolutely everything as strings.

Shortly thereafter, this is what i had. You'll also notice that this approach also works on iOS, which is cool. 

Also, in order to find out how the framework performed in the "real world" i decided to start developing a game using it. The still-unnamed and work-in-progress project was going to be either a multiplayer-enabled WWI dogfighting game or an arcade-ish game where you have to go through several waypoints located in the air. It all depended on how Tracy performed.

You can try the current state of the game here (use Safari or Chrome, refresh a couple of times if it doesn't work)

I could have chosen a much simpler project, like a BlockOut clone (a 3D tetris), which i now know it would have worked just fine, but i really wanted to take this thing to the limit. I should also mention that i also developed the foundations of a very simple minecraft-like game and that worked too.

I started the modelling process (i also had to add support for textures/texture mapping) and a couple of days later this is what i had:

Fokker Dr.1

 

Unfortunately, i was using Safari to test Tracy. Turns out Safari is simply the best browser to render 3D stuff using CSS, all the others (and even Safari as well) have some sort of clipping issue that cuts surfaces by half. I'm guessing that it's some sort of rounding bug as it happens consistently with very specific coordinates but i'm not completely sure.

I'll try to solve many of these clipping problems by simplyfing the model of the plane and let you know how it works out. Stay tuned and thanks for reading.