详解透明贴图和三元光栅操作
透明贴图,是指贴图时某些部分是完全透明的或半透明的。
本文介绍多种透明贴图的方案,包括:
- 指定透明色贴图(基于 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 <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 |
结论:
- 随着 CPU 技术的发展,过去高效的光栅操作,现在有更好的替代方案。
- 指定透明色比半透明贴图的运算量少,显然速度更快。
- 自己写代码操作显示缓冲区的方式,由于去掉了不需要的功能,可以实现更快的速度。以上范例代码仅仅实现了基本的功能,并没有进一步进行 SSE 等优化,有兴趣的同学可以尝试下。
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
前三种可以指定透明色,建议当且仅当需要指定某些颜色为透明色时使用。
理由:对于色彩比较丰富的图片,难以寻找合适的透明色。
前两种很不建议使用,特别是一些分辨率比较高的图片。
理由:效果太差。我用 ps 对一张图片进行处理,使其背景颜色为纯黑色,用这两方法进行透明贴图,结果非常不理想,绘制出来的图片周围有一圈黑色的描边。究其原因不是这两方法不好,而是在(用 ps)处理图片时,不管是将一张原本就带有透明背景的 png 图片放到纯色背景上,还是用套索工具圈住图片要绘制的部分,再对其他部分进行纯色处理时,在纯色背景和图片交界的地方的像素信息会改变。即使我把画笔工具的大小调到一,也还是有这种问题,所以暂时无解。。。如果哪位大佬有好的解决方案,请回复我。
三元光栅不怎么建议使用,特殊情况除外。
理由:效率低,而且还要准备掩码图,麻烦。不过简单而且适用性更好,jpg之类的都支持。
后两种的最大缺点仅支持 png,但是支持 alpha 就是不一样,效果都很好。
但是,有 bug,我这里出了一个 bug,具体原因还不清楚,反正挺奇怪的。。。
总之还是比较推荐后两种
解决方案请参考:https://codebus.cn/lostperson/rotate
(说个无关的事,这个网站codebus.cn有个奇怪的地方,我的edge浏览器进不了,chrome就行,有点奇怪)