BitBlt

 图象合成是通过对两张图片像素值的运算,以产生一张新的含有两张图片信息的图象,在多媒体编程中有广泛的应用。图象合成的一个典型的例子就是制作透明位图,在许多精灵动画的实现中都有应用。

精灵动画

  实现一个简单的精灵动画,可以有以下几种方法来实现:
  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位图数据制作合成图象,基本方法就是按照一定的算法来从新组合位图的光栅数据,形成不同的图象效果。写这篇文章,也就是希望能给大家起到一个抛砖引玉的作用,给大家一个提示,帮助大家做出更好的图形特效。

posted @ 2020-05-22 16:02  Joe_du  阅读(915)  评论(0编辑  收藏  举报