Going Into Depth
Most 3D objects rendered by computers are defined as hollow shells, a collection of zero-thickness surfaces, describing empty volumes. But the world is full of solids, each with its own inner life of nooks and crannies, cracks and dings, secret layers, bubbles, inclusions, and imperfections.
So I made a magic brush that paints inside of things! I made it, specifically, to add sparkles to crystals. Here’s a stripped-down example:
This crystal has no internal details, and is not performing any ray-tracing or fancy lightbending math. It’s drawn with a shader that covers it with a texture made of a single image, then manipulates the texture based on the view angle. The result is that certain parts of the texture appear to be more distant than the faces they’re applied to, suggesting internal structures which would otherwise require a much more detailed mesh to render.
(For an introduction to shaders, I recommend The Book of Shaders.)
Let’s break down the effect.
This trick is entirely based on the parallax effect, which makes distant objects appear to move more slowly. Conceptually, it’s a very simple idea – you can even fake it in CSS! Drag the box below:
This box appears to be a window to a separate, more-distant starry layer, which is really just a repeating
div.style.backgroundPositionX = - div.getBoundingClientRect().left/2 + 'px';
- sign shows that the background is being moved in the opposite direction of the div, as otherwise the background would inherit the div’s motion. The result is that the opposing motion adds a bit of “drag”. The
/2 sets the rate of the background’s motion to half the rate of the div itself, which suggests distance.
This effect also works during rotation, which can also be simulated in CSS. Drag this box to see it in action:
In the example above, the background position is moved relative to the tangent of the div’s rotation. This approximates the relationship between rotating layers of different distance from the viewer.
div.style.backgroundPositionX = - Math.tan(divRotationInRadians)/2 + 'px';
If you can imagine the rotating div above as a face in a 3D mesh, this is the basic principle of the crystal shader. Here’s one more CSS prototype, using multiple copies of the last example as faces of a cube, and applying 3D transforms:
The net effect is something like refraction, as though the object were a solid cube of glass in front of a distant star field.
Turning up the Volume
However, to add the illusion of volume to the shape, we have to provide depth cues at various distances. To do this, we can simulate a parallax effect between individual stars.
Here’s a simple example using canvas elements to split the image up into layers of constant brightness, and CSS transformations to simulate the parallax between layers, with one important difference from the crystal’s method: brighter pixels appear to be pulled towards the viewer.
This is similar to the standard behavior of parallax shaders, a class of shaders used to add the illusion of depth to a surface. Generally, they assume a continuous, opaque surface, and are used to add small amounts of subtle protruding detail to an otherwise flat face – bricks and cobblestones are a very common use case.
We’re looking for something slightly different, but most of the same principles apply. In fact, you could describe the crystal shader as a parallax shader turned inside out:
You’ll notice that in the previous examples, “closer” layers occlude “further” layers. This order is simply determined by painting order, which in CSS is determined by
z-index. Occlusion could be useful in certain cases, but when dealing with sparkly crystals, you want to maximize the sparkle levels, and minimize any apparent occlusion.
To achieve this the crystal shader uses a technique which equates to a “lighten” blending mode in a layer-based image editor like Photoshop. This is done in CSS with
mix-blend-mode: lighten, and the result is that where pixels overlap, the lightest pixel is shown. (For the full effect we’ll need to ensure that the background is darker than the foreground.)
This is the essence of the technique! It’s relatively simple to do this on a flat plane, but to do this on an arbitrary 3D object, we’ll need something more powerful than CSS, capable of slightly more intense math.
In the next post, we’ll walk through the building blocks of the shader implementation, and then we’ll dig into the shader code itself.
Until then, enjoy a shiny purple gem. Thanks for reading!