慢羊羊的空间

无为,无我,无欲,居下,清虚,自然

详解透明贴图和三元光栅操作

透明贴图,是指贴图时某些部分是完全透明的或半透明的。

本文介绍多种透明贴图的方案,包括:

  1. 指定透明色贴图(基于 Windows API 函数 TransparentBlt)
  2. 指定透明色贴图(基于直接操作显示缓冲区)
  3. 使用三元光栅操作实现透明贴图
  4. 根据 png 的 alpha 信息实现半透明贴图(基于 Windows API 函数 AlphaBlend)
  5. 根据 png 的 alpha 信息实现半透明贴图(基于直接操作显示缓冲区)

各种方法各有利弊,大家可以根据自己的需求选择。

1. 指定透明色贴图(基于 Windows API 函数 TransparentBlt)

这是最简单的透明贴图方法。

该方法要求图片素材的透明部分为纯色,因此建议使用 gif 或 png 格式的图片素材。如果使用 jpg 格式的图片素材,那么由于 jpg 的有损压缩,会造成边缘颜色有微小差异,与指定的透明色并不完全相同,从而导致透明贴图效果较差。

关于如何制作图片素材,请参考文章:制作图片素材的必备知识

处理好的素材文件必须确保透明部分的颜色只有一种。例如,处理成下面这样:

准备好图片素材后,直接使用 Windows API 函数 TransparentBlt 即可实现透明贴图。使用该函数需要引入库文件 MSIMG32.LIB,完整的贴图代码如下(请使用最新版本的 EasyX):

#include <graphics.h>		// EasyX_20190219(beta)
#include <conio.h>
// 引用该库才能使用 TransparentBlt 函数
#pragma comment( lib, "MSIMG32.LIB")


// 透明贴图函数
// 参数:
//		dstimg: 目标 IMAGE 对象指针。NULL 表示默认窗体
//		x, y:	目标贴图位置
//		srcimg: 源 IMAGE 对象指针。NULL 表示默认窗体
//		transparentcolor: 透明色。srcimg 的该颜色并不会复制到 dstimg 上,从而实现透明贴图
void transparentimage(IMAGE *dstimg, int x, int y, IMAGE *srcimg, UINT transparentcolor)
{
	HDC dstDC = GetImageHDC(dstimg);
	HDC srcDC = GetImageHDC(srcimg);
	int w = srcimg->getwidth();
	int h = srcimg->getheight();

	// 使用 Windows GDI 函数实现透明位图
	TransparentBlt(dstDC, x, y, w, h, srcDC, 0, 0, w, h, transparentcolor);
}


// 主函数
int main()
{
	initgraph(600, 400); // 初始化图形窗口

	IMAGE src;
	loadimage(&src, _T("D:\\src1.gif"));

	// 画个简单背景
	setlinecolor(GREEN);
	for (int y = 0; y < 480; y += 3)
		line(0, y, 639, y);

	// 普通贴图
	putimage(0, 0, &src);
	// 透明贴图
	transparentimage(NULL, 120, 0, &src, 0xffc4c4);

	// 按任意键退出
	_getch();
	closegraph();

	return 0;
}

以上代码的执行效果如下:

相信大家已经注意到 TransparentBlt 函数的丰富参数了,该函数还可以实现缩放贴图、使用源中的不同位置贴图等,只需要修改参数就可以,这里不再详述。

2. 指定透明色贴图(基于直接操作显示缓冲区)

基于 Windows API 函数 TransparentBlt 的贴图方案的额外功能,比如缩放等,很多时候用不到。所以可以自己通过操作显示缓冲区实现效率更高的指定透明色贴图。相关的源图与方法 1 相同,代码如下:

#include <graphics.h>		// EasyX_20190219(beta)
#include <conio.h>


// 透明贴图函数
// 参数:
//		dstimg: 目标 IMAGE 对象指针。NULL 表示默认窗体
//		x, y:	目标贴图位置
//		srcimg: 源 IMAGE 对象指针。NULL 表示默认窗体
//		transparentcolor: 透明色。srcimg 的该颜色并不会复制到 dstimg 上,从而实现透明贴图
void transparentimage(IMAGE *dstimg, int x, int y, IMAGE *srcimg, UINT transparentcolor)
{
	// 变量初始化
	DWORD *dst = GetImageBuffer(dstimg);
	DWORD *src = GetImageBuffer(srcimg);
	int src_width = srcimg->getwidth();
	int src_height = srcimg->getheight();
	int dst_width = (dstimg == NULL ? getwidth() : dstimg->getwidth());
	int dst_height = (dstimg == NULL ? getheight() : dstimg->getheight());
	
	// 计算贴图的实际长宽
	int iwidth = (x + src_width > dst_width) ? dst_width - x : src_width;
	int iheight = (y + src_height > dst_height) ? dst_height - y : src_height;
	
	// 修正贴图起始位置
	dst += dst_width * y + x;

	// 修正透明色,显示缓冲区中的数据结构为 0xaarrggbb
	transparentcolor = 0xff000000 | BGR(transparentcolor);

	// 实现透明贴图
	for (int iy = 0; iy < iheight; iy++)
	{
		for (int ix = 0; ix < iwidth; ix++)
		{
			if (src[ix] != transparentcolor)
				dst[ix] = src[ix];
		}
		dst += dst_width;
		src += src_width;
	}
}


// 主函数
int main()
{
	initgraph(600, 400); // 初始化图形窗口

	IMAGE src;
	loadimage(&src, _T("D:\\src1.gif"));

	// 画个简单背景
	setlinecolor(GREEN);
	for (int y = 0; y < 480; y += 3)
		line(0, y, 639, y);

	// 普通贴图
	putimage(0, 0, &src);
	// 透明贴图
	transparentimage(NULL, 120, 0, &src, 0xffc4c4);

	// 按任意键退出
	_getch();
	closegraph();

	return 0;
}

以上代码的执行效果如下:

3. 使用三元光栅操作实现透明贴图

基本概念

“三元光栅操作”是指源图像与目标图像的位合并操作。

操作对象涉及三个:源图像、目标图像、当前填充颜色(注:透明贴图不使用“当前填充颜色”)。

位操作包括:AND、NOT、OR、XOR。

全部的三元光栅操作码请参考 EasyX 在线帮助:https://docs.easyx.cn/三元光栅操作码

例如,三元光栅操作码“PATPAINT”,查表得对应的布尔功能为“DPSnoo”(逆波兰表示法,其中 D、S、P 分别表示目标图像、源图像、当前填充颜色),该表达式展开后为:D or (P or (not S)),表示先将源图像按位取反,再与当前填充颜色执行 OR 操作,在与目标图像执行 OR 操作。这就是 putimage 函数以 PATPAINT 参数执行后的显示结果。

一种基于三元光栅操作的透明贴图法

首先准备图片:

  • 原图:需要透明的部分,用纯黑色表示。
  • 掩码图:与原图对应。原图需要透明的部分,用纯黑色表示;原图需要显示的部分,用纯白色表示。
  • 目标图:通常就是屏幕,不用担心屏幕显示什么。

注意:准备的图片与后面的代码是配套的。例如,原图 src3.gif 和掩码图 mask3.gif 处理成这样:

 

然后用以下代码实现贴图:

#include <graphics.h>		// EasyX_20190219(beta)
#include <conio.h>

// 透明贴图函数
// 参数:
//		x, y:	目标贴图位置
//		srcimg: 源 IMAGE 对象指针。NULL 表示默认窗体
//		maskimg:掩码 IMAGE
void transparentimage(int x, int y, IMAGE *srcimg, IMAGE *maskimg)
{
	putimage(x, y, maskimg, SRCAND);
	putimage(x, y, srcimg, SRCPAINT);
}

// 主函数
int main()
{
	initgraph(600, 400); // 初始化图形窗口

	IMAGE src, mask;
	loadimage(&src, _T("D:\\src3.gif"));
	loadimage(&mask, _T("D:\\mask3.gif"));

	// 画个简单背景
	setlinecolor(GREEN);
	for (int y = 0; y < 480; y += 3)
		line(0, y, 639, y);

	// 普通贴图
	putimage(0, 0, &src);
	// 透明贴图
	transparentimage(120, 0, &src, &mask);

	// 按任意键退出
	_getch();
	closegraph();

	return 0;
}

以上代码的执行效果如下:

原理讲解

现在用一维数字的形式来讲解光栅操作的原理。假设:

源 图:00 00 00 56 78 9a bc 00 (00 表示透明的部分,其它数字表示显示的部分)
掩码图:ff ff ff 00 00 00 00 ff (ff 表示透明的部分,00 表示显示的部分)
目标图:12 34 12 34 12 34 12 34

执行步骤:

初始目标图:12 34 12 34 12 34 12 34

执行:putimage(x, y, 掩码图, SRCAND);	// SRCAND 表示“掩码图 AND 目标图”
目标图变为:12 34 12 00 00 00 00 34

执行:putimage(x, y, 源图, SRCPAINT);	// SRCPAINT 表示“源图 OR 目标图”
目标图变为:12 34 12 56 78 9a bc 34

根据以上原理可知:通过三元光栅操作实现透明贴图的办法有很多种,可以根据自己的源图、掩码图的状态写对应的代码。在前面的例子中,源图的透明部分是纯黑色,掩码图用纯白色表示透明的部分、纯黑色表示显示的部分。

这里再举一个不同的例子,在这个例子中,源图的透明部分用的是纯白色,掩码图用纯黑色表示透明的部分、纯白色表示显示的部分,如下:

源 图:ff ff ff 56 78 9a bc ff (ff 表示透明的部分,其它数字表示显示的部分)
掩码图:00 00 00 ff ff ff ff 00 (00 表示透明的部分,ff 表示显示的部分)
目标图:12 34 12 34 12 34 12 34

对应的执行步骤为:

初始目标图:12 34 12 34 12 34 12 34

执行:putimage(x, y, 掩码图, NOTSRCERASE);	// NOTSRCERASE 表示“NOT(掩码图 OR 目标图)”
目标图变为:ed cb ed 00 00 00 00 cb

执行:putimage(x, y, 源图, SRCINVERT);		// SRCINVERT 表示“源图 XOR 目标图”
目标图变为:12 34 12 56 78 9a bc 34

4. 根据 png 的 alpha 信息实现半透明贴图(基于 Windows API 函数 AlphaBlend)

这里说的“半透明贴图”,是指每个像素的透明度都依据 .png 图片的透明度信息。最新版本的 EasyX 支持在加载 png 图片的时候保留每个像素的 alpha 属性。这种技术可以在贴图的时候使图片的边缘非常平滑,不那么生硬。

先准备源图 src4.png,���下:

该图片是包含有透明信息的,在绘图软件里面看是这样的(以 paint.net 为例):

准备好图片素材后,直接使用 Windows API 函数 AlphaBlend 即可实现半透明贴图。使用该函数同样需要引入库文件 MSIMG32.LIB,完整的贴图代码如下:

#include <graphics.h>		// EasyX_20190219(beta)
#include <conio.h>
// 引用该库才能使用 AlphaBlend 函数
#pragma comment( lib, "MSIMG32.LIB")


// 半透明贴图函数
// 参数:
//		dstimg: 目标 IMAGE 对象指针。NULL 表示默认窗体
//		x, y:	目标贴图位置
//		srcimg: 源 IMAGE 对象指针。NULL 表示默认窗体
void transparentimage(IMAGE *dstimg, int x, int y, IMAGE *srcimg)
{
	HDC dstDC = GetImageHDC(dstimg);
	HDC srcDC = GetImageHDC(srcimg);
	int w = srcimg->getwidth();
	int h = srcimg->getheight();

	// 结构体的第三个成员表示额外的透明度,0 表示全透明,255 表示不透明。
	BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
	// 使用 Windows GDI 函数实现半透明位图
	AlphaBlend(dstDC, x, y, w, h, srcDC, 0, 0, w, h, bf);
}


// 主函数
int main()
{
	initgraph(600, 400); // 初始化图形窗口

	IMAGE src;
	loadimage(&src, _T("D:\\src4.png"));

	// 画个简单背景
	setlinecolor(GREEN);
	for (int y = 0; y < 480; y += 3)
		line(0, y, 639, y);

	// 普通贴图
	putimage(0, 0, &src);
	// 透明贴图
	transparentimage(NULL, 120, 0, &src);

	// 按任意键退出
	_getch();
	closegraph();

	return 0;
}

以上代码的执行效果如下:

函数 AlphaBlend 的功能与 TransparentBlt 类似,同样可以实现缩放、指定源的不同位置大小,并且 AlphaBlend 还支持设置贴图时额外加成的透明度,只需要修改参数就可以,这里不再详述。

5. 根据 png 的 alpha 信息实现半透明贴图(基于直接操作显示缓冲区)

该方法使用直接操作显示缓冲区的方法实现半透明贴图,所用的图片素材与方法 4 相同。

准备好 src.png 以后,使用以下代码,实现半透明贴图:

#include <graphics.h>		// EasyX_20190219(beta)
#include <conio.h>


// 半透明贴图函数
// 参数:
//		dstimg:目标 IMAGE(NULL 表示默认窗体)
//		x, y:	目标贴图位置
//		srcimg: 源 IMAGE 对象指针
void transparentimage(IMAGE *dstimg, int x, int y, IMAGE *srcimg)
{
	// 变量初始化
	DWORD *dst = GetImageBuffer(dstimg);
	DWORD *src = GetImageBuffer(srcimg);
	int src_width  = srcimg->getwidth();
	int src_height = srcimg->getheight();
	int dst_width  = (dstimg == NULL ? getwidth()  : dstimg->getwidth());
	int dst_height = (dstimg == NULL ? getheight() : dstimg->getheight());

	// 计算贴图的实际长宽
	int iwidth = (x + src_width > dst_width) ? dst_width - x : src_width;		// 处理超出右边界
	int iheight = (y + src_height > dst_height) ? dst_height - y : src_height;	// 处理超出下边界
	if (x < 0) { src += -x;				iwidth -= -x;	x = 0; }				// 处理超出左边界
	if (y < 0) { src += src_width * -y;	iheight -= -y;	y = 0; }				// 处理超出上边界

	// 修正贴图起始位置
	dst += dst_width * y + x;

	// 实现透明贴图
	for (int iy = 0; iy < iheight; iy++)
	{
		for (int ix = 0; ix < iwidth; ix++)
		{
			int sa = ((src[ix] & 0xff000000) >> 24);
			int sr = ((src[ix] & 0xff0000) >> 16);	// 源值已经乘过了透明系数
			int sg = ((src[ix] & 0xff00) >> 8);		// 源值已经乘过了透明系数
			int sb =   src[ix] & 0xff;				// 源值已经乘过了透明系数
			int dr = ((dst[ix] & 0xff0000) >> 16);
			int dg = ((dst[ix] & 0xff00) >> 8);
			int db =   dst[ix] & 0xff;

			dst[ix] = ((sr + dr * (255 - sa) / 255) << 16)
					| ((sg + dg * (255 - sa) / 255) << 8)
					|  (sb + db * (255 - sa) / 255);
		}
		dst += dst_width;
		src += src_width;
	}
}


// 主函数
int main()
{
	initgraph(600, 400); // 初始化图形窗口

	IMAGE src;
	loadimage(&src, _T("D:\\src4.png"));

	// 画个简单背景
	setlinecolor(GREEN);
	for (int y = 0; y < 480; y += 3)
		line(0, y, 639, y);

	// 普通贴图
	putimage(0, 0, &src);
	// 透明贴图
	transparentimage(NULL, 120, 0, &src);

	// 按任意键退出
	_getch();
	closegraph();

	return 0;
}

以上代码的执行效果如下:

总结

以上的五个例子,使用每个例子对应的图片,针对贴图函数的 1000 次执行时间分别做了统计。

  • 测试电脑配置:i7 + 16G + 240G SSD
  • 编译配置:VC2017,Release-x64

统计如下:

方法 说明 执行 1000 次耗时(秒)
1. 指定透明色贴图(基于 Windows API 函数 TransparentBlt) 可以指定某颜色为透明色。同时支持缩放、选择源区域。 0.01600
2. 指定透明色贴图(基于直接操作显示缓冲区) 可以指定某颜色为透明色。自由度高,可以补充代码实现更多功能。 0.01134
3. 使用三元光栅操作实现透明贴图 可以指定某颜色为透明色。 0.05837
4. 根据 png 的 alpha 信息实现半透明贴图(基于 Windows API 函数 AlphaBlend) 可以实现 256 级透明度,同时支持缩放、选择源区域、透明度加成。 0.04719
5. 根据 png 的 alpha 信息实现半透明贴图(基于直接操作显示缓冲区) 可以实现 256 级透明度,自由度高,可以补充代码实现更多功能。 0.03111

结论:

  1. 随着 CPU 技术的发展,过去高效的光栅操作,现在有更好的替代方案。
  2. 指定透明色比半透明贴图的运算量少,显然速度更快。
  3. 自己写代码操作显示缓冲区的方式,由于去掉了不需要的功能,可以实现更快的速度。以上范例代码仅仅实现了基本的功能,并没有进一步进行 SSE 等优化,有兴趣的同学可以尝试下。
分享到