How to write pass-through vertex and fragment shaders for a framebuffer with attached texture?

12,881

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.

Share:
12,881

Related videos on Youtube

Adam McKee
Author by

Adam McKee

Updated on October 15, 2022

Comments

  • Adam McKee
    Adam McKee over 1 year

    I'm attempting to use shaders to modify a texture that is bound to a framebuffer, but I'm confused as to how the shaders get the "original" input values.

    I'm doing the following:

    GLuint textureId = 0;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);
    glTexImage2D(GL_TEXTURE_2D, ...);
    
    GLuint framebufferId = 0;
    glGenFramebuffers(1, &framebufferId);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBufferId);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
    
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) { ... }
    
    glUseProgram(programId);
    const GLenum buffer = GL_COLOR_ATTACHMENT0;
    glDrawBuffers(1, &buffer);
    

    What would empty vertex and fragment shaders look like? As I'm not drawing primatives, how do I set gl_Position in the vertex shader? What about passing through the input colour as the output colour of the fragment shader?

    Empty vertex shader:

    #version 330
    
    void main()
    {
        gl_Position = ??;
    }
    

    Empty fragment shader:

    #version 330
    
    layout(location = 0) out vec4 out_colour;
    
    void main()
    {
        out_colour = ???;
    }
    
    • Christian Rau
      Christian Rau
      "As I'm not drawing primatives" - You have to draw a primitive, otherwise, well, nothing will be drawn. What is it what you are actually trying to do? To achieve some output (e.g. as result from "modifying a texture") there has to be drawn something. Likewise your shader code doesn't match your question's tags. Don't spam unrelated tags if the question doesn't even work for them.
  • Bartek Banachewicz
    Bartek Banachewicz almost 11 years
    I wanted to answer, but I can't add nothing. +1 and "this is the right answer" :)
  • Adam McKee
    Adam McKee almost 11 years
    So do you recommend using a Framebuffer + RenderBuffer for reading pixels back later? What do you do if you want to have multiple passes? i.e. multiple shaders where, shader1 output = shader2 input, shader2 output = shader3 input, etc.
  • Christian Rau
    Christian Rau almost 11 years
    @MarkIngram If you need the data on the GPU for further processing, then of course write it into a texture, since you cannot do much with a renderbuffer other than reading and copying its data. If your need it on the CPU, either use a renderbuffer with glReadPixels or a texture with glGetTexImage, shouldn't make a difference. In the end I think nowadays (now that textures store depth and stencil data and even multisampled data) there isn't really a need for renderbuffers at all and you can just use textures as render targets all the time.
  • Christian Rau
    Christian Rau almost 11 years
    @MarkIngram As to multiple passes, just pass the rendered texture into the next shader as input texture, again rendering into a different texture. In the second pass you can even reuse the old input texture (from pass 1) as new output texture. This is commonly called the pinp-pong technique and can be done by either changing the FBOs attachment between passes, or letting the textures attached to an own FBO each and binding a different FBO each pass. However done in the end you only need two textures for the whole workflow, a current input and a current output and just switch their use-cases.
  • Adam McKee
    Adam McKee almost 11 years
    @ChristianRau, I assume for multi-pass I would need to use Framebuffer + texture, as you can't get a texture from a RenderBuffer? I read that RenderBuffer would be quicker to render to because it uses native pixel format?
  • Christian Rau
    Christian Rau almost 11 years
    @MarkIngram "I assume for multi-pass I would need to use Framebuffer + texture, as you can't get a texture from a RenderBuffer?" - Exactly! "I read that RenderBuffer would be quicker to render to because it uses native pixel format?" - No, I don't know in which context you read that, but as it stands this is rubbish, since you yourself specify the rendebruffer's format, like for a texture. Maybe it was refering to the default framebuffer in contrast to an FBO, but I would still doubt if that statement was true in that case.
  • Adam McKee
    Adam McKee almost 11 years
  • HuaTham
    HuaTham over 9 years
    @ChristianRau AFAIK, if we are concerned with OpenGL ES 3.0 on iOS, we have GL_EXT_SHADER_FRAMEBUFFER_FETCH extension where one can actually read and modify framebuffer "in-place", but I do not see an equivalent extension available on desktop platform like Mac.
  • PeterT
    PeterT almost 9 years
    @ChristianRau I'm pretty sure that it suits me right for being lazy and copy&pasting but could you maybe make out vec4 out_colour; and out_color = use a uniform spelling of "colour"? Just saying because this link is the first result for "pass through shader" on google for me.