如何使用 OpenGL 在 SDL 中获得正确的 SourceOver alpha 合成

How to get correct SourceOver alpha compositing in SDL with OpenGL

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

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

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

相反,背景颜色 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 article on compositing,指定co = αs x Cs + αb x Cb x (1 – αs),无论αb为0,Cb的权重都应该为0。

English Wiki 显示目的地 ("B") 根据 αb(以及间接的 αs)加权。

German Wiki 显示了 50% 透明度的示例,显然透明背景的原始 RGB 值不会干扰绿色或品红色源,还表明交点明显不对称,有利于元素"on top".

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

另一个建议是用目标 alpha 预乘颜色,因为 OpenGL 只能有 1 个因子,但它需要 2 个因子才能获得正确的 SourceOver。但是,我根本无法理解这一点。如果我预乘 (1, 0, 0) 与目标 alpha 值,比方说,(0.1),我得到 (0.1, 0, 0)(例如建议 here)。现在我可以告诉 OpenGL 这个因子 GL_ONE_MINUS_SRC_ALPHA(并且只有 GL_SRC_ALPHA 的来源),但是我实际上是在与黑色混合,这是不正确的。尽管我不是该主题的专家,但我付出了相当多的努力来尝试理解(并且至少达到了我设法编写出每种合成模式的工作纯软件实现的程度)。我的理解是,将 0.1 "via premultiplication" 的 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);
    }
}

多亏了@HolyBlackCat 和@Rabbid76,我才得以阐明这整件事。我希望这可以帮助其他想了解正确的 alpha 混合和预乘 alpha 背后细节的人。

基本问题是正确的 "Source Over" alpha 混合实际上无法通过 OpenGL 的内置混合功能(即 glEnable(GL_BLEND)glBlendFunc[Separate](...)glBlendEquation[Separate](...))实现(顺便说一下,这对于 D3D 也是一样的)。原因如下:

在计算混合操作的结果颜色和 alpha 值时(根据 correct 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 是结果color/alpha,
  • sub s 是来源(最上面的)color/alpha,
  • d 是目的地(底部是什么)color/alpha,
  • alpha 是处理过的像素的 alpha 值
  • 并且 RGB 表示像素的红色、绿色或蓝色值之一

但是,OpenGL 只能处理有限种类的附加因素以配合源值或目标值(颜色中的RGB_s 和RGB_d equation) (see here),本例中的相关方程为 GL_ONEGL_SRC_ALPHAGL_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,此公式等效于 correct。换句话说,当渲染到没有 alpha 通道的帧缓冲区(例如 window backbuffer), 这很好,否则会产生不正确的结果。

如果 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)

我们还可以消除有问题的 alpha_d 因素,方法是确保所有目标图像的 RGB 值在某个时刻都已与其各自的 alpha 值相乘。例如,如果我们 想要 背景颜色 (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 颜色值,如果源 没有 将预乘 alpha 应用于其 RGB 值,我们使用

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 值。请注意,correct 混合函数对于每个渲染对象可能会有所不同,具体取决于源是否具有预乘 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。这里有 2 种可能性,我们可能想用这个纹理做什么:

如果我们想将其导出为正确 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)