第5章 遮羞的艺术——Windows游戏绘图技巧

有人会说道,我们直接去用支持很多图片格式的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 逻辑或〉运算,所以,与我们透明遮罩法相关的光栅操作代码
( Raster 值〉为以下两个:
  • SRCAND:通过使用AND (与)操作符来将源和目标矩形区域内的颜色合并。
  • SRCPAINT:通过使用布尔型的OR (或)操作符将源和目标矩形区域的颜色合并。
接下来我们了解一下具体实现的思路,透明遮罩法的具体实现步骤如下:
  1. 将这罩图与背景图做AND 逻辑与运算(尤栅操作代码为SRC挝、ID ),将运算后得到的图片贴到目的设备环境DC 中。
  2.  将前景图与背景图做OR 逻辑或运算(光栅操作代码为SRCPAINT),将运算后得到的图片贴到目的设备环境DC 中,且坐标与第一步的贴图坐标完全相同,即在同一个地方进行了两次贴图操作,得到了最后的效果图。
为什么经过上面的两个步骤就能产生背景透明的效果呢?
为了解答这个问题,首先我们分析一下素材图与遮罩图的颜色组成,如下图。

下面我们来具体说明上述两个步骤所产生图点的二进制色彩点的二进制运算过程。
第一步,遮罩图与背景图做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_lnit()函数:
//-----------------------------------【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 ,色键),这样就可以方便地进行透明贴图。
在使用这种方法进行透明贴图时,需要事先把素材图中需要在贴图时透明显示的区域设为某种颜色,且这种颜色值不能与希望在贴图时显示出来的部分有颜色上的相近或者相同,具体表现是RGB 值不能相同或者相近。
下面我们就来看一下这种透明贴图方式的核心, 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 来做透明贴图的短板,图片中如果有和透明色接近的颜色值,那么这时候透明效果做出来会显得不细腻, 经常有杂边。谁叫你是用透明色彩法TransparentBlt 函数投机取巧呢?对图片轮廓要求高的话,还是老老实实去用透明遮罩法吧。

5.4 自己动手处理图片素材

关于如何将其他格式的图片转换成所需的位图素材,大家可自己通过百度查阅相关方法;

5.5 章节小憩

我们学习了两套透明贴图的体系, 透明遮罩法和透明色彩法, 这两套方法各有优劣,需要在实战时灵活选用。如果想要游戏开发进度快一些, 就用透明色彩法,如果有些地方需要贴图轮廓的准确性, 就用透明遮罩法。



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值