1

我正在使用具有 alpha 通道 (32bpp ARGB) 的 FBO(或“渲染纹理”)并使用不完全不透明的颜色清除它,例如(R=1、G=0、B=0、A= 0)(即完全透明)。然后我正在渲染一个半透明的对象,例如一个带有颜色(R=1,G=1,B=1,A=0.5)的矩形,在此之上。(所有值从 0 标准化到 1)

根据常识,还有 GIMP 和 Photoshop 等成像软件,以及 Porter-Duff 合成的几篇文章,我希望得到的纹理是

  • 矩形外完全透明
  • 白色 (1.0, 1.0, 1.0),矩形内不透明度为 50%。

像这样(你不会在 SO 网站上看到这个):

使用 gimp 创建的预期结果

相反,背景颜色 RGB 值,即 (1.0, 0.0, 0.0) 总体上使用 (1 - SourceAlpha) 而不是 (DestAlpha * (1 - SourceAlpha)) 加权。实际结果是这样的:

实际结果

我已经直接使用 OpenGL、使用 SDL 的包装器 API 和 SFML 的包装器 API 验证了这种行为。使用 SDL 和 SFML,我还将结果保存为图像(带有 alpha 通道),而不是仅仅渲染到屏幕上,以确保最终渲染步骤没有问题。

我需要做什么来产生预期的 SourceOver 结果,无论是使用 SDL、SFML 还是直接使用 OpenGL?

一些资料来源:

W3 关于合成的文章,指定 co = αs x Cs + αb x Cb x (1 – αs),如果 αb 为 0,则 Cb 的权重应为 0,无论如何。

英语 Wiki显示目的地(“B”)根据 αb(以及 αs,间接地)加权。

German Wiki显示了 50% 透明度的示例,透明背景的原始 RGB 值显然不会干扰绿色或洋红色源,还表明交叉点明显不对称,有利于“顶部”元素。

乍一看,还有几个关于 SO 的问题似乎处理了这个问题,但我找不到任何与这个特定问题相关的内容。人们建议使用不同的 OpenGL 混合函数,但普遍的共识似乎是glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA),这是 SDL 和 SFML 默认使用的。我也尝试过不同的组合,但没有成功。

另一个建议是将颜色与目标 alpha 预乘,因为 OpenGL 只能有 1 个因子,但正确的 SourceOver 需要 2 个因子。但是,我完全无法理解这一点。如果我将 (1, 0, 0) 与目标 alpha 值(例如 (0.1))进行预乘,我会得到 (0.1, 0, 0)(例如这里建议的)。现在我可以告诉OpenGL这个因素GL_ONE_MINUS_SRC_ALPHA(和源GL_SRC_ALPHA),但后来我有效地与黑色混合,这是不正确的。虽然我不是该主题的专家,但我付出了相当多的努力来试图理解(并且至少达到了我设法编写每个合成模式的工作纯软件实现的地步)。我的理解是,将 0.1 的 alpha 值“通过预乘”应用于 (1.0, 0.0, 0.0) 与将 alpha 值正确地视为第四个颜色分量完全不同。

这是一个使用 SDL 的最小且完整的示例。需要 SDL2 本身来编译,如果要另存为 PNG,则可以选择 SDL2_image。

// Define to save the result image as PNG (requires SDL2_image), undefine to instead display it in a window
#define SAVE_IMAGE_AS_PNG

#include <SDL.h>
#include <stdio.h>

#ifdef SAVE_IMAGE_AS_PNG
#include <SDL_image.h>
#endif

int main(int argc, char **argv)
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0)
    {
        printf("init failed %s\n", SDL_GetError());
        return 1;
    }
#ifdef SAVE_IMAGE_AS_PNG
    if (IMG_Init(IMG_INIT_PNG) == 0)
    {
        printf("IMG init failed %s\n", IMG_GetError());
        return 1;
    }
#endif

    SDL_Window *window = SDL_CreateWindow("test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
    if (window == NULL)
    {
        printf("window failed %s\n", SDL_GetError());
        return 1;
    }

    SDL_Renderer *renderer = SDL_CreateRenderer(window, 1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
    if (renderer == NULL)
    {
        printf("renderer failed %s\n", SDL_GetError());
        return 1;
    }

    // This is the texture that we render on
    SDL_Texture *render_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 300, 200);
    if (render_texture == NULL)
    {
        printf("rendertexture failed %s\n", SDL_GetError());
        return 1;
    }

    SDL_SetTextureBlendMode(render_texture, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    printf("init ok\n");

#ifdef SAVE_IMAGE_AS_PNG
    uint8_t *pixels = new uint8_t[300 * 200 * 4];
#endif

    while (1)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_QUIT)
            {
                return 0;
            }
        }

        SDL_Rect rect;
        rect.x = 1;
        rect.y = 0;
        rect.w = 150;
        rect.h = 120;

        SDL_SetRenderTarget(renderer, render_texture);
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 0);
        SDL_RenderClear(renderer);
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 127);
        SDL_RenderFillRect(renderer, &rect);

#ifdef SAVE_IMAGE_AS_PNG
        SDL_RenderReadPixels(renderer, NULL, SDL_PIXELFORMAT_ARGB8888, pixels, 4 * 300);
        // Hopefully the masks are fine for your system. Might need to randomly change those ff parts around.
        SDL_Surface *tmp_surface = SDL_CreateRGBSurfaceFrom(pixels, 300, 200, 32, 4 * 300, 0xff0000, 0xff00, 0xff, 0xff000000);
        if (tmp_surface == NULL)
        {
            printf("surface error %s\n", SDL_GetError());
            return 1;
        }

        if (IMG_SavePNG(tmp_surface, "t:\\sdltest.png") != 0)
        {
            printf("save image error %s\n", IMG_GetError());
            return 1;
        }

        printf("image saved successfully\n");
        return 0;
#endif

        SDL_SetRenderTarget(renderer, NULL);
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer, render_texture, NULL, NULL);
        SDL_RenderPresent(renderer);
        SDL_Delay(10);
    }
}
4

1 回答 1

3

感谢@HolyBlackCat 和@Rabbid76,我能够对整个事情有所了解。我希望这可以帮助其他想了解正确 Alpha 混合以及预乘 Alpha 背后的细节的人。

基本问题是正确的“Source Over”alpha 混合实际上不可能使用 OpenGL 的内置混合功能(即glEnable(GL_BLEND), )(顺便说一句glBlendFunc[Separate](...)glBlendEquation[Separate](...)这对于 D3D 也是如此)。原因如下:

在计算混合操作的结果颜色和 alpha 值时(根据正确的Source Over),必须使用以下函数:

每个 RGB 颜色值(从 0 标准化到 1):

RGB_f = ( alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s) ) / alpha_f

alpha 值(从 0 标准化到 1):

alpha_f = alpha_s + alpha_d x (1 - alpha_s)

在哪里

  • sub f 是结果颜色/alpha,
  • sub s 是源(顶部是什么)颜色/alpha,
  • d 是目标(底部是什么)颜色/alpha,
  • alpha 是处理后像素的 alpha 值
  • RGB 表示像素的红色、绿色或蓝色值之一

然而,OpenGL 只能处理有限的各种附加因素与源或目标值(颜色方程中的 RGB_s 和 RGB_d)(见这里),在这种情况下相关的因素是GL_ONE, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA。我们可以使用这些选项正确指定 alpha 公式,但我们可以为 RGB 做的最好的事情是:

RGB_f = alpha_s x RGB_s + RGB_d x (1 - alpha_s)

它完全缺少目的地的 alpha 分量 (alpha_d)。注意,如果\alpha_d = 1,这个公式等价于正确的公式。换句话说,当渲染到没有alpha通道的帧缓冲区(例如窗口后缓冲区)上时,这很好,否则会产生不正确的结果。

如果 alpha_d 不等于 1,为了解决这个问题并实现正确的 alpha 混合,我们需要一些棘手的解决方法。上面的原始(第一个)公式可以重写为

alpha_f x RGB_f = alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s)

如果我们接受结果颜色值太暗的事实(它们将乘以结果 alpha 颜色)。这已经摆脱了分裂。然而,要获得正确的RGB 值,必须将结果 RGB 值除以结果 alpha 值,因为事实证明通常不需要转换。我们引入了一个新符号 (pmaRGB),它表示通常太暗的 RGB 值,因为它们已乘以相应像素的 alpha 值。

pmaRGB_f = alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s)

我们还可以通过确保目标图像的所有 RGB 值在某个点都与它们各自的 alpha 值相乘来消除有问题的 alpha_d 因子。例如,如果我们想要背景颜色 (1.0, 0.5, 0, 0.3),我们不使用该颜色清除帧缓冲区,而是使用 (0.3, 0.15, 0, 0.3)。换句话说,我们正在执行 GPU 必须提前完成的步骤之一,因为 GPU 只能处理一个因素。如果我们要渲染到现有纹理,我们必须确保它是使用预乘 alpha 创建的。我们混合操作的结果将始终是纹理有预乘的 alpha,所以我们可以继续渲染到那里,并始终确保目标确实有预乘的 alpha。如果我们渲染到一个半透明的纹理,半透明的像素总是会太暗,这取决于它们的 alpha 值(0 alpha 表示黑色,1 alpha 表示正确的颜色)。如果我们渲染到没有 alpha 通道的缓冲区(就像我们用于实际显示内容的后台缓冲区),alpha_f 隐含为 1,因此预乘的 RGB 值等于正确混合的 RGB 值。这是当前的公式:

pmaRGB_f = alpha_s x RGB_s + pmaRGB_d x (1 - alpha_s)

当源还没有预乘 alpha时可以使用此函数(例如,如果源是来自图像处理程序的常规图像,具有正确混合的 alpha 通道且没有预乘 alpha)。

有一个原因我们可能也想摆脱 \alpha_s,并为源使用预乘 alpha:

pmaRGB_f = pmaRGB_s + pmaRGB_d x (1 - alpha_s)

如果源恰好具有预乘 alpha,则需要采用此公式- 因为源像素值都是 pmaRGB 而不是 RGB。如果我们使用上述方法渲染到带有 Alpha 通道的屏幕外缓冲区,则总是会出现这种情况。默认情况下,将所有纹理资源与预乘 alpha 一起存储也是合理的,以便始终可以采用此公式。

回顾一下,计算 alpha 值,我们总是使用这个公式:

alpha_f = alpha_s + alpha_d x (1 - alpha_s)

,对应于 ( GL_ONE, GL_ONE_MINUS_SRC_ALPHA)。为了计算 RGB 颜色值,如果源没有对其RGB 值应用预乘 alpha,我们使用

pmaRGB_f = alpha_s x RGB_s + pmaRGB_d x (1 - alpha_s)

,对应于 ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)。如果它确实应用了预乘 alpha,我们使用

pmaRGB_f = pmaRGB_s + pmaRGB_d x (1 - alpha_s)

,对应于 ( GL_ONE, GL_ONE_MINUS_SRC_ALPHA)。


这在 OpenGL 中实际上意味着什么:当渲染到带有 alpha 通道的帧缓冲区时,相应地切换到正确的混合功能,并确保 FBO 的纹理始终将预乘 alpha 应用于其 RGB 值。请注意,根据源是否具有预乘 alpha,每个渲染对象的正确混合函数可能会有所不同。示例:我们想要一个背景 [1, 0, 0, 0.1],并在其上渲染一个颜色为 [1, 1, 1, 0.5] 的对象。

// Clear with the premultiplied version of the real background color - the texture (which is always the destination in all blending operations) now complies with the "destination must always have premultiplied alpha" convention.
glClearColor(0.1f, 0.0f, 0.0f, 0.1f); 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//
// Option 1 - source either already has premultiplied alpha for whatever reason, or we can easily ensure that it has
//
{
    // Set the drawing color to the premultiplied version of the real drawing color.
    glColor4f(0.5f, 0.5f, 0.5f, 0.5f);

    // Set the blending equation according to "blending source with premultiplied alpha".
    glEnable(GL_BLEND);
    glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquationSeparate(GL_ADD, GL_ADD);
}

//
// Option 2 - source does not have premultiplied alpha
// 
{
    // Set the drawing color to the original version of the real drawing color.
    glColor4f(1.0f, 1.0f, 1.0f, 0.5f);

    // Set the blending equation according to "blending source with premultiplied alpha".
    glEnable(GL_BLEND);
    glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquationSeparate(GL_ADD, GL_ADD);
}

// --- draw the thing ---

glDisable(GL_BLEND);

在任何一种情况下,生成的纹理都具有预乘 alpha。以下是我们可能想要对这个纹理做的两种可能性:

如果我们想将其导出为正确alpha 混合的图像(根据 SourceOver 定义),我们需要获取其 RGBA 数据并明确地将每个 RGB 值除以相应像素的 alpha 值。

如果我们想将它渲染到后台缓冲区(其背景颜色应为 (0, 0, 0.5)),我们照常进行(对于这个例子,我们还想用 (0, 0, 1, 0.8)):

// The back buffer has 100 % alpha.
glClearColor(0.0f, 0.0f, 0.5f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// The color with which the texture is drawn - the modulating color's RGB values also need premultiplied alpha
glColor4f(0.0f, 0.0f, 0.8f, 0.8f);

// Set the blending equation according to "blending source with premultiplied alpha".
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquationSeparate(GL_ADD, GL_ADD);

// --- draw the texture ---

glDisable(GL_BLEND);

从技术上讲,结果将应用预乘 alpha。但是,由于每个像素的结果 alpha 始终为 1,因此预乘的 RGB 值始终等于正确混合的 RGB 值。

要在 SFML 中实现相同的效果:

renderTexture.clear(sf::Color(25, 0, 0, 25));

sf::RectangleShape rect;
sf::RenderStates rs;
// Assuming the object has premultiplied alpha - or we can easily make sure that it has
{
    rs.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);
    rect.setFillColor(sf::Color(127, 127, 127, 127));
}

// Assuming the object does not have premultiplied alpha
{
    rs.blendMode = sf::BlendAlpha; // This is a shortcut for the constructor with the correct blending parameters for this type
    rect.setFillColor(sf::Color(255, 255, 255, 127));
}

// --- align the rect ---

renderTexture.draw(rect, rs);

同样将其绘制renderTexture到后缓冲区

// premultiplied modulation color
renderTexture_sprite.setColor(sf::Color(0, 0, 204, 204));
window.clear(sf::Color(0, 0, 127, 255));
sf::RenderStates rs;
rs.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);
window.draw(renderTexture_sprite, rs);

不幸的是,这对于 SDL afaik 是不可能的(至少不能在 GPU 上作为渲染过程的一部分)。与向用户公开对混合模式的细粒度控制的 SFML 不同,SDL 不允许设置单独的混合功能组件 - 它仅SDL_BLENDMODE_BLEND使用glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA).

于 2017-08-21T18:58:47.953 回答