You have your solution already, but hey, it is possible to go without the alpha channel and make a certain color transparent. While participating in Global Game Jam (48 hours limited game competition), we needed to make a lot of sprites for different objects quickly, without using any complicated tools.
We actually ended up using a digital camera and the windows paint (mspaint). We set up a rule that the upper left corner of the image must always contain the transparent color (so the transparent color could be pretty much any color the artist chose). When the image was loaded, the alpha channel was set accordingly to the occurence of the transparent color. While that worked well, it still left some of the transparent color leak into the image (thanks to texture filtering).
/**
* @brief a simple raster image with fixed RGBA8 storage
*
* The image data are always RGBA8. Alpha is stored in the most significant byte,
* followed by red, green and blue with decreasing significance.
*
* The storage is very simple, each 32 bits in the buffer contains a single pixel,
* the first pixel is in top left corner, there is no scanline padding.
*/
struct TBmp {
char n_former_bpp; /**< @brief former bpp, before conversion to RGBA8 */
bool b_grayscale; /**< @brief grayscale flag (if set, the bitmap is assumed
to contain grayscale image, stored as RGBA8) */
bool b_alpha; /**< @brief alpha channel flag (if set, the alpha channel is significant;
otherwise it's expected to be 0xff in all image pixels) */
int n_width; /**< @brief image width, in pixels */
int n_height; /**< @brief image height, in pixels */
uint32_t *p_buffer; /**< @brief pointer to image data */
};
void TransparentColor_to_Alpha(TBmp *p_sprite, bool b_force_alpha_recalc = false)
{
if(b_force_alpha_recalc || !p_sprite->b_alpha) {
uint32_t n_transparent_color = p_sprite->p_buffer[0] & 0xffffff;
// get transparent color from lower left corner
for(int i = 0, n = p_sprite->n_width * p_sprite->n_height; i < n; ++ i) {
uint32_t n_color = p_sprite->p_buffer[i];
if(n_color == n_transparent_color)
;//p_sprite->p_buffer[i] = n_color; // do nothing, color is transparent and alpha is zero
else if((n_color & 0xffffff) == n_transparent_color)
p_sprite->p_buffer[i] = n_color & 0xffffff; // clear alpha
else
p_sprite->p_buffer[i] = n_color | 0xff000000U; // set alpha
}
// calculate alpha based on transparent color (binary only)
p_sprite->b_alpha = true;
}
// build alpha channel using "transparent color"
}
In order to remove the transparent color from the image, we wrote additional function that would duplicate color of the boundary pixels, effectively erasing the transparent color from the image (that can be done because the transparency is now in alpha channel).
bool Sprite_FloodEdgeColor(TBmp *p_sprite, int n_max_grow_step_num = 0)
{
{
uint32_t n_transparent_color = p_sprite->p_buffer[0] & 0xffffff;
// get transparent color from lower left corner
TBmp *p_clone;
if(!(p_clone = p_sprite->p_Clone()))
return false;
// clone the bitmap
uint32_t *p_buffer = p_sprite->p_buffer;
uint32_t *p_buffer_pong = p_clone->p_buffer;
for(int i = 0; !n_max_grow_step_num || i < n_max_grow_step_num; ++ i) {
bool b_change = false;
for(int y = 0, w = p_sprite->n_width, h = p_sprite->n_height; y < h; ++ y) {
for(int x = 0; x < w; ++ x) {
if(p_buffer[x + w * y] == n_transparent_color) {
int n_neigh_rb = 0, n_neigh_g = 0;
int n_neigh_num = 0;
for(int sy = max(1, y) - 1, ey = min(y + 1, h - 1); sy <= ey; ++ sy) {
for(int sx = max(1, x) - 1, ex = min(x + 1, w - 1); sx <= ex; ++ sx) {
if(sx == x && sy == y)
continue; // skip self (it's transparent anyway)
uint32_t n_neigh = p_buffer[sx + w * sy];
if(n_neigh != n_transparent_color) {
n_neigh_rb += n_neigh & 0xff00ff;
n_neigh_g += n_neigh & 0xff00;
++ n_neigh_num;
}
}
}
// gather neighbour colors
if(n_neigh_num > 2) {
int r = (n_neigh_rb & 0xffff0000) / n_neigh_num;
int g = n_neigh_g / n_neigh_num;
int b = (n_neigh_rb & 0xffff) / n_neigh_num;
uint32_t n_color = (0xff0000 & min(0xff0000, r)) |
(0xff00 & min(0xff00, g)) | (0xff & min(0xff, b));
// calculate average neighbor color
p_buffer_pong[x + w * y] = n_color;
b_change = true;
}
} else
p_buffer_pong[x + w * y] = p_buffer[x + w * y]; // just copy
}
}
// grow 1px into transparent color
if(b_change || p_buffer != p_sprite->p_buffer)
std::swap(p_buffer, p_buffer_pong);
// swap the buffers ...
if(!b_change)
break;
}
if(p_buffer != p_sprite->p_buffer) {
memcpy(p_sprite->p_buffer, p_buffer,
p_sprite->n_width * p_sprite->n_height * sizeof(uint32_t));
}
// in case the last result is not in
p_clone->Delete();
// cleanup
}
// bleed colors on edge into the transparent space (to enable hifi blending)
return true;
}
That was almost it, but the pictures of the objects we took using the digital camera often had brighter pixels at the edge which were particularly disturbing for the player to look at. So we wrote one more function that would use the median filter to remove the bright pixels from the boundary (while the rest of the image is unaffected).
bool SpriteEdge_MedianFilter(TBmp *p_sprite,
bool b_prefer_darker = true, bool b_5x5_median = true)
{
{
uint32_t n_transparent_color = p_sprite->p_buffer[0] & 0xffffff;
// get transparent color from lower left corner
TBmp *p_clone;
if(!(p_clone = p_sprite->p_Clone()))
return false;
// clone the bitmap
uint32_t *p_buffer = p_sprite->p_buffer;
uint32_t *p_buffer_pong = p_clone->p_buffer;
{
const int n_off = (b_5x5_median)? 2 : 1;
const int n_thresh = (b_5x5_median)? 25 : 9;
bool b_change = false;
for(int y = 0, w = p_sprite->n_width, h = p_sprite->n_height; y < h; ++ y) {
for(int x = 0; x < w; ++ x) {
if(p_buffer[x + w * y] != n_transparent_color) {
uint32_t p_neigh_color[25];
int n_neigh_num = 0;
for(int sy = max(n_off, y) - n_off,
ey = min(y + n_off, h - 1); sy <= ey; ++ sy) {
for(int sx = max(n_off, x) - n_off,
ex = min(x + n_off, w - 1); sx <= ex; ++ sx) {
uint32_t n_neigh = p_buffer[sx + w * sy];
if(n_neigh != n_transparent_color) {
p_neigh_color[n_neigh_num] = n_neigh;
++ n_neigh_num;
}
}
}
// gather neighbour colors (including self)
if(n_neigh_num < n_thresh) { // if the pixel is on the edge ...
uint32_t r[25], g[25], b[25];
for(int i = 0; i < n_neigh_num; ++ i) {
r[i] = p_neigh_color[i] & 0xff0000;
g[i] = p_neigh_color[i] & 0xff00;
b[i] = p_neigh_color[i] & 0xff;
}
std::sort(r, r + n_neigh_num);
std::sort(g, g + n_neigh_num);
std::sort(b, b + n_neigh_num);
// calculate median neighbor color
uint32_t n_self = p_buffer[x + w * y];
int mr, mg, mb;
if(b_prefer_darker) {
mr = min(r[n_neigh_num / 2], n_self & 0xff0000);
mg = min(g[n_neigh_num / 2], n_self & 0xff00);
mb = min(b[n_neigh_num / 2], n_self & 0xff);
} else {
mr = r[n_neigh_num / 2];
mg = g[n_neigh_num / 2];
mb = b[n_neigh_num / 2];
}
int a = n_self & 0xff000000U;
p_buffer_pong[x + w * y] = mr | mg | mb | a;
b_change = true;
}
} else
p_buffer_pong[x + w * y] = p_buffer[x + w * y]; // just copy
}
}
// grow 1px into transparent color
if(b_change || p_buffer != p_sprite->p_buffer)
std::swap(p_buffer, p_buffer_pong);
// swap the buffers ...
}
if(p_buffer != p_sprite->p_buffer) {
memcpy(p_sprite->p_buffer, p_buffer,
p_sprite->n_width * p_sprite->n_height * sizeof(uint32_t));
}
// in case the last result is not in
p_clone->Delete();
// cleanup
}
return true;
}
We actually wrote one more function that would erode the opaque parts of the image, effectively making the sprite a selected amount of pixels smaller and removing the problematic regions in case they could not be removed using the median function. That's pretty much it, although it was written in about an hour, it is pretty much ultimate tool for creating quick'n'dirty sprites.
Get full source code.