opengl - blending with previous contents of framebuffer
Asked Answered
A

4

22

I am rendering to a texture through a framebuffer object, and when I draw transparent primitives, the primitives are blended properly with other primitives drawn in that single draw step, but they are not blended properly with the previous contents of the framebuffer.

Is there a way to properly blend the contents of the texture with the new data coming in?

EDIT: More information requsted, I will attempt to explain more clearly;

The blendmode I am using is GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA. (I believe that is typically the standard blendmode)

I am creating an application that tracks mouse movement. It draws lines connecting the previous mouse position to the current mouse position, and as I do not want to draw the lines over again each frame, I figured I would draw to a texture, never clear the texture and then just draw a rectangle with that texture on it to display it.

This all works fine, except that when I draw shapes with alpha less than 1 onto the texture, it does not blend properly with the texture's previous contents. Let's say I have some black lines with alpha = .6 drawn onto the texture. A couple draw cycles later, I then draw a black circle with alpha = .4 over those lines. The lines "underneath" the circle are completely overwritten. Although the circle is not flat black (It blends properly with the white background) there are no "darker lines" underneath the circle as you would expect.

If I draw the lines and the circle in the same frame however, they blend properly. My guess is that the texture just does not blend with it's previous contents. It's like it's only blending with the glclearcolor. (Which, in this case is <1.0f, 1.0f, 1.0f, 1.0f>)

Argolis answered 31/1, 2010 at 8:50 Comment(2)
blending should work with whatever is in the framebuffer. (that's what blending does). Provide more data (what blending mode are you using, what do you call blending "not properly" ?Autograft
I have updated with more information. Thank you for your quick response.Argolis
P
29

I think there are two possible problems here.

Remember that all of the overlay lines are blended twice here. Once when they are blended into the FBO texture, and again when the FBO texture is blended over the scene.

So the first possibility is that you don't have blending enabled when drawing one line over another in the FBO overlay. When you draw into an RGBA surface with blending off, the current alpha is simply written directly into the FBO overlay's alpha channel. Then later when you blend the whole FBO texture over the scene, that alpha makes your lines translucent. So if you have blending against "the world" but not between overlay elements, it is possible that no blending is happening.

Another related problem: when you blend one line over another in "standard" blend mode (src alpha, 1 - src alpha) in the FBO, the alpha channel of the "blended" part is going to contain a blend of the alphas of the two overlay elements. This is probably not what you want.

For example, if you draw two 50% alpha lines over each other in the overlay, to get the equivalent effect when you blit the FBO, you need the FBO's alpha to be...75%. (That is, 1 - (1-.5) * (1-0.5), which is what would happen if you just drew two 50% alpha lines over your scene. But when you draw the two 50% lines, you'll get 50% alpha in the FBO (a blend of 50% with...50%.

This brings up the final issue: by pre-mixing the lines with each other before you blend them over the world, you are changing the draw order. Whereas you might have had:

blend(blend(blend(background color, model), first line), second line);

now you will have

blend(blend(first line, second line), blend(background color, model)).

In other words, pre-mixing the overlay lines into an FBO changes the order of blending and thus changes the final look in a way you may not want.

First, the simple way to get around this: don't use an FBO. I realize this is a "go redesign your app" kind of answer, but using an FBO is not the cheapest thing, and modern GL cards are very good at drawing lines. So one option would be: instead of blending lines into an FBO, write the line geometry into a vertex buffer object (VBO). Simply extend the VBO a little bit each time. If you are drawing less than, say, 40,000 lines at a time, this will almost certainly be as fast as what you were doing before.

(One tip if you go this route: use glBufferSubData to write the lines in, not glMapBuffer - mapping can be expensive and doesn't work on sub-ranges on many drivers...better to just let the driver copy the few new vertices.)

If that isn't an option (for example, if you draw a mix of shape types or use a mix of GL state, such that "remembering" what you did is a lot more complex than just accumulating vertices) then you may want to change how you draw into the VBO.

Basically what you'll need to do is enable separate blending; initialize the overlay to black + 0% alpha (0,0,0,0) and blend by "standard blending" the RGB but additive blending the alpha channels. This still isn't quite correct for the alpha channel but it's generally a lot closer - without this, over-drawn areas will be too transparent.

Then, when drawing the FBO, use "pre-multiplied" alpha, that is, (one, one-minus-src-alph).

Here's why that last step is needed: when you draw into the FBO, you have already multiplied every draw call by its alpha channel (if blending is on). Since you are drawing over black, a green (0,1,0,0.5) line is now dark green (0,0.5,0,0.5). If alpha is on and you blend normally again, the alpha is reapplied and you'l have 0,0.25,0,0.5.). By simply using the FBO color as is, you avoid the second alpha multiplication.

This is sometimes called "pre-multiplied" alpha because the alpha has already been multiplied into the RGB color. In this case you want it to get correct results, but in other cases, programmers use it for speed. (By pre-multiplying, it removes a mult per pixel when the blend op is performed.)

Hope that helps! Getting blending right when the layers are not mixed in order gets really tricky, and separate blend isn't available on old hardware, so simply drawing the lines every time may be the path of least misery.

Pomposity answered 31/1, 2010 at 23:0 Comment(1)
Thank you for the incredibly indepth response! I am sorry it has taken me this long to get back to you, but if I had more points to give you, I would! Thank you!Argolis
S
23

Clear the FBO with transparent black (0, 0, 0, 0), draw into it back-to-front with

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

and draw the FBO with

glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

to get the exact result.

As Ben Supnik wrote, the FBO contains colour already multiplied with the alpha channel, so instead of doing that again with GL_SRC_ALPHA, it is drawn with GL_ONE. The destination colour is attenuated normally with GL_ONE_MINUS_SRC_ALPHA.

The reason for blending the alpha channel in the buffer this way is different:

The formula to combine transparency is

resultTr = sTr * dTr

(I use s and d because of the parallel to OpenGL's source and destination, but as you can see the order doesn't matter.)

Written with opacities (alpha values) this becomes

    1 - rA = (1 - sA) * (1 - dA)
<=> rA = 1 - (1 - sA) * (1 - dA)
       = 1 - 1 + sA + dA - sA * dA
       =         sA + (1 - sA) * dA

which is the same as the blend function (source and destination factors) (GL_ONE, GL_ONE_MINUS_SRC_ALPHA) with the default blend equation GL_FUNC_ADD.


As an aside:

The above answers the specific problem from the question, but if you can easily choose the draw order it may in theory be better to draw premultiplied colour into the buffer front-to-back with

    glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE);

and otherwise use the same method.

My reasoning behind this is that the graphics card may be able to skip shader execution for regions that are already solid. I haven't tested this though, so it may make no difference in practice.

Sisley answered 28/8, 2013 at 20:23 Comment(3)
I wanted to say that ~5 years later, I was googling this problem again, read your post, and thought to myself "Wow, this solves my problem, is so simple, and just fits so perfectly!". I then thought to myself "I wonder who asked the original question".Argolis
@Argolis Heh, I'm glad this reached you. I was looking this up for someone else trying to render nested UI controls (I'm still bad at practical graphics programming.) and the previous answer tipped me off about separate blend modes, but I thought it would be very strange if an exact solution was impossible. Thinking about it again this solution is only optimal if you have to draw over the previous previous buffer though. I'll add a note for cases where you can choose the order, like when rendering GUI controls into a buffer, since it's not unlikely for people doing that to end up here.Sisley
Works like a charm! This helped me render transparent text from an FBO to screen. As for understanding why/how it works, I think I have to do some more reading :)Untouchability
A
5

As Ben Supnik said, the best way to do this is rendering the entire scene with separate blend functions for color and alpha. If you are using the classic non premultiplied blend function try glBlendFuncSeparateOES(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE) to render your scene to FBO. and glBlendFuncSeparateOES(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) to render the FBO to screen.

It is not 100% accurate, but in most of the cases that will create no unexpected transparency.

Keep in mind that old Hardware and some mobile devices (mostly OpenGL ES 1.x devices, like the original iPhone and 3G) does not support separated blend functions. :(

Afrikaner answered 2/11, 2010 at 9:15 Comment(0)
B
0

Although a bit late to the game, I want to answer this question, because it was still relevant for me and other people might have a similar issue with blending being wrong when using multiple fbos.

When rendering back-to-front to and using multiple fbos, I found this solution more suitable for my use case:

glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);

// set the alpha value to 1.0 for each fbo used
glClearColor(0.0, 0.0, 0.0, 1.0);

glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT);

glBindFramebuffer(GL_FRAMEBUFFER, fbo2);
glClear(GL_COLOR_BUFFER_BIT);

gl[Draw|Bind|Draw|Bind|...]

This effectively ensures that the alpha value in the destination fbo color buffer stays at 1.0, which is what I wanted for post-processing.

However, this also makes the stored image fully opaque!

Explanation:

Here is what happens explained in GLSL. Think about two colors src and dest that come from two textures that you want to blend with each other. Note that we want to draw src over dest. Here the code in the fragment shader:

in vec2 tex_coords;

out vec4 target;

uniform sampler2D s;
uniform sampler2D d;

void main() {
    vec4 src = texture(s, tex_coords);
    vec4 dest = texture(d, tex_coords);
    [...]
    target = dest;
}

In reality, one would provide the src color in the draw call and dest is unaccessible, because it is the color currently stored in the fbo. I.e., something like "currently stored" target. If you render the last two frames to textures without blending enabled, you can access these pixels and their original alpha values with the same texture coordinates and perform blending yourself.

Result with default settings (replace above [...] with this):

// glBlendEquation(GL_FUNC_ADD)
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
dest.rgba = src.rgba * (src.a) + dest.rgba * (1.0 - src.a);

Note how the + comes from GL_FUNC_ADD, (src.a) from GL_SRC_ALPHA and (1.0 - src.a) from GL_ONE_MINUS_SRC_ALPHA.

It is important to know that only dest.rgb is actually printed to screen without any consideration of the stored dest.a value! Hence, the calculated alpha in dest is completely irrelevant when it comes to which color on the screen is printed, only dest.rgb matters. However, the alpha that is stored has changed. So when you run the fragment shader again with the previously stored dest value as the new src value, it applies the alpha again and the color changes. This is what happens after rendering to the texture first and then from texture to default fbo / screen.

The proposed settings to keep the alpha at 1.0 in dest:

// glBlendEquation(GL_FUNC_ADD)
// glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE)
dest.rgb = src.rgb * (src.a) + dest.rgb * (1.0 - src.a);
dest.a = src.a * (0.0) + dest.a * (1.0);

This time glBlendFuncSeparate split the calculation for the RGB and the alpha part of the dest color. Calling glClearColor(0.0, 0.0, 0.0, 1.0) sets dest.a initially to 1.0. This means that the righthand side of the term is always 1.0, because GL_ONE causes the term to be 1.0 * 1.0 = 1.0. The left side is always 0.0, because GL_ZERO forces the src alpha to be mulitplied by that. Hence, the alpha value of dest stays 0.0 + 1.0 = 1.0. If the dest value is used again as the src value, it will be fully opaque and will overwrite dest.rgb entirely (see dest.rgb equation).

Alternatively, as already pointed out by previous answers, one could also simply disable blending when writing to the default fbo, which would basically not apply the alpha value again. However, this gets tricky when having multiple post-processing stages or when trying to render to multiple targets at once and then combine results. Hence, the above proposed solution.

Bandur answered 18/1 at 19:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.