Home   LARC   Art

ASCII Art on a Pixel Shader


I want to learn how to write a pixel shader that uses two textures, but I'm bored with all of the examples on the internet. I want to do something that I can't easily find code for online. I get the idea of writing a pixel shader for ASCII art. Here's how it went.

The Pixel Shader (2011)

I used one texture for the image and a second texture for the 8 ASCII characters. I picked the characters MNFV$I*:. from darkest to lightest to use as my greyscales.


How did I choose which 8 characters to use? It wasn't completely random. I typed some ASCII characters into a texture and wrote a quick DirectX program to read in the image and average out the grayscales in each 8 by 8 square.

void CRenderer::InterrogateTexture(LPDIRECT3DTEXTURE9 pTexture){
  HRESULT hr = pTexture->LockRect(0, &r, NULL, 0);
  if(hr == S_OK){
    int grey;
    for(int k=0; k<31; k++){
      grey = 0;
      for(int i=0; i<8; i++)
        for(int j=0; j<8; j++)
          grey += GetGray(r, k, i, j);
      grey = grey/64;
      printf("%d\t%d\n", k, grey);

The above code uses this function to return a gray value for an individual pixel in a given row and column of a given character.

int GetGray(D3DLOCKED_RECT r, int chr, int row, int col){
  BYTE* offset =
    (BYTE*)(r.pBits) + row*r.Pitch + chr*4*8 + col*4;
  return (int)(*offset) + *(offset + 1) + *(offset + 2);

I got the results below, and picked the 8 characters indicated in grey.

Character Greyscale
. 245
: 237
* 218
I 197
$ 191
J 188
T 187
L 186
C 184
Y 183
Z 183
V 181
S 175
U 174
G 173
P 172
A 169
O 169
X 169
F 168
D 166
E 166
# 162
R 160
K 159
H 158
B 157
Q 157
N 156
W 146
M 144

I whomped up some pixel shader code in HLSL for use in DirectX. The fx file starts with some declarations:

texture Texture0;
texture Texture1;

sampler2D Sampler0 = sampler_state{
  Texture = <Texture0>;

sampler2D Sampler1 = sampler_state{
  Texture = <Texture1>;

Here is the pixel shader code.

float4 PS (in float2 Tex: TEXCOORD0): COLOR0{
  int CHARSIZE = 8;
  int GRAYLEVELS = 8;
  int IMGTEXTURESIZE = 2048;

  float4 pix = tex2D(Sampler0, floor(BLOCKSCALE*Tex)/BLOCKSCALE);
  int gray = floor((pix.r + pix.g + pix.b)*GRAYLEVELS/3.0f);

  int2 inttex = floor(Tex * (IMGTEXTURESIZE - 1));
  int2 block = (inttex/CHARSIZE)*CHARSIZE;

  float2 offset = inttex - block + 1;
  offset.x += gray*CHARSIZE;
  offset /= CHARTEXTURESIZE - 1;
  return tex2D(Sampler1, offset);

Here is the pixel shader output for a photograph of Ian Parberry taken by Jon Doran, with an inset showing a magnified view of the top left-hand corner.


Here are some more examples of images created by my pixel shader. All original photgraphs were taken by Ian Parberry except for the one of himself (which obviously wasn't), the one of Jon Doran, and the one of the ladybug. Click on the thumbnails to get a larger image. Top-to-bottom, left-to-right they are:

Row 1: Jennifer Alford; Mary Yingst; me;
Row 2: Joshua Taylor; Jon Doran and Sasha (original photo by Jon Doran); Vincent Liguori;
Row 3: Market Block Building in Troy, NY; Trojan Hotel in Troy, NY; sign for the Trojan Hotel;
Row 4: crab in Cape May, NJ; kangaroo in Australia; hibiscus in Australia;
Row 5: ladybug in England (original photo by Lizzie Parberry); butterfly in Denton, TX; columbine in Denton, TX;

Image   Image   Image

Image   Image   Image

Image   Image   Image

Image   Image   Image

Image   Image   Image

A propaganda picture of the University of North Texas:


ASCII Art at 60fps (2011)

Here is my pixel shader in an old version of Ned's Turkey Farm, the 2D game I use to teach my Game Programming 1 class. Click on one of the thumbnails below to get a larger screenshot.

Image   Image

Here's what it looks like in real time:


Here's a video of Ned's Turkey Farm ASCII Art Version rendered at 60fps (recorded at 30fps). It's best viewed fullscreen.

This video shows a quick close-up of the monitor using a video camera. Again, it's best viewed fullscreen.

High Resolution Images (2011)

We can increase the quality of our greyscale images by increasing the resolution. The ASCII art thus far has been about 128 characters wide, like this one. The Beatles are totally unrecognizable:


It starts to look better when we double the resolution to 256 characters wide. Now George and Paul are recognizable, but Ringo is still a bit vague:


It's even better if we double the resolution again to 512 characters wide. There's Ringo:


That was for a photograph. The following 128 character wide image doesn't capture the detail of the original woodblock print. In particular, the artful spirals at the wave edges are completely scrambled:


However, increasing the width to 256 characters gives a definite improvement:


How far do we need to take this? As the Mythbusters say, anything worth doing is worth overdoing. When we ramp up the resolution by almost a factor of 4 to over 1000 characters wide, we get an image composed of almost 700,000 characters (click for more detail):


Zooming in on the small part outlined here in red:


The following is from a photograph taken by Ian Parberry in Australia, at 512 characters wide:


Created October 19, 2011. Written in HTML 4.01 and CSS 3 using vi. Last updated December 15, 2011.

Valid HTML 4.01 Strict Valid CSS!