📜 Jelle Pelgrims

Business card demo

After reading a few articles about people building business card sized raytracers I decided to do something similar for my amusement. Also, I had a job fair soon and needed something to impress recruiters on the spot. Because I'm not very knowledgable on raytracing I decided to write a business card sized raycaster instead. Raycasting is a similar algorithm for drawing psuedo-3D graphics. The specific algorithm I implemented was originally used in the game Comanche. It can be programmed in 20 lines of python code, which is perfect for a small demo.

Besides the raycasting I also had my mind set on the demo including procedurally generated terrain and rendering (i.e. simply not using any external libraries for those aspects). For the terrain generation I used an algorithm that randomly places hills. For the rendering I reused the ANSI terminal renderer that I wrote for a previous project.

I chose to program this demo in C++, seeing as it allows for certain "optimizations" regarding code size. For example, it is easy to rename the int keyword to i with a simple typedef. It also doesn't care about whitespace, as Python does (I wrote a prototype of my demo in Python before doing it properly in C++, but due to python having significant whitespace it was of course completely unusable for the purpose of writing a business card sized program).

The minified code

The end result of this project is the code you can see below. It contains 1724 characters (20 lines of maximum 85 characters), with 90 characters for the comment. It is most likely still possible to further reduce this code in size (blame my limited knowledge of C++). One thing that should be possible is to remove the std::max_element function usage. This would free up some characters and one extra line (because of the #include that can be removed).

#include <iostream>                  // https://jellepelgrims.com/posts/business_card
#include <algorithm>                 // g++ card.cpp -o card && ./card && reset
typedef double d;typedef int i;i K(i x){return x*x;}i J=255;i W=200;i H=800;i E=80;i 
F=48;i* h(){i* m=new i[W*H];d M=0;for(i z=0;z<W;z++){i X=rand()%W;i Y=rand()%H;i B=
rand()%(26)+5;for(i x=0;x<W;x++){for(i y=0;y<H;y++){d b=K(B)-(K(x-X)+K(y-Y));if(b>0){
d n=m[y*W+x]+b;m[y*W+x]=i(n);M=n>M?n:M;}}}}for(i l=0;l<=W*H;l++){i H=m[l];m[l]=i((H/M
)*100);}return m;}struct R{i r,g,b;};R* c(i* m){d a=*std::max_element(m,m+W*H);R* A=
new R[W*H];for(i g;g<=W*H;g++){i v=i((m[g]/a)*J);R C={v,v,v};A[g]=C;}for(i x=0;x<W;x
++){for(i y=0;y<H;y++){R C=A[y*W+x];i h=m[y*W+x];A[y*W+x]=h<=5?R{0,0,40}:h<=a*(.1)?R{
C.r-10,C.g-10,C.b+40}:h<=a*(.2)?R{C.r+120,C.g+80,C.b+40}:h<=a*(.5)?R{C.r-10,C.g+20,C.
b-10}:h<=a*(.9)?R{C.r-40,C.g-50,C.b-40}:R{J,J,J};if(m[y*W+(x-1)]>m[y*W+x]){R C=A[y*W+
x];d w=.85;C.r*=w;C.g*=w;C.b*=w;A[y*W+x]=C;}}}return A;}struct P{d x,y;};R* r(i* H,R*
C,P p,i o){R* B=new R[E*F];i a=50+H[i(p.y)*W+i(p.x)];for(i x=0;x<E;x++){for(i y=0;y<F
;y++){B[y*E+x]=R{86+y-x,213+y-x,J+y-x};}}for(i z=100;z>1;z--){P l={-z+p.x,-z+p.y};P u
={z+p.x,-z+p.y};d U=(u.x-l.x)/E;for(i x=0;x<E;x++){d n=(a-H[i(l.y)*W+i(l.x)])/d(z)*10
+7;if(n>=F)continue;if(!n)n=0;for(i y=i(n);y<i(F);y++){R c=C[i(l.y)*W+i(l.x)];if(z>o)
{d m=1-((o-z)/d(o))*1.5;c=R{i(c.r*m),i(c.g*m),i(c.b*m)};}B[y*E+x]=c;}l.x+=U;}}return 
B;}i D(R* B){printf("\e[?25l\e[H");for(i y=0;y<F;y+=2){for(i x=0;x<E;x++){R c=B[y*E+x
];R k=B[(y+1)*E+x];printf("\e[38;2;%d;%d;%dm\e[48;2;%d;%d;%dm▀",c.r,c.g,c.b, k.r,k.g,
k.b);}}}i main(){i* A=h();P p={100,H};for(;p.y>i(98);p.y-=2){D(r(A,c(A),p,65));}}

You can test this code by compiling it and running it in a terminal that supports ANSI terminal escape codes.

# Compile with g++
$ g++ card.cpp -o card
# Run compiled demo and reset terminal
$ ./card && reset

This demo will show the following:

Gif of demo

I have managed to stuff the following features into the 1724 characters of code:

Below you can see the actual business card with the demo placed on the back.

Business card

Code walkthrough

The full source code can be found on GitHub.

1. Rendering

Raycasting

struct pos {
    double x, y;
};

rgb* render(int* heightmap, rgb* colormap, pos p, int height, int horizon, int scale_height, int distance, int fog_distance, int screen_width, int screen_height, int map_width, int map_height) {
    rgb* buffer = new rgb[screen_width*screen_height];

    int actual_height = height + heightmap[int(p.y)*map_width + int(p.x)];

    for (int x = 0; x < screen_width; x++) {
        for (int y = 0; y < screen_height; y++) {
            buffer[y*screen_width + x] = rgb{ 86 + y - x, 213 + y - x, 255 + y - x };
        }
    }

    for (int z = distance; z > 1; z--) {
        pos pleft = { -z + p.x, -z + p.y };
        pos pright = { z + p.x, -z + p.y };

        double dx = (pright.x - pleft.x) / screen_width;

        for (int x = 0; x < screen_width; x++) {
            double height_on_screen = (actual_height - heightmap[int(pleft.y)*map_width + int(pleft.x)]) / double(z) * scale_height + horizon;

            if (height_on_screen >= screen_height) { continue; }
            if (height_on_screen < 0) { height_on_screen = 0; }

            for (int y = floor(height_on_screen); y < floor(screen_height); y++) {
                rgb color = colormap[int(pleft.y)*map_width + int(pleft.x)];
                if (z > fog_distance) {
                    double m = 1 - ((fog_distance - z) / double(fog_distance))*1.5;
                    color = rgb{int(color.r*m), int(color.g*m), int(color.b*m) };
                }
                buffer[y*screen_width + x] = color;
            }

            pleft.x += dx;
        }
    }

    return buffer;
}

Coloring

struct rgb {
    int r, g, b;
};

rgb* generate_colormap(int* heightmap, int map_width, int map_height) {
    double max_height = *std::max_element(heightmap, heightmap + map_width * map_height);

    rgb* colormap = new rgb[map_width*map_height];

    for (int x = 0; x < map_width; x++) {
        for (int y = 0; y < map_height; y++) {
            int grey_value = std::min(255, int((heightmap[y*map_width + x] / max_height) * 255));
            rgb color = { grey_value, grey_value, grey_value };
            colormap[y*map_width + x] = color;
        }
    }

    for (int x = 0; x < map_width; x++) {
        for (int y = 0; y < map_height; y++) {
            rgb color = colormap[y*map_width + x];
            int height = heightmap[y*map_width + x];

            if (height <= 5) {
                color = rgb{ 0, 0, 40 };
            } else if (height <= max_height * (0.1)) {
                color = rgb{ color.r - 10, color.g - 10, color.b + 40 };
            } else if (height <= max_height * (0.2)) {
                color = rgb{ color.r + 120, color.g + 80, color.b + 40 };
            } else if (height <= max_height * (0.5)) {
                color = rgb{ color.r - 10, color.g + 20, color.b - 10 };
            } else if (height <= max_height * (0.9)) {
                color = rgb{ color.r - 40, color.g - 50, color.b - 40 };
            }

            colormap[y*map_width + x] = color;

            if (x > 0 && x < map_width && heightmap[y*map_width + (x - 1)] > heightmap[y*map_width + x]) {
                rgb color = colormap[y*map_width + x];
                color.r *= 0.85;
                color.g *= 0.85;
                color.b *= 0.85;
                colormap[y*map_width + x] = color;
            }

        }
    }

    return colormap;

}

2. Terrain generation

int* generate_heightmap(int width, int height) {
    int* heightmap = new int[width*height]{0};

    const int MIN_HILL_RADIUS = 5;
    const int MAX_HILL_RADIUS = 30;
    const int NR_OF_HILLS = 200;

    for (int i = 0; i < NR_OF_HILLS; i++) {
        int hill_x = rand() % width;
        int hill_y = rand() % height;
        int hill_radius = rand() % (MAX_HILL_RADIUS - MIN_HILL_RADIUS + 1) + MIN_HILL_RADIUS;

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double hill_height = pow(hill_radius, 2) - (pow(x - hill_x, 2) + pow(y - hill_y, 2));
                if (hill_height > 0) {
                    double new_height = heightmap[y*width + x] + hill_height;
                    heightmap[y*width + x] = int(new_height);
                }
            }
        }
    }

    double max_height = *std::max_element(heightmap, heightmap + width * height);

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            int cur_height = heightmap[y*width + x];
            heightmap[y*width + x] = int((cur_height / max_height) * 100);
        }
    }

    return heightmap;
}

3. Terminal drawing

void draw(rgb* buffer, int screen_width, int screen_height) {
    printf("\e[?25l\e[H");

    for (int y = 0; y < screen_height; y += 2) {
        for (int x = 0; x < screen_width; x++) {
            rgb color1 = buffer[y*screen_width + x];
            rgb color2 = buffer[(y + 1)*screen_width + x];
            printf("\e[38;2;%d;%d;%dm", color1.r, color1.g, color1.b);
            printf("\e[48;2;%d;%d;%dm▀", color2.r, color2.g, color2.b);
        }
    }
    fflush(stdout);
}

4. Movement

void fly() {
    system("clear");
    srand( time(0) );

    int screen_width = 80;
    int screen_height = 48;

    int map_width = 200;
    int map_height = 400;

    int* heightmap = generate_heightmap(map_width, map_height);
    rgb* colormap = generate_colormap(heightmap, map_width, map_height);

    pos point = { 100, 400 };

    rgb* screenmap = new rgb[screen_width*screen_height];

    for (; point.y > int(100-2); point.y-=2) {
        rgb* buffer = render(heightmap, colormap, point, 50, 7, 10, 100, 65, screen_width, screen_height, map_width, map_height);
        draw(buffer, screen_width, screen_height);
        usleep(50000);
    }
}

5. Minification of code

  1. Remove whitespace
  2. Rename all functions and variables to single letters (either upper- or lowercase)
  3. Rename types to single letters using typedefs

References

  1. Github page describing the raycasting algorithm
  2. Tutorial describing generated terrains using randomly placed hills