Background
I am trying to create a 2D tile engine from scratch using c# 4.0 and WPF. I am not attempting to build the next greatest 2D game on the planet, rather I am trying to learn how to manipulate images and graphics using .NET.
Goal
I am trying to apply a mask to a bitmap. My bitmap is in colour and my mask is a monochrome bitmap. Where ever the colour white appears, I am trying to replace the colour in the original bitmap at the corresponding location with a transparent colour.
My goal is to be able to store a collection of images in memory which have been masked at runtime so that I can build a composite of them in layers - i.e. a background first, an item on the map on top of it, and finally a player avatar on top of that on demand.
What I have looked at so far...
I have been at this for quite a while now, hence my posting on SO - the following details what I have looked at so far:
I have looked at this post Alpha masking in c# System.Drawing? which shows how to use an unsafe method to manipulate an image using pointer arithmetic.
Also, this post Create Image Mask which shows how to use SetPixel / GetPixel to swap colours.
Various MSDN pages and other ad-hoc blogs encompassing the topic.
What I have tried...
I have tried the unsafe pointer arithmetic method: Something like this (Please be aware this has been butchered, put together again, and repeated because I have been tinkering trying to understand why this wasn't doing what I wanted it to do):
private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
{
Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
output.MakeTransparent();
var rect = new Rectangle(0, 0, input.Width, input.Height);
var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
unsafe
{
for (int y = 0; y < input.Height; y++)
{
byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
for (int x = 0; x < input.Width; x++)
{
//I think this is right - if the blue channel is 0 than all of them are which is white?
if (ptrMask[4*x] == 0)
{
ptrOutput[4*x] = ptrInput[4*x]; // blue
ptrOutput[4*x + 1] = ptrInput[4*x + 1]; // green
ptrOutput[4*x + 2] = ptrInput[4*x + 2]; // red
}
else
ptrOutput[4 * x + 3] =255; // alpha
}
}
}
mask.UnlockBits(bitsMask);
input.UnlockBits(bitsInput);
output.UnlockBits(bitsOutput);
return output;
}
Also tried a variation on the other method which looked a bit like this (sorry I appear to have binned the code along the way - this is part real code/ part pseudo code - just to get across the approach I was trying...)
{
Bitmap input...
Bitmap mask...
Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
output.MakeTransparent();
for (int x = 0; x < output.Width; x++)
{
for (int y = 0; y < output.Height; y++)
{
Color pixel = mask.GetPixel(x, y);
//Again - not sure - my thought is if any channel has colour then it isn't white - so make it transparent.
if (pixel.B > 0)
output.SetPixel(x, y, Color.Transparent);
else
{
pixel = input.GetPixel(x,y);
output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
}
}
}
My actual problem...
So the methods above yield either the colour image on a black background or a colour image on a white background (as far as I can tell!) When I try to composite the images using code like this:
public Bitmap MakeCompositeBitmap(params string[] names)
{
Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
output.MakeTransparent();
foreach (string name in names)
{
//GetImage() returns an image which I have previously masked and cached.
Bitmap layer = GetImage(name);
for (int x = 0; x < output.Width; x++)
{
for (int y = 0; y < output.Height; y++)
{
Color pixel = layer.GetPixel(x, y);
if (pixel.A > 0)
output.SetPixel(x, y, Color.Transparent);
else
output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
}
}
}
return output;
}
I am calling this code with the names of various layers I want composited at runtime, the background first, then the foreground to give me an image I can show on my UI. I am expecting the cached image to have a transparency set by the mask, and I am expecting this transparency to be ignored when rendering my image (and I have given up on pointer arithmetic at the moment, I just want to get it working before optimizing it!). What I end up with is the foreground image only.
So - I must re-iterate again, I am totally new at this, and know that there is probably going to be a faster way of doing this - but it seems like such a simple problem and I just want to get to the bottom of it!
I am using WPF, c# 4.0, VS2013, displaying the images in an Image element which is hosted in a grid, which is hosted by a listviewItem, hosted in a ListView, which is hosted in a grid, and finally hosted by a window (if that is of any relevance at all...)
So to summarize my approach.. I am Getting an image and a corresponding mask. The image is colour, the mask is monochrome. I am applying my mask to my image and caching it. I am building a composite image from multiple images in the cache; drawing each one in turn (apart from the transparent bits) onto a new bitmap
And now I am pulling my hair out, because I have been at this for a while, I'm only seeing the foreground image! I can see a lot of information on the topic but don't have the necessary experience to apply that information to create a solution to my problem.
Edit: My solution (thanks to thumbmunkeys for pointing out the errors in my logic:
Fixed DoApplyMask method
private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
{
Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
output.MakeTransparent();
var rect = new Rectangle(0, 0, input.Width, input.Height);
var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
unsafe
{
for (int y = 0; y < input.Height; y++)
{
byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
for (int x = 0; x < input.Width; x++)
{
//I think this is right - if the blue channel is 0 than all of them are (monochrome mask) which makes the mask black
if (ptrMask[4 * x] == 0)
{
ptrOutput[4 * x] = ptrInput[4 * x]; // blue
ptrOutput[4 * x + 1] = ptrInput[4 * x + 1]; // green
ptrOutput[4 * x + 2] = ptrInput[4 * x + 2]; // red
//Ensure opaque
ptrOutput[4*x + 3] = 255;
}
else
{
ptrOutput[4 * x] = 0; // blue
ptrOutput[4 * x + 1] = 0; // green
ptrOutput[4 * x + 2] = 0; // red
//Ensure Transparent
ptrOutput[4 * x + 3] = 0; // alpha
}
}
}
}
mask.UnlockBits(bitsMask);
input.UnlockBits(bitsInput);
output.UnlockBits(bitsOutput);
return output;
}
Fixed MakeCompositeBitmap() method
public Bitmap MakeCompositeBitmap(params string[] names)
{
Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
output.MakeTransparent();
foreach (string name in names)
{
Bitmap layer = GetImage(name);
for (int x = 0; x < output.Width; x++)
{
for (int y = 0; y < output.Height; y++)
{
Color pixel = layer.GetPixel(x, y);
if (pixel.A > 0) // 0 means transparent, > 0 means opaque
output.SetPixel(x, y, Color.FromArgb(pixel.A, pixel.R, pixel.G, pixel.B));
}
}
}
return output;
}