How I Developed Tracy: A CSS-Powered 3D Rendering Framework

by Andres Pagella

Back in a hot day of December in 2011 during my holidays, I had some free time so I started to do some experiments with some features that, although I had tried previously when they were implemented in Firefox Nightly, I had never studied seriously. One of such features were 3D Transformations in CSS.

My first experiment was (obviously, I must say) to make a cube, which turned out to be extremely easy; you just need 6 divs with the same width and height, and then you need to set the position for each one of them in 3D space using the transform property. However, the first problem I encountered was how to make that cube “rotate” in 3D space without having to manually set new positions and rotations for each element. After reading the spec for a couple of minutes, I found the solution: To place the six elements within an additional element, and then use transform-style: preserve-3d. This simple trick would allow me to rotate a single element (the “container”) and it would automatically affect all the other elements inside it.

While Firefox and Chrome performed extremely well with this remarkably simple prototype, I was extremely surprised at how well this example performed on Safari and, specially, Safari for iOS on my iPad 1 with iOS 4.3.

The fact that it performed so well on an iPad made me wonder… How far would I be able to take this idea? Would I be able to use it to make other things than nice looking “gizmos”? Would I be able to make something decent to use as a replacement of WebGL on mobile devices?

My second prototype was a simple “FPS Level”. In essence, it worked almost exactly like the cube example that I had made before, but this time I added some textures plus a bit of JS code to be able to control the “scene”. Unlike WebGL, there is no concept of “cameras” in CSS, so I had to figure out a way to simulate that. After a bit of experimentation I settled with what I considered to satisfy my requirements: To place the entire “scene” inside an additional element (that itself had transform-style: preserve-3d), and move that element around. Bringing it closer to the viewport while reducing the default perspective attribute would be a “zoom in”, and moving it far away while increasing the perspective attribute somewhat worked as a “zoom out”. The idea turned out to be visually convincing, and the only thing I had to adjust was the transformation-origin property – doing this would allow me to rotate the scene and it’d give the impression that the camera is being rotated.


The FPS Level

However, at this point I still had to manually code the position, rotation, width, height and color or texture of every single element I added to the scene, which turned out to be extremely time consuming and sometimes frustrating. Clearly, this approach would not be able to scale well to more complex scenes. I needed “something” to make the development of these things much easier.

And thus, “Tracy” was born.

The reason why it’s called Tracy is because, at the time, it looked like a fantastic idea to combine the words “Three-D, CSS and Crazy“, ergo, I called it Tracy. Now I know better.

At first, Tracy only had a handful of objects; A Group object (used to contain other objects), a 2D Plane, a 3D Rect (6 2D planes plus a Group) plus some utility objects such as an Entity object as well as a 2D and a 3D Tuple.

I simply kept experimenting, and as I progressed I came to realize that I’d need additional objects, such as a Viewport (that at this point I should rename to “Camera”), a Core structure, an RGBA and Texture object to define colors and background textures and so on. By January 4th, 2011 I also had cylinders and cones. Both of these objects would allow me to define how many “sides” I’d want it to have, such as 5 (as a minimum) or several more. The more sides you’d add, the more defined (and rounded) the object would be. A video of the current progress at the time can be seen here. Some days later, I had it up and running on iOS as well.


A Cylinder with 200 sides

However, at this point I started to experiment some serious performance problems, even with simple scenes. Keep in mind that, at the time, Safari 5 was still the most performant browser (by a big margin) for these sort of things. After profiling and examining all the different performance bottlenecks, I decided to branch the code and started working on a different way of applying the transformations. At first, I was using the style tag to add the (massive) string with all the CSS transformations for each element, which meant that I was triggering many reflows and repaints. Additionally, I wasn’t caching stuff or listening for changes, which meant that I always replaced all the styles of all the elements in my scene many times per second.

While some people consider that “premature optimisation is the root of all evil”, I most certainly had to add some sort of optimisation to prevent this sort of thing from happening. I settled with a flag that checked if any of the properties of the element had been changed, it would need to be “refreshed”, otherwise, I wouldn’t have to update the style tag of that element.

This proved to be an excellent idea, and it turned out to increase the overall performancealmost 50%. I branched out again, and decided to change a couple more things:

  • Instead of adding the prefixed properties for every browser, I’d detect the User Agent and just use the prefix for the current browser, this meant handling shorter transformation strings, which resulted in an overall reduction of the memory required to run the script.
  • Additionally, instead of setting the style property for each element, I dynamically created a stylesheet and inserted it into the DOM. This would allow me to substantially reduce the number of repaints and reflows being triggered by each transformation.

Both of these changes allowed me, besides substantially increasing performance, to run more complex scenes.

Up to this point, everything on Tracy was somewhat rectangular – Yes, you could make cylinders and cones, but they’d be made out of 2D Planes. What if I needed to form far more complex shapes such as a star? Or something as simple as a triangle?

After a lot of experimentation I settled with, what I believe, was a good solution: To use the HTML5 Canvas object to generate “polygons” on the fly dynamically. This “Custom2DPolygon” object would require you to define an array with X and Y coordinates (one for each vertex), allowing you to do things such as:

var vertices = [0, 0,
 
               150, 0,
 
               0, 100];
 
var poly = new T.Custom2DPolygon(vertices, new T.Tuple3D());

That would draw a triangle on the screen. Additionally, I made an “Ellipse2D” to be able to draw ellipses.

Now that I had all these tools in place, I’d be able to draw more complex models such as one of my favorite planes: Baron Manfred von Richthofen’s Fokker Dr. 1 – a beautiful red triplane from WW1. While I seriously considered modeling the aircraft using Blender and then adding support for COLLADA, it didn’t make sense as COLLADA works with vertices – and it would have required me to add hundreds of DOM elements to display the plane, and I knew from experience that I had to keep the DOM element count as low as possible. I decided to model it manually, and a couple of hours later I had an “alpha” model up and running on the iOS simulator as seen here. However, I started to experience some small display errors when I rotated the scene. This became extremely evident when I finished the model as seen here.


The finished model of the Fokker Dr. 1

At first, I thought that these things were being caused by some problem in my code, but after extensive analysis and testing discovered that it’s a display problem that, to this day, still occurs in all browsers. Little did I know that this was only the beginning of a series of problems I’d have to face if I were to make Tracy work properly.


The wireframe render of the Fokker Dr.1 (basically, I’m tracing lines and use transparent background colors)

My vacations ended and I had to go back to work and couldn’t play so much with Tracy for many months. When I finally got back to work on it, many things had changed: When Safari 6 was released it went from being the best browser to run these sort of CSS 3D demos to being the absolute worst. At the same time, Firefox which was the slowest before (when I got started) somehow became the fastest. Things that worked just fine in Chrome simply stopped working overnight when it got updated to the latest version and the list of bugs goes on and on.

On September 2012 I got invited to speak at onGameStart 2012 where I decided to announce that I was developing Tracy, that it was a free, open-source project and that it’d be released as soon as I got to somehow fix these bugs (or to luckily get browser vendors to fix their problems). If you’re interested, you can view the video of my talk here.


The model of an F117 Stealth Fighter

During the conference, I met Emeric Florence, another chap that at the time was also experimenting with CSS 3D transformations, and started talking with Keith Clark via Twitter, which recently made a very impressive CSS-Powered FPS demo. Both had had the same problems that I did, and continue to have. Actually, if you play Keith’s demo, you’ll notice that some objects “flicker” or randomly disappear or reappear. That’s exactly the sort of bug I was experiencing.

So at this point you must be wondering, what’s the current status of Tracy? When will you release it? The thing is that I’m still not satisfied with it, and while I could release it right now as-is and it’d still be a nice thing to play with (such as a nice tool to make 3D charts that work everywhere), I’m trying to find enough time to make some architectural changes and to further improve the performance, especially on devices with Retina displays.

Conclusion

CSS3 3D Transformations were not designed to do the sort of things me, as well as many other people, are doing with it – but the reality is that, unlike WebGL, CSS3 really does work everywhere, including IE10 and 11 (although I don’t think Tracy will work in them as they don’t support transform-style: preserve-3d, which is a shame). The truth is that I developed Tracy not because I wanted to fill a gap in the market, but out of technical curiosity; I wanted to see how far I could take it and to find out where is the limit with this approach. If I haven’t released it yet is because when I look at Keith’s impressive FPS demo, or Florence’s incredibly fast Sprite3D I believe that CSS 3D transformations still have much to offer – we just need to keep experimenting and learn how to get the most out of them.


Multiple F117s

For more complete information about compiler optimizations, see our Optimization Notice.