Rendering to Text

Technologies

One of the cooler rendering effect's I've seen in the demoscene has to be rendering 3d imagery to trippy dithered text mode output. Curious one weekend I decided to spend a bit of time figuring out how to do this myself. Rather than re-invent the wheel though, I first went searching for some helpful libraries to assist and cut my work down my an enormous amount!

For the 3d rendering side I went with OpenGL (of course). A quick Google for ASCII rendering revealed an odd library named libcaca. Scatological naming aside*, libcaca is highly flexible and brought to you by some of the same maintainers for the awesome VLC media player. Not too surprising then that you can choose to pipe VLC to text output via this library (ever wanted to see The Matrix in "matrix" mode?). To boot, this library is released under the "Do What the Fuck You Want to Public License" (wtfpl)

*Fitting, considering ASCII art can look like crap when done wrong!

Getting a cross platform windowing context is usually desirable as well. For this the obvious choice is SDL2 used and pioneered by the developers at Valve and open source community.

The Code

Here's the code to make this:

Transform into this:

main.c


#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <stdio.h>
#include <caca.h>

#define BUFFER_WIDTH 800
#define BUFFER_HEIGHT 600
#define BUFFER_DEPTH 32

#define TERMINAL_WIDTH 24
#define TERMINAL_HEIGHT 80

char pixelBuffer [BUFFER_HEIGHT][BUFFER_WIDTH][BUFFER_DEPTH>>3];
char charBuffer [TERMINAL_HEIGHT][TERMINAL_WIDTH];
caca_canvas_t *cv;
caca_display_t *dp;
caca_event_t ev;

void renderBufferToTerminal();
int drawGLScene(GLvoid);

/* function to reset our viewport after a window resize */
int resizeWindow(int width, int height) {
    /* Height / width ration */
    GLfloat ratio;
 
	if (height == 0) {
		height = 1;
	}

    ratio = ( GLfloat )width / ( GLfloat )height;

    /* Setup our viewport. */
    glViewport( 0, 0, ( GLsizei )width, ( GLsizei )height );

    /* change to the projection matrix and set our viewing volume. */
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity( );

    /* Set our perspective */
    gluPerspective( 45.0f, ratio, 0.1f, 100.0f );

    /* Make sure we're chaning the model view and not the projection */
    glMatrixMode( GL_MODELVIEW );

    /* Reset The View */
    glLoadIdentity( );

    return 1;
}

/* general OpenGL initialization function */
int initGL(GLvoid) {
    glShadeModel( GL_SMOOTH );
    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    glClearDepth( 1.0f );
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    return 1;
}

int main(int argc, char** argv) {
	int quit = 0;

	SDL_Init(SDL_INIT_VIDEO);
	SDL_Window *window = SDL_CreateWindow("OffscreenRendering", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, BUFFER_WIDTH, BUFFER_HEIGHT, SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN);
	SDL_GLContext glContext = SDL_GL_CreateContext(window); // Create an OpenGL context associated with the window.

	initGL();
	resizeWindow(BUFFER_WIDTH, BUFFER_HEIGHT);

    cv = caca_create_canvas(80, 24); //Init Caca
    if(cv == NULL) { 
        printf("Failed to create canvas\n");
        return 1;
    }

    dp = caca_create_display_with_driver(cv, "ncurses");
    if(dp == NULL) {
        printf("Failed to create display\n");
        return 1;
    }

    caca_set_display_time(dp, 4000);
    caca_set_mouse(dp, 0); //disable cursor
    caca_refresh_display(dp);

	while(!quit) { // Main Loop
		glLoadIdentity();
		glPushMatrix();
		glScalef(1.0f, -1.0f, 1.0f);
		drawGLScene();
		glPopMatrix();

		caca_set_color_ansi(cv, CACA_LIGHTGRAY, CACA_BLACK);
		caca_clear_canvas(cv);
		renderBufferToTerminal();

		// Refresh
		//caca_set_color_ansi(cv, CACA_LIGHTGRAY, CACA_BLACK);
		//caca_draw_thin_box(cv, 1, 1, caca_get_canvas_width(cv) - 2, caca_get_canvas_height(cv) - 2);
		//caca_printf(cv, 4, 1, "[%i.%i fps]----", 1000000 / caca_get_display_time(dp), (10000000 / caca_get_display_time(dp)) % 10);
		caca_refresh_display(dp);

		drawGLScene();
		SDL_GL_SwapWindow(window);

        while(caca_get_event(dp, CACA_EVENT_ANY, &ev, 0)) {
            if(caca_get_event_type(&ev) & CACA_EVENT_KEY_PRESS) {
				if(caca_get_event_key_ch(&ev) == 'q') {
					quit = 1;
				}
			}
		}
	}

	// Cleanup
    caca_free_display(dp);
    caca_free_canvas(cv);

	SDL_GL_DeleteContext(glContext);
	SDL_DestroyWindow(window);
	SDL_Quit();
	
	return 0;
}

GLvoid drawPyramid(GLvoid) { 
    glBegin( GL_TRIANGLES );             /* Drawing Using Triangles       */
      glColor3f(   1.0f,  0.0f,  0.0f ); /* Red                           */
      glVertex3f(  0.0f,  1.0f,  0.0f ); /* Top Of Triangle (Front)       */
      glColor3f(   0.0f,  1.0f,  0.0f ); /* Green                         */
      glVertex3f( -1.0f, -1.0f,  1.0f ); /* Left Of Triangle (Front)      */
      glColor3f(   0.0f,  0.0f,  1.0f ); /* Blue                          */
      glVertex3f(  1.0f, -1.0f,  1.0f ); /* Right Of Triangle (Front)     */

      glColor3f(   1.0f,  0.0f,  0.0f ); /* Red                           */
      glVertex3f(  0.0f,  1.0f,  0.0f ); /* Top Of Triangle (Right)       */
      glColor3f(   0.0f,  0.0f,  1.0f ); /* Blue                          */
      glVertex3f(  1.0f, -1.0f,  1.0f ); /* Left Of Triangle (Right)      */
      glColor3f(   0.0f,  1.0f,  0.0f ); /* Green                         */
      glVertex3f(  1.0f, -1.0f, -1.0f ); /* Right Of Triangle (Right)     */

      glColor3f(   1.0f,  0.0f,  0.0f ); /* Red                           */
      glVertex3f(  0.0f,  1.0f,  0.0f ); /* Top Of Triangle (Back)        */
      glColor3f(   0.0f,  1.0f,  0.0f ); /* Green                         */
      glVertex3f(  1.0f, -1.0f, -1.0f ); /* Left Of Triangle (Back)       */
      glColor3f(   0.0f,  0.0f,  1.0f ); /* Blue                          */
      glVertex3f( -1.0f, -1.0f, -1.0f ); /* Right Of Triangle (Back)      */

      glColor3f(   1.0f,  0.0f,  0.0f ); /* Red                           */
      glVertex3f(  0.0f,  1.0f,  0.0f ); /* Top Of Triangle (Left)        */
      glColor3f(   0.0f,  0.0f,  1.0f ); /* Blue                          */
      glVertex3f( -1.0f, -1.0f, -1.0f ); /* Left Of Triangle (Left)       */
      glColor3f(   0.0f,  1.0f,  0.0f ); /* Green                         */
      glVertex3f( -1.0f, -1.0f,  1.0f ); /* Right Of Triangle (Left)      */
    glEnd( );                            /* Finished Drawing The Triangle */
}

GLvoid drawCube(GLvoid) {
    glBegin( GL_QUADS );                 /* Draw A Quad                      */
      glColor3f(   0.0f,  1.0f,  0.0f ); /* Set The Color To Green           */
      glVertex3f(  1.0f,  1.0f, -1.0f ); /* Top Right Of The Quad (Top)      */
      glVertex3f( -1.0f,  1.0f, -1.0f ); /* Top Left Of The Quad (Top)       */
      glVertex3f( -1.0f,  1.0f,  1.0f ); /* Bottom Left Of The Quad (Top)    */
      glVertex3f(  1.0f,  1.0f,  1.0f ); /* Bottom Right Of The Quad (Top)   */

      glColor3f(   1.0f,  0.5f,  0.0f ); /* Set The Color To Orange          */
      glVertex3f(  1.0f, -1.0f,  1.0f ); /* Top Right Of The Quad (Botm)     */
      glVertex3f( -1.0f, -1.0f,  1.0f ); /* Top Left Of The Quad (Botm)      */
      glVertex3f( -1.0f, -1.0f, -1.0f ); /* Bottom Left Of The Quad (Botm)   */
      glVertex3f(  1.0f, -1.0f, -1.0f ); /* Bottom Right Of The Quad (Botm)  */

      glColor3f(   1.0f,  0.0f,  0.0f ); /* Set The Color To Red             */
      glVertex3f(  1.0f,  1.0f,  1.0f ); /* Top Right Of The Quad (Front)    */
      glVertex3f( -1.0f,  1.0f,  1.0f ); /* Top Left Of The Quad (Front)     */
      glVertex3f( -1.0f, -1.0f,  1.0f ); /* Bottom Left Of The Quad (Front)  */
      glVertex3f(  1.0f, -1.0f,  1.0f ); /* Bottom Right Of The Quad (Front) */

      glColor3f(   1.0f,  1.0f,  0.0f ); /* Set The Color To Yellow          */
      glVertex3f(  1.0f, -1.0f, -1.0f ); /* Bottom Left Of The Quad (Back)   */
      glVertex3f( -1.0f, -1.0f, -1.0f ); /* Bottom Right Of The Quad (Back)  */
      glVertex3f( -1.0f,  1.0f, -1.0f ); /* Top Right Of The Quad (Back)     */
      glVertex3f(  1.0f,  1.0f, -1.0f ); /* Top Left Of The Quad (Back)      */

      glColor3f(   0.0f,  0.0f,  1.0f ); /* Set The Color To Blue            */
      glVertex3f( -1.0f,  1.0f,  1.0f ); /* Top Right Of The Quad (Left)     */
      glVertex3f( -1.0f,  1.0f, -1.0f ); /* Top Left Of The Quad (Left)      */
      glVertex3f( -1.0f, -1.0f, -1.0f ); /* Bottom Left Of The Quad (Left)   */
      glVertex3f( -1.0f, -1.0f,  1.0f ); /* Bottom Right Of The Quad (Left)  */

      glColor3f(   1.0f,  0.0f,  1.0f ); /* Set The Color To Violet          */
      glVertex3f(  1.0f,  1.0f, -1.0f ); /* Top Right Of The Quad (Right)    */
      glVertex3f(  1.0f,  1.0f,  1.0f ); /* Top Left Of The Quad (Right)     */
      glVertex3f(  1.0f, -1.0f,  1.0f ); /* Bottom Left Of The Quad (Right)  */
      glVertex3f(  1.0f, -1.0f, -1.0f ); /* Bottom Right Of The Quad (Right) */
    glEnd( );                            /* Done Drawing The Quad            */

}

int drawGLScene(GLvoid) {
    static GLfloat rtri, rquad;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glPushMatrix();
    glTranslatef( -1.5f, 0.0f, -6.0f );
    glRotatef( rtri, 0.0f, 1.0f, 0.0f );
	drawPyramid();
	glPopMatrix();

	glPushMatrix();
    glTranslatef( 1.5f, 0.0f, -6.0f );
    glRotatef( rquad, 1.0f, 0.0f, 0.0f );
	drawCube();
	glPopMatrix();

    rtri  += 0.5f; /* Increase The Rotation Variable For The Triangle ( NEW ) */
    rquad -=0.45f; /* Decrease The Rotation Variable For The Quad     ( NEW ) */

    return 1;
}

void renderBufferToTerminal() {
	glReadPixels(0, 0, BUFFER_WIDTH, BUFFER_HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, pixelBuffer);
    caca_dither_t *dither = caca_create_dither(BUFFER_DEPTH, BUFFER_WIDTH, BUFFER_HEIGHT, (BUFFER_DEPTH>>3) * BUFFER_WIDTH, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
    //caca_set_dither_gamma(dither, -1.0);
    caca_dither_bitmap(cv, 0, 0, caca_get_canvas_width(cv), caca_get_canvas_height(cv), dither, (char *)pixelBuffer);
    caca_free_dither(dither);
}

Makefile


CC=gcc
LIBS=gl X11
CFLAGS=-Os -s -Wall -std=c99 `pkg-config --cflags gl glu sdl2 caca`
LDFLAGS=`pkg-config --libs gl glu sdl2 caca`
OBJECTS=main.o

main: main.o
	$(CC) $(CFLAGS) $(OBJECTS) $(LDFLAGS) -o main

.PHONY: clean
clean:
	-rm main
	-rm *.o

Explanation

So most of this code should be familiar to anyone who's done much with OpenGL. If you haven't, I strongly recommend taking a tour of Neon Helium's tutorials. Past getting a basic window set up with SDL and drawing some geometry, there really isn't too much to this. The only tricky bits are getting the RGB bits in the right order and storing the image to an off screen buffer with glReadPixels. By default glReadPixels will store the image upside down, so you can either render the scene upside down with glScale or use the raster glPixelZoom function with a negative value. Here I render the scene twice to allow for simultaneous normal rendering and ASCII rendering if desired (remove the SDL_WINDOW_HIDDEN flag). Although using pixelZoom would have been much faster and smarter in retrospect.

Final Thoughts

This has a ways to go before becoming small enough to fit in to a 4k demoscene executable size limit. For starters I've set the compiler flags in the makefile to strip symbols and optimize for size, but this is not nearly enough.

Targetting x86_64 and the dependency on SDL are the largest contributors to the bloat here. By targeting 32 bit and using X11 directly with glx will instantly cut the size down by several kilobytes. Many other techniques can be used to bring our binary size down much lower than the 4k limit. Stay tuned for a future post on this very issue!

Making the gif animations for this post was a lot of fun. I used the Linux program simplescreenrecorder to capture window output (to H.264 mkv format). VLC to convert the video to a series of png files:


#!/bin/sh

vlc renderascii.mkv --rate=1 --video-filter scene --vout=dummy --aout=dummy --scene-format=png --scene-ratio=24 --scene-prefix=snap --scene-path=renderascii/ vlc://quit

And finally imagemagick's convert utility to animate the png files into a looping gif:


#!/bin/sh

convert -delay 24 -loop 0 *.png renderascii.gif

Pretty cool stuff!