5

I have very simple CCScene with ONLY 1 CCLayer containing:

  1. CCSprite for background with standard blending mode
  2. CCRenderTexture to draw paint brushes, with its sprite attached to root CCLayer above background sprite:
_bgSprite = [CCSprite spriteWithFile:backgroundPath];
_renderTexture = [CCRenderTexture renderTextureWithWidth:self.contentSize.width height:self.contentSize.height];
[_renderTexture.sprite setBlendFunc:(ccBlendFunc){GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA}];
[self addChild:_bgSprite z:-100];
[self addChild:_renderTexture];

Brush rendering code:

[_renderTexture begin];
glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ONE); // 1.
// calculate vertices code,etc...
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)count);
[_renderTexture end];

When user brushes with first colored brush, it blends with background as expected. But when when continues brushing with another color on top of the previous brush, it goes wrong (soft alpha edges loses opacity when 2 brushes overlap each other):

enter image description here

I tried many blending options but somehow I cannot find correct one.

Is there something special about CCRenderTexture that it does not blend with itself (with previously drawn content) as expected?

My fragment shader used for brushing is just standard texture shader with minor change to preserve input color alpha in texture:

void main()
{
    gl_FragColor = texture2D(u_texture, v_texCoord);
    gl_FragColor.a = v_fragmentColor.a;
}

UPDATE - ALMOST PERFECT SOLUTION : by jozxyqk

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
                        GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

in rendering code (in place of // 1. and

[_renderTexture.sprite setBlendFunc:(ccBlendFunc){GL_ONE, GL_ONE_MINUS_SRC_ALPHA}];

THIS WORKS GREAT AND GIVES ME WHAT I WANT...

enter image description here

...BUT ONLY WHEN _rederTexture is in full opacity.

When opacity of _rendertexture.sprite is lowered, brushes get lightened up instead of fading out as one could expect:

enter image description here

Why alphas of the brushes are blending with background correctly when parent texture is in full opacity but go bananas when opacity is lowered? How can I make brushes to blend with background correctly?

Lukasz
  • 19,816
  • 17
  • 83
  • 139

1 Answers1

9

EDIT

Blending brush -> layer -> background

OK, what's happening is glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) is working for blending the brush strokes into the brush texture, but the resulting alpha values in the texture are wrong. Each added fragment needs to 1. add it's alpha to the final alpha value - it has to remove exactly that much light for the interaction and 2. scale the previous alpha by the remainder - previous surfaces reduce the light by the previous value, but since a new surface is added there is less light for them to reduce. I'm not sure if that made sense but it leads to this...

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
                    GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Now the colour channel of the brush texture contains the total colour to be blended with the background (pre-multiplied with alpha) and the alpha channel gives the weight (or the amount the colour obscures the background). Since the colour is pre-multiplied with alpha, the default RenderTexture blending GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA scales with alpha again and hence darkens the overall colour. You now need to blend the brush texture with the background using the following function, which I gather must be set in Cocos2D:

glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Hopefully this is possible. I haven't given a lot of thought on how to manage the possibility of setting up the brush texture to blend with GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA but it may require a floating point texture and/or an extra pass to divide/normalize the alpha, which sounds painful.

Alternatively, splat the background into your render texture before drawing and keep the lot there without any blending of layers.

This worked for me:

glDisable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);

fbo.bind();
glClear(GL_COLOR_BUFFER_BIT);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
                    GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
drawTexture(brush1);
drawTexture(brush2);
fbo.unbind();

drawTexture(grassTex); //tex alpha is 1.0, so blending doesn't affect background
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
drawTexture(fbo.getColour(0)); //blend in the brush layer

small test in GL

Brush layer opacity

Using GL_ONE, GL_ONE_MINUS_SRC_ALPHA causes issues with the library's implementation of opacity in layer blending since it assumes the colour is multiplied by alpha. By reducing the opacity value, the alpha of the brush layer is scaled down during blending. GL_ONE_MINUS_SRC_ALPHA then causes the amount of background colour to increase, however GL_ONE sums 100% of the brush layer and oversaturates the image.

The simplest solution imo is to find a way to scale down the colour by the global layer opacity yourself and continue to use GL_ONE, GL_ONE_MINUS_SRC_ALPHA.

  • Actually using GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_ALPHA might be an answer if the library supported it, but apparently it doesn't.
  • You could use fixed pipeline rendering to scale the colour: glColor4f(opacity, opacity, opacity, opacity), but this will require a second render target and doing the blend manually, similarly to the code above, where you draw a full screen quad once for the background and again for the brush layer.
  • If you're doing the blend manually it would be more robust to use a fragment shader instead of the glColor method. This would allow far greater control if you ever wanted to play with more complex blending functions, especially where divisions and temporaries outside the 0 to 1 range are concerned: gl_FragColour = texture(brushTexture, coord) * layerOpacity;

END EDIT


The standard alpha blending function is glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);, not quite the GL "initial"/default function.

Summing alpha values as you do in glBlendFuncSeparate will oversaturate alpha and the underneath colour is completely replaced. Saturation blending may give decent results: glBlendFunc(GL_SRC_ALPHA_SATURATE, GL_ONE). It might also be worth experimenting with glBlendEquationSeparate and MAX blending, if it's supported. The advantage of playing with MAX would be reducing the overlapping artefacts (hard triangular bits) from your line drawing code - eg replace colour, but only until total alpha value X is reached. EDIT: Both cases will require blending and clearing after each stroke.

I can only assume blending the render texture onto the background is in fact working. (not for the current layer values)

On a side note and largely unrelated there's also "Under Blending", where you keep a transmittance value instead of alpha/opacity (from here):

glBlendEquation(GL_FUNC_ADD); 
glBlendFuncSeparate(GL_DST_ALPHA, GL_ONE, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA); 
jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • glBlendFunc(GL_SRC_ALPHA_SATURATE, GL_ONE) did not work - see updated part of the question (on the bottom). – Lukasz Sep 11 '13 at 10:17
  • I'm assuming you're referring to the darkening around the edges. normalizing (dividing the colour by the alpha value) when blending texture would fix that but I'm guessing you don't have control over it. It'd also be nice if you could blend and save the brush texture with the background - allowing alpha blending between strokes but saturation within the stroke. Why doesn't standard alpha blending work? – jozxyqk Sep 11 '13 at 11:07
  • Yes, I was referring to the darkening around edges. Why alpha blending does not work? Well - if I knew that I wouldn't asked the question. I am newbie in Cocos2D and OpenGL and tried to use standard blending modes with this very simple scene. I have no ides if Cocos2D changes something behind the scenes or is I am doing something terribly wrong. – Lukasz Sep 11 '13 at 11:28
  • glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) sounds like it's what you want. The only reason I mentioned it is because your code has glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA), which over-saturates the colour – jozxyqk Sep 11 '13 at 15:59
  • @Lukasz see the edit. Is it easy to change the blend function for the brush render texture (eg http://www.cocos2d-iphone.org/forums/topic/ccrendertexture-blendingalpha-problems-on-iphone-4s/ ) – jozxyqk Sep 12 '13 at 10:03
  • see my IIIrd update. Your latest blending settings work perfectly when render texture opacity is full (100%). But when I lower it down, brushes get lightened up crazy way (instead of fading out as expected). Can you explain why is that happening? – Lukasz Sep 12 '13 at 20:26
  • Since the brush texture contains the total colour, and GL_ONE is used there is no scaling down. Reducing the alpha value without reducing the colour results in the background not being subtracted while the brush gets fully added, hence brighter. If you found a way to scale the colour back by the same opacity as the render texture it'd work. Unfortunately, it doesn't looking like there's an easy way to do it: http://www.cocos2d-iphone.org/forums/topic/gl_constant_color-support-for-ccblendfunc/ . – jozxyqk Sep 13 '13 at 03:58
  • Some fixes might be 1. put the background in the brush texture and don't bother blending layers; 2. find another way to either scale the colour by the layer opacity or divide the colour by the alpha and use default blending when the library blends the layers (might need a second/intermediate render target and shader). 3. Find a way to render the brush in such a way as to support the default `GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA` – jozxyqk Sep 13 '13 at 04:04
  • I appreciate your expertise but that sounds just incredible. Final brush rendering is just texture (CCRenderTexture has even CCSprite property) and therefore should blend with backgrounds as other CCSprites. Does it mean one can not change opacity of the CCSprites which use dynamically rendered textures (CCRenderTexture). But isn't it one of the main purposes for CCRenderTexture class? I haven't found even one sentence in Cocos2D documentation saying: CCRenderTexture is great ... you can use it to create great procedural CCSprite objects, but please do NOT DARE to fade in / fade out them ;-)! – Lukasz Sep 13 '13 at 07:19
  • It's to do with the opacity feature not expecting `GL_ONE, GL_ONE_MINUS_SRC_ALPHA`. The real difficulty here is generating the brush texture with correct alpha. This explains the exact same issue (hopefully better than I have)... http://stackoverflow.com/questions/2171085/opengl-blending-with-previous-contents-of-framebuffer . Unfortunately it doesn't offer an answer for you either, other than also suggesting keeping everything in the one render target. – jozxyqk Sep 13 '13 at 09:31
  • If I keep everything in one CCRenderTexture, will I be able to have 'erase brush' feature? Will not background get erased together with brush? – Lukasz Sep 13 '13 at 09:42
  • You could make the erase brush draw the background texture back in. If you're serious about implementing layers, I'd advise using a custom shader to blend the layers as you'll get far more control (You could implement your own layer opacity and scale the colour accordingly in the shader). – jozxyqk Sep 13 '13 at 14:07
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/37366/discussion-between-lukasz-and-jozxyqk) – Lukasz Sep 13 '13 at 20:32