Introduction
This article is about a simple but effective method of rendering soft-shadows and shape-glowing using plain Windows GDI. The goal here is to show the reader that not only GDI+ (from .NET) and different commercial 3rd-party graphical libraries are capable of doing this type of work, but it can also be done by means of applying simple image processing techniques on the selected image. I have found some resources here on The Code Project considering this topic but most of them are done using the new Microsoft GDI+ graphics library that is far away from the traditional Windows GDI API.
Background
Rendering glowing and soft-shadows probably looks like a hard thing to do, but it really isn't. We can start from the original bitmap and build the separate layers (new bitmaps) for, say shadow or glow. The point is that the original data (original bitmap) is drawn two times, and a simple blur technique is applied as an intermediate operation.
Shadows - Where Do They Come From?
Basically, from some source of light. They are the projection of an object on different surfaces. The original bitmap is copied (excluding the transparent pixels) to the new temporary bitmap. Usually some horizontal and vertical offsets are used to simulate shadow distance. This temporary bitmap is then low-pass filtered (usually blur kernel is applied) and copied to the final destination bitmap (excluding the transparent pixels). The original image is then again copied to the destination bitmap this time (excluding the transparent pixels). Here are the steps:
- Copy original bitmap to a new one of the same size but with the small offset (the shadow offset) and make all non-transparent pixels of the same color (the shadow color)
- Apply blurring on the shadow bitmap
- Render this new bitmap to the destination bitmap
- Render original bitmap to the destination bitmap (skipping all transparent pixels)
This method is explained through the CreateShadow(COLORREF transparentColor, COLORREF shadowColor)
function, as shown below:
void CreateShadow(COLORREF transparentColor, COLORREF shadowColor)
{
int i,j, k, l;
RECT rect = {0, 0, 300, 200};
HDC hDC = ::GetDC(NULL);
HDC hTempDC = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap = (HBITMAP)::SelectObject(hTempDC, hTempBitmap);
HDC hTempDC2 = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap2 = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap2 = (HBITMAP)::SelectObject(hTempDC2, hTempBitmap2);
HDC hTempDC3 = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap3 = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap3 = (HBITMAP)::SelectObject(hTempDC3, hTempBitmap3);
::ReleaseDC(NULL, hDC);
HBRUSH hBgBrush = ::CreateSolidBrush(transparentColor);
::FillRect(hTempDC, &rect, hBgBrush);
::FillRect(hTempDC2, &rect, hBgBrush);
::BitBlt(hTempDC3, 0, 0, 300, 200, m_hBgDC, 0, 0, SRCCOPY);
::DeleteObject(hBgBrush);
CreateDrawing(hTempDC2);
int shadowOffset = 5;
::TransparentBlt(hTempDC, shadowOffset, shadowOffset, 300, 200,
hTempDC2, 0, 0, 300, 200, transparentColor);
BITMAP bmpOrig;
GetObject(m_hMemBitmap, sizeof(BITMAP), &bmpOrig);
int sizeOrig = bmpOrig.bmWidthBytes * bmpOrig.bmHeight;
BYTE* pDataOrig = new BYTE[sizeOrig];
GetBitmapBits(m_hMemBitmap, sizeOrig, pDataOrig);
int bppOrig = bmpOrig.bmBitsPixel >> 3;
BITMAP bmpSrc;
GetObject(hTempBitmap, sizeof(BITMAP), &bmpSrc);
int sizeSrc = bmpSrc.bmWidthBytes * bmpSrc.bmHeight;
BYTE* pDataSrc = new BYTE[sizeSrc];
GetBitmapBits(hTempBitmap, sizeSrc, pDataSrc);
int bppSrc = bmpSrc.bmBitsPixel >> 3;
BITMAP bmpSrc2;
GetObject(hTempBitmap2, sizeof(BITMAP), &bmpSrc2);
int sizeSrc2 = bmpSrc2.bmWidthBytes * bmpSrc2.bmHeight;
BYTE* pDataSrc2 = new BYTE[sizeSrc2];
GetBitmapBits(hTempBitmap2, sizeSrc2, pDataSrc2);
int bppSrc2 = bmpSrc2.bmBitsPixel >> 3;
BITMAP bmpSrc3;
GetObject(hTempBitmap3, sizeof(BITMAP), &bmpSrc3);
int sizeSrc3 = bmpSrc3.bmWidthBytes * bmpSrc3.bmHeight;
BYTE* pDataSrc3 = new BYTE[sizeSrc3];
GetBitmapBits(hTempBitmap3, sizeSrc3, pDataSrc3);
int bppSrc3 = bmpSrc3.bmBitsPixel >> 3;
BITMAP bmpDst;
GetObject(m_hShadowBitmap, sizeof(BITMAP), &bmpDst);
int sizeDst = bmpDst.bmWidthBytes * bmpDst.bmHeight;
BYTE* pDataDst = new BYTE[sizeDst];
GetBitmapBits(m_hShadowBitmap, sizeDst, pDataDst);
int bppDst = bmpDst.bmBitsPixel >> 3;
BYTE redTransparent = GetRValue(transparentColor);
BYTE greenTransparent = GetGValue(transparentColor);
BYTE blueTransparent = GetBValue(transparentColor);
BYTE redShadow = GetRValue(shadowColor);
BYTE greenShadow = GetGValue(shadowColor);
BYTE blueShadow = GetBValue(shadowColor);
int verticalOffset = 0;
int horizontalOffset;
int totalOffset;
BYTE red, green, blue;
for (i=0; i<bmpSrc.bmHeight; i++)
{
horizontalOffset = 0;
for (j=0; j<bmpSrc.bmWidth; j++)
{
totalOffset = verticalOffset + horizontalOffset;
blue = pDataSrc[totalOffset];
green = pDataSrc[totalOffset+1];
red = pDataSrc[totalOffset+2];
if ((red != redTransparent) || (green != greenTransparent) ||
(blue != blueTransparent))
{
pDataSrc3[totalOffset] = blueShadow;
pDataSrc3[totalOffset+1] = greenShadow;
pDataSrc3[totalOffset+2] = redShadow;
}
horizontalOffset += bppSrc;
}
verticalOffset += bmpSrc.bmWidthBytes;
}
BYTE* pDataTemp = new BYTE[sizeDst];
memcpy(pDataTemp, pDataSrc3, sizeDst);
BYTE* pDataTemp2 = new BYTE[sizeDst];
memcpy(pDataTemp2, pDataSrc, sizeDst);
int filterSize = 5;
int filterHalfSize = filterSize >> 1;
int filterX, filterY, filterOffset;
int resultRed, resultGreen, resultBlue;
int resultRed2, resultGreen2, resultBlue2;
verticalOffset = 0;
for (i=filterHalfSize; i<bmpDst.bmHeight-filterHalfSize; i++)
{
horizontalOffset = 0;
for (j=filterHalfSize; j<bmpDst.bmWidth-filterHalfSize; j++)
{
totalOffset = verticalOffset + horizontalOffset;
if ((i>=filterHalfSize) && (i<bmpDst.bmHeight-filterHalfSize) &&
(j>=filterHalfSize) && (j<bmpDst.bmWidth-filterHalfSize))
{
resultRed = resultGreen = resultBlue = 0;
resultRed2 = resultGreen2 = resultBlue2 = 0;
filterY = verticalOffset;
for (k=-filterHalfSize; k<=filterHalfSize; k++)
{
filterX = horizontalOffset;
for (l=-filterHalfSize; l<=filterHalfSize; l++)
{
filterOffset = filterY + filterX;
resultBlue += pDataSrc3[filterOffset];
resultGreen += pDataSrc3[filterOffset+1];
resultRed += pDataSrc3[filterOffset+2];
resultBlue2 += pDataSrc[filterOffset];
resultGreen2 += pDataSrc[filterOffset+1];
resultRed2 += pDataSrc[filterOffset+2];
filterX += bppDst;
}
filterY += bmpDst.bmWidthBytes;
}
pDataTemp[totalOffset] = resultBlue / (filterSize*filterSize);
pDataTemp[totalOffset+1] = resultGreen / (filterSize*filterSize);
pDataTemp[totalOffset+2] = resultRed / (filterSize*filterSize);
pDataTemp2[totalOffset] = resultBlue2 / (filterSize*filterSize);
pDataTemp2[totalOffset+1] = resultGreen2 / (filterSize*filterSize);
pDataTemp2[totalOffset+2] = resultRed2 / (filterSize*filterSize);
}
horizontalOffset += bppDst;
}
verticalOffset += bmpDst.bmWidthBytes;
}
verticalOffset = 0;
double alpha=1.0, alpha_koef;
double shadow_default_intensity = (redShadow + greenShadow + blueShadow) / 3;
double shadow_intenzity, shadow_koef;
for (i=0; i<bmpDst.bmHeight; i++)
{
horizontalOffset = 0;
for (j=0; j<bmpDst.bmWidth; j++)
{
totalOffset = verticalOffset + horizontalOffset;
if ((pDataTemp2[totalOffset+2] != redTransparent) ||
(pDataTemp2[totalOffset+1] != greenTransparent) ||
(pDataTemp2[totalOffset] != blueTransparent))
{
shadow_intenzity = (pDataTemp2[totalOffset] +
pDataTemp2[totalOffset+1] + pDataTemp2[totalOffset+2]) / 3;
shadow_koef =
(shadow_intenzity - 255) / (shadow_default_intensity - 255);
if (fabs(shadow_koef) > 0.5)
shadow_koef = 0.5 * (fabs(shadow_koef) / shadow_koef);
if (shadow_default_intensity == 255)
alpha_koef = 0.0;
else
alpha_koef = alpha * shadow_koef;
blue = (BYTE)(alpha_koef*pDataTemp[totalOffset] +
(1.0-alpha_koef)*pDataDst[totalOffset]);
green = (BYTE)(alpha_koef*pDataTemp[totalOffset+1] +
(1.0-alpha_koef)*pDataDst[totalOffset+1]);
red = (BYTE)(alpha_koef*pDataTemp[totalOffset+2] +
(1.0-alpha_koef)*pDataDst[totalOffset+2]);
pDataSrc3[totalOffset] = blue;
pDataSrc3[totalOffset+1] = green;
pDataSrc3[totalOffset+2] = red;
}
else
{
pDataSrc3[totalOffset] = pDataDst[totalOffset];
pDataSrc3[totalOffset+1] = pDataDst[totalOffset+1];
pDataSrc3[totalOffset+2] = pDataDst[totalOffset+2];
}
horizontalOffset += bppDst;
}
verticalOffset += bmpDst.bmWidthBytes;
}
::SetBitmapBits(m_hShadowBitmap, sizeDst, pDataSrc3);
delete pDataOrig;
delete pDataTemp;
delete pDataTemp2;
delete pDataSrc;
delete pDataSrc2;
delete pDataSrc3;
delete pDataDst;
if (hTempDC)
{
::SelectObject(hTempDC, hOldTempBitmap);
::DeleteDC(hTempDC);
::DeleteObject(hTempBitmap);
}
if (hTempDC2)
{
::SelectObject(hTempDC2, hOldTempBitmap2);
::DeleteDC(hTempDC2);
::DeleteObject(hTempBitmap2);
}
if (hTempDC3)
{
::SelectObject(hTempDC3, hOldTempBitmap3);
::DeleteDC(hTempDC3);
::DeleteObject(hTempBitmap3);
}
CreateDrawing(m_hShadowDC);
}
Glowing - Where Am I Then?
A similar story goes for object glowing effect. It appears as if there is a shining aura around the object. The steps are almost the same:
- Copy original bitmap to a new one of the same size but with small offsets in all four directions (the glowing directions) and make all non-transparent pixels of the same color (the glowing color)
- Apply blurring on the glowing bitmap
- Render this new bitmap to the destination bitmap
- Render original bitmap to the destination bitmap (skipping all transparent pixels)
This method is explained through the CreateGlow(COLORREF transparentColor, COLORREF glowColor)
function, as shown below:
void CreateGlow(COLORREF transparentColor, COLORREF glowColor)
{
int i,j, k, l;
RECT rect = {0, 0, 300, 200};
HDC hDC = ::GetDC(NULL);
HDC hTempDC = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap = (HBITMAP)::SelectObject(hTempDC, hTempBitmap);
HDC hTempDC2 = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap2 = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap2 = (HBITMAP)::SelectObject(hTempDC2, hTempBitmap2);
HDC hTempDC3 = ::CreateCompatibleDC(hDC);
HBITMAP hTempBitmap3 = CreateCompatibleBitmap(hDC, 300, 200);
HBITMAP hOldTempBitmap3 = (HBITMAP)::SelectObject(hTempDC3, hTempBitmap3);
::ReleaseDC(NULL, hDC);
HBRUSH hBgBrush = ::CreateSolidBrush(transparentColor);
::FillRect(hTempDC, &rect, hBgBrush);
::FillRect(hTempDC2, &rect, hBgBrush);
::BitBlt(hTempDC3, 0, 0, 300, 200, m_hBgDC, 0, 0, SRCCOPY);
::DeleteObject(hBgBrush);
CreateDrawing(hTempDC2);
int glowingOffset = 4;
::TransparentBlt(hTempDC, -glowingOffset, 0, 300, 200, hTempDC2,
0, 0, 300, 200, transparentColor);
::TransparentBlt(hTempDC, glowingOffset, 0, 300, 200, hTempDC2,
0, 0, 300, 200, transparentColor);
::TransparentBlt(hTempDC, 0, -glowingOffset, 300, 200, hTempDC2,
0, 0, 300, 200, transparentColor);
::TransparentBlt(hTempDC, 0, glowingOffset, 300, 200, hTempDC2,
0, 0, 300, 200, transparentColor);
BITMAP bmpOrig;
GetObject(m_hMemBitmap, sizeof(BITMAP), &bmpOrig);
int sizeOrig = bmpOrig.bmWidthBytes * bmpOrig.bmHeight;
BYTE* pDataOrig = new BYTE[sizeOrig];
GetBitmapBits(m_hMemBitmap, sizeOrig, pDataOrig);
int bppOrig = bmpOrig.bmBitsPixel >> 3;
BITMAP bmpSrc;
GetObject(m_hMemBitmap, sizeof(BITMAP), &bmpSrc);
int sizeSrc = bmpSrc.bmWidthBytes * bmpSrc.bmHeight;
BYTE* pDataSrc = new BYTE[sizeSrc];
GetBitmapBits(hTempBitmap, sizeSrc, pDataSrc);
int bppSrc = bmpSrc.bmBitsPixel >> 3;
BITMAP bmpSrc2;
GetObject(hTempBitmap2, sizeof(BITMAP), &bmpSrc2);
int sizeSrc2 = bmpSrc2.bmWidthBytes * bmpSrc2.bmHeight;
BYTE* pDataSrc2 = new BYTE[sizeSrc2];
GetBitmapBits(hTempBitmap2, sizeSrc2, pDataSrc2);
int bppSrc2 = bmpSrc2.bmBitsPixel >> 3;
BITMAP bmpSrc3;
GetObject(hTempBitmap3, sizeof(BITMAP), &bmpSrc3);
int sizeSrc3 = bmpSrc3.bmWidthBytes * bmpSrc3.bmHeight;
BYTE* pDataSrc3 = new BYTE[sizeSrc3];
GetBitmapBits(hTempBitmap3, sizeSrc3, pDataSrc3);
int bppSrc3 = bmpSrc3.bmBitsPixel >> 3;
BITMAP bmpDst;
GetObject(m_hGlowBitmap, sizeof(BITMAP), &bmpDst);
int sizeDst = bmpDst.bmWidthBytes * bmpDst.bmHeight;
BYTE* pDataDst = new BYTE[sizeDst];
GetBitmapBits(m_hGlowBitmap, sizeDst, pDataDst);
int bppDst = bmpDst.bmBitsPixel >> 3;
BYTE redTransparent = GetRValue(transparentColor);
BYTE greenTransparent = GetGValue(transparentColor);
BYTE blueTransparent = GetBValue(transparentColor);
BYTE redGlow = GetRValue(glowColor);
BYTE greenGlow = GetGValue(glowColor);
BYTE blueGlow = GetBValue(glowColor);
int verticalOffset = 0;
int horizontalOffset;
int totalOffset;
BYTE red, green, blue;
for (i=0; i<bmpSrc.bmHeight; i++)
{
horizontalOffset = 0;
for (j=0; j<bmpSrc.bmWidth; j++)
{
totalOffset = verticalOffset + horizontalOffset;
blue = pDataSrc[totalOffset];
green = pDataSrc[totalOffset+1];
red = pDataSrc[totalOffset+2];
if ((red != redTransparent) || (green != greenTransparent) ||
(blue != blueTransparent))
{
pDataSrc3[totalOffset] = blueGlow;
pDataSrc3[totalOffset+1] = greenGlow;
pDataSrc3[totalOffset+2] = redGlow;
}
horizontalOffset += bppSrc;
}
verticalOffset += bmpSrc.bmWidthBytes;
}
BYTE* pDataTemp = new BYTE[sizeDst];
memcpy(pDataTemp, pDataSrc3, sizeDst);
BYTE* pDataTemp2 = new BYTE[sizeDst];
memcpy(pDataTemp2, pDataSrc, sizeDst);
int filterSize = 11;
int filterHalfSize = filterSize >> 1;
int filterHorizontalOffset = filterHalfSize * bppDst;
int filterVerticalOffset = filterHalfSize * bmpSrc.bmWidthBytes;
int filterTotalOffset = filterVerticalOffset + filterHorizontalOffset;
int filterX, filterY, filterOffset;
int resultRed, resultGreen, resultBlue;
int resultRed2, resultGreen2, resultBlue2;
verticalOffset = 0;
for (i=filterHalfSize; i<bmpDst.bmHeight-filterHalfSize; i++)
{
horizontalOffset = 0;
for (j=filterHalfSize; j<bmpDst.bmWidth-filterHalfSize; j++)
{
totalOffset = verticalOffset + horizontalOffset;
if ((i>=filterHalfSize) && (i<bmpDst.bmHeight-filterHalfSize) &&
(j>=filterHalfSize) && (j<bmpDst.bmWidth-filterHalfSize))
{
resultRed = resultGreen = resultBlue = 0;
resultRed2 = resultGreen2 = resultBlue2 = 0;
filterY = verticalOffset;
for (k=-filterHalfSize; k<=filterHalfSize; k++)
{
filterX = horizontalOffset;
for (l=-filterHalfSize; l<=filterHalfSize; l++)
{
filterOffset = filterY + filterX;
resultBlue += pDataSrc3[filterOffset];
resultGreen += pDataSrc3[filterOffset+1];
resultRed += pDataSrc3[filterOffset+2];
resultBlue2 += pDataSrc[filterOffset];
resultGreen2 += pDataSrc[filterOffset+1];
resultRed2 += pDataSrc[filterOffset+2];
filterX += bppDst;
}
filterY += bmpDst.bmWidthBytes;
}
pDataTemp[totalOffset+filterTotalOffset] =
resultBlue / (filterSize*filterSize);
pDataTemp[totalOffset+1+filterTotalOffset] =
resultGreen / (filterSize*filterSize);
pDataTemp[totalOffset+2+filterTotalOffset] =
resultRed / (filterSize*filterSize);
pDataTemp2[totalOffset+filterTotalOffset] =
resultBlue2 / (filterSize*filterSize);
pDataTemp2[totalOffset+1+filterTotalOffset] =
resultGreen2 / (filterSize*filterSize);
pDataTemp2[totalOffset+2+filterTotalOffset] =
resultRed2 / (filterSize*filterSize);
}
horizontalOffset += bppDst;
}
verticalOffset += bmpDst.bmWidthBytes;
}
verticalOffset = 0;
double alpha=1.0, alpha_koef;
double glow_default_intensity = (redGlow + greenGlow + blueGlow) / 3;
double glow_intenzity, glow_koef;
for (i=0; i<bmpDst.bmHeight; i++)
{
horizontalOffset = 0;
for (j=0; j<bmpDst.bmWidth; j++)
{
totalOffset = verticalOffset + horizontalOffset;
if ((pDataTemp2[totalOffset+2] !=
redTransparent) || (pDataTemp2[totalOffset+1]
!= greenTransparent) || (pDataTemp2[totalOffset] != blueTransparent))
{
glow_intenzity = (pDataTemp2[totalOffset] + pDataTemp2[totalOffset+1] +
pDataTemp2[totalOffset+2]) / 3;
glow_koef = (glow_intenzity - 255) / (glow_default_intensity - 255);
if (fabs(glow_koef) > 0.5)
glow_koef = 0.5 * (fabs(glow_koef) / glow_koef);
if (glow_default_intensity == 255)
alpha_koef = 0.0;
else
alpha_koef = alpha * glow_koef;
blue = (BYTE)(alpha_koef*pDataTemp[totalOffset] +
(1.0-alpha_koef)*pDataDst[totalOffset]);
green = (BYTE)(alpha_koef*pDataTemp[totalOffset+1] +
(1.0-alpha_koef)*pDataDst[totalOffset+1]);
red = (BYTE)(alpha_koef*pDataTemp[totalOffset+2] +
(1.0-alpha_koef)*pDataDst[totalOffset+2]);
pDataSrc3[totalOffset] = blue;
pDataSrc3[totalOffset+1] = green;
pDataSrc3[totalOffset+2] = red;
}
else
{
pDataSrc3[totalOffset] = pDataDst[totalOffset];
pDataSrc3[totalOffset+1] = pDataDst[totalOffset+1];
pDataSrc3[totalOffset+2] = pDataDst[totalOffset+2];
}
horizontalOffset += bppDst;
}
verticalOffset += bmpDst.bmWidthBytes;
}
::SetBitmapBits(m_hGlowBitmap, sizeDst, pDataSrc3);
delete pDataOrig;
delete pDataTemp;
delete pDataTemp2;
delete pDataSrc;
delete pDataSrc2;
delete pDataSrc3;
delete pDataDst;
if (hTempDC)
{
::SelectObject(hTempDC, hOldTempBitmap);
::DeleteDC(hTempDC);
::DeleteObject(hTempBitmap);
}
if (hTempDC2)
{
::SelectObject(hTempDC2, hOldTempBitmap2);
::DeleteDC(hTempDC2);
::DeleteObject(hTempBitmap2);
}
if (hTempDC3)
{
::SelectObject(hTempDC3, hOldTempBitmap3);
::DeleteDC(hTempDC3);
::DeleteObject(hTempBitmap3);
}
CreateDrawing(m_hGlowDC);
}
So, Where is the Difference Then?
The algorithms are almost the same except for one thing: you make just one/two offset[s] (horizontal and/or vertical) when you create the shadow effect, but four offsets or more (in all possible directions) when you create the glowing effect. Applying the blur kernel is very important since it makes shadows and glowing appear more realistic on the final bitmap.
The Blurring Kernel
The blurring kernel in this example is the following one (also called the lowpass-filter):
{[1 1 1][1 1 1][1 1 1]} * 1/9
So, the sum of the 9 surrounding source pixels is averaged (divided with the number of pixels) and the result represents the color of the destination pixel.
Points of Interest
I am very interested in simple image processing techniques like the ones shown in this article, by using basic graphical APIs, like Windows GDI.
History
- 28th October, 2007: Initial post