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

Posted

技术标签:

【中文标题】如何使用 OpenGL 在 SDL 中获得正确的 SourceOver alpha 合成【英文标题】:How to get correct SourceOver alpha compositing in SDL with OpenGL 【发布时间】:2018-01-28 14:49:51 【问题描述】:

我正在使用具有 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合成的几篇文章,我希望得到的纹理是

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

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

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

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

使用 SDL、SFML 或直接使用 OpenGL,我需要做什么才能产生预期的 SourceOver 结果?

一些来源:

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 值显然不会干扰绿色或洋红色源,还表明交叉点明显不对称,有利于“顶部”元素.

还有几个关于 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)(例如建议 here)。现在我可以告诉 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);
    

【问题讨论】:

你是说glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA) 不适合你吗?请注意,生成的图像将具有预乘 alpha,这可能会让您感到困惑。 @HolyBlackCat 是的,在渲染四边形时,该函数正在传递给 OpenGL(通过 SDL 库)。结果是我发布的微红图像。如果生成的图像“具有预乘 alpha”(如果这确实是问题所在),那么您能否澄清一下 实际上 的含义以及如何将其转换为预期结果? 我想我知道出了什么问题。可能目标图像也必须具有预乘 alpha,这对于您的红色矩形而言并非如此。 “这实际上意味着什么以及如何将其转换为预期结果” 这意味着结果看起来就像是应用了color.rgb *= color.a。要将其转换回来,您可以使用color.rgb =/ color.a。但请注意,通常只有在保存到文件时才需要将其转换回来。 如果您使用该技术渲染到屏幕上,生成的图像将看起来就好像它是在黑色背景上以适当的混合方式渲染的(而不是忽略生成的 alpha照常)。这不是问题,因为通常屏幕背景是不透明的,因此不会有任何视觉差异。 @HolyBlackCat “可能目标图像也必须具有预乘 alpha” - 您是指源图像吗?无论如何,如果我要这样做,然后将纹理渲染到不透明的背景上,那么只有当背景是统一的黑色时,结果才可能是正确的。所有其他背景颜色都会有可见的伪影。在这种情况下,首先使用 alpha 混合的全部好处就没有了,我还不如渲染到不透明的黑色背景上,而不是完全透明的背景上。 【参考方案1】:

感谢@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)(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,此公式等价于 正确。换句话说,当渲染到没有 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)

【讨论】:

以上是关于如何使用 OpenGL 在 SDL 中获得正确的 SourceOver alpha 合成的主要内容,如果未能解决你的问题,请参考以下文章

c++:如何使用 sdl 将位图加载到 opengl 中的多维数据集中?

带有 OpenGL 4.4 的 SDL2:三角形未正确渲染

在 SDL 1.2 中使用核心 OpenGL?

OpenGL + SDL 有时会从坐标中提取

OpenGL 深度测试不工作 (GLEW/SDL2)

如何在 SDL 中进行硬件加速 Alpha 混合?