So here's some context. I'm working on this game called ShiftOS which takes place in an OS that starts off as a bare-bones run of the mill 80s operating system with not many features.
I'm trying to add a mechanic in where the user has to start out with binary (2-color) color depth and can only display black and white on the screen. Then they have to upgrade the color depth from 1-bit to 2-bit to 4-bit all the way to 24-bit. It's a really neat mechanic, but in practice it's seeming to be extremely difficult.
Of course, older systems around this time did at least TRY to make images look nice but of course they were limited by the color palettes given by the engineers, so they had to dither the images to arrange pixels in a way that made it look like the image was using more colors when in all reality it could only be using 2.
So I looked up some good dithering algorithms and started to learn the Floyd-Steinberg algorithm and soon ported it over to C#
and System.Drawing
.
Here's the code that I use.
var bmp = new Bitmap(source.Width, source.Height);
var sourceBmp = (Bitmap)source;
int error = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color c = sourceBmp.GetPixel(x, y);
int gray = ((c.R + c.G + c.B) / 3);
if (gray >= 127)
{
error = gray - 255;
bmp.SetPixel(x, y, Color.White);
}
else
{
error = gray;
bmp.SetPixel(x, y, Color.Black);
}
/*
* Pixel error diffusion map: Floyd-Steinberg. Thanks to Wikipedia.
*
* pixel[x + 1][y ] := pixel[x + 1][y ] + quant_error * 7 / 16
* pixel[x - 1][y + 1] := pixel[x - 1][y + 1] + quant_error * 3 / 16
* pixel[x ][y + 1] := pixel[x ][y + 1] + quant_error * 5 / 16
* pixel[x + 1][y + 1] := pixel[x + 1][y + 1] + quant_error * 1 / 16
*/
if(x - 1 >= 0 && y + 1 != bmp.Height)
{
var bottomRightColor = sourceBmp.GetPixel(x - 1, y + 1);
int bottomRightGray = ((bottomRightColor.R + bottomRightColor.G + bottomRightColor.B) / 3) + ((error * 3) / 16);
if (bottomRightGray < 0)
bottomRightGray = 0;
if (bottomRightGray > 255)
bottomRightGray = 255;
sourceBmp.SetPixel(x - 1, y + 1, Color.FromArgb(bottomRightGray, bottomRightGray, bottomRightGray));
}
if (x + 1 != sourceBmp.Width)
{
var rightColor = sourceBmp.GetPixel(x + 1, y);
int rightGray = ((rightColor.R + rightColor.G + rightColor.B) / 3) + ((error * 7) / 16);
if (rightGray < 0)
rightGray = 0;
if (rightGray > 255)
rightGray = 255;
sourceBmp.SetPixel(x + 1, y, Color.FromArgb(rightGray, rightGray, rightGray));
}
if (x + 1 != sourceBmp.Width && y + 1 != sourceBmp.Height)
{
var bottomRightColor = sourceBmp.GetPixel(x + 1, y + 1);
int bottomRightGray = ((bottomRightColor.R + bottomRightColor.G + bottomRightColor.B) / 3) + ((error) / 16);
if (bottomRightGray < 0)
bottomRightGray = 0;
if (bottomRightGray > 255)
bottomRightGray = 255;
sourceBmp.SetPixel(x + 1, y + 1, Color.FromArgb(bottomRightGray, bottomRightGray, bottomRightGray));
}
if (y + 1 != sourceBmp.Height)
{
var bottomColor = sourceBmp.GetPixel(x, y + 1);
int bottomGray = ((bottomColor.R + bottomColor.G + bottomColor.B) / 3) + ((error * 5) / 16);
if (bottomGray < 0)
bottomGray = 0;
if (bottomGray > 255)
bottomGray = 255;
sourceBmp.SetPixel(x, y + 1, Color.FromArgb(bottomGray, bottomGray, bottomGray));
}
}
}
Note that source
is an Image
that is passed through to the function via an argument.
This code works pretty well, however the problem is, the dithering is happening on a separate thread to minimize slowdowns/lag in the game, and while the dithering is occurring, the regular 24-bit colors/images of the operating system are shown. This would be fine if the dithering didn't take so long.
However I notice that the algorithm is EXTREMELY slow in this code and depending on the size of the image I'm dithering, the dithering process could take up to more than a minute!
I have applied all optimizations I can think of - such as running things in a separate thread from the game thread and invoking an Action that's given to the function when the thread finishes but this only shaves off a tiny bit of time if any.
So I'm wondering if there are any further optimizations to make this operate any faster, a few seconds total if possible. I'd like to also note that while the dithering operation is occurring I am having noticeable system lag - the mouse even jitters and jumps around at times. Not cool for those must-have-60FPS PC master race guys.