有人会说道,我们直接去用支持很多图片格式的GDI+加载有背景通道的png 图片就可以了啊,干嘛要学这个繁琐的GDI 透明贴图呢?这个问题问得好。这个问题的解答相信大家在读过上一章中对GDI+的描述后,都已经有了答案。对,是执行效率的问题。GDI+很好用,但是它是对GDI的再封装,执行效率完全达不到我们制作游戏的要求。现在这个时代我们用GDI 做游戏都嫌它慢了,更别说慢得一塌糊涂的GDI+了。
5.1 透明贴图的两套体系
为了游戏编写的需要,我们通常会把游戏人物贴到背景图片上去,而我们要知道的是,所有的图像文件都是以一个四四方方的矩形来储存的。用GDI 中支持的图片格式BMP制作成的游戏素材,如果不经过处理, 直接进行贴图的话,就会出现下图这样错误的显示效果。
我们当然不希望在游戏画面显示的过程中,出现这种黑框的“穿帮镜头”,如果我们认定GDI 做为我们游戏开发过程中的图形库,如何正确地把子图形的背景透明化,将子图形正确地显示在背景图之上呢?
在GDI 中, 想要透明贴图,我们主要有两套解决方案:
- 透明遮罩法
- 透明色彩法
5.2 透明遮罩法
它主要是利用BitBlt() 函数中Raster (光栅〉值的运算,来将图片中我们不希望出现的部分处理掉,我们可以称这种方法为“去背”
经过这样的透明遮罩处理,就可以得到我们想要的效果。
经过这样的透明遮罩处理,就可以得到我们想要的效果。
接下来我们就来开始讲解如何使用透明遮罩法进行相关实现。
我们以上面的图示中的女巫图为例,首先必须准备一张经过加工的位图(如何加工这种位图我们稍后有讲到),如下图:
我们以上面的图示中的女巫图为例,首先必须准备一张经过加工的位图(如何加工这种位图我们稍后有讲到),如下图:
从上面的图中我们可以看到,左边的图为素材图, 里面包含了我们需要最终显示在背景上女巫的形象。右边的黑白图我们称之为遮罩图,在后面的透明操作中会用到它。可以看到, 素材图和遮罩图在这里拼接成了一张图,后面再根据需要在内存中按像素坐标进行读取。这样在读取图像资源的时候,就可以减少加载图片的次数,节省了游戏程序资源的开销。
5.2.1 具体实现细节
首先我们介绍一下这套实现透明背景方案所使用的核心贴图函数BitBlt,之前讲解GDI 基础知识的时候,我们己经重点介绍过了。
BOOL BitBlt(
__in HDC hdcDest,
__in int nXDest,
__in int nYDest,
__in int nWidth,
__in int nHeight,
__in HDC hdcSrc,
__in int nXSrc,
__in int nYSrc,
__in DWORD dwRop
);
- 第九个参数, DWORD 类型的dwRop ,指定光栅操作代码,即贴图的方式。
介绍BitBlt 函数时说过,它的最后一个参数DWORD 类型的dwRop ,用于指定光栅操作代码,即贴图的方式。随后给出了一系列的光栅操作代码,并说明了通常直接贴图用到的光栅操作代码为SRCCOPY 。
现在我们的思路是,用代码实现图片的OR (逻辑与)与AND C 逻辑或〉运算,所以,与我们透明遮罩法相关的光栅操作代码
现在我们的思路是,用代码实现图片的OR (逻辑与)与AND C 逻辑或〉运算,所以,与我们透明遮罩法相关的光栅操作代码
( Raster 值〉为以下两个:
- SRCAND:通过使用AND (与)操作符来将源和目标矩形区域内的颜色合并。
- SRCPAINT:通过使用布尔型的OR (或)操作符将源和目标矩形区域的颜色合并。
- 将这罩图与背景图做AND 逻辑与运算(尤栅操作代码为SRC挝、ID ),将运算后得到的图片贴到目的设备环境DC 中。
- 将前景图与背景图做OR 逻辑或运算(光栅操作代码为SRCPAINT),将运算后得到的图片贴到目的设备环境DC 中,且坐标与第一步的贴图坐标完全相同,即在同一个地方进行了两次贴图操作,得到了最后的效果图。
为什么经过上面的两个步骤就能产生背景透明的效果呢?
为了解答这个问题,首先我们分析一下素材图与遮罩图的颜色组成,如下图。
为了解答这个问题,首先我们分析一下素材图与遮罩图的颜色组成,如下图。
下面我们来具体说明上述两个步骤所产生图点的二进制色彩点的二进制运算过程。
第一步,遮罩图与背景图做AND 运算,我们将遮罩图中的黑色的人物轮廓与白色的背景分开来讲解:
• 遮罩图中人物轮廓(为纯黑色)与彩色的背景图做AND 运算:
第一步,遮罩图与背景图做AND 运算,我们将遮罩图中的黑色的人物轮廓与白色的背景分开来讲解:
• 遮罩图中人物轮廓(为纯黑色)与彩色的背景图做AND 运算:
所以,遮罩图与背景图做“ AND ”运算, 遮罩图中背景部分还是原来的背景图颜色。
经过这一阶段,结果图中人物轮廓部分为纯黑色, 背景部分还是原来的背景图颜色,我们得到的效果如下图所示。
经过这一阶段,结果图中人物轮廓部分为纯黑色, 背景部分还是原来的背景图颜色,我们得到的效果如下图所示。
第二步, 素材图与背景图做OR 运算,我们依然将人物轮廓部分与背景图部分分开来讲解:
所以,素材图与背景图做OR 运算,背景部分依然还是我们希望得到的彩色背景图。
经过前面两步的运算,我们就可以得出最终的透明效果图了。
经过前面两步的运算,我们就可以得出最终的透明效果图了。
一次逻辑与, 一次逻辑或, 这就是我们对图片进行去背景的透明遮罩法。具体的思想就可以浓缩在这两句代码中:
BitBlt(g_hdc,50,WINDOW_HEIGHT-579,320,640,g_mdc,320,0,SRCAND);//透明遮罩法第一步,即将屏蔽图与背景图做"AND"运算
BitBlt(g_hdc,50,WINDOW_HEIGHT-579,320,640,g_mdc,0,0,SRCPAINT);//透明遮罩法第二步,即将前景图与背景图做"OR"运算
这样利用透明遮罩法来进行绘图, 绘制效率比GDI+中直接绘制含透明通道的PNG 图片高得多,这是PC 游戏早期,用GDI 进行游戏开发所使用的贴图经典方法之一。
5.2.2 示例程序GDldemo4
在这个示例程序中, 我们用透明遮罩法实现背景透明贴图, 在窗口中首先绘制一张背景, 然后用透明遮罩法贴出两张动漫图片。先看看使用的素材吧:
我们来看看核心代码。
程序代码片段一,全局变量声明:
程序代码片段一,全局变量声明:
//-----------------------------------【全局变量声明部分】-------------------------------------
// 描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC g_hdc=NULL,g_mdc=NULL; //全局设备环境句柄与全局内存DC句柄
HBITMAP g_hBackGround,g_hCharacter1,g_hCharacter2; //定义3个位图句柄,用于3张图片的存放
//-----------------------------------【Game_Init( )函数】--------------------------------------
// 描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
g_hdc = GetDC(hwnd); //获取设备环境句柄
//-----【位图绘制四步曲之一:加载位图】-----
//从文件加载3张位图
g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
g_hCharacter1 = (HBITMAP)LoadImage(NULL,L"character1.bmp",IMAGE_BITMAP,640,579,LR_LOADFROMFILE);
g_hCharacter2 = (HBITMAP)LoadImage(NULL,L"character2.bmp",IMAGE_BITMAP,800,584,LR_LOADFROMFILE);
//-----【位图绘制四步曲之二:建立兼容DC】-----
g_mdc = CreateCompatibleDC(g_hdc); //建立兼容设备环境的内存DC
Game_Paint(hwnd);
ReleaseDC(hwnd,g_hdc); //释放设备环境
return TRUE;
}
程序代码片段三, Game Paint()函数:
//-----------------------------------【Game_Paint( )函数】--------------------------------------
// 描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
//先贴上背景图
SelectObject(g_mdc,g_hBackGround);
BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY); //采用BitBlt函数在g_hdc中先贴上背景图
//用透明遮罩法绘制出第一个人物
SelectObject(g_mdc,g_hCharacter1);
BitBlt(g_hdc,50,WINDOW_HEIGHT-579,320,640,g_mdc,320,0,SRCAND);//透明遮罩法第一步,即将屏蔽图与背景图做"AND"运算
BitBlt(g_hdc,50,WINDOW_HEIGHT-579,320,640,g_mdc,0,0,SRCPAINT);//透明遮罩法第二步,即将前景图与背景图做"OR"运算
//用透明遮罩法绘制出第二个人物
SelectObject(g_mdc,g_hCharacter2);
BitBlt(g_hdc,450,WINDOW_HEIGHT-584,400,584,g_mdc,400,0,SRCAND);//透明遮罩法第一步,即将屏蔽图与背景图做"AND"运算
BitBlt(g_hdc,450,WINDOW_HEIGHT-584,400,584,g_mdc,0,0,SRCPAINT);//透明遮罩法第二步,即将前景图与背景图做"OR"运算
}
在GDI 中, 通过BitBlt()贴图函数中光栅运算值的设定, 经过两次贴图,非常简单地就制作出了我们需要的透明效果,这就是GDI 透明贴图两套体系之一的透明遮罩法。
5.3 透明色彩法
透明色彩法,这种方法以后用到的机会比透明遮罩法更多,因为它用起来简单,不需要专门去制作遮罩图, 只要给背景指定一种特定的颜色就好了。
5.3.1 具体实现细节
透明色彩法,就是利用在贴图时可以设置某种颜色为透明色的函数,比如TransparentBIt()函数、AlphaBlend()函数等等来达到直观的透明背景显示方法。
透明色彩法中我们最常用的是TransparentBlt 函数,TransparentBlt()函数可以在进行贴图操作时, 把源位图中的某种颜色值看作透明色(也就是我们常说的ColorKey ,色键),这样就可以方便地进行透明贴图。
透明色彩法中我们最常用的是TransparentBlt 函数,TransparentBlt()函数可以在进行贴图操作时, 把源位图中的某种颜色值看作透明色(也就是我们常说的ColorKey ,色键),这样就可以方便地进行透明贴图。
在使用这种方法进行透明贴图时,需要事先把素材图中需要在贴图时透明显示的区域设为某种颜色,且这种颜色值不能与希望在贴图时显示出来的部分有颜色上的相近或者相同,具体表现是RGB 值不能相同或者相近。
下面我们就来看一下这种透明贴图方式的核心, TransparentBlt()函数的使用方法,我们在MSDN 中查到它的原型如下:
下面我们就来看一下这种透明贴图方式的核心, TransparentBlt()函数的使用方法,我们在MSDN 中查到它的原型如下:
BOOL TransparentBlt(
__in HDC hdcDest, //目标设备环境的句柄
__in int xoriginDest, //目标矩形左上角的x轴坐标
__in int yoriginDest, // 目标矩形左上角的Y 轴坐标
__in int wDest, //目标矩形的宽度
__in int hDest, //目标矩形的高度
__in HDC hdcSrc, //源设备环境的句柄
__in int xoriginSrc, //源矩形左上角的X 轴坐标
__in int yoriginSrc, //源矩形左上角的Y 轴坐标
__in int wSrc, //源矩形的宽度
__in int hSrc, //源矩形的高度
__in UINT crTransparent // 指定视为透明色的RGB 颜色值
);
MSDN 中对该函数的英文描述翻译过来是对指定的源设备环境中的矩形区域像素的颜色数据进行位块( bit_block )转换,井将结果置于目标设备环境中。
注意: 调用TransparentBlt 函数需要加载msimg32.lib 库文件,即需要在源文件中添加#pragma comment( lib, ” msimg32 . lib”)语句, 或者在项目页面中添加这个库文件。
而为了达到精细的显示效果,避免透明贴图中杂边的出现,我们在素材图中人物之外的颜色最好选用和人物颜色反差非常大的颜色。如果人物中黑色成分很低,我们素材图中人物之外的颜色就是推荐用纯黑色,也就是RGB(0, 0, 0) 。
给大家依然是配上一个调用实例,指定透明色为RGB(0 ,0 ,0)进行贴图:
TransparentBlt(g_hdc,0,WINDOW_HEIGHT-650,535,650,g_mdc,0,0,535,650,RGB(0,0,0));//透明色为RGB(0,0,0)
注意: 对于32 位的位图, TransparentBlt 函数有可能会将透明值直接复制过来。如果这种情况下达不到效果的话, 那我们就转而使用AlphaBlend()函数。
5.3.2 示例程序GDldemo5
首先我们看看素材文件,如下图所示。
然后来看看核心代码。
程序代码片段一,库文件、全局变量声明:
程序代码片段一,库文件、全局变量声明:
//-----------------------------------【库文件包含部分】---------------------------------------
// 描述:包含程序所依赖的库文件
//------------------------------------------------------------------------------------------------
#pragma comment(lib,"winmm.lib") //调用PlaySound函数所需库文件
#pragma comment(lib,"Msimg32.lib") //添加使用TransparentBlt函数所需的库文件
//-----------------------------------【全局变量声明部分】-------------------------------------
// 描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC g_hdc=NULL,g_mdc=NULL; //全局设备环境句柄与全局内存DC句柄
HBITMAP g_hBackGround,g_hCharacter1,g_hCharacter2; //定义3个位图句柄,用于3张图片的存放
程序代码片段二, Game_Init()函数:
//-----------------------------------【Game_Init( )函数】--------------------------------------
// 描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
g_hdc = GetDC(hwnd); //获取设备环境句柄
//-----【位图绘制四步曲之一:加载位图】-----
//从文件加载3张位图
g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
g_hCharacter1 = (HBITMAP)LoadImage(NULL,L"character1.bmp",IMAGE_BITMAP,535,650,LR_LOADFROMFILE);
g_hCharacter2 = (HBITMAP)LoadImage(NULL,L"character2.bmp",IMAGE_BITMAP,506,650,LR_LOADFROMFILE);
//-----【位图绘制四步曲之二:建立兼容DC】-----
g_mdc = CreateCompatibleDC(g_hdc); //建立兼容设备环境的内存DC
Game_Paint(hwnd);
ReleaseDC(hwnd,g_hdc); //释放设备环境
return TRUE;
}
程序代码片段三, Game_ Paint() 函数:
//-----------------------------------【Game_Paint( )函数】--------------------------------------
// 描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
//先贴上背景图
SelectObject(g_mdc,g_hBackGround);
BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY); //采用BitBlt函数在g_hdc中先贴上背景图
//贴第一张人物图
SelectObject(g_mdc,g_hCharacter1);
TransparentBlt(g_hdc,0,WINDOW_HEIGHT-650,535,650,g_mdc,0,0,535,650,RGB(0,0,0));//透明色为RGB(0,0,0)
//贴第二张人物图
SelectObject(g_mdc,g_hCharacter2);
TransparentBlt(g_hdc,500,WINDOW_HEIGHT-650,506,650,g_mdc,0,0,506,650,RGB(0,0,0));//透明色为RGB(0,0,0)
}
程序运行效果如下图。
细心观察我们用TransparentBlt 做出来的透明效果可以发现,人物轮廓还是有些地方是残留着我们制定的透明色(这里为黑色),显得不细腻。这就是我们使用TransparentBlt 来做透明贴图的短板,图片中如果有和透明色接近的颜色值,那么这时候透明效果做出来会显得不细腻, 经常有杂边。谁叫你是用透明色彩法TransparentBlt 函数投机取巧呢?对图片轮廓要求高的话,还是老老实实去用透明遮罩法吧。
5.4 自己动手处理图片素材
关于如何将其他格式的图片转换成所需的位图素材,大家可自己通过百度查阅相关方法;
5.5 章节小憩
我们学习了两套透明贴图的体系, 透明遮罩法和透明色彩法, 这两套方法各有优劣,需要在实战时灵活选用。如果想要游戏开发进度快一些, 就用透明色彩法,如果有些地方需要贴图轮廓的准确性, 就用透明遮罩法。