Pixel-art friendly 2x scaling algorithm (EPX)

Hi All,

I just wanted to share my findings here, there are a couple of different upscaling algos out there that cater especially to pixel art (see more details on the subject at Pixel-art scaling algorithms - Wikipedia)

One that I found very useful (as I render by drawing pixels and then upscaling the final result) was "EPX" or "Eric's Pixel Expansion". The explanation of the algorithm on wikipedia is kind of hard to read in my opinon so I thought I would just share this one here written in C (it uses the pixel manipulation macros dustin has shared from here C macros for working with Playdate bitmap data)

// The pixel manipulation code is from Dustin who has a posted them in a separate post here but I will post it once again:
// Macros to work with playdate bitmaps....
// Determine pixel at x, y is black or white.
#define samplepixel(data, x, y, rowbytes) (((data[(y)*rowbytes+(x)/8] & (1 << (uint8_t)(7 - ((x) % 8)))) != 0) ? kColorWhite : kColorBlack)

// Set the pixel at x, y to black.
#define setpixel(data, x, y, rowbytes) (data[(y)*rowbytes+(x)/8] &= ~(1 << (uint8_t)(7 - ((x) % 8))))

// Set the pixel at x, y to white.
#define clearpixel(data, x, y, rowbytes) (data[(y)*rowbytes+(x)/8] |= (1 << (uint8_t)(7 - ((x) % 8))))

// Set the pixel at x, y to the specified color.
#define drawpixel(data, x, y, rowbytes, color) (((color) == kColorBlack) ? setpixel((data), (x), (y), (rowbytes)) : clearpixel((data), (x), (y), (rowbytes)))

#define SCALE_A 0
#define SCALE_B 2 // Nope that is not a misstake
#define SCALE_C 1
#define SCALE_D 3

void scale_epx2(uint8_t *org, unsigned int orgWidth, unsigned int orgHeight, int orgRowbytes,
                uint8_t *dest, int destRowbytes)
{
    for(unsigned int y = 0; y < orgHeight; y++)
    {
        for(unsigned int x = 0; x < orgWidth; x++)
        {
            // Expand this pixel into 4 new (be aware of corners ofc)            
            unsigned char pixOrg = samplepixel(org, x, y, orgRowbytes);

            // Corners/edges - use pixOrg
            if(x == 0 || y == 0 || x == orgWidth - 1 || y == orgHeight - 1)
            {
                drawpixel(dest, 2 * x ,    2 * y,     destRowbytes, pixOrg);
                drawpixel(dest, 2 * x + 1, 2 * y,     destRowbytes, pixOrg);
                drawpixel(dest, 2 * x,     2 * y + 1, destRowbytes, pixOrg);
                drawpixel(dest, 2 * x + 1, 2 * y + 1, destRowbytes, pixOrg);
            }
            else
            {
                unsigned char ACBD[4];
                ACBD[0] = samplepixel(org, x, y - 1, orgRowbytes);
                ACBD[1] = samplepixel(org, x - 1, y, orgRowbytes);
                ACBD[2] = samplepixel(org, x + 1, y, orgRowbytes);
                ACBD[3] = samplepixel(org, x, y + 1, orgRowbytes);

                // --------------- Pixel 1 ---------------
                // C == A and there are not 3 pixels that are identical
                if( ACBD[SCALE_C] == ACBD[SCALE_A] && 
                    ACBD[SCALE_C] != ACBD[SCALE_D] && 
                    ACBD[SCALE_A] != ACBD[SCALE_B])
                {
                    drawpixel(dest, 2 * x, 2 * y, destRowbytes, ACBD[SCALE_A]); // Use pixel A
                }
                else
                {
                    drawpixel(dest, 2 * x, 2 * y, destRowbytes, pixOrg);
                }
                // ------------------------------------------

                // --------------- Pixel 2 ---------------
                if( ACBD[SCALE_A] == ACBD[SCALE_B] && 
                    ACBD[SCALE_A] != ACBD[SCALE_C] && 
                    ACBD[SCALE_B] != ACBD[SCALE_D])
                {
                    drawpixel(dest, 2 * x + 1, 2 * y, destRowbytes, ACBD[SCALE_B]); // Use pixel B
                }
                else
                {
                    drawpixel(dest, 2 * x + 1, 2 * y, destRowbytes, pixOrg);
                }
                // ------------------------------------------

                // --------------- Pixel 3 ---------------
                if( ACBD[SCALE_D] == ACBD[SCALE_C] && 
                    ACBD[SCALE_D] != ACBD[SCALE_B] && 
                    ACBD[SCALE_C] != ACBD[SCALE_A])
                {
                    drawpixel(dest, 2 * x, 2 * y + 1, destRowbytes, ACBD[SCALE_C]); // Use pixel C
                }
                else
                {
                    drawpixel(dest, 2 * x, 2 * y + 1, destRowbytes, pixOrg);
                }
                // ------------------------------------------

                // --------------- Pixel 4 ---------------
                if( ACBD[SCALE_B] == ACBD[SCALE_D] && 
                    ACBD[SCALE_B] != ACBD[SCALE_A] && 
                    ACBD[SCALE_D] != ACBD[SCALE_C])
                {
                    drawpixel(dest, 2 * x + 1, 2 * y + 1, destRowbytes, ACBD[SCALE_D]); // Use pixel D
                }
                else
                {
                    drawpixel(dest, 2 * x + 1, 2 * y + 1, destRowbytes, pixOrg);
                }
                // ------------------------------------------                
            }
        }
    }
}

This helped me out a lot in my 3D FPS "Red Terror", you can see a post show casing this here:

That was all :slight_smile: I just kind of stumbled upon this gem for scaling pixel art and I dont think that all that many people are aware of this due to its rather specific use case. But for the playdate I think this can be really helpfull.

Cheers
/Jimmie

4 Likes