图象合成是通过对两张图片像素值的运算,以产生一张新的含有两张图片信息的图象,在多媒体编程中有广泛的应用。图象合成的一个典型的例子就是制作透明位图,在许多精灵动画的实现中都有应用。
精灵动画
实现一个简单的精灵动画,可以有以下几种方法来实现:
1.用SetPixel函数直接在屏幕上逐点画,这是最笨也是效率最低的方法,我们就不要考虑这种方法了。
2.BitBlt函数:需要为精灵图片制作一张黑白掩膜图片,然后用掩膜图片分别对背景和精灵图片进行处理,最后把处理过的精灵图片拷贝到背景图上。前后要使用三个BitBlt函数,如果为了防止屏幕闪烁,需要使用内存DC,这样就要使用五个BitBlt函数,如果图片比较大的话,处理速度是比较慢的。
3.最快的方法:直接写屏。在游戏编程中,一般都有大量的精灵,用Windows的GDI函数根本无法实现。一个很好的方法就是使用直接写屏技术,通过直接操作显存来加快显示速度,这在DOS时代很轻易的实现,由于Windows程序不能直接访问硬件,需要借助外挂环境来实现,比如Microsoft DirectX就被广泛用于游戏编程中,具体做法可以参阅DirectX编程方面的书籍,这里将不做介绍。
4.直接修改数据缓冲区。这种方法比较简单,其实现原理和直接写屏类似,速度也还可以。另外,通过修改数据缓冲区,你还可以实现一些其他的特殊效果,本文将重点对这种方法进行讲解。
BitBlt函数方法:
GDI的BitBlt函数的功能是将图形数据块从一个位置搬移到另一个位置,源和目标位图可以在同一个设备文本对象,也可以在不同的设备文本对象,函数原型如下:
BitBlt(HDC hDC,int x,int y,int cx,int cy,HDC hDCSrc,int xSrc,int ySrc,DWORD dwRop);
参数dwRop为光栅操作码,决定位图的显示方式,这里介绍三个下面画透明位图需要用到的的光栅操作码:
光栅操作码:MERGEPAINT
效果:源的反向 "或上 "目标(即:dest=(NOT src) OR dest)
说明:白色或上任何颜色都等于白色;黑色或上任何颜色颜色都不变
光栅操作码:NOTSRCERASE
效果:源的反向 "与上 "目标的反向(即:dest=(NOT src) AND (NOT dest))
说明: 黑色与上任何颜色都等于黑色;白色与上任何颜色颜色都不变
光栅操作码:SRCINVERT
效果:源与目标 "异或 "起来(即:dest=src XOR dest)
说明:黑色与任何颜色异或都等于原来颜色;白色与任何颜色异或都等于原来颜色的反色
例子:先准备一张精灵图片、一张精灵的掩膜图和一张背景图(见下图)
1.用精灵掩膜图处理精灵图片(BitBlt函数用MERGEPAINT光栅操作码):
dcFairy.BitBlt(0,0,bm.bmWidth,bm.bmHeight,&dcMask,0,0,);
处理过的精灵图片:
2.用精灵掩膜图处理背景(BitBlt函数用NOTSRCERASE光栅操作码)
pDC-> BitBlt(x,y,bm.bmWidth,bm.bmHeight,&dcMask,0,0,NOTSRCERASE);
处理过的背景图:
3.把处理过的精灵图片贴到背景上(BitBlt函数用SRCINVERT光栅操作码)
pDC-> BitBlt(x,y,bm.bmWidth,bm.bmHeight,&dcFairy,0,0,);
合成后的图形:
完整代码如下:
void DrawFairy(CDC *pDC, int x, int y) // pDC为窗口DC指针 { CDC dcFairy; CDC dcMask; dcFairy.CreateCompatibleDC(pDC); dcMask.CreateCompatibleDC(pDC); CBitmap *pMask=dcMask.SelectObject(&m_bmMask); // m_bmMask为精灵掩膜图 CBitmap *pFairy=dcFairy.SelectObject(&m_bmFairy); // m_bmFairy为精灵图片 // 得到精灵图片的大小 BITMAP bm; m_bmFairy.GetObject(sizeof(bm),&bm); // 处理精灵图片 dcFairy.BitBlt(0,0,bm.bmWidth,bm.bmHeight,&dcMask,0,0,MERGEPAINT); // 处理背景图片(用来贴精灵图片的那一部分) pDC-> BitBlt(x,y,bm.bmWidth,bm.bmHeight,&dcMask,0,0,NOTSRCERASE); // 将处理过的精灵图片与背景经过处理的部分 "异或 "起来 pDC-> BitBlt(x,y,bm.bmWidth,bm.bmHeight,&dcFairy,0,0,SRCINVERT); // Release dcMask.SelectObject(pMask); dcFairy.SelectObject(pFairy); }
上面是用BitBlt函数的实现方法,我们也可以用直接操作位图数据缓冲区的方法:
直接操作位图数据缓冲区:
这种方法也很简单,首先创建精灵图片和背景图片的设备无关位图对象(DIB),然后读取精灵图片各个像素的颜色值,如果颜色值等于我们设定的透明颜色(Mask Color),就把该点的颜色换为背景图上相对位置的点的颜色值,然后将经过处理的精灵图片拷贝到背景上就行了。不过需要要注意的是位图的颜色深度,不同的颜色深度决定了数据缓冲区中一个像素值的长度,需要自己写一些代码来判断这些情况,下面以一个256色的位图来做一个例子:
完整的例子代码:
void CComposeDoc::DrawFairy(HDC hDC, int left, int top, int mask) { LPBITMAPINFO lpbmif; LPBITMAPINFOHEADER lpbmifh; if ( m_hDIBFairy == NULL || m_hDIBBack == NULL ) // 分别是精灵图片的DIB对象和背景图片的DIB对象 return; // // 得到精灵图片信息 // lpbmifh=(LPBITMAPINFOHEADER)m_hDIBFairy; lpbmif=(LPBITMAPINFO)m_hDIBFairy; // 这里假设精灵图片的颜色深度为8位(256色) ASSERT( lpbmifh-> biBitCount==8 ); int cx=lpbmifh-> biWidth; // 长度 int cy=lpbmifh-> biHeight; // 宽度 int nBytesPerLineFairy=((lpbmifh-> biWidth*lpbmifh-> biBitCount+31)&~31)/8; // 每行字节数 UINT nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; // 颜色数 LPVOID lpvBufFairy=lpbmif-> bmiColors+nColors; // 精灵图片数据指针 // // 得到背景图片信息 // lpbmif=(LPBITMAPINFO)m_hDIBBack; lpbmifh=(LPBITMAPINFOHEADER)m_hDIBBack; // 同样假设背景图片的颜色深度是8位(256色) ASSERT( lpbmifh-> biBitCount == 8 ); int cxBack=lpbmifh-> biWidth; // 宽度 int cyBack=lpbmifh-> biHeight; // 高度 int nBytesPerLineBack=((cxBack*lpbmifh-> biBitCount+31)&~31)/8;// 每行字节数 nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; LPVOID lpvBufBack=lpbmif-> bmiColors+nColors;// 背景图片数据指针 // // 创建精灵图片的临时DIB对象 // int nSize=GlobalSize(m_hDIBFairy); LPVOID lpvBufTemp=GlobalAlloc(0,nSize); if ( lpvBufTemp == NULL ) return ; memcpy(lpvBufTemp,m_hDIBFairy,nSize); // 由于这里假设图片的颜色深度数8位的,用BYTE指针来表示一个像素 LPBYTE lpbBufFairy=NULL; LPBYTE lpbBufBack=NULL; for ( int y=cy; y> 0; y-- ) { // 读取的精灵图片数据指针 lpbBufFairy=(LPBYTE)lpvBufFairy+(y-1)*nBytesPerLineFairy; // 相对位置的背景图片数据指针 lpbBufBack=(LPBYTE)lpvBufBack+(cyBack-top-cy+y-1)*nBytesPerLineBack+left; for ( int x=0; x <cx; x++ ) { // 如果当前像素等于我们设定的透明颜色索引值,修改当前像素索引值 if ( *lpbBufFairy == mask ) *lpbBufFairy = * lpbBufBack; lpbBufFairy++; lpbBufBack++; } } // 画精灵图片到屏幕上 SetDIBitsToDevice(hDC,left,top,cx,cy,0,0,0,cy,lpvBufFairy, (LPBITMAPINFO)m_hDIBFairy,DIB_RGB_COLORS); // 回复原来的精灵图片数据 memcpy(m_hDIBFairy,lpvBufTemp,nSize); GlobalFree(lpvBufTemp); }
用上面的两种方法都可以输出一个透明位图到屏幕上,只要修改在屏幕上的显示位置,就可以轻易的制作出一个精灵动画,不过用这两种方法有一个缺点,就是显示的精灵有一个明显的轮廓,特别是前景和背景颜色反差很大时。如何避免这种情况呢?就是下面要介绍的利用Alpha通道输出位图的方法。
在介绍Alpha通道之前,先来看一个如何利用Alpha值合成两张图片的效果。
Imgae1 Image2 合成图象
Alpha图象合成
Alpha图象合成的方法:合成图象的各点像素值是由用来制作合成图的两张图片的相应点的像素值按一定比例混合而成的,这个比例由Alpha值决定,具体算式见下:
newPixeValR= (pixel1ValR*(255-Alpha)+pixel2ValR*Alpha)/255; // Alpha取值范围从0到255
newPixeValG= (pixel1ValG*(255-Alpha)+pixel2ValG*Alpha)/255; // Alpha取值范围从0到255
newPixeValB= (pixel1ValB*(255-Alpha)+pixel2ValB*Alpha)/255; // Alpha取值范围从0到255
从上面的算式可以看出,只要修改Alpha的值,就可以改变合成后的图象中用来合成的两张图片各自所占的比值,改变合成后的显示效果。利用这个方法,我们就可以很轻易的制作出生动的淡入淡出效果和图片间的平滑过度特效。下面给出一个制作合成图的具体源码:
BOOL CompoundDIB(HANDLE hDIB,HANDLE hDIBSrc,int alpha) { LPVOID lpvBuf=NULL; // 目标图象数据指针 LPVOID lpvBufSrc=NULL; // 源图数据指针 // // 源图象信息 // LPBITMAPINFO lpbmif=(LPBITMAPINFO)hDIBSrc; LPBITMAPINFOHEADER lpbmifh=(LPBITMAPINFOHEADER)lpbmif; // 计算图象数据偏移量 UINT nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; if ( nColors > 256 ) nColors=0; // 如果颜色数大于256色,则没有调色板 lpvBufSrc=(LPVOID)((LPBYTE)lpbmif-> bmiColors+nColors*sizeof(RGBQUAD)); int cxSrc=lpbmifh-> biWidth; // 源图象宽度 int cySrc=lpbmifh-> biHeight; // 源图象高度 // 计算图象每行的字节数(图象位数 x 图象宽度,如果不能被2整除则在每行后面添加一个0字节) int nBytesPerLineSrc=((cxSrc*lpbmifh-> biBitCount+31)&~31)/8; // // 目标图象信息 // lpbmif=(LPBITMAPINFO)hDIB; lpbmifh=(LPBITMAPINFOHEADER)lpbmif; nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; if ( nColors > 256 ) nColors=0; lpvBuf=(LPVOID)((LPBYTE)lpbmif-> bmiColors+nColors*sizeof(RGBQUAD)); int cx=lpbmifh-> biWidth; int cy=lpbmifh-> biHeight; int nBytesPerLine=((cx*lpbmifh-> biBitCount+31)&~31)/8; LPBYTE lpbPnt=NULL; LPBYTE lpbPntSrc=NULL; // // 通过alpha值合并两张图象的像素值 // // 这里假设是24位真彩色图象,其他深度的图象处理方法可以以次类推 for ( int y=(cy <cySrc ? cy : cySrc); y> 0 ;y-- ) { lpbPnt=(LPBYTE)lpvBuf+nBytesPerLine*(y-1); lpbPntSrc=(LPBYTE)lpvBufSrc+nBytesPerLineSrc*(y-1); for ( int x=0; x <(cx <cxSrc ? cx : cxSrc); x++ ) { for ( int i=0 ;i <3 ;i++ ) *lpbPnt++=(*lpbPnt*(255-alpha)+*(lpbPntSrc++)*alpha)/255; } } return TRUE; }
回到刚才讨论的问题,如何避免画出的透明图有一个明显的轮廓?想一想刚才介绍的利用Alpha值合成图象方法,如果我们在合成的过程中动态修改Alpha值,使它的轮廓部分从(背景的)0慢慢过度到(前景的)255,这样不就可以使前景逐步地渗透到背景里面了。下面来看看具体做法吧!
Alpha通道
上面所说的动态修改的Alpha值,一般是使用一张256级的灰度图来实现的(这张灰度图就称为Alpha通道),灰度图的各点值对应着前景图片相应点的Alpha值。灰度图的黑色部分是透明的(Alpha值为0),白色部分为不透明部分(Alpha值为255),灰度部分就是前景和背景的融合部分。看一看合成效果吧!
Alpha通道 前景图
背景图 合成图
可以看出,利用Alpha通道,合成后的图象前景和背景非常完美的融合在一起了。
Alpha通道合成图象代码:
BOOL CompoundDIB(int left,int top,HANDLE hDIB,HANDLE hDIBSrc,HANDLE hDIBAlpha) { LPVOID lpvBuf=NULL; // 目标图象数据指针(背景) LPVOID lpvBufSrc=NULL; // 源图数据指针(前景) LPVOID lpvBufAlpha=NULL; // Alpha通道数据指针 // // 源图象信息 // LPBITMAPINFO lpbmif=(LPBITMAPINFO)hDIBSrc; LPBITMAPINFOHEADER lpbmifh=(LPBITMAPINFOHEADER)lpbmif; // 计算图象数据偏移量 UINT nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; if ( nColors > 256 ) nColors=0; // 如果颜色数大于256色,则没有调色板 lpvBufSrc=lpbmif-> bmiColors+nColors; int cxSrc=lpbmifh-> biWidth; // 源图象宽度 int cySrc=lpbmifh-> biHeight; // 源图象高度 // 计算图象每行的字节数(图象位数 x 图象宽度,如果不能被2整除则在每行后面添加一个0字节) int nBytesPerLineSrc=((cxSrc*lpbmifh-> biBitCount+31)&~31)/8; // // 目标图象信息 // lpbmif=(LPBITMAPINFO)hDIB; lpbmifh=(LPBITMAPINFOHEADER)lpbmif; nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 1 < <lpbmifh-> biBitCount; if ( nColors > 256 ) nColors=0; lpvBuf=lpbmif-> bmiColors+nColors; int cx=lpbmifh-> biWidth; int cy=lpbmifh-> biHeight; int nBytesPerLine=((cx*lpbmifh-> biBitCount+31)&~31)/8; // // Alpha通道信息 // lpbmif=(LPBITMAPINFO)hDIBAlpha; lpbmifh=(LPBITMAPINFOHEADER)hDIBAlpha; ASSERT(lpbmifh-> biWidth==cxSrc && lpbmifh-> biHeight==cySrc && lpbmifh-> biBitCount==8 ); nColors=lpbmifh-> biClrUsed ? lpbmifh-> biClrUsed : 256; lpvBufAlpha=lpbmif-> bmiColors+nColors; int nBytesPerLineAlpha=((cxSrc*8+31)&~31)/8; // // 用来读取颜色值的指针 // LPBYTE lpbPnt=NULL; LPBYTE lpbPntSrc=NULL; LPBYTE lpbPntAlpha=NULL; // // 通过alpha值合并两张图象的像素值 // // 这里假设是24位真彩色图象,其他深度的图象处理方法可以以次类推 for ( int y=cySrc; y> 0 ;y-- ) { lpbPnt=(LPBYTE)lpvBuf+nBytesPerLine*(cy-top-cySrc+y-1)+left*3; lpbPntSrc=(LPBYTE)lpvBufSrc+nBytesPerLineSrc*(y-1); lpbPntAlpha=(LPBYTE)lpvBufAlpha+nBytesPerLineAlpha*(y-1); for ( int x=0; x <cxSrc; x++ ) { int alpha=*lpbPntAlpha++; for ( int i=0 ;i <3 ;i++ ) *lpbPnt++=(*lpbPnt*(255-alpha)+*(lpbPntSrc++)*alpha)/255; } } return TRUE; }
上面介绍的只是几种简单的图象合成知识。直接修改DIB位图数据制作合成图象,基本方法就是按照一定的算法来从新组合位图的光栅数据,形成不同的图象效果。写这篇文章,也就是希望能给大家起到一个抛砖引玉的作用,给大家一个提示,帮助大家做出更好的图形特效。