如何使用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网站):
相反,背景色的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,指定共=α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);
}
}
感谢@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)
。
你是说'glBlendFuncSeparate(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA,GL_ONE,GL_ONE_MINUS_SRC_ALPHA)'不适合你吗?请注意,生成的图像将预乘alpha,这可能会让您感到困惑。 – HolyBlackCat
@HolyBlackCat是的,当渲染四边形时,该函数正在传递给OpenGL(通过SDL库)。结果是我发布的红色图像。如果生成的图像“预乘alpha”(如果这是实际问题),那么请您清楚*实际上*的含义以及如何将其转换为预期结果? – dialer
我想我知道什么是错的。可能目标图像也必须预乘alpha,这对于您的红色矩形不适用。 *“这实际上意味着什么以及如何将其转换为预期结果”*这意味着结果看起来好像应用了color.rgb * = color.a'。要将其转换回来,您可以执行'color.rgb =/color.a'。但请注意,通常只有在保存到文件时才需要将其转换回来。 – HolyBlackCat