1
\$\begingroup\$

I'm rendering a scene using OpenGL. The scene consists of a grassy environment and a small lake (visible as two surfaces). To achieve water surface transparency, I'm using a stencil buffer to render water surfaces separately from other geometry. For context, here's a screenshot of the scene without water surfaces:

Scene without water surfaces rendered.

Here's the same scene, but with water surfaces stenciled out (the black portions of the screen):

Water surfaces stenciled out, but not rendered.

Here's the fragment shader I'm using (where fSource is fullscreen UV coordinates). As you can see, it simply samples from a previously-used frame buffer and outputs the pixel directly to the screen. With stenciling enabled, only pixels not covered by a water surface are rendered.

#version 440 core

in vec2 fSource;

out vec4 outColor;

uniform sampler2D image;

void main()
{
    outColor = texture(image, fSource);
}

Here's the problem. When I change this fragment shader to output a solid color, those underwater pixels are suddenly visible (the stencil buffer and stencil settings have not changed). What I'd expect here is non-submerged pixels to be purple, but the water surface pixels (previously black) to remain black (since they should still be rejected through stenciling). Instead, everything is purple, including the water surface.

#version 440 core

out vec4 outColor;

void main()
{
    outColor = vec4(1, 0, 1, 1);
}

Direct color output (purple) causing pixels to not be rejected through stenciling.

As an additional test, I tried rendering only pixels under the water surfaces with a solid color. These pixels should be rejected via stenciling (appearing black, just like before), but for some reason, they're visible.

#version 440 core

in vec2 fSource;

out vec4 outColor;

uniform sampler2D image;
uniform sampler2D positions;

void main()
{
    vec3 position = texture(positions, fSource).xyz;

    // The water surfaces happen to sit at Y value 7.85.
    if (p.y < 7.85)
    {
        outColor = vec4(1, 1, 0, 1);
    }
    else
    {
        outColor = texture(image, fSource);
    }
}

Submerged pixels showing up, seemingly ignoring the stencil buffer.

I confirmed the problem by instead sampling with shifted UV coordinates (rather than outputting solid yellow). Again, pixels are showing up on the water surfaces, seemingly ignoring the stencil buffer.

#version 440 core

in vec2 fSource;

out vec4 outColor;

uniform sampler2D image;
uniform sampler2D positions;

void main()
{
    vec3 position = texture(positions, fSource).xyz;

    // The water surfaces happen to sit at Y value 7.85.
    if (p.y < 7.85)
    {
        outColor = texture(image, fSource + vec2(0.1, 0));
    }
    else
    {
        outColor = texture(image, fSource);
    }
}

Shifted UV pixels still showing up through the stencil buffer.

My understanding of stenciling is that it prevents the fragment shader from running at all on certain portions of the screen (i.e. fragments pass or fail based on stencil settings). If that were true, then no matter what the fragment shader outputs, that pixel should remain black (in this context). Clearly, the fragment shader is still being run for pixels that should be rejected due to stenciling, which means I must be misunderstanding how the stencil buffer works.

Why is the fragment shader still running on pixels that should be rejected through stenciling?

Update 1: here are my stencil settings. No other objects (apart from water surfaces) write to the stencil buffer.

// Render water surfaces. 0x10 represents water.
glEnable(GL_STENCIL_TEST);
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 0x10, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

BindWaterFrameBuffer();
BindWaterShader();
DrawWaterSurfaces();

// Render the scene. Due to the stencil settings, only non-surface pixels should be rendered.
glStencilMask(0x00);
glStencilFunc(GL_EQUAL, 0x00, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

BindDefaultFrameBuffer();
BindSceneShader();
DrawFullscreenQuad();

Update 2: here's another screenshot showing the submerged solid color test, but with a lower Y threshold (three meters below the water). What appears to be happening, then, is that pixels sampled using the GLSL texture function correctly honor the stencil buffer, while direct color outputs don't. However, even that can't quite be true since, as shown above, sampling pixels outside the stenciled portion of the screen still results in those pixels showing up.

#version 440 core

in vec2 fSource;

out vec4 outColor;

uniform sampler2D image;
uniform sampler2D positions;

void main()
{
    vec3 position = texture(positions, fSource).xyz;

    // The water surfaces happen to sit at Y value 7.85, but here, I'm using a threshold three meters below that.
    if (p.y < 4.85)
    {
        outColor = vec4(1, 1, 0, 1);
    }
    else
    {
        outColor = texture(image, fSource);
    }
}

Submerged pixels rendered using a solid color, but a few meters below the water surface.


\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

I solved the problem. Through other work, I discovered that my stencil buffer wasn't clearing properly due to not calling glStencilMask(0xFF) before glClear() (a stencil mask of 0x00 was still active from the end of the previous frame). The reason this was difficult to track down as a problem source is that glStencilMask's interaction with glClear isn't currently documented (see https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glStencilMask.xhtml), at least not explicitly.

As for why this fixed the issue presented in this post, I'm still not sure, since even if the stencil buffer wasn't cleared, those water surface pixels should have still been rejected via stenciling. I considered deleting the question, but decided to keep it up in case someone else runs into a similar problem.

\$\endgroup\$

You must log in to answer this question.

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