如何使用OpenGL

时间:2017-08-20 11:34:05

标签: opengl rendering sdl-2 alphablending compositing

我正在使用具有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网站上看到这个):

Expected result, created with the gimp

相反,背景颜色RGB值(1.0,0.0,0.0)整体加权(1 - SourceAlpha)而不是(DestAlpha *(1 - SourceAlpha))。实际结果如下:

Actual result

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

使用SDL,SFML或直接使用OpenGL生成预期的SourceOver结果需要做什么?

一些消息来源:

W3 article on compositing,指定co =αs×Cs +αb×Cb x(1 - αs),如果αb为0,则Cb的权重应为0,无论如何。

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

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

关于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个因子。但是,我根本无法理解这一点。如果我使用目标alpha值(例如,(0.1))预乘(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);
    }
}

1 个答案:

答案 0 :(得分:2)

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

基本问题是纠正&#34; Source Over&#34;使用OpenGL的内置混合功能(即glEnable(GL_BLEND)glBlendFunc[Separate](...)glBlendEquation[Separate](...))实际上无法进行Alpha混合(顺便说一句,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是结果color / alpha,
  • sub s是源(顶部是什么)color / alpha,
  • d是destionation(底部是什么)color / 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颜色值,如果源已将预乘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的纹理始终具有应用于其RGB值的预乘alpha。请注意,根据源是否具有预乘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值除以相应的像素&# 39; s 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_BLENDglBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)硬编码。