The matrix rain effect is a really common one among creative coders/shader enthusiasts, and for very good reason: it’s a cool-looking effect and it’s not incredibly complicated to achieve.
Because I’m a fan of retro-futuristic visuals and because it felt like a rite of passage as a shader programmer to create this effect, I decided to code my own version with a unique twist to it – instead of being the traditional matrix rain where the letters move downwards on the screen in 2D, mine would appear to scroll in 3D.
Here’s a gif of the result (click on the gif to see the code):
The characters themselves were rendered by sampling a glyph-containing texture provided by shadertoy.
To achieve the 3D scrolling effect, two primary techniques were used:
- Faking a pair of parallel 2D planes.
- Using a modified version of parallax scrolling to simulate depth.
We’ll go over each of these techniques in turn. Like my other shader tutorials, this one assumes that you have some prior experience writing shaders.
Faking Parallel 2D Planes
The effect can, in a nutshell, be thought of as two parallel textured planes, each split up into columns of text that are scrolling at different speeds and which have different opacity values. The first step is to take the above texture and get it to render on these planes:
This is actually a pseudo-3D effect – there’s no raymarching, matrix multiplication, or any traditional 3D math being used. Instead, we just modify the x and y values of the sampled coordinate based on the vertical distance of the pixel from the origin. Here’s the responsible code snippet:
vec2 uv = fragCoord.xy / iResolution.xy - vec2(.5); // in the above example, the vec2 "lookup" defined below is used to // sample the glyph texture directly vec2 lookup = vec2(uv.x / uv.y, .5 / uv.y);
Dividing uv.x by uv.y causes the x value to to increase more slowly as uv.y, which is the vertical offset from the origin, increases. This is why the grid squares at the top and bottom edges of the screen are wider than those closer to the center.
The calculation of lookup.y uses the function .5 / uv.y, which looks like this when plotted on a graph:
As you can see, the values of lookup.y change very quickly near the origin and slow down as uv.y increases. Observe the absolute value of the derivative of the above function to get another look into how the uv-value changes with respect to vertical distance from center:
The value of lookup.y changes more slowly as vertical UV distance increases, thus causing the squares closer to the top and bottom edges to be more vertically stretched out (in addition to being wider, as we illustrated previously). These distortions of lookup.x and lookup.y account for the planar appearance of the rendered result.
If you’ve dabbled in gamedev or web design, you’re likely familiar with the concept of parallax scrolling. Although in practice the technique is almost exclusively applied to scrolling backgrounds, the concept can actually be used for other interesting pseudo-3D effects. The multi-tiered appearance of each of the flat parallel planes in my matrix rain effect is created by two simple tricks:
- Each column within each plane scrolls at a different pseudo-random speed.
- The opacity of each column depends on its scroll speed.
Trick #1 uses a very similar principle to standard parallax scrolling: things that scroll more slowly appear to be further away. It turns out that this sort of applies even if the things that are scrolling are tightly packed columns. However, it’s really trick #2 that sells the illusion. Things that are fainter in color appear further away (interestingly, this is a technique that is commonly used in traditional landscape painting – desaturated colors are frequently used to convey distance from the viewer).
By making the slower-scrolling columns also less opaque, these columns get pushed even further into the background.
That’s basically all there is to it. In my explanation of the effect, I purposely refrained from explaining certain aspects of the shader (coloring the text green, generating a random speed per column, etc.) in order to avoid over-explaining things that aren’t particularly complex. Here’s a link to the code again though, in case you’d like to take a closer look at the implementation details. It’s a short read – only 23 lines.