Home > Enterprise >  OpenGL ping pong feedback texture not completely clearing itself. Trail is left behind
OpenGL ping pong feedback texture not completely clearing itself. Trail is left behind

Time:06-08

The goal:

Effectively read and write to the same texture, like how Shadertoy does their buffers.

The setup:

I have a basic feedback system with 2 textures each connected to a framebuffer. As I render to frame buffer 1, I bind Texture 2 for sampling in the shader. Then, as I render to frame buffer 2, I bind texture 1 for sampling, and repeat. Finally, I output texture 1 to the whole screen with the default frame buffer and a sperate shader.

The issue:

This almost works as intended as I'm able to read from the texture in the shader and also output to it, creating the desired feedback loop.

The problem is that the frame buffers do not clear completely to black it seems.

To test, I made a simple trailing effect.

In shadertoy, the trail completely disappears as intended:

Shadertoy result image

My app result image

My thoughts are I'm not clearing the frame buffers correctly or I am not using GLFW's double buffering correctly in this instance. I've tried every combination of clearing the framebuffers but I must be missing something here.

The code:

Here is the trailing effect shader with a moving circle (Same as above images)

#version 330
precision highp float;

uniform sampler2D samplerA; // Texture sampler

uniform float uTime; // current execution time
uniform vec2 uResolution; // resolution of window

void main()
{
    vec2 uv = gl_FragCoord.xy / uResolution.xy; // Coordinates from 0 - 1
    vec3 tex = texture(samplerA, uv).xyz;// Read ping pong texture that we are writing to 

    vec2 pos = .3*vec2(cos(uTime), sin(uTime)); // Circle position (circular motion around screen)
    vec3 c = mix(vec3(1.), vec3(0), step(.0, length(uv - pos)-.07)); // Circle color

    tex = mix(c, tex, .981); // Replace some circle color with the texture color

    gl_FragColor = vec4(tex, 1.0); // Output to texture
}

Frame buffer and texture creation:

// -- Generate frame buffer 1 --
glGenFramebuffers(1, &frameBuffer1);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer1);


// Generate texture 1
glGenTextures(1, &texture1);

// Bind the newly created texture
glBindTexture(GL_TEXTURE_2D, texture1);

// Create an empty image
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1920, 1080, 0, GL_RGBA, GL_FLOAT, 0);

// Nearest filtering, for sampling
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

// Attach output texture to frame buffer
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture1, 0);



// -- Generate frame buffer 2 --
glGenFramebuffers(1, &frameBuffer2);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer2);


// Generate texture 2
glGenTextures(1, &texture2);

// Bind the newly created texture
glBindTexture(GL_TEXTURE_2D, texture2);

// Create an empty image
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1920, 1080, 0, GL_RGBA, GL_FLOAT, 0);

// Nearest filtering, for sampling
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

// Attach texture 2 to frame buffer 2
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2, 0);

Main loop:

while(programIsRunning){

// Draw scene twice, once to frame buffer 1 and once to frame buffer 2 
for (int i = 0; i < 2; i  )
{
    // Start trailing effect shader program
    glUseProgram(program);

    glViewport(0, 0, platform.windowWidth(), platform.windowHeight());


    // Write to frame buffer 1
    if (i == 0)
    {
        // Bind and clear frame buffer 1
        glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer1); 
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Bind texture 2 for sampler
        glActiveTexture(GL_TEXTURE0   0);
        glBindTexture(GL_TEXTURE_2D, texture2); 
        glUniform1i(uniforms.samplerA, 0);
    }
    else // Write to frame buffer 2
    {
        // Bind and clear frame buffer 2
        glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer2);
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Bind texture 1 for sampler
        glActiveTexture(GL_TEXTURE0   0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glUniform1i(uniforms.samplerA, 0);
    }

    // Render to screen
    glDrawArrays(GL_TRIANGLES, 0, 6);
}


// Start screen shader program
glUseProgram(screenProgram);

// Bind default frame buffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glViewport(0, 0, platform.windowWidth(), platform.windowHeight());


// Bind texture 1 for sampler (binding texture 2 should be the same?)
glActiveTexture(GL_TEXTURE0   0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(uniforms.samplerA, 0);



// Draw final rectangle to screen
glDrawArrays(GL_TRIANGLES, 0, 6);

// Swap glfw buffers
glfwSwapBuffers(platform.window());

}

If this is an issue with clearing I would really like to know why. Changing which frame buffer gets cleared doesn't seem to change anything. I will keep experimenting in the meantime. Thank you!

CodePudding user response:

The problem is that you are creating a texture with too little precision for your exponential moving average computations to ultimately discretize to zero.

In your call to:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1920, 1080, 0, GL_RGBA, GL_FLOAT, 0);

you are using the unsized internal format GL_RGBA (third argument), which will very likely ultimately result in the GL_RGBA8 internal format actually being used. So, all channels will have a precision of 8 bits.

You probably believed that using GL_FLOAT as the argument for the type parameter results in a 32-bit floating-point texture being allocated: It does not. The type parameter is used to indicate to OpenGL how it should interpret your data (last parameter of the function) when/if you actually specify data to be uploaded. You use 0/NULL so the type parameter really does not influence the call, as there is no memory to be interpreted as float values to be uploaded.

So, your texture will have a precision of 8 bits per channel and therefore each channel can hold at most 256 different values.

Given that in your shown RGB image the RGB value is 24 for each channel, we can do the math how OpenGL gets to this value and why it won't get any lower than that:

First, let's do another round of your exponential moving average between (0, 0, 0) and (24, 24, 24)/255 with a factor of your 0.981:

d = (24, 24, 24)/255 * 0.981

If we had infinite precision, this value d would be 0.09232941176.

Now, let's see what RGB value within the representable range [0, 255] this comes close to: 0.09232941176 * 255 = 23.5439999988.

So, this value is actually (when correctly rounded to the nearest representable value within the [0, 255] discretization) 24 again. And that's where it stays.

In order to fix this, you likely need to use a higher precision internal texture format, such as GL_RGBA32F (which is actually what ShaderToy itself uses).

  • Related