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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#include // EasyX_20190219(beta) #include // 引用该库才能使用 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; }

Copy

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

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

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

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

#include // EasyX_20190219(beta) #include // 透明贴图函数 // 参数: //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; }

Copy

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

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 // EasyX_20190219(beta) #include // 透明贴图函数 // 参数: //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; }

Copy

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

原理讲解

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

源 图: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

Copy

执行步骤:

初始目标图: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

Copy

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

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

源 图: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

Copy

对应的执行步骤为:

初始目标图: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

Copy

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

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

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

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

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

#include // EasyX_20190219(beta) #include // 引用该库才能使用 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; }

Copy

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

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

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

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

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

#include // EasyX_20190219(beta) #include // 半透明贴图函数 // 参数: //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; }

Copy

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

总结

以上的五个例子,使用每个例子对应的图片,针对贴图函数的 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

结论:

随着 CPU 技术的发展,过去高效的光栅操作,现在有更好的替代方案。

指定透明色比半透明贴图的运算量少,显然速度更快。

自己写代码操作显示缓冲区的方式,由于去掉了不需要的功能,可以实现更快的速度。以上范例代码仅仅实现了基本的功能,并没有进一步进行 SSE 等优化,有兴趣的同学可以尝试下。

(0)

相关推荐