65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Win32 MsImg32.dll 进行图像合成指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (90投票s)

2011 年 9 月 8 日

CPOL

52分钟阅读

viewsIcon

127582

downloadIcon

7873

就图像合成而言,您的想象力是唯一的限制。

Image Compositing Demo Screenshot

Bit Blender Demo Screenshot

Image Composition Screenshot

引言

哇!我刚经历了一段多么漫长的旅程!这并非一次轻松的巡游,但我相当享受。我还拍了不少照片要分享!几周前(再过几天就可以算作几个月了),我写了一篇题为《Win32 内存 DC 指南》的文章。我希望展示内存 DC 的一个用途是它能够用于双缓冲绘图。我脑海中有一个画面,想要它看起来的样子。在开发过程中,它绕了远路,最终变成了在屏幕上扫过的**渐变混合**过渡,并与多个图像进行** Alpha 混合**。

本文将描述我为实现我所追求的视觉效果而克服的挑战和困难(不过,我将不侧重于上一篇文章中的效果,稍后会详细介绍)。我发现有很多示例展示了如何使用 ::GradientFill::AlphaBlend。即使 MSDN 上的示例展示了 ::GdiAlphaBlend 的所有功能,屏幕上显示的结果也未提供关于该函数如何工作的任何新见解。所有效果都可以通过调用 ::Rectangle::GradientFill 来重现。示例也都在白色背景上进行。当我脱离纯白色背景进行 alpha 混合的初步尝试时,我对结果感到困惑。

在查找前两个函数的示例时,我发现该库只导出了另外一个已记录的函数,即 ::TransparentBlt。因此,我决定在本文中介绍这三个函数,并涵盖整个 DLL。

在构思这些函数的可能演示应用时,我开始看到这些函数多么有用和灵活,尽管它们看起来相当僵硬和简单。最终,本文是关于图像合成的。借助 MsImg32.dll 中的函数以创造性的方式应用,即使是用 C++ 编写并使用 Win32 API,也可以在几行代码内生成看起来很棒的动态图像。

如何将 MsImg32.dll 与您的程序链接

MsImage32.dll 不在 Visual Studio 的默认链接库列表中。当您第一次使用本文中描述的任何函数而不将库 msimg32.lib 添加到链接器输入设置中时,您将收到类似于以下的链接器错误:

1>------ Build started: Project: win32_msimg32, Configuration: Debug Win32 ------
1>Compiling...
1>MsImg32Usage.cpp
1>Linking...
1>MsImg32Usage.obj : error LNK2019: unresolved external 
  symbol __imp__TransparentBlt@44 referenced in function @X...
1>MsImg32Usage.obj : error LNK2019: unresolved external 
  symbol __imp__GradientFill@24 referenced in function @Y...
1>MsImg32Usage.obj : error LNK2019: unresolved external 
  symbol __imp__AlphaBlend@44 referenced in function @Z...
1>win32_msimg32.exe : fatal error LNK1120: 3 unresolved externals
1>win32_msimg32 - 4 error(s), 1 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

链接器抱怨它不知道如何在编译后的代码中映射函数调用。链接器需要知道如何找到错误中描述的三个函数,但它在程序中找不到任何定义。在这种情况下,这些函数将在运行时从动态链接库 (DLL) MsImg32.dll 中加载。

据我所知,在 Visual Studio 中有两种基于项目的方法可以配置链接器输入库。最常见的是项目设置,另一种是链接器的 #pragma 语句。这两种方法都会在链接器启动时将输入链接库的名称添加到命令行参数列表中。

项目链接器设置

使用 Visual Studio 2008 访问链接库输入设置

  1. 选择需要链接库定义的项目
  2. 通过菜单导航到 **项目 | 属性**
  3. 在左侧树状视图中选择 **配置属性 | 链接器 | 输入**
  4. 在“附加链接器输入”中输入附加链接器输入库的名称,在本例中为 msimg32.lib

#pragma comment(lib, "lib_name")

当我不编写可移植的跨平台代码,或者代码模块本身是 Win32 特定时,我更倾向于在需要附加 DLL 的代码模块中声明一个 #pragma comment(lib,"msimg32") 链接器指令。库名的 .lib 扩展名是隐含的,但如果您愿意,也可以加上。使用这种方法,如果我将此代码模块移到另一个项目中,设置将自动为我设置好。当有大量项目都需要自定义配置时,项目管理可能会变得一团糟。这种配置方法几乎为我消除了这个问题。

// MsImg32Usage.cpp ...
#include <windows.h>

/* Linker Directives *********************************************************/
#pragma comment( lib, "Msimg32" )   // Linker directive for the topic dll 

...

在我那个年代,我们只有 1 和 0 才能编程……

当这些函数在早已被遗忘的操作系统(Windows 98 和 Windows 2000)中发布时,上述描述的过程是访问这些函数所必需的方法。自那时以来,这些函数也已添加到 GDI32.DLL 中,可以像任何其他 GDI 调用一样访问,而无需额外的链接器命令。但是,这些函数被赋予了不同的名称,以避免冲突和破坏向后兼容性。每个函数的前面都带有 Gdi。如果您使用以下名称引用这些函数,则无需在应用程序中指定链接库 MsImg32.lib。嘿,至少您现在知道其他将其他库链接到代码模块的方法了。

::GdiAlphaBlend(...)
::GdiGradientFill(...)
::GdiTransparentBlt(...)

成员介绍

当介绍以下三个函数时,令人惊讶的是有很多内容可以讨论。表面上看,它们很简单。它们的名称准确地描述了它们的作用。但是,如果您开始考虑这些函数如何交互以及如何使用它们来创建复合图像,那么这些函数可以组合起来创建出色的效果。我确定的展示所有关于这些函数信息的最佳方法是,首先介绍导出的成员,并附带快速示例。然后,深入探讨更强大的合成技术,以及我遇到并得以解决的一些非直观的问题。

::GdiTransparentBlt

::GdiTransparentBlt 是这三个函数中最简单的。该函数提供了使用颜色键控或色度键类型效果的能力,也称为蓝屏(不要与 BSOD 混淆)。调用时会指定一种颜色,该颜色在源位图绘制到目标位图上时将完全透明。如果您熟悉 ::BitBlt,那么使用 ::GdiTransparentBlt 应该不成问题。

BOOL GdiTransparentBlt(
  __in  HDC hdcDest,        // Handle to the destination DC
  __in  int xoriginDest,    // X coordinate of the upper-left corner in destination rectangle
  __in  int yoriginDest,    // Y coordinate of the upper-left corner in destination rectangle
  __in  int wDest,          // The width of the destination rectangle
  __in  int hDest,          // The height of the destination rectangle
  __in  HDC hdcSrc,         // Handle to the source DC
  __in  int xoriginSrc,     // X coordinate of the upper-left corner in source rectangle
  __in  int yoriginSrc,     // Y coordinate of the upper-left corner in source rectangle
  __in  int wSrc,           // The width of the source rectangle
  __in  int hSrc,           // The height of the source rectangle
  __in  UINT crTransparent  // The RGB color in the source bitmap to treat as transparent
);

MSDN 的文档指出,此函数的所有尺寸都以逻辑单位列出。该术语与 Win32 GDI 支持的缩放模式和视口变换有关。本文将坚持使用默认的映射模式,称为 MM_TEXT。在此模式下,一个逻辑单位映射到一个设备像素,正 X 轴向右,正 Y 轴向下。

我之前将此函数与 ::BitBlt 进行了比较,但实际上它更像 ::StretchBlt 函数。它提供了两个额外的尺寸参数用于源图像。这些值是要从位图中读取的所需宽度和高度,类似于目标图像的定义。这里的区别是,如果源和目标的两个尺寸不匹配,源图像将被缩放以匹配指定的目标尺寸。总的来说,这很方便而且效果很好;但是,如果依赖 GDI 来缩放所有图像,您的图像质量可能会受到严重影响。我建议您谨慎使用此函数的拉伸功能。

::GdiTransparentBlt 中还有一个参数,这个参数与 ::BitBlt::StretchBlt 中的 ROP 代码不同。crTransparent 参数是识别要消除以实现透明的颜色的颜色。这是位图中的色度键颜色。一种常见的约定是将图像的第一个像素设为透明色。但是,使用 ::GdiTransparentBlt,您可以指定任何所需的 RGB 元组。

关于此函数,我想提及最后一个话题,但不想过多深入。有几种选项可用于指定在将图像压缩到较小区域时如何缩放图像。您可以在调用 ::GdiTransparentBlt 之前,通过调用 ::SetStretchBltMode 来设置此模式。以下是四种模式及其在合并像素时的基本行为:

  • BLACKONWHITE:使用 AND 运算符确定哪些像素被消除。此模式偏好黑色而非白色像素。
  • COLORONCOLOR:简单地删除被移除的线条的像素。
  • HALFTONE:将像素映射到块,并取块的平均值来决定目标像素的颜色。
  • WHITEONBLACK:使用 OR 运算符确定哪些像素被消除。此模式偏好白色而非黑色像素。

查看以下可视化解释,了解在调用 ::GdiTransparentBlt 序列时发生的情况。

No don't look, this codes not ready yet!

下面的示例代码将使用两个设备上下文表面,并使用 ::GdiTransparentBlt 将第二个图像放在第一个图像之上,透明键为 RGB(0,0,0xFF)

HDC hImageDC = ::CreateCompatibleDC(hdc);
  
// Common method to load bitmap images from a file or a resource.
article::AutoBitmap bmpBob((HBITMAP)::LoadImage(NULL, 
                                              _T("codeproject_bob.bmp"), 
                                              IMAGE_BITMAP, 
                                              0, 0, 
                                              LR_LOADFROMFILE | LR_DEFAULTCOLOR));
::SelectObject(hImageDC, bobBmp);
                                              
// Method to request the properties of the GDI Object.
BITMAP bm;
::GetObject(bmpBob, sizeof(bm), &bm);


// This example scales the image down by 50%.
// Set the Stretch Blt Mode that will average the value of the combined pixels. 
::SetStretchBltMode(hImageDC, HALFTONE);

// Blt and scale the image to the destination DC, 
// while processing a transparency layer.
::GdiTransparentBlt(hdc, 
                  0, 0, bm.bmWidth / 2, bm.bmHeight / 2, 
                  hImageDC, 
                  0, 0, bm.bmWidth, bm.bmHeight, 
                  RGB(0, 0, 255));    // Remember Uno, Wild Four? I choose Blue.

::DeleteDC(hImageDC);

::GdiGradientFill

过去十年,渐变变得非常流行。它们可以以很少的工作量创建令人印象深刻的效果。MSDN 指出,三角形的填充不必“像素完美”,以简化硬件加速。这对我来说意味着,通过使用此函数,您将获得比实现自己的算法版本更快的速度。如果您需要比我的演绎推理更多的信息,可以使用 API ::GetDeviceCaps 查询 SHADEBLENDCAPS 索引。这将指示设备是否能够混合矩形(SB_GRAD_RECT)和三角形(SB_GRAD_TRI)。

在写这篇文章之前,我只用这个函数来创建矩形渐变填充。在那种情况下,这个函数感觉像是要做很多工作。所以,我通常避免使用它,除非它正好是我需要的东西;但是,不一定非要这样,希望在接下来的部分之后,您会觉得这个 API 更容易使用。

BOOL GdiGradientFill(
  __in  HDC hdc,            // Handle to the DC
  __in  PTRIVERTEX pVertex, // Array of vertices which are the points of the polygon
  __in  ULONG dwNumVertex,  // Size of the vertices array
  __in  PVOID pMesh,        // Array of mesh triangles to fill with the gradient
  __in  ULONG dwNumMesh,    // Size of the mesh array
  __in  ULONG dwMode        // Gradient fill mode
);

粗略一看定义,只发现一个我们以前用过的参数,即设备上下文。最后一个参数是创建渐变效果类型的模式。令人惊讶的是,只有三种模式,这似乎功能不多。前两种模式是 GRADIENT_FILL_RECT_HGRADIENT_FILL_RECT_V,分别用于水平或垂直渐变填充。两种模式都用于矩形填充?!这就像得到三个愿望,却用前两个愿望许下了不知道该许什么愿。

第三种模式是 GRADIENT_FILL_TRIANGLE。渐变分布在三角形的三个独立顶点上。每个三角形的顶点都可以分配自己的颜色。这种模式实际上是实现我们可能想要的任何渐变效果所需的唯一模式(我可能在夸大,但差别不大)。对于实时计算机图形应用程序,模型被分成称为**网格**的三角形组。这个过程称为镶嵌。任何离散多边形都可以分解为一组三角形。因此,我们可以构建一个三角形网格来为任何形状、图案或方向创建渐变填充。前两种模式可以通过创建两个三角形的网格来轻松地用 GRADIENT_FILL_TRIANGLE 模式实现。所以这个 API 的开发者充分利用了第三个愿望。

新数据类型

为该函数特别定义了三个新的 struct

typedef struct _TRIVERTEX {
  LONG        x;            // Specifies the x-coordinate of the vertex location.
  Long        y;            // Specifies the y-coordinate of the vertex location.
  BYTE        Red;          // Indicates the red channel color information at (x,y)
  BYTE        Green;        // Indicates the green channel color information at (x,y)
  BYTE        Blue;         // Indicates the blue channel color information at (x,y)
  BYTE        Alpha;        // Indicates the alpha channel information at (x,y)
} TRIVERTEX, *PTRIVERTEX;

TRIVERTEX 将定义您希望用渐变填充的形状的点。此结构允许顶点的位置和颜色定义在单个结构中相互关联。创建成功的渐变需要少量 TRIVERTEX 结构。此结构中有两件事很突出。我们以前没有遇到过 COLOR16 数据类型,并且其中一个字段称为 Alpha

COLOR16

COLOR16 只是 USHORT 类型的重定义,它的大小为两字节。这很重要,因为到目前为止,所有颜色通道都由单个 BYTE 定义。不幸的是,我不知道有任何功能可以利用这种增加的值的大小。另一个缺点是,通常提供的单字节颜色信息需要放置在 COLOR16 字段的高位字节中。真是麻烦!就好像他们真的不想让开发者使用这个函数一样。

我声明了一组 inline 函数来帮助我度过这个难关;我现在几乎注意不到额外的处理工作了。每个函数都将提取指定颜色通道的 BYTE 数据,并向左移动 8 位。

inline COLOR16 ToColor16(BYTE byte)   { return byte << 8;}
inline COLOR16 RVal16(COLORREF color) { return ToColor16(GetRValue(color));}
inline COLOR16 GVal16(COLORREF color) { return ToColor16(GetGValue(color));}
inline COLOR16 BVal16(COLORREF color) { return ToColor16(GetBValue(color));}

Alpha 通道

这是渐变填充功能集的一个很棒的补充。不幸的是,在调用 ::GdiGradientFill 时,Alpha 通道并未应用于 DC 表面。但是,当创建渐变并指定 Alpha 通道设置时,会生成 Alpha 通道。因此,调用 ::GdiAlphaBlend 可以使用该通道信息进行更详细的混合。您将在应用程序中看到我经常利用这一点。创建一些不错的混合效果是一个非常简单的过程。

网格

网格是计算机图形学中的一个通用术语。网格是由其公共边连接的三角形集合。网格可以表示 2D 和 3D 曲面。为 ::GdiGradientFill 函数定义的最后两个结构在性质上是相似的。根据选择的 dwMode,每次调用函数时只会使用其中一个。这两个结构引用 pVertex 数组参数中顶点的索引,以定义网格中的一个多边形。可以创建这些结构的数组并将其传递给 ::GdiGradientFill 调用,以一次调用绘制大量多边形。

// Use GRADIENT_RECT to define a mesh of rectangles with a set of TRIVERTEX objects
typedef struct _GRADIENT_RECT {
  ULONG    UpperLeft;       // The index of the upper-left point of the rectangle to fill.
  ULONG    LowerRight;      // The index of the lower-right point of the rectangle to fill.
} GRADIENT_RECT, *PGRADIENT_RECT;

// Use GRADIENT_TRIANGLE to define a mesh of triangles with a set of TRIVERTEX objects
typedef struct _GRADIENT_TRIANGLE {
  ULONG    Vertex1;         // The index of First point of the triangle.
  ULONG    Vertex2;         // The index of Second point of the triangle.
  ULONG    Vertex3;         // The index of Third point of the triangle.
} GRADIENT_TRIANGLE, *PGRADIENT_TRIANGLE;

渐变示例

以下是使用 MSDN 中提供的函数创建的渐变的一些图像,以及 MSDN 演示的示例代码。

水平和垂直矩形的渐变

MSDN:绘制着色矩形.

// Note: I did modify the name of the call to ::GdiGradientFill from the 
//       original in the sample GradientFill so you wouldn't have to link msimg32.lib.

// Create an array of TRIVERTEX structures that describe 
// positional and color values for each vertex. For a rectangle, 
// only two vertices need to be defined: upper-left and lower-right. 
TRIVERTEX vertex[2] ;
vertex[0].x     = 0;
vertex[0].y     = 0;
vertex[0].Red   = 0x0000;
vertex[0].Green = 0x8000;
vertex[0].Blue  = 0x8000;
vertex[0].Alpha = 0x0000;

vertex[1].x     = 300;
vertex[1].y     = 80; 
vertex[1].Red   = 0x0000;
vertex[1].Green = 0xd000;
vertex[1].Blue  = 0xd000;
vertex[1].Alpha = 0x0000;

// Create a GRADIENT_RECT structure that 
// references the TRIVERTEX vertices. 
GRADIENT_RECT gRect;
gRect.UpperLeft  = 0;
gRect.LowerRight = 1;

// Draw a shaded rectangle. 
::GdiGradientFill(hdc, vertex, 2, &gRect, 1, GRADIENT_FILL_RECT_H);

// You wanted the gradient to be vertical instead.
// Change the last parameter:
::GdiGradientFill(hdc, vertex, 2, &gRect, 1, GRADIENT_FILL_RECT_V);

我认为,不使用视频加速的情况下,编写一个 for 循环来绘制相同的水平或垂直渐变可能需要更少的代码。这是上面代码的结果。

MSDN sample rectangular horizontal gradient. MSDN sample rectangular vertical gradient.

三角形渐变

同样,这是 MSDN 提供的绘制着色三角形的示例。

MSDN:绘制着色三角形.

// Note: I did modify the name of the call to ::GdiGradientFill from the 
//       original in the sample GradientFill so you wouldn't have to link msimg32.lib.

// Create an array of TRIVERTEX structures that describe
// positional and color values for each vertex.
TRIVERTEX vertex[3];
vertex[0].x     = 150;
vertex[0].y     = 0;
vertex[0].Red   = 0xff00;
vertex[0].Green = 0x8000;
vertex[0].Blue  = 0x0000;
vertex[0].Alpha = 0x0000;

vertex[1].x     = 0;
vertex[1].y     = 150;
vertex[1].Red   = 0x9000;
vertex[1].Green = 0x0000;
vertex[1].Blue  = 0x9000;
vertex[1].Alpha = 0x0000;

vertex[2].x     = 300;
vertex[2].y     = 150; 
vertex[2].Red   = 0x9000;
vertex[2].Green = 0x0000;
vertex[2].Blue  = 0x9000;
vertex[2].Alpha = 0x0000;

// Create a GRADIENT_TRIANGLE structure that
// references the TRIVERTEX vertices.
GRADIENT_TRIANGLE gTriangle;
gTriangle.Vertex1 = 0;
gTriangle.Vertex2 = 1;
gTriangle.Vertex3 = 2;

// Draw a shaded triangle.
::GdiGradientFill(hdc, vertex, 3, &gTriangle, 1, GRADIENT_FILL_TRIANGLE);

MSDN sample triangular gradient.

简化 GdiGradientFill 的使用

在撰写本文时,我开始厌倦为演示应用程序中创建的大量渐变设置所有顶点定义。然后我决定编写一个小型辅助函数库。这些辅助函数定义在两个 C++ 文件中,名为 BitBlender.h/cpp。我前面提到的用于 16 位颜色移位的 inline 函数也包含在头文件中。

这里有两个用于创建矩形渐变的定义。第一个仅处理 RGB 颜色元组,第二个还允许您指定 Alpha 通道值。是的!如果需要,您可以指定 Alpha 通道。我添加了一些升级功能,用于计算像素的强度级别,并将该值分配给该像素的 Alpha 通道。这会稍微增加一些处理能力,但现在需要时它就在那里了。

namespace article
{
bool RectGradient(
  HDC hDC,                  // The device context to write to.
  const RECT &rc,           // The rectangle coordinates to fill with the gradient.
  COLORREF c1,              // The color to use at the start of the gradient.
  COLORREF c2,              // The color to use at the end of the gradient.
  BOOL isVertical)          // true creates a vertical gradient, false a horizontal

bool RectGradient(
  HDC hDC,                  
  const RECT &rc,           
  COLORREF c1,              
  COLORREF c2,              
  BOOL isVertical,          
  BYTE alpha1,              // Starting alpha level to associate with the gradient.
  BYTE alpha2               // Ending alpha level to associate with the gradient.
  )
} // namespace article

这些函数实现只是对与 MSDN 示例中提供的代码非常相似的代码的一个更方便的封装。这是创建水平渐变矩形所需的新调用。

// Horizontal Gradient
RECT rc = {0,0,300,80};
COLORREF black = RGB(0,0,0);
COLORREF blue  = RGB(0,0,0xff);
artical::RectGradient(hdc, rc, black, blue, false);

// Let's draw a vertical gradient right beside the horizontal gradient:
::OffsetRect(&rc, 310, 0);
artical::RectGradient(hdc, rc, black, blue, true);

径向渐变

三角形是最简单的封闭多边形。通过以不同的配置排列三角形的网格,可以创建几乎任何想象中的渐变效果。三角形渐变的第一个扩展用法是我从 Feng Yuan 的书《Windows Graphics Programming》中学到的一个例子。他演示了如何创建从一点发出的径向渐变。

对于熟悉微积分的人来说,可以考虑积分近似来计算曲线下的面积。这是一个 pretty picture 来刷新您的记忆。

Approximating the area under a curve by sub-division.

图像来源:Wikipedia

计算矩形的面积是一个简单的公式:宽度 * 高度。如果您继续细分矩形并将它们放入曲线中,然后对所有矩形的面积求和,您就可以更接近曲线下方实际面积的近似值。随着子分割数量趋于无穷大,您的近似值将越接近实际值。有关更严谨的解释和证明,请查阅您的微积分书籍。

Feng Yuan 将这个原理应用于一个圆,并将其分解为三角形(镶嵌),而不是矩形。中心点是每个三角形共有的一个顶点,并且该点被分配了起始渐变颜色。每个三角形的另外两个顶点共享第二个渐变颜色。在子分割中包含的三角形样本越多,您将越接近径向渐变。下面图像中每个形状旁边的数字表示用于渲染径向渐变的三角形数量。

Approximating a radial gradient by sub-division of a circle with triangles.

RadialGradient 函数

这是我编写的用于创建上面图像中的径向渐变形状的函数声明。Feng Yuan 将他的实现进一步发展,允许起始点偏离中心。我保持了这个接口的简单性,以减少使用函数所需的参数数量。另外,我不确定 GDI+ 中存在什么类型的功能,并且我想避免重复该库中可能已存在的内容。

您可以在与 RectGradient 函数相同的文件 BitBlender.cpp 中查看此函数的源代码。

namespace article
{
bool RadialGradient(
  HDC hdc,                  // The device context to write to.
  int x,                    // The x-coordinate of the center of the gradient.
  int y,                    // The y-coordinate of the center of the gradient.
  int r,                    // The radius of the gradient fill.
  COLORREF c1,              // The color to use at the center of the gradient.
  COLORREF c2,              // The color to use at the edge of the gradient.
  size_t segments           // The number of triangle segments to use in the fill.
  );

bool RadialGradient(
  HDC hdc, 
  int x, 
  int y, 
  int r,
  COLORREF c1,
  COLORREF c2,
  size_t segments,
  BYTE alpha1,              // Starting alpha level to associate with the gradient.
  BYTE alpha2               // Ending alpha level to associate with the gradient.
  );
} // namespace article

角度渐变

到目前为止,在我探索渐变和位图混合技术时,我曾认为我已经掌握了所有容易实现的功能。我已准备好转向 Alpha 混合并认为可以了。然后我突然想到,在矩形中创建任意角度的渐变填充会多么简单,而不是只针对水平和垂直情况。

作为参考,这就是我们追求的目标。

Gradient fill of a rectangle area at an arbitrary angle.

AngularGradient 函数

这是我编写的函数的声明,紧随其后的是用于创建上面图像的调用。

namespace article
{
bool AngularGradient(
  HDC hdc,                  // The device context to write to.
  const RECT &rc,           // The rectangular boundary for the gradient fill.
  double angle,             // The angle in radians which to draw the gradient.
  COLORREF c1,              // The color to use at the center of the gradient.
  COLORREF c2,              // The color to use at the edge of the gradient.
  );

bool AngularGradient(
  HDC hdc,                  
  const RECT &rc,           
  double angle,             
  COLORREF c1,              
  COLORREF c2,              
  BYTE alpha1,              // Starting alpha level to associate with the gradient.
  BYTE alpha2               // Ending alpha level to associate with the gradient.
  );
} // namespace article


/*****************************************************************************/
// Usage:
// Constants
const COLORREF k_white        = RGB(0xFF, 0xFF, 0xFF);
const COLORREF k_cpDarkGreen  = 
                              // You can never have too much pi
const double   k_pi           = 3.1415926535897932384626433832795;

// Theta convert to radians
double angle = (k_pi / 180.0) * theta; 
RECT bounds  = {0, 0, 300, 300};

COLORREF c1  = RGB(0x48, 0x8E, 0x00);  // Code Project Green
COLORREF c2  = RGB(0xFF, 0xFF, 0xFF);  // White
article::AngularGradient(hBufferDC, bounds, angle, k_cpDarkGreen, k_white);

::GdiTransparentBlt 有两个特性使解决这个问题变得更加简单。

  1. 为每个顶点分配不同颜色的能力。
  2. 定义描述矩形的三角形网格的能力。

输入矩形的四个点是已知的,两个颜色,以及渐变的某个角度。这留下了两个颜色需要确定。我当时正站在马桶上挂钟,结果摔倒了,头撞到了水槽。那时我才想出了如何轻松获得另外两种颜色的主意。幸运的是,只花了 1 瓦特就实现了。

想象目标矩形被一个与期望渐变角度对齐的更大矩形所包围。对于那些想象力不佳的人来说,这里还有另一张 pretty picture。

Gradient fill of a rectangle area at an arbitrary angle inscribed in a rectangle aligned with the gradient angle.

挺好,但我是指带颜色的。

Gradient fill of a rectangle area at an arbitrary angle inscribed in a rectangle aligned with the gradient angle.

完美!

外接矩形可以通过一些计算来确定旋转矩形的四个点,以及原始的两种颜色。原始矩形中需要定义两个三角形来创建传递给 ::GdiGradientFill 的网格。C1 与该矩形的起始边共线,C2 与该矩形的结束边共线。因此,P1 和 P2 的颜色分别为 C1 和 C2。

Gradient fill of a rectangle rotated an arbitrary angle.

我们需要找到 P3 和 P4 的颜色。

Gradient fill of a rectangle rotated an arbitrary angle.

以下是从上述图表中得到的一些重要观察结果:

  • 线性渐变可以映射到离散线,以创建从 C1C2凸组合
  • P3P4 分叉它们各自的边。分段将标记为 AB
  • A 的长度可以通过线段 P1P3 和角度 theta 计算得出:A = cos(theta)
  • B 的长度可以通过线段 P3P2 和角度 theta 计算得出:B = sin(theta)

有两件重要的事情使得计算这两个交点的颜色变得非常简单:

  1. 有四个相似的直角三角形,通过斜边连接在目标矩形外部。我们可以使用基本三角学从三角形计算到交点的边长。
  2. 这是从 P1P2 拉伸的线性渐变。从第一步计算出的值可以转换为一对比率。每个值都可以通过将该部分的比例映射到线性渐变来确定。

一旦计算出比率,就可以通过将点的比率乘以起始颜色 C1 和结束颜色 C2 之间的差值来确定该点的颜色。该图演示了前两个点的信息。左侧的图显示了确定比率的所有计算。中间的图显示了四个点到线性渐变的映射。右侧的图描绘了如何计算每个通道的颜色以创建最终颜色 C3 和 C4(注意:Alpha 通道在图中无法检测到,因为它无色无味)。

Gradient fill of a rectangle rotated an arbitrary angle.

The interpolated color points calculated by multiplying the ratio of the point along the sides intersection.

这是计算边比率并推导出交点颜色的实际代码。

...

// Familiar calculations also shown in image above.
double s1 = (width/2)  * cos(angle);
double s2 = (height/2) * sin(angle);

double len= fabs(s1) + fabs(s2);
double r1 = fabs(s1 / len);
double r2 = fabs(s2 / len);

// Calculate the difference in color level for each color channel.
int rDiff = 0;
int gDiff = 0;
int bDiff = 0;

article::GetColorDiff(c1, c2, rDiff, gDiff, bDiff);

// Calculate the other colors by multiplying the color diff with each ratio.
COLORREF cA = RGB(GetRValue(c1) + (rDiff * r1),
                GetGValue(c1) + (gDiff * r1),
                GetBValue(c1) + (bDiff * r1));
COLORREF cB = RGB(GetRValue(c1) + (rDiff * r2),
                GetGValue(c1) + (gDiff * r2),
                GetBValue(c1) + (bDiff * r2));
...

总有角边情况

在我让它运行起来后,我惊喜地发现它第一次就能按设计工作,只有一个例外。当我在 0 到 2pi 之间旋转动画时,颜色渐变会“跳跃”来回。我将问题追溯到根据给定角度所在的象限计算的值的符号。需要进行两次小的调整才能实现完整的 360° 平滑渐变动画。

  1. 两个插值点的颜色需要与彼此交换,每当象限改变时。这是由于 cos 波谷底部的龙潜伏。
  2. double quad     = (angle / (k_pi / 2));
    int    offset   = int(ceil(quad) - 1);
    if (0 == (offset % 2)) 
    { 
        std::swap(cA, cB); std::swap(alphaA, alphaB); 
    }
  3. 旋转每个顶点的颜色以匹配原始起始颜色经过的象限数量。如果 C1 经过两个象限,则将所有颜色旋转两位。
  4. // Offset value used from above.
    COLORREF clr[4] = { c1, c3, c2, c4 };
    std::rotate(clr, clr + offset, clr + 4);

甜蜜的讽刺

对我来说,整个练习的讽刺之处在于,计算包围这个旋转渐变的矩形比计算渐变本身要复杂得多。我想在目标图像周围放一个正方形,并随着角度的变化旋转边界框。这是为了向自己证明我创建了正确的解决方案。起初,我以为我可以像计算颜色那样使用比率。然而,颜色是从线性函数插值的,而点则由具有 sincos 的周期性参数函数控制。

因此,最终我编写了矩阵旋转计算来旋转原始矩形不同点。然而,这只会旋转多边形本身,然后还有另一个计算来确定包含原始矩形所需的最小边界框的大小。最后,必须执行宽度和高度的旋转计算,并且最终点成为基于点所在象限的组件之和。这是在演示应用程序中旋转边界形状的代码。

// The "sign" of the operations will flip in the even quadrants.
LONG factor = (offset % 2) 
            ? -1 
            : 1;

LONG halfWidth = GetWidth() / 2;
LONG halfHeight= GetHeight()/ 2;

double q1 = (halfWidth)   * cos(angle*2)           * factor;
double q2 = (halfWidth)   * sin(angle*2)           * factor;
double q3 = (halfHeight)  * sin((angle-k_pi_2)*2)  * factor;
double q4 = (halfHeight)  * cos((angle-k_pi_2)*2)  * factor;

pts[0].x = LONG(ctr.x - q1);
pts[0].y = LONG(ctr.y - halfHeight - q2);

pts[1].x = LONG(ctr.x - halfWidth + q3);
pts[1].y = LONG(ctr.y - q4);

pts[2].x = LONG(ctr.x + q1);
pts[2].y = LONG(ctr.y + halfHeight + q2);

pts[3].x = LONG(ctr.x + halfWidth - q3);
pts[3].y = LONG(ctr.y + q4);

// Rotate the points through the different quadrants 
// as the specified angle changes.
std::rotate(pts, pts + offset, pts + 4);

这是演示应用程序中的一张图像,显示了矩形渐变旋转,以及用于查找另外两种颜色的计算出的比率和其他中间值。

Demo App Screenshot: Gradient at an Arbitrary Angle

更复杂的渐变

可以创建许多更复杂的渐变。我看到的一些包括圆锥形多色带有过渡点的线性,甚至还有应用了变换的渐变,以创建螺旋、涟漪和其他效果。其中一些我想进一步探索。但是,还有很多其他我想深入研究的 Windows GDI 内容。我还想评估 GDI+ 的功能,然后再尝试重新发明轮子。因此,我必须结束渐变的主题,然后继续介绍 msimg32.dll 中的最后一个函数。

::GdiAlphaBlend

Sample App Screenshot: MSDN Alpha-Blend Sample

MSDN AlphaBlend 示例应用程序。

Alpha 混合是一种合成过程,可创建半透明或完全透明图像叠加在背景图像上的效果。Alpha 混合非常有用。效果可以从使背景透过前景对象的透明“孔”,到发光闪烁的半透明对象。

最基本的合成形式可以认为是上面描述的 ::GdiTransparentBlt API 调用。Alpha 混合的行为与该 API 非常相似。但是,Alpha 混合功能更强大,更通用,并且可能要复杂得多。要正确执行 ::GdiAlphaBlend 的演示,肯定需要更多的代码和耐心。

BOOL GdiAlphaBlend(
  __in  HDC // Handle to the destination DC
  __in  int xoriginDest,    // X coordinate of the upper-left corner in destination rectangle
  __in  int yoriginDest,    // Y coordinate of the upper-left corner in destination rectangle
  __in  int wDest,          // The width of the destination rectangle
  __in  int hDest,          // The height of the destination rectangle
  __in  HDC hdcSrc,         // Handle to the source DC
  __in  int xoriginSrc,     // X coordinate of the upper-left corner in source rectangle
  __in  int yoriginSrc,     // Y coordinate of the upper-left corner in source rectangle
  __in  int wSrc,           // The width of the source rectangle
  __in  int hSrc,           // The height of the source rectangle
  __in  BLENDFUNCTION ftn   // alpha-blending function
);

此函数声明看起来几乎与 ::GdiTransparentBlt 相同,只是最后一个参数 UINT crTransparentBLENDFUNCTION ftn 参数替换了。BLENDFUNCTION 是一个由四个单字节字段组成的结构。因此,它适合 32 位缓冲区。这是 BLENDFUNCTION 的声明。

typedef struct _BLENDFUNCTION {
  BYTE     BlendOp;               // Specifies the source blend operation.
  BYTE     BlendFlags;            // Must be zero. 
  BYTE     SourceConstantAlpha;   // Global level of transparency for the image blend.
  BYTE     AlphaFormat;           // Indicates the type of source image (RGB | ARGB).
}BLENDFUNCTION, *PBLENDFUNCTION, *LPBLENDFUNCTION;

这些字段中的每一个都需要比单行注释所能容纳的更多描述,但差别不大。

  • BlendOp:此时只有一个混合操作定义,即 AC_SRC_OVER。当混合两个图像时,源图像将放置在目标图像的上方,然后进行混合。每个源像素的 Alpha 值将决定目标图像的可见程度。
  • BlendFlags:此值将有助于延迟不可避免的 ::GdiAlphaBlendEx。目前,它必须保持不变并设置为 0。
  • SourceConstantAlpha:这是一个全局透明度常量,将在混合操作期间应用于每个像素。值的范围是从 0 到 255;其中 0 是 100% 透明,逐渐增加到 255,即 0% 透明,完全不透明。此值还将与任何源 Alpha 结合。
  • 例如:如果您有一个源像素 Alpha 为 50%,源常量 Alpha 为 50%,结果将在预混合期间从源像素中取 50%,然后与常量混合再取 50%,留下 25% 源和 75% 目标。

  • AlphaFormat:指示图像中编码的 Alpha 通道信息的类型。如果此值设置为 0,则仅使用 SourceConstantAlpha 参数混合图像。
  • AC_SRC_ALPHA 表示图像使用每像素混合进行编码。但是有一个陷阱,请阅读 MSDN 的细则:

    含义
    AC_SRC_ALPHA 设置此标志表示位图具有 Alpha 通道(即每像素 Alpha)。**请注意,API 使用预乘 Alpha**,这意味着位图中的红色、绿色和蓝色通道值必须预先乘以 Alpha 通道值。例如,如果 Alpha 通道值为 **x**,则在调用之前,红色、绿色和蓝色通道**必须乘以 x 再除以 0xff**。

函数本身不会执行每像素混合。您,即函数的调用者,必须确保在调用 ::GdiAlphaBlend 并将 AC_SRC_ALPHA 值设置为 1 之前,图像已进行预混合。

预混合 Alpha 通道对图像有什么影响?

24 位 RGB 颜色方案能够显示每种颜色(红色、绿色、蓝色)的 256 种色调;每个颜色通道的最大和最亮的赋值为 255 或十六进制的 0xFF。Alpha 混合将两个像素的一部分组合起来创建新的像素。两个像素通道值的总和不能超过 255 的最大值。Alpha 乘法将源图像缩放到所需的阈值内,其中通道的最大值将等于 Alpha 通道的值。

因此,预混合步骤将降低源图像的亮度,并留下目标通道颜色值与最大值之间的差值。当预混合的源图像传递给 ::GdiAlphaBlend 时,API 将缩放目标像素的值,然后添加源像素的值以创建最终颜色。

下图显示了位图如何受到预混合步骤的影响。

original image An artists representation of the alpha channel in grayscale. Remember, the alpha channel is odorless and colorless. The source image after the pre-blend process.
源图像
Alpha 通道
预混合源图像
Background destination image. Maybe this is the look you were going for, I won't judge you. Final Image after the source has been pre-blended.
目标图像
无预混合的最终图像
带预混合的最终图像

几周前,当我试图在此文章 Win32 内存 DC 指南中挤出我想要的混合效果时,AC_SRC_ALPHA 的含义让我非常沮丧。我的图像中的值变得饱和,即两个像素的总和超过了该通道的最大值 0xFF。这导致图像上出现奇怪的伪影,例如大片漂白区域或意外颜色的添加带。我注意到了 MSDN 上指示图像应进行预混合的句子,但我继续忽略它,因为直到遇到问题之前,我一直得到我想要的结果。

有时您的图像在不预混合的情况下也能工作。

在我完成关于 Win32 Memory DCs 的文章时,我并没有揭开 ::GdiAlphaBlend 函数的所有秘密。当我第一次创建屏幕擦除器时,它是在一个空白的白色画布上。我想让擦除器的颜色渐变到画布。在空白画布上,我通过将擦除器的颜色与白色混合轻松实现了这一点。然后,我在背景中添加了一些矩形以使闪烁更明显。这是我创建的:

This is what happened to my display when I no longer had a solid white background.

::GdiAlphaBlend 可能创造的众多怪异事物之一。

我徘徊了一会儿,排查问题,试图弄清楚发生了什么。是的,问题是我没有预混合图像的 Alpha。但是,在我弄清楚我错误的原因之前,我偶然发现了一个在这种情况下有效的解决方案。我创建的擦除器渐变到黑色而不是白色。黑色为源图像的每个颜色通道增加了 0 的强度。如果您的渐变不使用更亮的颜色,那么达到饱和点的几率会小得多。所以那个程序有可能创建饱和的 Alpha 混合,但目前它不使用会创建该条件的颜色。

如果使用 ::BitBlt 而不是 ::GdiAlphaBlend 将原始擦除器图像绘制到屏幕上,它是这样的:

The work-around I stumbled upon to make that apps effect work the way I wanted.

使用 BitBlt 绘制的擦除器。

如果您还没有访问过我的内存 DC 文章,最终结果看起来是这样的。

Success!!! Even if I didn't understand why. But hey, that probably describes 3/4 of the programmers out there :\

让我们剖析一个参考混合图像

这张图像是一个将从绿色过渡到红色的渐变矩形进行 Alpha 混合的示例。渐变混合到五个不同颜色的条形上,以演示混合时发生的效果。源图像未进行预混合。因此,只执行了像素的加法,这在下面的图像中产生了各种伪影。

我在图像中标记了感兴趣的点。项目 1-6 绘制在白色背景上。更多细节描述了其他感兴趣的点。

  1. 绿色到红色渐变条,简单绘制在屏幕上。
  2. 红色参考条。
  3. 黑色参考条。
  4. 绿色参考条。
  5. 白色参考条。
  6. 蓝色参考条。
  7. 绿色到红色渐变条,类似于项目一。但是,这两个条是使用 ::GdiAlphaBlend 添加到图像中的。
    1. 绿色 + 红色向橙色累积。但是,红色变得饱和并溢出到绿色通道。
    2. 绿色继续减少,并返回到红色。
    1. 绿色和红色随着它们向中心移动而淡出。但是,它们被添加到已经达到最大值的白色背景上。绿色 + 红色的最大值创建黄色,然后发生饱和。
    2. 值溢出到蓝色通道。随着过渡继续向右,绿色通道饱和,最终蓝色通道也饱和。结果是白色。
  8. 纯粹的精彩!在写上一篇文章时,我在混乱中摸索,发现如果我在一个渐变成黑色的渐变上混合我的图像,一切都会完美运行。起初这有点违反直觉。但是,一旦我意识到我没有预混合任何 Alpha 值,并且黑色是 0,我就明白这种组合的混合不会使图像饱和。
    1. 当绿色渐变成红色时,没有通道饱和,最大绿色+红色在右侧达到以创建黄色。
    2. 类似于 a。
  9. 绿色渐变到一小组蓝色色调。但是,红色以其最大值开始,对于 RGB 来说,红色 + 蓝色会产生品红色。

AlphaBlendDemo image, displays how blending of colors will behave with mixed with RGB, black and white.

注意:这不是色盲测试。但是,如果您只在上面的框中看到两种颜色,您可能需要进行测试。

当您最终必须将 Alpha 通道预乘到图像中时,计算是直接的。最困难的部分是访问像素数据来对图像中的每个像素执行计算。有两个函数可以访问像素的读写,::GetPixel::SetPixel。但是,它们不够好,就像你女儿的那个男朋友一样。我不知道那个男孩为什么不够好,但是,我可以告诉你这些函数的缺点。这些函数只提供对像素 RGB 组件的访问。即使 ::GetPixel::SetPixel 可以访问 Alpha 通道,它们也非常慢,就像那个男孩一样。

需要直接访问位图内存缓冲区。这可以通过设备无关位图 (DIB) 来实现。这种方法的计算速度要快几个数量级,但是,它也需要涵盖另一个需要很多注意力才能做好它的主题,即 BITMAP 和 DIB。这就是为什么我决定略过这个话题。也许下次我会写一篇关于位图的文章。当您拥有对 32 位彩色位图的原始访问权限时,以下代码将为您预混合 Alpha 通道。

// The algorithm to pre-multiply the alpha channel on a bitmap.
// This code is modified  (a lot) from the MSDN AlphaBlend Sample listed above.
for (y = 0; y < ulBitmapHeight; y++)
{
  for (x = 0; x < ulBitmapWidth; x++)
  {
    UINT32 &rPixel = (UINT32*)pvBits[x + y * ulBitmapWidth]; 
    // Get the alpha value from the current pixel.
    UCHAR ubAlpha = rPixel >> 24;

    //calculate the factor by which we multiply each component 
    float fAlphaFactor = (float)ubAlpha / (float)0xff; 
    
    // multiply each pixel by fAlphaFactor, so each component  
    // is less than or equal to the alpha value. 
    rPixel =  (ubAlpha << 24) |                                   //0xaa000000 
              ((UCHAR)(GetRValue(rPixel) * fAlphaFactor) << 16) | //0x00rr0000 
              ((UCHAR)(GetGValue(rPixel) * fAlphaFactor) << 8)  | //0x0000gg00 
              ((UCHAR)(GetBValue(rPixel) * fAlphaFactor));        //0x000000bb 
  }
}

我创建了一个易于调用的函数,该函数将 32 位位图转换为 DIB,执行 Alpha 乘法步骤,并将结果图像放回原始位图中。

// Call this function once before you call ::AlphaBlend with the bitmap.
bool PreMultiplyBitmapAlpha(
  HDC hdc,        // Handle to the destination DC
  HBITMAP hBmp    // Handle to the bitmap to pre-blend
);

希望您还在听,因为所有枯燥的内容都已完成,从现在开始只有好东西了。

锯齿

对于不熟悉这个术语的人来说,这是计算机俚语,意为“糟糕的图形”。实际上,它是由于显示器的分辨率与细节比例低而在光栅(位图)显示器上出现的别名伪影。这通常通过抗锯齿技术来克服。Alpha 混合是其中一种抗锯齿技术。Alpha 混合用于模糊低分辨率边缘的边缘,并模拟半填充像素的外观。子像素采样是这种方法的另一个名称。

让我们近距离看一个例子。下面显示了三个图像。

  1. Bob 有“锯齿”。
  2. Bob 与背景进行 Alpha 混合。
  3. Bob 在位图编辑器中,Alpha 通道可见。注意橙色高亮显示的子像素混合。

Bob rendered with no anti-aliasing processing.

Bob rendered with alpha-blend against a white background. (If Kenny G could paint Bob with his clarinet, I bet this is what it would look like.)

Bob displayed with alpha-channel and Sub-pixel rendering highlighted in orange (now that's smooth jagz).

在大多数情况下,图像 2 比图像 1 有很大改进。但是,比较图像 1 和图像 2 中 Bob 的左脚。图像 2 中 Bob 脚的边缘变得模糊了。清晰锐利的边缘已被柔和光滑的边缘取代,细节略有损失。这是离散光栅图像与尝试通过子像素渲染来近似颜色之间存在的权衡。

图像 2 需要从我的图像编辑器导出才能放入易于加载到我的示例程序中的格式。在导出过程中,我必须使用背景色渲染图像。背景色设置为白色。因此,图像 3 中工作图像中的所有 Alpha 值都与白色背景混合。只要我总是在白色背景画布上绘制图像 2,它看起来都很棒。如果图像 2 在不同颜色的背景上绘制,结果如下:

  1. Bob 绘制在白色背景上。
  2. 白色背景被蓝色填充以改变背景颜色。
  3. Bob 从一开始就在蓝色背景上绘制,可以获得更清晰的图像。

Bob rendered on a white background. Bob rendered on a white background, and painted on a blue background. Bob rendered on a blue background.

图像 2 中的白色轮廓是 Alpha 混合通道与白色背景结合的伪影。当图像在任何颜色的背景上渲染时,Alpha 通道信息都会丢失。为了减少“锯齿”效果而进行的平滑处理将黑色(对象的轮廓颜色)的不同阴影与背景颜色混合。我们准备回答问题。

为什么我需要一个具有每个像素 Alpha 通道值的图像?

保持能够以编程方式将图像合成到任何可能的背景之上,包括随时间变化的动态背景。

为什么 Microsoft 不在 ::GdiAlphaBlend 中直接乘以 Alpha 通道?

性能

除非您的源图像不断变化,否则预乘步骤只需要执行一次。如果 ::GdiAlphaBlend 在静态源图像上被反复调用,这可能会对系统资源造成极大的负担,并导致性能大幅下降。查看以下场景的统计数据,以帮助您从这个角度看待:

  • 一个 24 位彩色、200x300 像素的图像包含 60,000 像素。
  • 3 个颜色通道。
  • 以每秒 30 帧的速率动画。
  • 需要 60,000 * 3 * 30 次预乘(~每次计算 1 次乘法,1 次除法)。
  • 总计每秒 540 万次冗余计算。

如果您关心性能和 ::GdiAlphaBlend,请查询您的显示设备以获取其 SHADEBLENDCAPS 功能。您可以使用 ::GetDeviceCaps 查询此信息。如果设备具有此功能,则混合操作将由您的适配器加速。抱歉,我没有数字来表明此函数加速后的性能如何。我可以告诉你,我从未遇到过使用此 API 造成的任何限制性绘图问题。如果我要写另一个游戏,我不会使用 GDI 或 GDI+,我会使用 DirectX。

演示

这个应用程序最初是一个空白画布,我在上面试验了用本文介绍的三个函数组合图像的各种方法。我探索的第一个概念是很明显的,即光泽球体。我在网上查阅了一些 Photoshop 教程,并用代码创建了一个。这是一个例子:

Bob,你怎么被困在那个球体里了?

Bob stuck in a glass orb.

“图像合成”

该应用程序具有三个不同的功能区域,展示了我通过试验 API 和开发简化其使用的工具而开发的 I 代码。每个按钮、控件或任何其他元素都是由我编写的代码绘制的。我一直在尝试创建漂亮的矩形按钮。当我达到我喜欢的东西时就停了下来。

I took a graphical step backwards about 1 decade when I reinvented this style of button.

我使用了两天,然后我意识到我重新发明了 Windows XP 的银色主题按钮。好吧,如果这是一篇关于按钮的文章,我就会创建别的东西,但事实不是,所以我就这样了。

我继续玩弄这些函数,试图重现类似径向渐变的效果。就在我准备继续前进的时候,我又想出了一个可以应用这些函数来创建渐变效果的方法。当我转向 Alpha 混合时,也发生了同样的事情。程序看起来仍然有点繁忙和复杂。当我意识到我正在编程时,手里却拿着一个闪光器,我决定是时候停止添加闪光元素,并试着把它包装起来了。

总而言之,演示应用程序展示了使用少量原始 GDI 函数创建视觉效果的多种方法。有些东西用 GDI+ 或等效的 .NET Graphics 对象可能要简单得多。但是,现在您可以看到 GDI 中原始基本函数的力量有多大,图像合成就在您的指尖。

Angular Gradient Demo.

角度渐变演示

这个演示位于程序的右上角。我当时正在用它来证明我确实创建了正确的方法来计算和绘制任意角度的渐变。按下播放按钮,角度将开始以您选择的任意方向计数,并动画显示渐变以匹配当前角度。有银色按钮可以打开和关闭各种功能,例如多边形轮廓、外渐变以及跟踪边界框创建的轮廓。您还可以选择方形和水平或垂直矩形。

Image Composition Demo.

图像合成演示

这是应用程序右下角的窗口。这一部分逐步演示了创建最终球体所需的图像合成过程。有几个控件可以交互以更改球体的外观,例如球体是否半透明、是否使用反射、是否透明地绘制图像。对于每个步骤,都会列出要添加到合成中的图像以及用于组合图像的函数名称。

Bit Blender Demo.

位图混合器演示

我在创建位图混合演示时非常开心,所以我把它留到了最后。我对此有很多话要说。老实说,我有点惭愧把 Orb Composition 演示放在这个应用程序旁边,旁边是位图混合器。起初,Orb 的质量是我唯一想达到的目标,最终,那个演示得到的关注度远不如位图混合器。

免责声明:自行承担风险。我不对您创建的可能出现的糟糕颜色组合作任何保证,也不对您创建的任何令人作呕的颜色组合负责。

通过实验学习

BLENDFUNCTION 结构的文档包含一个计算表,该表指示源图像和目标图像如何根据配置的参数进行混合。除非您花时间玩转颜色混合,否则该表可能无法帮助您确定您想要哪种配置。尽管 MSDN 的 AlphaBlend 示例程序确实显示了所有配置的输出,但其工作原理并不明显。这个示例使用有很多不足之处。

最明显的原因是,使用更简单的 API 可以用少得多的代码创建相同的输出,所以为什么还要费力去演示呢?我描述了在将白色背景与彩色背景混合时遇到的问题。我想创建一个 BLENDFUNCTION 计算表的视觉表示,并使其更容易查看 ::GdiAlphaBlend 的工作原理。我们上面剖析的图像是我第一次尝试可视化可能性。我很快就放弃了这条道路,因为它太复杂了,甚至还没有开始演示实际发生的事情。

然后,我转向了为单个像素的源颜色和目标颜色选择方案。布局在开发过程中发生了很大的变化。然后,我开始用实际的 BlendFunction 结构和对 ::GdiAlphaBlend 的调用来表示混合机的隐喻。该程序现在允许您试验单个通道颜色和 Alpha 级别的混合,以查看结果颜色。

BLENDFUNCTION

BLENDFUNCTION 似乎是一段简单的代码机械。但是,在使用此工具时,您需要小心。它可能会变得非常复杂,并且有时会不直观。您会发现自己经常需要翻阅用户手册。

Watch your fingers!!!

源和目标的颜色通道各有三个,它们组合起来创建单个像素的颜色。左侧是源,我用 Σ(Sigma)来表示任何引用。右侧是目标像素,它将贡献于混合,我用 Δ(Delta)来引用它。两个长长的盒子代表 ::GdiAlphaBlend 运行的输入像素。RGB 代码在顶部附近的编辑框中显示。如果您创建了喜欢的颜色,可以复制框中的文本,但我没有让它们能够接受输入(也许如果请求足够多,我会在 BitBlender 2012 Pro Edition 中添加)。

α 是 Alpha

现在左侧看起来比右侧要繁忙得多。如果您觉得它很俗气,那您是对的,但前提是您来自法国。源有一个额外的像素组件,即 Alpha 通道。输入值代表 8 位(0-255)。目前,您可以单击以更新 Alpha 输入的位置和值;我没有添加键盘支持或像滚动条那样的拖放(这也计划在 Pro Edition 中)。当滑块完全向左(0xFF)选择时,α 通道框将实心填充,颜色与源像素相同。当滑块向右移动时,值会减小。透明度通过从指定通道的颜色淡出到白色和灰色棋盘格图案来表示。

Transparency representation in Bit Blender.

预混合按钮

我想为每个计算过程创建一个视觉表示。我最喜欢的部分是 Pre-Blend 按钮。该按钮启用了预混合 Alpha 通道并调整颜色。您可以按下按钮并关闭该效果。该效果只是另一个渐变。我可能已经画了几百个了。按钮很酷。我最终创建了一个玻璃面板,它以半透明和模拟的折射效果悬停在颜色区域上。当预混合处于活动状态时,面板底部的颜色将通过 α 指定的适当数量进行调整。

Transparent glass with a simulated refraction effect.

Κ 是 SourceConstAlpha

BLENDFUNCTION 有两个有意义的输入参数。还记得吗?!SourceConstantAlpha (Κ) 是这两个值中较容易控制和操作的一个。Κ 会均匀地衰减源图像中的每个像素,而 α 不同,它为每个像素定义了一个特定值。如果您想创建淡入或淡出动画,或者只是在显示器的一个区域上创建均匀混合,Κ 是一个很好的值。

如果同时使用 Κ 和 α,Κ 将在 Alpha 混合发生之前按比例值进一步衰减 α。

在 BitBlender 中,您可以启用和禁用每个 Alpha 混合组件:预混合、每像素混合和 SourceConstantAlpha。您已经了解了 Pre-Blend 按钮。以下是另外两个功能的按钮。

Angular Gradient Demo.

SourceConstantAlpha Κ 始终被处理。如果 Κ = 0x00,源将完全透明,只使用目标图像。如果 Κ = 0xFF,源图像将完全不透明,混合时只考虑源图像的 α。

Image Composition Demo.

AlphaFormat = AC_SRC_ALPHA 时,执行每像素混合。这基本上是一个开启或关闭功能。另一个选项是 0,它将导致 Alpha 通道被完全忽略,并且仅使用 Κ 的值进行混合。

示例

α 配置为使用 50% 的源像素和 50% 的目标像素。Κ 的配置值与 α 相同。为了防止任何值饱和颜色通道,值被归一化为 0 到 1.0 之间。对于示例,将使用 0 到 1.0 之间的值。

α = 0.5

Κ = 0.5

预混合步骤将首先按适当的 α 衰减颜色通道。让我们进行计算以确定源像素对最终像素的总贡献。暂时,我们只关心如何分割每个像素输入的份额。总共有 1.0 要在两个源之间分配。

Σin = 1 * α = 1.0 * 0.5

Σin = 0.5

现在将处理 Κ。此值将与进入 BLENDFUNCTION 的源像素相乘,即 Σin 的值。Σα 将代表源对像素混合的最终贡献。

Σα = Σin * Κ = 0.5 * 0.5

Σα = 0.25 = 25% => 0x40

在最终混合之前还有一步。需要计算目标像素的贡献。目标像素将接收源像素未占用的百分比的剩余部分。目标贡献将用 Δα 表示。

Δα = 1 - Σα = 1 - 0.25

Δα = 0.75 = 75% => 0xC0

下图显示了此示例计算的可视化表示。

到目前为止,所有组件都已解释,并且应该能够与上面给出的示例方程匹配,除了中间的银色框。这个值是不可配置的。把它想象成一个刻度。它从左到右移动,并根据每个像素源对最终值贡献的量来定位。根据上面的示例,输入值的 25% 来自源像素。因此,指示器刻度显示源的 25% 部分,目标为 75% 部分。

每个结果的 Alpha 值和像素颜色都显示在底部的框集中。顺序与之前相同,源在左侧,目标在右侧。

测量一次,剪切两次……

至少在我尝试获得正确的视觉效果时,计算机图形学总是如此。最后一步是混合源 Σα 和目标 Δα 值。结果将是最终图像中单个像素的颜色。这张图像描绘了混合。

上面示例中使用的计算列在银色部分框下方。公式会根据 α 和 Κ 的输入配置组合而改变。下表细分了基于不同输入组合将显示的方程。有两种值可以开启或关闭,因此有四种可能的计算。

Κ α 方程 Δα 方程 Σα Image 描述
255 0 0.0 1.0 Κ 设置为最大值,并且禁用每像素 α。因此,整个位图只使用源图像。
255 AC_SRC_ALPHA 1.0 - Σα α / 255.0 Κ 设置为最大值,并启用每像素 α。因此,α 被归一化然后用于衰减源图像 Σα。Δα 接收剩余的部分。
< 255 0 1.0 - Σα Κ / 255.0 Κ 部分透明,并且禁用每像素 α。因此,Κ 被归一化然后用于衰减源图像 Σα。Δα 接收剩余的部分。如果图像有 Alpha 通道,则会忽略其值。
< 255 AC_SRC_ALPHA 1.0 - Σα Κ * α / 255.0 Κ 部分透明,并且启用每像素 α。因此,α 和 Κ 被归一化然后用于衰减源图像 Σα。Δα 接收剩余的部分。

一些额外内容

Inspect each pixel in the blended images by moving the mouse over the pictures.

像素检查器

如果您想在更改混合图像的设置时检查计算过程,请将光标移到图像上。将读取像素的源、目标和 Alpha 通道值,并自动配置到位图混合器中。当您滚动图像时,设置将为每个新像素动态更改。如果您想检查特定像素,请单击鼠标左键,直到您滚动离开图像并再次滚动到任意一个位图混合器底部的三个图像之一,更新才会禁用。

Saturation LED indicators.

饱和通道

我添加了一些指示器来报告当前颜色和 Alpha 设置是否会创建饱和像素。我不相信我在此条件下计算的值会与您的显卡返回值匹配。但是,当您在玩设置而不预混合 Alpha 通道时,这是一个方便的指示器。

Quick colors select bar.

快速颜色

在玩耍时,我厌倦了手动重新输入颜色。所以我添加了一小部分颜色来快速设置源或目标通道的像素颜色。箭头指向左侧,表示将配置的源像素,指向右侧表示目标像素。侧面还会有一个垂直条在按钮的左侧或右侧运行,作为配置哪个像素的另一个指示器,当您选择一个快速颜色时。

Magnification view of selected image for closer inspection.

缩放

当光标移到底部混合图像上时,缩放窗口将放大光标所在的位置。放大率为 x4。我添加这个是为了在不启动调试程序时能够近距离检查细节。那个程序运行起来非常烦人,我需要停止它才能启动和停止我正在调试的程序。缩放视图将跟随光标,直到单击左键。放大视图将在该点冻结,直到光标移出该图像并移到任意一个混合图像上。

关于代码

该程序使用标准的 C++ 访问 Win32 API 调用编写,因此不需要 MFC 和 WTL 来构建和运行示例。我包含了 VS2008 项目。我已经将大部分为演示而编写的代码分到了一个名为 MsImgUsage.cpp 的单独文件中。还有两个我创建的实用文件,以使代码更简洁,并提取 GDI 编程中繁琐且易出错的部分。您可能会发现这些文件很有用。

我将所有演示代码封装在 article:: 命名空间中,以帮助不熟悉 Win32 GDI 的开发者区分我编写的函数和 API 调用。我试图使用全局命名空间运算符来引用所有 Win32 API 调用,例如 ::CreateCompatibleDC(...)。希望我没有遗漏任何。

我觉得需要更多 Paulish 的风格

所有的演示都已到位,我将要实现的全部行为都已完成。我正在截屏更新文章,这是我看到的内容:

Don't stare too long, you'll go blind. I have been staring at the jaggies the entire time I wrote this app. Composite image with jaggies, you can create this image in the composition demo.

其他一切看起来都很漂亮和流畅。对于大多数颜色组合,动态按钮颜色效果很好。然后就是这些锯齿,它们毁了显示效果。我一直在想,“我应该处理一下,不行,这超出了范围。我以后再写一篇关于它的文章,然后改进它。”

然后我记起了我在 2004 年制作的一个未完成的抗锯齿圆形实现,至少根据注释是这样。圆形部分效果很好,但有什么闪亮的东西在附近晃悠,我在完成函数的其余部分之前就分心了。所以它并不完美,但对我来说足够好了。我把它放进去了,经过一点调整,这就是我最终得到的结果:

Smoooooooooooth! I have been staring at the jaggies the entire time I wrote this app. Composite image with jaggies, you can create this image in the composition demo.

我不敢相信有了如此大的改进。我知道它会看起来更好,只是没想到会帮助那么多。抗锯齿圆形算法基于 Xiaolin Wu 的抗锯齿圆形算法。我希望将来能重新审视这个函数并完成它。您可以在 aa_ellipse.cpp 文件中找到当前实现。

最后一点似乎很有用的是,这些控件和渐变在它们显示的大小(大约 50 像素)下看起来相当不错。但是,在放大版本中,您可以看到混合伪影,颜色几乎混合但又未完全混合的微弱线条。这些球体是用 32 个扇段在径向渐变中渲染的。如果这个伪影成为您要显示的任何项目的问,增加扇段的数量将有助于解决它。

有用的代码

我认为您在演示中会发现有用的代码部分标记如下 //!!!。还有一个简短的注释描述了您应该能从代码片段中获得的。 (预计会看到很多 //!!!,这是令人兴奋的内容!)

//!!! Save DC / Restore DC Sample
int ctx = ::SaveDC(hDC);

// Draw the set of rectangles.
::SelectObject(hBufferDC, g_hPenBlack);
::SelectObject(hBufferDC, ::GetStockObject(WHITE_BRUSH));

// Draw Right Side:
DrawShapes(hBufferDC, hRightRgn, width, height, g_group1, g_isGroup1Active);
// Draw Left Side:
DrawShapes(hBufferDC, hLeftRgn, width, height, g_group2, !g_isGroup1Active);

::RestoreDC(hDC, ctx);

这次,您可能会从这个演示应用程序中发现不少有用的代码块。在文章前面,我介绍了 BitBlender.h 文件。该文件创建了几个实用包装器,用于 msimg32.dll 中的函数,以减少使用它们所需的代码量,甚至增加了新功能。还有其他几个您可能会发现有用的实用文件和对象。

AutoGdi.h

虽然我省略了 MFC 和 WTL 的依赖项,但我并没有完全摒弃 C++ 的风格和约定。AutoGdi.h 包含一组实用类,用于帮助管理 GDI 资源。这些对象将常见的样板代码从您执行实际工作的代码中提取出来。这有望让您编写更正确的代码,而不是迷失在细节中。这些对象提供了一个简洁的实现,可能并不适合所有情况下的最方便使用。但是,每当我突然想用新颜色创建画笔时,它们都很好地为我清理了代码。

article::Auto<HGDIOBJ> 和 article::AutoSaveDC

我在这个实用头文件中实现的第一个类是一个模板,它提供了一组 GDI 清理对象,这些对象旨在创建在栈上。当对象超出范围时,GDI 句柄将自动删除。此文件中定义的另一个类是 AutoSaveDC。该类将创建一个设备上下文的保存点,并自动将您的 DC 恢复到创建 AutoSaveDC 对象之前的状态。

这些对象的复制构造函数和赋值运算符已被禁用。这大大简化了维护对象所需的内部管理。该对象具有为它要维护的 HGDIOBJ 类型声明的显式构造函数,它还具有一个类型转换函数,可以将对象转换回它所代表的句柄类型。

{
  article::AutoPen    greenPen(::CreatePen(PS_SOLID, 1, RGB(0,255,0)));
  article::AutoBrush  orangeBrush(::CreateSolidBrush(RGB(200,200,0)));

  if (...)
  {
    article::AutoSaveDC save(hdc);
    ::SelectObject(hdc, greenPen);
    
    // ... Lots of drawing and HDC mode changes
    
  } // The hdc is restored to the same state as when it entered the if statement.
  
} // The green pen is destroyed at this point when the object leaves its declared scope.

我将明确说明这一点,以免造成任何混淆。这些对象仅打算作为在函数或类内部的栈上创建的 auto 对象。如果您使用 new 动态分配它们并忘记删除它们,或者为其定义添加 static 限定符,它们将不会为您做任何事情。

article::MemDCBuffer

我发现自己为创建内存设备上下文(memory DC)的双缓冲(double-buffer)而大量复制设置和清理代码。因此,我也创建了MemDCBuffer类。这是另一个轻量级的类,可以在栈上声明,或在允许它被正确释放的某个上下文中声明。该类将创建并管理一个内存设备上下文和一个与指定设备上下文兼容的位图。然后,该对象可用于您通常使用设备上下文句柄的所有调用。当您想将内容复制回原始设备上下文时,请使用指向目标设备上下文的句柄调用MemDCBuffer::Flush(HDC);在大多数情况下,使用创建MemDCBuffer时使用的相同设备上下文是有意义的。

case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = ::BeginPaint(hWnd, &ps);
    
    // Create the double buffer object.
    article::MemDCBuffer dblBuf(hdc);
    
    // Start drawing.
    // Use the MemDCBuffer object as if it were an HDC.
    ::Rectangle(dblBuf, 10, 10, 250, 250);
    
    // Another Motley display of computer artistry...
    
    // Flush the cached bitmap to the display in one swift action.
    dblBuf.Flush(hdc);
    
    ::EndPaint(hWnd, &ps);
}
break;

结论

我花了比预期更长的时间来完成这篇文章(略超6周),以便以我认为应该的方式来描述三个函数。这是我最后一次写一篇关于整个DLL的文章。从这里开始,Windows的情况只会变得更糟。我认为GDI32.DLL导出了690个符号,我猜我以后将一次只关注几个。

关于图像合成(Image Composition),您的想象力是唯一的限制。每次我认为已经达到了一个足以演示和有信息量的简单程度的平台时,我就会想出另一个可以创建原始API中没有但看起来很棒的东西。我计划继续开发我在这里开始的辅助函数库。当我改进它们时,我将更新本文,或撰写第二部分。到目前为止,我从::GdiGradientFill那里获得了最多的用途,但是,随着我对::GdiAlphaBlend的熟悉程度越来越高,我也开始看到这种函数的新颖独特的应用方式。

如今流行的所有基于向量的GUI语言和框架都依赖于本文中演示的技术和概念。通过一些精心设计的对象,您可以将高质量的缩放向量界面通过图像合成提供给您本地开发的原生应用程序。所有的原始组件都在那里。事实上,GdiPlus就像我在本文中开始做的那样,是一个建立在Win32 GDI之上的库。所以,走出去,创造一些美好的东西。

历史

  • 2011年9月8日:首次发布。
© . All rights reserved.