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

问题描述:

我正在使用具有Alpha通道(32bpp ARGB)的FBO(或“渲染纹理”),并使用不完全不透明的颜色清除该例如( R = 1,G = 0,B = 0,A = 0)(即完全透明)。然后我渲染一个半透明物体,例如一个带颜色的矩形(R = 1,G = 1,B = 1,A = 0.5)。 (从0到1归一化的所有值)如何使用OpenGL在SDL中获得正确的SourceOver alpha合成

根据常识,以及GIMP和Photoshop等成像软件以及Porter-Duff合成的几篇文章,我预计会得到一个纹理,其格式为

  • 完全透明的矩形
  • 白色(1.0,1.0,1.0)与所述矩形的内部50%的不透明度的外部。

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

Expected result, created with the gimp

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

Actual result

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

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

一些来源:

W3 article on compositing,指定共=αSX CS +αBX的Cb×(1 - αS),重量Cb的应该是0,如果αB为0,不管是什么。

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

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个因子,但它需要2个因子才能保证正确的SourceOver。但是,我根本无法理解这一点。如果我用(0.1)的目标alpha值预乘(1,0,0),我得到(0.1,0,0)(例如,建议here例如)。现在我可以告诉OpenGL这个因素GL_ONE_MINUS_SRC_ALPHA(而源码只有GL_SRC_ALPHA),但是后来我有效地与黑色混合,这是不正确的。尽管我不是这个主题的专家,但我花了相当多的努力去尝试理解(至少到了我设法编写每个合成模式的纯软件实现的工作点)。我的理解是,将通过预乘的0.1“通过预乘”应用于(1.0,0.0,0.0)与将第四个颜色分量正确对待alpha值完全不同。

这是使用SDL的最小且完整的示例。如果要保存为PNG,则需要SDL2本身进行编译,可选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); 
    } 
} 
+0

你是说'glBlendFuncSeparate(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA,GL_ONE,GL_ONE_MINUS_SRC_ALPHA)'不适合你吗?请注意,生成的图像将预乘alpha,这可能会让您感到困惑。 – HolyBlackCat

+0

@HolyBlackCat是的,当渲染四边形时,该函数正在传递给OpenGL(通过SDL库)。结果是我发布的红色图像。如果生成的图像“预乘alpha”(如果这是实际问题),那么请您清楚*实际上*的含义以及如何将其转换为预期结果? – dialer

+1

我想我知道什么是错的。可能目标图像也必须预乘alpha,这对于您的红色矩形不适用。 *“这实际上意味着什么以及如何将其转换为预期结果”*这意味着结果看起来好像应用了color.rgb * = color.a'。要将其转换回来,您可以执行'color.rgb =/color.a'。但请注意,通常只有在保存到文件时才需要将其转换回来。 – HolyBlackCat

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

的基本问题是正确的“源代码在” alpha混合在实际上不可能用OpenGL内置的混合功能(即glEnable(GL_BLEND)glBlendFunc[Separate](...)glBlendEquation[Separate](...))(这是D3D相同的方式)。其原因如下:

当(根据正确源超过),人们必须使用这些函数计算所述混合操作的结果颜色和alpha值:

每个RGB颜色值(归一化从0到1):

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

α值(从0归一化到1):

alpha_f = alpha_s + alpha_d×(1 - alpha_s)

  • 子f是结果的颜色/α,
  • 子s是源代码(什么是顶部)颜色/ alpha,
  • d是destionation(什么是底部)颜色/ alpha,
  • a lpha是处理的像素的α值
  • 和RGB表示像素的红色,绿色或蓝色色彩值中的一个

然而,OpenGL的只能处理有限种类的额外因素去与源或目的值(颜色方程中的RGB_s和RGB_d)(see here),在这种情况下的相关值为GL_ONE,GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA。我们可以正确地使用这些选项指定字母的公式,但我们可以为RGB做的最好的是:

RGB_f = alpha_s X RGB_s + RGB_d×(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×(1 - alpha_s)

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

pmaRGB_f = alpha_s X RGB_s + alpha_d X RGB_d×(1 - alpha_s)

我们还可以得到通过确保所有目标图像的RGB值已被乘以摆脱问题alpha_d因素与他们各自的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×(1 - alpha_s)

此功能可用于当源尚不已预乘α(例如,如果源是来自图像处理程序的正常图像,并且alpha通道与没有预乘alpha的正确混合)。

有我们可能希望得到alpha_s摆脱\为好,并使用预乘alpha的来源,以及一个原因:

pmaRGB_f = pmaRGB_s + pmaRGB_d×(1 - alpha_s)

如果源发生有预乘alpha,则需要采用此公式,因为那么源像素值都是pmaRGB而不是RGB。如果我们正在使用上述方法转向带有alpha通道的离线缓冲区,情况总是如此。默认情况下将所有纹理资产与预乘alpha进行存储也可能是合理的,因此可以采用此公式总是

总括来说,为了计算α值,我们总是使用以下公式:

alpha_f = alpha_s + alpha_d×(1 - alpha_s)

,其对应于(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)。为了计算RGB颜色值,如果源确实已经预乘施加到其RGB值α,我们使用

pmaRGB_f = alpha_s X RGB_s + pmaRGB_d×(1 - alpha_s)

,相当于(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)。如果它确实具有施加到其预乘α,我们使用

pmaRGB_f = pmaRGB_s + pmaRGB_d×(1 - alpha_s)

,其对应于(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)。


什么,实际上意味着在OpenGL:当渲染与alpha通道,切换到相应的正确的混合功能的帧缓冲,并确保该FBO的质感总是有预乘应用到它的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,0.5))调制纹理, 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据我所知(至少不上GPU作为渲染过程的一部分)。与向用户公开对混合模式的细粒度控制的SFML不同,SDL不允许设置各个混合功能组件 - 它只有SDL_BLENDMODE_BLEND硬编码为glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)