ASCII Tower Breakdown

Learn how to use deferred rendering with ascii by packing values into color channels.

When you draw pixels on the screen with a shader, you give each object a very specific color. You pick the three numbers for each color: R, G, and B (RGB). You make sure they are the most vibrant and beautiful color you can imagine. You are elated. You've completed your purpose.

After all, the shader says gl_fragColor . Who are you to not provide the shader with what it asks from you?

Turns out that colors, RGB, is just a bunch of numbers. It's number data that represents a color. What. A. Surprise.

And if you are not rendering to the screen, you can store whatever you want in these numbers and use it however you want. As long as you render color to the screen at the very end. This is what I did for this demo.

Since I'm going to be talking about placing data where color should be, I want you to keep in mind I'm not talking about the color, but the place where that color component is usually stored:

When I say Red Channel, you say color.r = somevalue

When I say Green Channel, you say color.g = somevalue

When I say Blue Channel, you say color.b = somevalue

When I say RGB, you say BEEP BOOP BEEP.

Inspiration

All my ASCII love comes from Andreas Gysin. In their case, it's all HTML. Changing the set of characters in HTML is trivial because you don't have to pre-render anything. Text is native to HTML.

In WebGL, this little change involved a bunch more trickery.

Towers #5 by Andreas Gysin
Towers #5 by Andreas Gysin

sponsor

Base Scene and Deferred Rendering

The base scene is simple a couple of tubes on top of each other, and other tubes around them to create animated lines.

Instead of rendering color, these tubes send specific data about how they want to be rendered in the second render pass, in this case, the ASCII pass.

The red channel becomes: Ascii Selection

The green channel becomes: Color Inversing

The blue Channel becomes: Cosine palette color selection.

The ASCII post-process will read the first render, get the data, and compute the final color result.

This is what a deferred renderer does in games like GTA5 but for Physically Based Rendering.

First render pass without pixelation
First render pass without pixelation

Encoded Color data

If I'm not using the RGB values to send what they are supposed to send, that being color. How do you give color to the final result?

Every object encodes a cosine palette offset in the blue channel. The post-processing pass reads the blue channel color.b and generates the color with a cosine color palette.

But, you can do the same trick but with a texture holding all your object's colors instead and use the blue channel as UVs instead.

To give some variation in the flying lines, the green channel color.g holds a value to invert the color. I could have packed these two in a single value, but this was enough for my sketch.

Color data in the blue and red channel without ascii
Color data in the blue and red channel without ascii

ASCII selection

Instead of having a single set of ASCII characters. The ASCII texture holds multiple sets of characters. This allows for every object to select a specific set of characters to select from.

In my sketch the change is simple, every other cylinder says YES or NO and the top of every cylinder selects a completely different set of ASCII characters. But you can use whichever characters you want.

const glyphs = [
['.', '·', '°', '°', '*', '*', '=', '$', 'E', '@'],
['N', 'O', 'N', 'O', 'N', 'O', 'N', 'O', 'N', 'O'],
['Y', 'E', 'S', 'Y', 'E', 'S', 'Y', 'E', 'S', 'S'],
['T', 'U', 'T', 'U', 'T', 'U', 'T', 'U', 'T', 'U'],
['█', '█', '█', '█', '█', '█', '█', '█', '█', '█']
]

// Draw it onto a texture

However, a 2D array/texture means I need X and Y position to sample the correct character. This is a problem because we already used the Green and Blue channel, and only have the Red channel left. We don't have enough space.

Tower cylinders rendering a different set of characters
Tower cylinders rendering a different set of characters

Packing numbers in GLSL

The green channel is for the color inversion and the blue channel is the cosine palette color. We need two values to select our ASCII character ( X and Y), but we only have one spot left, the red channel. What do we do?

Packing and unpacking. Make the two values into one, and unpack them before use.

Packing is short. Multiply the Y axis by the number of X cells, and add the values together, in this case, 10.

Unpacking is tricky. The X axis is the leftover numbers after every multiple of 10. The Y axis is the number of 10s in the number, so we divide by 10 to count the number of 10s.

The value 23 is:

X = 3 because when we remove the 20s we have the 3 left. This is what mod does.

Y = 2 because we have two 10s in 23. We use division to count the 10s.

float packASCII(float X, float Y) {
// Make sure both numbers are within their respective ranges
X = clamp(X, 0.0, 9.9);
Y = clamp(Y, 0.0, 4.0);

// Pack the numbers into one float
float packedValue = X + Y * 10. ;

// Convert to a float to return
return packedValue / 50.;
}

vec2 unpackASCII(float packedValue) {

float big = packedValue * 50.;

float packedNumber1 = mod(big , 10.0);
float packedNumber2 = floor(big / 10. ) ;

// Convert to a float to return
return vec2(packedNumber1, packedNumber2);
}

void main(){
// ...
color.r = packASCII(x,y)
}

You can pack with complete precision but this was good enough for me.

Advanced ASCII rendering (8).png
Advanced ASCII rendering (8).png

Background selection

If the previous values are 0, meaning no objects are rendered in that pixel. Then, I render a background created directly in the ASCII post-process.

First render pass pixelated with ascii in the background
First render pass pixelated with ascii in the background