ASCII renderer in Unity

For a gamejam in association with the REPLAY Masters programme, I challenged myself to create a custom ASCII renderer in the Unity Engine. This idea stemmed from my recent new-found love for TUIs.

Unity Setup

The Unity version being used is version 2022.3.36f1 and Universal Render Pipeline. Although, I don’t see a reason this approach would not work in more recent/older versions. On the scene, I have 2 Cameras:

For the ASCII Render Texture, it I picked a resolution to match 16:9, in my case I chose 128x72px.

First approach: TextMeshPro

My initial thought was to use Unity’s TextMeshPro as the ASCII renderer. In my head, this idea never seemed like the propper and correct idea, but I still gave it a fair shot. After a few hours wrapping my head with the immense parammeters a modern font style that never ligned up where they should, I gave up and started looking towards a different approach.

// Show TMP filling the entire screen

Second approach: 1 Pixel, 1 Object

After some back and forth and help, I settled on a promissing but harduous solution: every letter-pixel would be a mesh quad and the only thing that changes are the UVs. This would allow for accurate pixel-perfect text rendering while taking some advantage of the GPU.

For a jam this seemed like too much, but I was already in too deep to give up. Fortunately, in the end, it worked out.

// Image of subdiv screen sketc

My first idea on how to implement such a thing was to Instantiate a quad Prefab, move and scale it to match the target Pixel. Which meant that I was spawning (128x72) 9216 individual objects… Unity running on a 2070 RTX did not like that a single bit. So I had to think of yet another approach to the problem.

// Show FPS and image

Third and final approach: Generate a custom Mesh

The final solution I came to was to generate a Mesh filling the entire screen where, for every pixel, there was a quad. This way, unity only has to render a single object with the same material and texture and I’m able to programatically control the UVs to match it to a letter (more on that later).

Matching mesh to screen

In order to have a “fullscreen” mesh, we take into account the Main Camera’s parameters, namely, the Orthographic size and Aspect. With these two values and the target size of the downsampled render texture, we have all we need to know about the mesh base positions.

// BaseX gives us the left edge of the screen
float baseX = -(camera.orthographicSize * camera.aspect);
// BaseY gives us the bottom edge of the screen 
float baseY = -(camera.orthographicSize);
// Cell size gives us the "Pixel" size in Main Camera units.
float cellSizeX = (camera.orthographicSize * camera.aspect * 2.0f) / bufferSize.x;
float cellSizeY = (camera.orthographicSize * 2.0f) / bufferSize.y;

By having these values, we can now iterate for every pixel and create a quad there.

for (int y = 0; y < pixelY; y++)
    for(int x = 0; x < pixelX; x++)
        Create Top Left Vertex
        Create Top Right Vertex
        Create Bottom Right Vertex
        Create Bottom Left Vertex

        Same for UVs

        Same for Tris, counter-clock-wise

After successful generation of the mesh data, we can now create a Mesh object:

Mesh mesh = new Mesh();     // Create object
mesh.name = "TextMesh";     // Add a name
mesh.SetVertices(vtxs);     // Add the vertices
mesh.SetUVs(0, uvs);        // Set the respective UVs to slot 0
mesh.SetTriangles(tris, 0); // Specify Tri order
mesh.RecalculateBounds();   // Force bound calculations to avoid wrong camera culling
mesh.MarkDynamic();         // Is going to be changed at runtime (UVs)
mesh.UploadMeshData(false); // Notify that all required data is complete!