flak rss random

css vs webgl cubes

I wanted to conduct a little experiment, and it turned into a few experiments in one. I was watching a youtube video about creating 3D scenes purely in CSS. At first, it seems pretty ridiculous. Surely this has to be too much effort, but then as it came together, it wasn’t that difficult. CSS has more potential as a lightweight 3D rendering language than I may have suspected.

So I figured what we need is a side by side comparison. I could just watch more videos, or look at some demos, but it’s helpful to retype things and change them around a little bit for more understanding.

This post is pretty long and boring if you aren’t interested in scrolling past lots of code. Here’s a link right to the finale and assessment.

As an additional meta experiment, I wanted as much of the live demo to come from the posted code snippets. And I did succeed, at least in one direction. Every code listing figure is actually used. The style and script tags are made visible with CSS magic. It’s not quite a live codepen, but at the very least I only have to update one source. The listings aren’t manually copied from some other project. The main struggle was ordering them properly and controlling side effects.

two squares

We're going to draw a cube. Two cubes, even. Eventually. But first, how about just a square? Or two squares. Let's get something on the screen, and then we'll manipulate it into a cube.

We'll do this whole process side by side. On the left, the CSS way. On the right, the webgl way.

For CSS, we'll start with one face, and give it a size, position, and color. For GL, we need a vertex shader and a fragment shader to start.

CSS styles
GL shaders

But that's only half of it. That's how to draw a face, but we still need to create the square, and have a place to put it.

For CSS, we toss a few divs onto the page. For GL, well, it's a bit complicated. We need some vertices, but man, getting those eight numbers into the shader is quite some work. (Plus a canvas element, but that's not very interesting.)

such a pretty face
so much code to draw a square

There's still more to it, especially on the GL side. The functions to compile a shader, link the program, etc. I don't think there's a lot to learn by seeing them, so let's move on.

Now let's take a look at our handiwork. If everything has gone right, we shoud have two blue squares.

css square
webgl rendering

They don't quite look the same for a number of reasons, but this is a reasonable start. We could make either square look like the other by adjusting a few values. For GL, I forgot to enable blending, so there's no alpha. The CSS square is much darker because it's only half there. They have different sizes and origins. That's okay, I kinda left it underspecified exactly how it should look.

enter the matrix

We've got a simple start, so now it's time to add a new dimension and gain some perspective. For CSS, we do this by specifying approximately where we want the camera to be. For GL, I leave the derivation of the viewing frustum as an exercise for the reader, but yeah, here comes some matrix math.

CSS for camera and perspective
Set up the view and projection matrices

I have elided the code for the barebones matrix math library for GL, or we'd be here all day, but it's somewhere in the page source. Moving on, we need to specify how our vertices are positioned in space. For CSS, we introduce a cube class which will allow us to apply some transforms and positioning. For GL, we update the vertex shader. GL has finally scored a win, with the shorter code listing here.

This is kinda like the vertex shader for CSS.
At least this part is pretty easy to understand.

A cube consists of six faces, oriented in different directions. We'll stick with the same rendering technique for both approaches here, as opposed to creating more vertices for GL. This isn't how you'd normally do things for just one cube, but it's closer to how you might assemble a larger scene of multiple objects.

For each face, we need to turn it so it's facing the right direction, and then translate it to the correct position. Order of operations matters here.

rotate and translate
translate and rotate

Hopefully we've managed to get all the faces pointed out. If some of the faces point in, there will be mischief later.

All that's left is to specify the HTML for the cube. We've already handled the vertex points for the cube faces when setting up the matrices and shaders, and mixed the draw calls in with the transforms on the GL side.

the html for a cube
finally an empty listing

Let's take a look.

still loading...
a blue cube
another blue cube

Again, I haven't tried very hard to render identical scenes, but we can see that we're approaching the same image given some artistic license. It's more or less obviously a blue cube.

shadow realm

But it's kinda flat. Let's add some shadows, or shadow like edges. If you're triggered by SSAO, look away. For CSS, this is really simple. Just literally use the shadow property. For GL, it's something, I guess.

If it works, it works.
This is not the SSAO tutorial.

The GL code isn't going to give us quite the same effect, but the idea is to see something. We can always fix it up to be more visually appealing later. And by we and later, I definitely mean not me and not now.

Ready for the big reveal?


still loading...
The final CSS cube.
The final GL cube.

And there we have it! (Can't lie, I'm not entirely happy with the GL cube edges, but E for effort.)

For bonus fun, try using the WASDQE keys to spin the cubes around. No complaining about the chosen axis of rotation.


Putting the CSS cube together was really quite easy. It helps that I was almost literally copying code from a video, but building it up in pieces like that felt pretty reasonable. Immediate feedback at every step. Thankfully, this was not my first trip to the GL rodeo, so I'm used to looking at empty black squares while fiddling with everything just to get something, anything to appear.

There were even some bonus gotchas specific to webgl, like ugly artifacts from rendering at the wrong resolution because I didn't set the canvas up right. And my personal favorite, no implicit conversions in baby GLSL so I have to type out 1.0 every time. Literally every edit: type 1, cube disappears, add .0. Type 2, cube disappears, add .0. That was fun.

If you spun the cubes around at all, you probably noticed the CSS cube flies out of its designated page position and obscures nearby elements. The GL cube, of course, is clipped by its viewport and goes nowhere. I actually think this is a quite nice effect.I've seen some carefully rendered webgl examples where the page text floats over the canvas and the GL objects are carefully rendered around it, but that seems like a lot of work to get right.

I used a fixed perspective for the CSS scene, but expanding the scene div so that it grew scrollbars resulted in the perspective shifting as one scrolls. Another nice effect you get for free.

I wanted to try this experiment, because it seemed like CSS might be a beginner friendly language to draw some 3D scenes. It kind of is. It's a bit fiddly to get certain looks and effects, and the syntax isn't ideal for this task, but overall the CSS was easy to write, and easy to read, and every line has its purpose. I didn't have to write (or copy) very much code at all before I started seeing immediate results. If I wanted to texture the cube, that's as simple as adding a background image to the cube face class. For GL, we're talking dozens more lines to load and bind and sample the texture. (I guess a comparison with three.js is in order.)

The meta experiment of reusing code listings as running code worked out pretty well. I made a number of last minute tweaks and checks (like getting the face direction correct for every face), and it's nice having the confidence of knowing the displayed code is exactly what I tested. It did require the code to be organized correctly. For instance, style sheets apply to the whole page, not just the tags after, so this requires some thought to prevent the examples from advancing too far. No fancy publishing platform needed, though.

I usually try to make pages work on mobile, and gripe about sites that break on phones, but I’ll make a slight exception here. The cubes spill out farther than they should.


For the CSS code, I mostly copied the cube from 3D CSS - lighting, animations, and more.

For the webgl code, I started by cribbing from WebGL2 Fundamentals.

Posted 20 May 2022 06:23 by tedu Updated: 20 May 2022 06:26
Tagged: programming web