ASCII Art is the use of ASCII characters to draw images an shapes. I was wondering if it is possible to convert an image into an ASCII Art using Animation Nodes in blender. Here are some examples for the required output:
ASCII Art rely on a fundamental concept, when characters are viewed from far, they no longer look like characters but rather some white and black blocks. Some characters look more black that others, for instance, a dot
. colors a single pixel black, while an
I colors more that one pixel black and thus it looks more black that a dot from far away.
Let us define the density of a character as the ratio between the white and black pixels in a rasterized version of the character. From this, it is clear what we have to do in order to draw an image using characters:
- Sample the pixels of an image and convert them to greyscale if they were colored.
- Compute the density of each character and store it in a list.
- For each pixel in the image:
- Find the character with the closest density to its value and append it.
- Break the list of characters into
nnumber of row where
nis the width of the image. Then output that text.
Sample Image Pixels
I am going to make a group that samples the pixels of the image as well as return its width. You can get the
R values of the pixels of an image with name
imageName using the expression:
And the width by the expression:
So the node group looks like this:
Notice that you can cache the results for faster execution in the future.
Compute Characters Density
To compute the character density, we are going to use a BVH tree. We will ray cast a group of ray in the bounding box of each character, count the rays that hit each character, divide it by the number of rays. This will get us the normalized density of each character. First, I am going to create a mesh with all possible characters aligned in a row. It is important to use a monospace font for this:
Next, I am going to create a loop that takes a grid of vectors, ray cast them on each character and compute the number of hits. Notice that at each iteration, I move the grid in the x direction by some offset value that is equal to the width of each character (Which is constant since we used a monospace font):
Next, we create the BVH tree and the grid that will be used as the input for the previous loop. The BVH tree is easy to compute using the Construct BVH Tree Node. However, to create the grid, we have to identify the width and height of each character, I couldn't really find anyway to do that, so I aligned a plane with one character and got the width and height from it, I also added an array modifier to get more accurate results by cross checking width and height with all characters:
Computing the grid in this case is easy, we get the difference between the lower left and upper right vertices locations of the plane to get the height and width, the Grid Mesh node returns a grid that is centered, to move it to the first quadrant, we subtract half of the difference vector. Having the grid in the first quadrant, we move it such that the first vector is aligned with the first vertex in the plane:
We then divide the number of hits by the total number of rays. Now the problem is to find the index of the character that its density is closest to each of the pixel values, so lets make a loop that returns the index of the closest value in a list to some reference value. We loop over the values of the density list, compute the difference between the current density and some reference value, if the difference is smaller that an initially infinity parameter
delta we reassign an integer parameter to be the current index and reassign delta to be the difference. I realize it is not easy to understand what is going on here, but it really is easy, at the first iteration, delta is infinity and thus any difference is definitely smaller than it, so delta becomes the difference between the density of the first character and the index becomes zero (Indicating that so far the closest value is at index zero), at the next number of iterations, if we find a smaller delta (Difference), we reassign again indicating that we have found a value that is closer. Make sure to output the
Closest Index parameter after you are done:
Now, we loop over the pixel values, use the previous loop to find the index of the character with density closest to it (The pixel value is the reference value here), then output those indices:
We now split the characters we used to create the mesh, get the characters at the indices we just generated, split them again at
nth character where
n is the width of the image, then join them with a line break as a separator. Notice that we reverse the lines, that is because blender reads the image from the lower left corner, while text is written from the upper left corner:
One thing to note is that if one normalized the density by dividing by the maximum number of hits, the result will be richer. All of this results in a text:
Not sure if you can see it, but Lenna is drawn here. There is something to note however. The line height is much larger than the width of the character, so we may want to reduce it, since we can't do that in blender, we are going to take another approach. We are going to use cycles.
What I am proposing is that we create a texture that is composed of the characters ordered from the lowest density to the highest density. We then compute a texture coordinates that position the characters in a grid where the the density of the character in each cell correspond to the pixel values in the image.
First we generate an image with the 84 characters we have ordered based on their density, we can do that by rendering a text object generates by this node tree:
We are basically sorting the characters, joining them and writing them to an object, then we render it producing an image like this:
Notice that the characters at the left are more sparse (lower in density) than those at the right, and that is exactly what we want.
We now have to compute the texture coordinates based on some input image, I shall not describe how the node tree is created because it is out of the scope of this answer, but I will share the blend so that you can study it yourself.
Lets say we have 80 different characters that are used in the ASCII Art, they may repeat a lot inside the art, so there is no good reason to repeat them that many times and consume memory like that. A better approach would be to create 80 text object each with a character, then create 80 meshes with vertices, each of which will be the base for a DupliVert of some character, for instance, the character
a was repeated a thousand time at different locations, we create a mesh with vertices locations equal to those of character location, then parent the object with character
a to it, this will result in instancing the character
a in all of the locations it was supposed to be in. By doing that for all objects, we create a complete grid of characters just like the one in the CSS and text editor example. So we use the Object Instancer Node to create the character objects as well as the DupliVert object as follows:
Then we hand it to the Instancer loop. This loop find the indices at which each character occurred, find it location in the text grid by means of modulo and floor division by the width, then the output locations are assigned to one of the mesh object, then
dupli_type is set to
VERTS and the character object is parented to the DupliVert Base:
And this immediately returns the results we are looking for, notice how by selecting one of the objects, we see all the places it occurred in:
We have about 9 million polygon in the scene and blender isn't complaining.
Here is the blend file for study and practice: