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:
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).