1

I am trying to figure out the best way to mask of sections of a texture when they ar drawn. My issue comes in the fact that I seem to have run our of alpha masks!

We are using openGL to draw a custom built 2D game engine. The game is built up off of sprites and simple block textures.

My desired outcome is like this:

  • A character sprite is drawn in place (using it's alpha color to not just be a box)
  • An item is drawn into the players hand (also using it's alpha color to draw into the scene without being a box)
  • The item should appear behind the characters arm/hand, but above the rest of the body.

For the moment the only way I can figure out how to accomplish this, is by drawing them in order (Body, Item, Arm) but I would like to avoid this to make art assets a bit easier to deal with. My idea solution would be to draw the character, then draw the item with an alpha mask that blocks out areas of the texture that should be "under" the arm.

Other solutions that I have seen are like this, where the glBlendFuncSeparate() function is used. I am trying to avoid bringing in extensions, as my current version of OpenGL doesn't support it. Not to say that I am opposed to the idea, but it seems a bit of a handle to brig it in just to draw an alpha mask?

I fully admit that this is a learning process for me, and I am using it as an excuse to really see how OpenGL handles. Any suggestions as to where I should head to get this to draw correctly? Is there a way for OpenGL in the fixed pipeline to take a texture, apply an alpha mask on top of it, and THEN draw it into the buffer? Should I give in and separate my character into several parts of its model?

[UPDATE: 8/12/12]

Tried to add the code suggested by Tim, but I seem to be having an issue. When I enable the stencil buffer, everything just gets blocked out, NOT just what I wanted. Here is my test example code.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

// Disable writing to any of the color fields
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); 

glStencilFunc(GL_ALWAYS, 0,0);

// Draw our blocking poly
glBegin(GL_POLYGON);                                   
    glVertex2f( 50,     50 );
    glVertex2f( 50,     50+128 );
    glVertex2f( 50+128, 50+128 );
glEnd();

glStencilFunc(GL_GREATER, 0, -1);

glEnable(GL_STENCIL_TEST);

// Re enable drawing of colors
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

// Enable use of textures
glEnable(GL_TEXTURE_2D);

// Bind desired texture for drawing
glBindTexture(GL_TEXTURE_2D,(&texture)[0]);

// Draw the box with colors
glBegin(GL_QUADS);                                   
    glTexCoord2d( 0, 0 );    glVertex2f( 50,     50 );
    glTexCoord2d( 0, 1 );    glVertex2f( 50,     50+128 );
    glTexCoord2d( 1, 1 );    glVertex2f( 50+128, 50+128 );
    glTexCoord2d( 1, 0 );    glVertex2f( 50+128, 50 );
glEnd();

// Swap buffers and display!
SDL_GL_SwapBuffers();

Just to be clear, here is my init code as well to set this system up.

When the code is run with stencil disabled, I get this: Without glEnable(GL_STENCIL_TEST);

When I use glEnable(GL_STENCIL_TEST), I get this: enter image description here

I've tried playing around with various options, but I cannot see a clear reason why my stencil buffer is blocking everything.

[Update#2 8/12/12]

We got some working code, Thanks tim! Here is what I ended up running to work correctly.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

// Disable writing to any of the color fields
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

glStencilOp(GL_INCR, GL_INCR, GL_INCR);

glEnable(GL_STENCIL_TEST);
// Draw our blocking poly
glBegin(GL_POLYGON);                                   
    glVertex2f( 50,     50 );
    glVertex2f( 50,     50+128 );
    glVertex2f( 50+128, 50+128 );
glEnd();

glStencilFunc(GL_EQUAL, 1, 1);

glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); 

// Re enable drawing of colors
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

// Enable use of textures
glEnable(GL_TEXTURE_2D);

// Bind desired texture for drawing
glBindTexture(GL_TEXTURE_2D,(&texture)[0]);

// Draw the box with colors
glBegin(GL_QUADS);                                   
    glTexCoord2d( 0, 0 );    glVertex2f( 50,     50 );
    glTexCoord2d( 0, 1 );    glVertex2f( 50,     50+128 );
    glTexCoord2d( 1, 1 );    glVertex2f( 50+128, 50+128 );
    glTexCoord2d( 1, 0 );    glVertex2f( 50+128, 50 );
glEnd();
glDisable(GL_STENCIL_TEST);

// Swap buffers and display!
SDL_GL_SwapBuffers();
Community
  • 1
  • 1
jmurrayufo
  • 379
  • 2
  • 5
  • 14
  • 3
    Inside the texture, how do you plan to distinguish 'arm' from 'body'? Are they separate draw calls, or what information do you have to differentiate them? Depending on what information you have to work with, there are several ways you could approach this (depth buffer, stencil buffer, alpha texture, etc). – Tim Aug 10 '12 at 08:31
  • I would LIKE to keep the "arm" and "body" as one single texture, then keep a separate alpha mask to tell other textures drawn over the overall character where the arm is so that they are drawn under it, and where the body is to be drawn over it. I understand what the depth buffer is, but I would like to avoid using it (it would require me to have 2 assets to draw the character, and that could get complex fast as characters get more complex). I have no clue what the stencil buffer is used for, and from my vague understanding it seems like the wrong tool for this job? – jmurrayufo Aug 10 '12 at 17:04
  • Don't you already have two separate assets to draw the character? 1) the texture and 2) the separate alpha mask? Or is there some other thing I'm not seeing? You can certainly do it this way, but I feel like I'm still missing something. – Tim Aug 10 '12 at 19:39
  • Hmm, your code looks correct to me as far as I can tell. Here's a few ideas to try: 1) Check for errors with glGetError. 2) Query the number of stencil bits with glGet(GL_STENCIL_BITS) and make sure it is not zero. 3) Try replacing the arguments in the first call to glStencilOp with (GL_INCR, GL_INCR, GL_INCR). 4) Try removing the calls to glColorMask. Not sure which of these might help, but it's some ideas. – Tim Aug 12 '12 at 17:18
  • Also, try moving the call to glEnable(GL_STENCIL_TEST) to right after glClear. I'm not sure if stencil test must be enabled for stencil op to work. – Tim Aug 12 '12 at 17:20
  • We do have the mask, but I am trying this the simple way first. – jmurrayufo Aug 12 '12 at 17:32
  • It works! I'll add my test code in a few minutes once I clean out by debug code again. – jmurrayufo Aug 12 '12 at 17:49

3 Answers3

1

Tim was pretty clear in his comment, but I want to present you the solution I find the most intuitive. It's 3D, so hold on... ;)

Basically, you can just use the Z coordinate of your images to create virtual "layers". It then doesnt' matter, in which order you draw them. Just alphatest every image individually, and draw it on correct Z value. If it still isn't enough, you could use separate texture containing "depth" of every pixel, and then use the 2nd texture to perform some sort of depth-testing.

Be sure to call glEnable(GL_DEPTH_TEST); if you want to use this approach.

Bartek Banachewicz
  • 38,596
  • 7
  • 91
  • 135
  • I don't mind doing it this way, but I am trying to see if I can do it with an alpha mask so that the Character art asset can be ONE image, and not a separate Arm and Body to accomplish this. Is it just easier to use the depth built into the scene to do this? – jmurrayufo Aug 10 '12 at 16:16
1

Here's my idea for the situation where you have one texture and one alpha mask:

  1. Draw the character onto the scene like normal.
  2. Lock the RGB color channels so that it cannot be changed with glColorMask
  3. Setup the stencil buffer with glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); glStencilFunc(GL_ALWAYS, 0,0);
  4. Draw the alpha mask with alpha testing enabled. This will increment the stencil buffer anywhere the alpha test passes (you may have to flip this based on your mask polarity)
  5. At this point, you have a character texture in the framebuffer, and a mask outline in the stencil buffer.
  6. Reenable the color channels with glColorMask
  7. Setup the stencil buffer for the weapon with glStencilFunc(GL_GREATER, 0, -1); This will only draw the weapon texels where the stencil buffer is greater than zero, and reject pixels where the stencil is not updated.
  8. Draw the weapon texture as normal.
Tim
  • 35,413
  • 11
  • 95
  • 121
  • Thanks for the awesome breakdown, i'll give it a try tonight. I have never done anything but draw to the default buffer, so it will be a bit of an adventure. – jmurrayufo Aug 10 '12 at 23:06
  • Aside from the opengl commands above, you have to make sure that your context gives you a stencil buffer, or it won't work. Check the documentation for whatever window management system you're using to make sure you request a stencil buffer at context creation time. @jmurrayufo – Tim Aug 10 '12 at 23:07
  • Posted some updated code with a question attached. This stencil buffer this is a bit odd... – jmurrayufo Aug 12 '12 at 16:51
0

As I see it, the problem is that you have one texture, but part of it represents the arm and part of it the rest of the character. The issue is that you want to draw the weapon over the character, but draw the arm over both.

This means, while drawing two objects, you want to put them into three different "layers". This fundamentally doesn't make sense, so you're kind of stuck.

Here's an idea though: use a fragment program (i.e., a shader).

I suggest you overload the character's texture's alpha channel to encode both transparency and layer. For example, let's use 0=transparent body, 64=opaque body, 128=transparent arm, 255=opaque arm.

From here, you draw your objects, but conditionally set the depth of your objects into three layers. Basically, you write a fragment program that draws your character into two different layers, the character gets pushed backward while the arm gets pulled forward. When the weapon is drawn, it is drawn without a shader, but it's tested against the characters' pixels' depths. It works something like this (untested, obviously).

Define a shader my_shader, which contains a fragment program:

uniform sampler2D character_texture;
void main(void) {
    vec4 sample = texture2D(character_texture,gl_TexCoord[0].st);

    int type; //Figure out what type of character texel we're looking at
    if      (fabs(sample.a-0.00)<0.01) type = 0; //transparent body
    else if (fabs(sample.a-0.25)<0.01) type = 1; //opaque body
    else if (fabs(sample.a-0.50)<0.01) type = 2; //transparent arm
    else if (fabs(sample.a-1.00)<0.01) type = 3; //opaque arm

    //Don't draw transparent pixels.
    if (type==0 || type==2) discard;

    gl_FragColor = vec4(sample.rgb,1.0);

    //Normally, you (can) write "gl_FragDepth = gl_FragCoord.z".  This
    //is how OpenGL will draw your weapon.  However, for the character,
    //we alter that so that the arm is closer and the body is farther.

    //Move body farther
    if      (type==1) gl_FragDepth = gl_FragCoord.z * 1.1;
    //Move arm closer
    else if (type==3) gl_FragDepth = gl_FragCoord.z * 0.9;
}

Here's some pseudocode for your draw function:

//...

//Algorithm to draw your character
glUseProgram(my_shader);
glBindTexture(GL_TEXTURE_2D,character.texture.texture_gl_id);
glUniform1i(glGetUniformLocation(my_shader,"character_texture"),1);
character.draw();
glUseProgram(0);

//Draw your weapon
glEnable(GL_DEPTH_TEST);
character.weapon.draw();
glDisable(GL_DEPTH_TEST);

//...
geometrian
  • 14,775
  • 10
  • 56
  • 132
  • That's a nice detailed answer, but unfortunately OP requests fixed pipeline solution :( – Tim Aug 10 '12 at 23:08
  • Aye, I do plan to learn the more current means of drawing in OpenGL, but not yet. One step at a time sort of learning right now. – jmurrayufo Aug 10 '12 at 23:44