The secrets behind rendering anything as ASCII

emilwidlund

image.png


This article was inspired by aniso - an open source ASCII tool.

Introduction

ASCII (American Standard Code for Information Interchange) is a common character encoding format, but has also found its way into graphic design through a technique commonly known as "ASCII Art". It's essentially a way to use letters, numbers & symbols to draw images.

In this article, I will explain how to implement a simple ASCII renderer in WebGL. You will be able to render anything imaginable as ASCII. And it's not really that complicated.

We can achieve this with these simple steps:

  1. Subdivide the scene into a grid
  2. Compute the average brightness for each cell
  3. Map the brightness value -> to an index number
  4. Draw a set of ASCII characters to a texture
  5. Use the index number to sample the ASCII texture
  6. Assign the sampled ASCII texture to the output color

emilwidlund - 1748261029778165795 (1).gif

Subdivide the scene into a grid

Create a fragment shader & subdivide the UV coordinates into a grid of cells.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

The pixelizedUV vector now holds subdivided coordinates which we can use to calculate the average brightness of each cell.

Calculating the brightness

If we think about it, ASCII art relies on an important factor - contrast. It works by utilizing letters, numbers & symbols which are visually distinguishable. These ASCII characters are equivalent to our cell's averaged brightness. Now we need a way to translate a given cell's average brightness to a specific character from a set of ASCII characters.

Let's start by converting a color & measure its monochromatic brightness (0 -> 1).

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Computing the brightness for a given color will now give us a value between 0 -> 1. Let's use this function to calculate a cell's average brightness.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Mapping cells to ASCII

Our goal with an ASCII set is to select a few characters that can represent various stages of brightness intensity. 0 in brightness could map to a dot, 1 in brightness could map to a hashtag. It all depends on what font you want to use - some characters are perceived more contrasty than other.

It's important to arrange the character set in order of contrast. A set can look something like this:

const characters = ` .:,'-^*+?!|=0#X%WM@`;

Creating the ASCII texture

As we want to use our calculated, averaged cell brightness as an index for retrieving an ASCII character, let's put all of our ASCII characters into a texture that can be sampled from.

This is a quite easy task if we utilize the Canvas API.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

This will now produce a Canvas with all our ASCII characters arranged in a nice grid. Call the function with your ASCII set and a font size of your choosing. Take the canvas and turn it into a WebGL texture.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Once you have the texture - upload it to our fragment shader using a uniform. Let's call it uCharacters and declare it as a sampler2D. We also need to supply the character length of our ASCII set.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Sampling ASCII characters to our cells

Now to the magic – drawing the ASCII characters as replacement for our inputBuffer. The idea is to map the average cell brightness to one of the characters in the set we defined earlier. If we have 20 characters to choose from, we scale the brightess float (0 -> 1) to an integer between 0 -> 20 and use that to index the characters. A brightness value of 0.7 would turn into index 14 & yield us the character # as our ASCII representation for that specific brightness.

First, produce an index that we can use to index the character cell we want to sample from. We do that by multiplying the averaged cell brightness by the maximum character index, and then flooring the result. Use that index to compute the character position & sample the position from the ASCII characters texture we provided as a uniform.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

The sampled ASCII character is the final piece of the puzzle. Assign it to gl_FragColor and voilà – you have yourself an ASCII output.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Wrapping up

emilwidlund - 1748261029778165795 (1).gif

I've created a version of this that works with THREE.js. Take a look at it if you need a concrete example on how to turn this guide into practicality. I've put it behind a small paywall below for my paying subscribers. Feel free to subscribe to my Supporter-tier for $5/month, and gain access to all my resources & premium content.

This section is for premium subscribers only. Subscribe to Emil Widlund to get access to it.

Thanks for tuning in and reading about my coding adventures. Until next time!