FBO-Render2Texture: Blending-Problems

Hey there everybody.

I’m new to OpenGL-Programming but I think, I’m slowly getting in. I’ve just messed around with Framebuffer Objects (That EXT-Extension) and finally got them working fine including depthbuffering. So, what I expected to get (and also needed to get - because that’s why I tried setting up FBOs) was a custom Framebuffer behaving just as the Backbuffer when drawing to it.
Unfortunately that’s not what I actually got.

I’ve experienced a slightly different behaviour in Blending when drawing to FBOs, making them completely unuseable for my purposes. It seems like RGB and Alpha-Values are treated seperately. I can (well… I would be glad if I could prevent it…) “overwrite” an 100%-Alpha by drawing a 50%-Alpha over it, making the texture again half-transparent when I’ve already completely filled out with a solid color.
Let’s say I’ve drawn a white rect on it, completely solid, 100% Alpha. Then I want to draw a black square in the middle of it with only 50% alpha. I expect to get a solid white square with a solid grey square in the middle of it. But the result is a solid white square with a 50%-Alpha grey square in the middle of it. And that’s just not the right behaviour for my purposes.
If it was just about drawing squares I could easily avoid it by doing my own calculations but when it comes to drawing textures on textures then I’m just unable to avoid anything. I need that FBO working as the backbuffer does.

What am I doing wrong? What can I do to get what I need?

If it helps, I’m adding my [C#-][Tao-]OpenGL-Code:


/// <summary>
            /// Creates a RenderBuffer. A RenderBuffer is neccessary for Render2Texture-Operations.
            /// The texture's content is being deleted as soon as a RenderBuffer is created.
            /// </summary>
            /// <param name="flags">
            /// TexFlags-Bitmask. Use Bit-Operations on TexFlag-Enumerator or just specify this parameter as TexFlag.None.
            /// </param>
            public void CreateRenderBuffer(int flags)
            {
                if (this.framebufferID != -1 && this.renderbufferID != -1) return;

                int oglWidth;
                int oglHeight;
                GetOGLTexSize(this.width, this.height, out oglWidth, out oglHeight);
                this.curUVRatio.x = (float)this.width / (float)oglWidth;
                this.curUVRatio.y = (float)this.height / (float)oglHeight;

                // Create objects
                Gl.glGenFramebuffersEXT(1, out this.framebufferID);
                Gl.glGenRenderbuffersEXT(1, out this.renderbufferID);
                Gl.glBindFramebufferEXT(Gl.GL_FRAMEBUFFER_EXT, this.framebufferID);

                // Set-up texture
                Bind(this);
                Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_RGBA, oglWidth, oglHeight, 0, Gl.GL_RGBA, Gl.GL_UNSIGNED_INT, null);
                this.texflags = flags;
                if ((flags & (int)TexFlag.Smooth) != 0)
                {
                    if ((flags & (int)TexFlag.MipMap) != 0)
                    {
                        Gl.glGenerateMipmapEXT(Gl.GL_TEXTURE_2D);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_LINEAR_MIPMAP_NEAREST);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_LINEAR);
                    }
                    else
                    {
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_LINEAR);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_LINEAR);
                    }
                }
                else
                {
                    if ((flags & (int)TexFlag.MipMap) != 0)
                    {
                        Gl.glGenerateMipmapEXT(Gl.GL_TEXTURE_2D);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_NEAREST_MIPMAP_NEAREST);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_NEAREST);
                    }
                    else
                    {
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_NEAREST);
                        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_NEAREST);
                    }
                }

                // Attach texture to FBO
                Gl.glFramebufferTexture2DEXT(Gl.GL_FRAMEBUFFER_EXT, Gl.GL_COLOR_ATTACHMENT0_EXT, Gl.GL_TEXTURE_2D, this.textureID, 0);
                
                // Initialize Renderbuffer (Depthbuffer)
                Gl.glBindRenderbufferEXT(Gl.GL_RENDERBUFFER_EXT, this.renderbufferID);
                Gl.glRenderbufferStorageEXT(Gl.GL_RENDERBUFFER_EXT, Gl.GL_DEPTH_COMPONENT24, oglWidth, oglHeight);
                
                // Attach Renderbuffer to Framebuffer-Depthbuffer
                Gl.glFramebufferRenderbufferEXT(Gl.GL_FRAMEBUFFER_EXT, Gl.GL_DEPTH_ATTACHMENT_EXT, Gl.GL_RENDERBUFFER_EXT, this.renderbufferID);

                // Status check
                int status = Gl.glCheckFramebufferStatusEXT(Gl.GL_FRAMEBUFFER_EXT);
                if (status == Gl.GL_FRAMEBUFFER_COMPLETE_EXT) { /* void */ }
                else if (status == Gl.GL_FRAMEBUFFER_UNSUPPORTED_EXT)
                {
                    throw new ApplicationException("Can't create Framebuffer. Choose different format.");
                }
                else
                {
                    throw new ApplicationException(String.Format("Can't create Framebuffer. Unknown error. Framebuffer-Status: {0}", status));
                }

                // Unbind all to regularily continue drawing operations
                Gl.glBindRenderbufferEXT(Gl.GL_RENDERBUFFER_EXT, 0);
                Gl.glBindFramebufferEXT(Gl.GL_FRAMEBUFFER_EXT, 0);
                Bind(lastBound);

                return;
            }


            /// <summary>
            /// Deletes the texture's Renderbuffer.
            /// </summary>
            public void DeleteRenderBuffer()
            {
                if (CurrentFBBound != 0) UnbindRenderBuffer();
                if (this.framebufferID != -1) { Gl.glDeleteFramebuffersEXT(1, ref this.framebufferID); }
                if (this.renderbufferID != -1) { Gl.glDeleteRenderbuffersEXT(1, ref this.renderbufferID); }
                this.framebufferID = -1;
                this.renderbufferID = -1;
                return;
            }


            /// <summary>
            /// Binds the RenderBuffer if created. All following graphic commands are used on this texture.
            /// </summary>
            public void BindRenderBuffer()
            {
                Gl.glBindFramebufferEXT(Gl.GL_FRAMEBUFFER_EXT, this.framebufferID);
                curFBBound = this.framebufferID;

                Gl.glPushMatrix();
                Gl.glTranslatef(0.0f, curWindowSize.y - this.height, 0.0f);

                return;
            }


            /// <summary>
            /// Unbinds the RenderBuffer.
            /// </summary>
            public void UnbindRenderBuffer()
            {
                Gl.glBindFramebufferEXT(Gl.GL_FRAMEBUFFER_EXT, 0);
                curFBBound = 0;

                Gl.glPopMatrix();

                if ((this.texflags & (int)TexFlag.MipMap) != 0)
                {
                    Bind(this);
                    Gl.glGenerateMipmapEXT(Gl.GL_TEXTURE_2D);
                    Bind(lastBound);
                }

                return;
            }


Texture        tex            = Texture.Create((int)Math.Ceiling(Font.CurrentBound.TextWidth(text)), (int)Math.Ceiling(Font.CurrentBound.TextHeight(text)));
            BlendMode    oldBlend    = DrawBlend;

            tex.CreateRenderBuffer(flags);
            tex.BindRenderBuffer();
            Cls();
            SetBlend(BlendMode.Alpha);
            SetColor(255, 255, 255, 255);
            DrawRect(0.0f, 0.0f, tex.Width, tex.Height);
            SetColor(0, 0, 0, 128);
            DrawRect(0.0f, 0.0f, tex.Width / 2.0f, tex.Height);
            SetBlend(oldBlend);

            tex.UnbindRenderBuffer();
            tex.DeleteRenderBuffer();

Do you have blending enabled?

What’s your glBlendFunc?

Ah, sorry… forgot about that SetBlend-Function. Here it is:


		/// <summary>
		/// Sets the current BlendMode to the specified one.
		/// </summary>
		public static void SetBlend(BlendMode mode)
		{
			curBlend = mode;
			switch (mode)
			{
				case BlendMode.Solid:
					Gl.glDisable(Gl.GL_BLEND);
					Gl.glDisable(Gl.GL_ALPHA_TEST);
					break;
				case BlendMode.Mask:
					Gl.glDisable(Gl.GL_BLEND);
					Gl.glEnable(Gl.GL_ALPHA_TEST);
					Gl.glAlphaFunc(Gl.GL_GEQUAL, 0.5f);
					break;
				case BlendMode.Alpha:
					Gl.glEnable(Gl.GL_BLEND);
					Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA);
					Gl.glDisable(Gl.GL_ALPHA_TEST);
					break;
				case BlendMode.Light:
					Gl.glEnable(Gl.GL_BLEND);
					Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE);
					Gl.glDisable(Gl.GL_ALPHA_TEST);
					break;
				case BlendMode.Shade:
					Gl.glEnable(Gl.GL_BLEND);
					Gl.glBlendFunc(Gl.GL_DST_COLOR, Gl.GL_ZERO);
					Gl.glDisable(Gl.GL_ALPHA_TEST);
					break;
				default:
					Gl.glDisable(Gl.GL_BLEND);
					Gl.glEnable(Gl.GL_ALPHA_TEST);
					Gl.glAlphaFunc(Gl.GL_GEQUAL, 0.5f);
					break;
			}
			return;
		}

Also, I’m doing this at initialize (Don’t know if this is relevant):


Gl.glDepthFunc(Gl.GL_LEQUAL);
			Gl.glClearDepth(1.0f);
			Gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
			Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);
			Gl.glHint(Gl.GL_GENERATE_MIPMAP_HINT, Gl.GL_NICEST);

Keep in mind that the same drawingcode (See first post) works as expected when not drawing to a texture.

…anyone.? =/
I don’t neccessarily need a correction of my code - a working example of how to draw the specified “scene” on an FBO avoiding that strange behaviour may help also!

Unfortunately that’s not what I actually got.

No, it is what you got. You just never cared about the framebuffer’s alpha value before. Or your initial framebuffer never had an alpha value.

A color’s Alpha channel, like R, G, and B, is entirely independent of the others. Which means that, like R, G, and B, the blend equation determines what Alpha color you write to the framebuffer.

Your blend equation is glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); This means that:

dstAlpha = srcAlpha * srcAlpha + dstAlpha * 1-srcAlpha

Which, if srcAlpha is 0.5, and dstAlpha is 1.0, gives you 0.75.

If you want the final destination Alpha value to always be 1.0, it’s probably best to just use the write mask to not write Alpha values, and instead just clear the buffer before use to have an Alpha of 1.0.

However, you can also use glBlendFuncSeparate (an extension, but in the 1.4 core and greater). This allows you to specify separate blend factors for the RGB and A components. So you would use glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE) to keep the destination Alpha constant.

AH, thanks! That makes sense.
But one question: Why can I run the also Alpha-Channelled Backbuffer with the same code and it won’t show the same behaviour?

Edit: And another one, quite significant: I want the Alpha values to be calculated like this, for example, they’re calculated in Photoshop or any graphic software when blending to alphachannelled layers one upon the other. What formula would fit best? I thought first about

newAlpha = srcAlpha * dstAlpha + dstAlpha * 1.0

which doesn’t seem correct.