I was under the impression that you could render to an offscreen
framebuffer, with an attached texture, then use shaders to modify the
texture, then use glReadPixels to get the modified data back. This is
what I'm trying to do.
Ah ok, so you want to feed a texture through a fragment shader to gain a new texture. First of all you have to keep in mind, that you cannot just modify a texture in-place, since you cannot read from the texture you're currently rendering to. You have to feed in the to be modified texture into the fragment shader as an ordinary texture and put out the result into the framebuffer as usual, which could be an FBO with a different texture attached, a renderbuffer (if you want to read it back to the CPU, anyway), or the default framebuffer. You don't need an FBO if you just want to transform one image into another one, only if you want the result to be written into an offscreen buffer or a texture.
Furthermore you still have to draw something in order for the rasterizer to generate actual fragments to invoke the fragment shader for. The usual way to do this is to just draw a screen-sized quad parallel to the viewing plane, in order to fill the complete viewport with fragments:
//initialization code
glGenVertexArrays(1, &quad_vao);
glBindVertexArray(quad_vao);
const GLfloat vertices[] = {
-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f };
glGenBuffers(1, &quad_vbo);
glBindBuffer(GL_ARRAY_BUFFER, quad_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
glDeleteBuffers(1, &quad_vbo);
...
//render code
glBindVertexArray(quad_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
As vertex shader a simple pass-thru shader is enough since the vertex positions already are in clip space:
#version 330
layout(location = 0) in vec4 in_position;
void main()
{
gl_Position = in_position;
}
In the fragment shader we take the texture as input. The texture coordinate is already given by the fragment's position on the screen, we just need to normalize it by dividing through the texture size (or maybe use a GL_TEXTURE_RECTANGLE
and a corresponsing samplerRect
to use the fragment coordinate directly):
#version 330
uniform sampler2D tex;
uniform vec2 tex_size;
layout(location = 0) out vec4 out_color;
void main()
{
vec4 in_color = texture(tex, gl_FragCoord.xy / tex_size);
out_color = //do whatever you want with in_color;
}
That's all, the modified texture is written to the framebuffer, no matter where that redirects or what you do with the framebuffer data afterwards.
EDIT: With OpenGL 4.3 and its compute shaders there is now a more direct way for such rather non-rasterization pure GPGPU tasks like image processing. You can just invoke a compute shader (which is more similar to other GPU computing frameworks, like CUDA or OpenCL, than the other OpenGL shaders) on a regular 2D domain and process a texture (using OpenGL 4.2's image load/store functionality) directly in-place. In this case all you need is the corresponding compute shader:
#version 430
layout(local_size_x=32,local_size_y=8) in; //or whatever fits hardware and shader
layout(binding = 0, rgba) uniform image2D img; //adjust format to the actual data
void main()
{
const uint2 idx = gl_GlobalInvocationID.xy;
vec4 color = imageLoad(img, idx);
//do whatever you want with color
imageStore(img, idx, color);
}
Then all you need to do is bind the texture to the corresponding image unit (0, as set in the shader) and invoke a compute shader over the 2-dimensional image domain:
//again use the format that fits the texture data
glBindImageTexture(0, textureId, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
glUseProgram(compute_program); //a program with a single GL_COMPUTE_SHADER
glDispatchCompute(texture_width, texture_height, 1);
And that's all, you don't need an FBO, you don't need any other shaders, you don't need to draw anything, just raw computation. But it has to be evaluated if this more direct approach also results in better performance. And likewise might you need to pay some attention to proper memory synchronization of the to be modified texture, especially when trying to read from it afterwards. But consult deeper materials on image load/store for further information.