0
\$\begingroup\$

I'm working on a game which uses sprite batching to render sprites efficiently where each sprite gets batched with its VBO data into a VAO every frame (containing vertex coordinates, texture data and color data). So far so good. I'm sorting them on the CPU and ordering the batch before each draw call according to render order to simulate z-ordering.

However, there's one problem: what do I do when I want to draw particles, for instance, which require a blending mode change? All of the sprites are drawn using glDrawArrays use the same blending mode, but for my particle data I want to change the blending mode. If I put the particles in the same batch then I won't have the ability to change it necessarily. My goal is to switch from normal blending (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) to additive blending (GL_SRC_ALPHA, GL_ONE).

1st solution: use a separate draw call for every sprite and don't batch them at all. then for each call I can change the mode appropriately

2nd solution: add a z-coordinate to each sprite to do z-ordering automatically on the GPU using depth testing (this means I can render particles separately in their respective batches with ~2 calls -> +1 for sprites & +1 for particles) which will allow me to change blending modes.

Is there another way of approaching this problem? What am I missing?

\$\endgroup\$
11
  • \$\begingroup\$ What blend modes are you using? You may be able to get the same output with some shader or texture trickery. \$\endgroup\$ Commented Jun 28, 2017 at 19:12
  • \$\begingroup\$ I'm using GL_SRC_ALPHA, GL_ONE for glBlendFunc. \$\endgroup\$ Commented Jun 28, 2017 at 20:34
  • \$\begingroup\$ And what's the other one? You say that you need to change blend modes so we need to know what both are. \$\endgroup\$ Commented Jun 28, 2017 at 20:47
  • \$\begingroup\$ Oh, sorry, it's GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA. \$\endgroup\$ Commented Jun 28, 2017 at 20:53
  • \$\begingroup\$ It seems like it wouldn't be practical to do on the shader/possible. stackoverflow.com/questions/11633950/… \$\endgroup\$ Commented Jun 28, 2017 at 21:14

2 Answers 2

3
\$\begingroup\$

Background

OpenGL blend funcs are just a means of specifying a mathematical formula as follows:

  • src: the output from your fragment shader.
  • dst: the colour in the framebuffer.
  • sfactor: multiply src by this.
  • dfactor: multiply dst by this.
  • And add the two results together.

(As a simplifying assumption I'm ignoring blend equation, blend func separate and multiple render targets, and also assuming that you don't care about destination alpha).

So if we look at your two blend funcs we see that they work out as follows:

glBlendFunc (GL_SRC_ALPHA, GL_ONE)                  =  src * src.a + dst * 1
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)  =  src * src.a + dst * (1 - src.a)

The objective is to find a way to use a single blend func for everything.


I'm going to assume, for the purposes of this answer, that your fragment shader writes it's output to gl_FragColor. If it doesn't, just replace any references to gl_FragColor in the remainder of this answer with whatever you do write to.


Let's repeat the two blend funcs again:

src * src.a + dst * 1
src * src.a + dst * (1 - src.a)

The only difference between the two is the value used for dfactor, and it's (1 - src.a) in the latter, so if we can modify our fragment shader to output 0 for src.a in the former case, then the two blend funcs become the same (because 1 - 0 = 1).

However, we see that src.a is also used as sfactor in both, so we can't modify src.a without also affecting that.

Let's see if we can find a way of modifying the fragment shader to remove that restriction. Since we don't care about destination alpha we can just do this:

gl_FragColor.rgb *= gl_FragColor.a;

After having done that, the two blend funcs become:

glBlendFunc (GL_ONE, GL_ONE)
glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA)

Or:

src * 1 + dst * 1
src * 1 + dst * (1 - src.a)

So the only remaining thing is to find a means of setting src.a to 0 in the former case; something like this in your fragment shader:

gl_FragColor.rgb *= gl_FragColor.a;
if (altBlend) gl_FragColor.a = 0;

So we want altBlend to evaluate to true in cases where you use GL_ONE, GL_ONE blending, or false in cases where you use GL_ONE, GL_ONE_MINUS_SRC_ALPHA blending, and we have the result; we can now use a single blend func for all drawing:

glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Or:

src * 1 + dst * (1 - src.a)

And in cases where src.a is 0 this is exactly equivalent to:

src * 1 + dst * (1 - 0)

Or:

src * 1 + dst * 1

Or:

glBlendFunc (GL_ONE, GL_ONE);

So all that's remaining is how to set the value of altBlend, and how you do that is entirely up to you. You could set an additional vertex attrib that you pass along to your fragment shader, you could bind an additional texture and sample from it for the different types of objects, or you could do something entirely different; it all depends on which is most appropriate for the rest of your code.

\$\endgroup\$
7
  • \$\begingroup\$ Thanks for this, awesome explanation! I'll implement this. I suppose I could also change src.a to 0 on the texture just for the particles which would be called "premultiplied alpha" right? \$\endgroup\$ Commented Jun 29, 2017 at 9:45
  • \$\begingroup\$ Actually wait, nevermind, thinking of something else. That wouldn't work since I'm looking at the fragment already defined right when a particle is rendered to do the blending not the texture itself. This solution is surpisingly simple & elegant! \$\endgroup\$ Commented Jun 29, 2017 at 10:00
  • \$\begingroup\$ This assumes that when a shader processes a fragment in the VBO that it has already processed every other VBO (or sprite, object, whatever i'm drawing) before it in correct order, so it's cumulative and I can apply blending correctly? (not sure how the GPU would do parallel processing/layered rendering any other way) \$\endgroup\$ Commented Jun 29, 2017 at 10:06
  • \$\begingroup\$ That's generally the case when drawing alpha objects, yes. \$\endgroup\$ Commented Jun 29, 2017 at 10:40
  • \$\begingroup\$ Am I missing something? If I change src.a to 0 then the eqn src * src.a + dst * (1 - src.a) becomes = dst and I don't get any of the source color at all. If my goal is to convert the equations to be equal it isn't possible. I need to keep src.a because it could be the alpha value for the other particles which is not just 1 or 0 but could be any value in between. \$\endgroup\$ Commented Jun 29, 2017 at 14:57
1
\$\begingroup\$

For anyone interested, my solution was to just change blending modes in my sprite batcher by keeping track of the indices where blending modes change and then call glDrawArrays as many times as I need whenever the blending mode changed. They're still batched, but the chunks are as small as the gaps between blending modes.

\$\endgroup\$
4
  • \$\begingroup\$ I started to write an answer, but then realized that Le Comte du Merde-fou already described anything. Do glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA), and then gl_FragColor.rgb *= gl_FragColor.a; gl_FragColor.a *= blending_mode;, where blending_mode = 1 will result in normal blending and blending_mode = 0 will result in additive blending. Why don't you want to do it? \$\endgroup\$ Commented Jun 30, 2017 at 8:32
  • \$\begingroup\$ Doesn't that blend mode make it src*1 + (1-src.a)*dst where dst is whatever is in the color buffer. The problem w/ this is gl_FragColor.a will always be 1? So I'll never get transparency.. (in the case where blending mode is 1) \$\endgroup\$ Commented Jun 30, 2017 at 8:33
  • \$\begingroup\$ Just try it and it will work. I've used the trick countless times. You also might want to know that the resulting image will have premultiplied alpha (as if you did color.rgb *= color.a on normal image; look it up). It doesn't matter when rendering to the screen, but after rendering to a texture you might want to do color.rgb /= color.a to revert the changes. \$\endgroup\$ Commented Jun 30, 2017 at 8:38
  • \$\begingroup\$ Edit: whoops, yeah nvm ok will do. my blend mode was wrong for sfactor. \$\endgroup\$ Commented Jun 30, 2017 at 8:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .