慢羊羊的空间

工作做不完了,300出,无瑕。

详解透明贴图和三元光栅操作 金牌收录

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

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

  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/ternary-raster-operations

例如,三元光栅操作码“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 等优化,有兴趣的同学可以尝试下。

评论 (15) -

  • 如果图像背景是黑色的,图片的内容也存在黑色的,在不使用Photoshop改变图像背景颜色的情况下只能用三元光栅的方法去消除黑色背景吗?
  • 总算不用去看libpng了,心满意足。
  • 用的第四个方法,运行不了啊。求大佬帮忙。
    C:\Program Files (x86)\Dev-Cpp\TDM-GCC-64\x86_64-w64-mingw32\bin\ld.exe  main.o: in function `transparentimage(IMAGE*, int, int, IMAGE*)':

    13    C:\Program Files (x86)\Dev-Cpp\code\test\main.cpp  undefined reference to `__imp_AlphaBlend'

    C:\Program Files (x86)\Dev-Cpp\code\test\collect2.exe  [Error] ld returned 1 exit status

    25    C:\Program Files (x86)\Dev-Cpp\code\test\Makefile.win  recipe for target 'text.exe' failed
    • 如果你用 mingw,需要手动链接 MSIMG32 库。如果不熟,还是用 vc 吧,方便。
  • 在十六进制处提醒一下代表颜色的意思为好,毕竟底色都不太会和博主相同,大家一试发现不行容易迷惑。
  • 个人对这五种方法的看法:
    前三种可以指定透明色,建议当且仅当需要指定某些颜色为透明色时使用。
    理由:对于色彩比较丰富的图片,难以寻找合适的透明色。

    前两种很不建议使用,特别是一些分辨率比较高的图片。
    理由:效果太差。我用 ps 对一张图片进行处理,使其背景颜色为纯黑色,用这两方法进行透明贴图,结果非常不理想,绘制出来的图片周围有一圈黑色的描边。究其原因不是这两方法不好,而是在(用 ps)处理图片时,不管是将一张原本就带有透明背景的 png 图片放到纯色背景上,还是用套索工具圈住图片要绘制的部分,再对其他部分进行纯色处理时,在纯色背景和图片交界的地方的像素信息会改变。即使我把画笔工具的大小调到一,也还是有这种问题,所以暂时无解。。。如果哪位大佬有好的解决方案,请回复我。

    三元光栅不怎么建议使用,特殊情况除外。
    理由:效率低,而且还要准备掩码图,麻烦。不过简单而且适用性更好,jpg之类的都支持。

    后两种的最大缺点仅支持 png,但是支持 alpha 就是不一样,效果都很好。
    但是,有 bug,我这里出了一个 bug,具体原因还不清楚,反正挺奇怪的。。。

    总之还是比较推荐后两种
    • 你遇到的“在纯色背景和图片交界的地方的像素信息会改变”的问题,是因为你保存为了 jpg 格式的图片。jpg 是有损压缩,必然会导致一些图像信息的损失。如果你使用 png 等无损格式,用前两种方法一样可以输出漂亮干净的透明贴图。再就是可能因为抠图的时候图片边缘没有扣干净。
  • 为什么图片旋转后就不行了
    • 因为rotateimage函数没有考虑alpha信息的问题,图片在旋转之后的alpha信息失真。
      解决方案请参考:https://codebus.cn/lostperson/rotate
  • 谢谢楼主了,受益匪浅,要是早点能发现这个帖子就不会探索那么多弯路了,现在决定选用最后一种半透明的方法,没想到效率会比三元光栅操作还高
    (说个无关的事,这个网站codebus.cn有个奇怪的地方,我的edge浏览器进不了,chrome就行,有点奇怪)
  • 使用 Windows GDI 函数实现半透明位图的函数 如果 *srcimg = NULL似乎会 报错

添加评论