Custom mipmap generation shader

Hi, I am trying to compute average and max luminance value of a color buffer, by compute manually mipmap levels down to the 1x1 level.
I have followed the usage example 5 in the FrameBufferObject specification, but what I am missing is how the shader should look like. Should I implement a bilinear filter (along side a max filter)? Do i have to use textureLod to access the previous level when rendering?

[QUOTE=Alakanu;1282510]Hi, I am trying to compute average and max luminance value of a color buffer, by compute manually mipmap levels down to the 1x1 level.
I have followed the usage example 5 in the FrameBufferObject specification, but what I am missing is how the shader should look like. Should I implement a bilinear filter (along side a max filter)? Do i have to use textureLod to access the previous level when rendering?[/QUOTE]
If you don’t actually need to generate the mipmap levels for a single texture, you may be better off using two textures and ping-pong’ing (i.e. the first pass reads from A and writes to B, the second pass reads from B and writes to A, and so on). In this case, you’d only use the base level of each texture.

If you use a single texture, you need to explicitly clamp the maximum mipmap level so the shader cannot read from the level being written to (note that “does not read” isn’t the same as “cannot read”). You also need to use glTextureBarrier() to ensure that texture writes from one pass are visible to subsequent passes. Otherwise, the behaviour is undefined.

To read the texels, you can either use textureLod() or texelFetch(). For computing a simple average, you’d probably want to use the former with GL_LINEAR (or GL_LINEAR_MIPMAP_NEAREST, if using mipmap levels), as you can get the averaging for free. Or you could just use glGenerateMipmap(). If you want to read individual pixels (e.g. if you wanted to perform gamma correction before averaging), texelFetch() is probably simpler.

If you use a single texture, you need to explicitly clamp the maximum mipmap level so the shader cannot read from the level being written to (note that “does not read” isn’t the same as “cannot read”). You also need to use glTextureBarrier() to ensure that texture writes from one pass are visible to subsequent passes. Otherwise, the behaviour is undefined.

I am using the example code, I guess forcing the texture max level e base level to level - 1 does that? Also I don’t see glTextureBarrier being used. Is it mandatory?

glGenFramebuffers(1, &fb);
        glGenTextures(1, &color_tex);
        glGenRenderbuffers(1, &depth_rb);

        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb);

        // initialize color texture and establish mipmap chain
        glBindTexture(GL_TEXTURE_2D, color_tex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 512, 512, 0,
                     GL_RGB, GL_INT, NULL);
        glGenerateMipmap(GL_TEXTURE_2D);
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
                                  GL_COLOR_ATTACHMENT0,
                                  GL_TEXTURE_2D, color_tex, 0);

        // initialize depth renderbuffer
        glBindRenderbuffer(GL_RENDERBUFFER, depth_rb);
        glRenderbufferStorage(GL_RENDERBUFFER,
                                 GL_DEPTH_COMPONENT24, 512, 512);
        glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER,
                                     GL_DEPTH_ATTACHMENT,
                                     GL_RENDERBUFFER, depth_rb);

        // Check framebuffer completeness at the end of initialization.
        CHECK_FRAMEBUFFER_STATUS();

        loop {
            glBindTexture(GL_TEXTURE_2D, 0);

            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb);
            glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
                                      GL_COLOR_ATTACHMENT0,
                                      GL_TEXTURE_2D, color_tex, 0);
            glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER,
                                         GL_DEPTH_ATTACHMENT,
                                         GL_RENDERBUFFER, depth_rb);

            <draw to the base level of the color texture>

            // custom-generate successive mipmap levels
            glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER,
                                         GL_DEPTH_ATTACHMENT,
                                         GL_RENDERBUFFER, 0);
            glBindTexture(GL_TEXTURE_2D, color_tex);
            foreach (level > 0, in order of increasing values of level) {
                glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
                                          GL_COLOR_ATTACHMENT0,
                                          GL_TEXTURE_2D, color_tex, level);
                glTexParameteri(TEXTURE_2D, TEXTURE_BASE_LEVEL, level-1);
                glTexParameteri(TEXTURE_2D, TEXTURE_MAX_LEVEL, level-1);

                <draw to level>
            }
            glTexParameteri(TEXTURE_2D, TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(TEXTURE_2D, TEXTURE_MAX_LEVEL, max);

            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
            <draw to the window, reading from the color texture>
        }

To read the texels, you can either use textureLod() or texelFetch(). For computing a simple average, you’d probably want to use the former with GL_LINEAR (or GL_LINEAR_MIPMAP_NEAREST, if using mipmap levels), as you can get the averaging for free. Or you could just use glGenerateMipmap(). If you want to read individual pixels (e.g. if you wanted to perform gamma correction before averaging), texelFetch() is probably simpler.

Let’s say I want to do something like box filter + max filter resulting in something like this:


a=avg(topleft,topright)
b = avg(bottomleft,bottomright)
result = avg(a,b)
result max = max(topleft,topright....)

From the past code, i know how to render to the right level, but how do i fetch topleft,topright…ecc? Do I have to pass a uniform “level” that is basically level i’m writing on minus 1 and use textureLod? Isn’t texture(mysampler,uv, level-1) the same of textureLod? From the specification I cannot understand the difference between Lod and mipmap bias.
P.s. I know that I should also pass the level-1 width and height so that I can sample the correct texel (e.g. topright = (topleft.s + 1/width, topleft.t) )

[QUOTE=Alakanu;1282518]I am using the example code, I guess forcing the texture max level e base level to level - 1 does that?
[/QUOTE]
Setting the maximum level is sufficient to ensure that the shader cannot read from the level being written.

§9.3.1 (Rendering Feedback Loops) says

If a texel has been written, then in order to safely read the result a texel fetch must be in a subsequent draw call separated by the command

void TextureBarrier( void );

TextureBarrier will guarantee that writes have completed and caches have been invalidated before subsequent draw calls are executed.

Using a minification filter which doesn’t use mipmaps (i.e. GL_NEAREST or GL_LINEAR) will cause all reads to use the specified base level. If you’re using mipmaps, then you need to use textureLod() or textureFetch() and specify the mipmap level explicitly.

The last parameter to textureLod() is the actual LoD value which is used, while the (optional) third parameter to texture() is a bias (offset) which is added to the LoD value computed from the implicit derivatives. texture() is roughly equivalent to


vec4 texture(sampler2D sampler, vec2 uv, float bias)
{
    float lod = log2(fwidth(uv));
    return textureLod(sampler, uv, lod+bias);
}

Thanks for being so exhaustive and patient. I hope I’ll be able to do the same for you one time or another.

Sorry if I still bother you, but I’ve encountered some difficulties and I thought that maybe I didn’t get you right.
My algorithm at the moment is:

  1. Render in HDR to a fbo.
  2. Render to another fbo converting all in log lum (cause that’s what I need)
  3. Set width and height of another fbo’s colorBuffer to previous fbo colorBuffer size halved by 2. (+1 if not power of two dimension, right?)
  4. Render to that fbo storing avg min and max lum in a chosen manner.
  5. Go to 3 if width and height are not 1x1.

This is what you intended with ping-ponging, am I right? Should I also set Viewport to fbo size at each iteration?

[QUOTE=Alakanu;1282548]
This is what you intended with ping-ponging, am I right? Should I also set Viewport to fbo size at each iteration?[/QUOTE]
With ping-pong, you use two textures. On each pass, one is bound to a texture unit and accessed via a sampler uniform, the other is bound to a FBO. After each pass, the two are swapped, so that the output from one pass becomes the input to the next.

If you’re reducing in size each pass, you can just reduce the viewport size rather than using smaller textures. One texture need only be half the base size (in each dimension), but if you already have a spare full-size texture (e.g. for other processing which doesn’t change the size) you can just use that.

If the base size isn’t a power of two, you need to round up when halving the size, otherwise you’ll miss pixels. One thing to consider in that case is that simply resampling with GL_LINEAR filtering won’t attribute equal weights to all pixels. E.g. resampling 3x1 to 2x1 will use weights of 3:1:0 and 0:1:3 for a total of 3:2:3. For an accurate average, you need to pad the source to an even size with a row or column of zeros and compute the sum, then divide the final result by the size. E.g. for 3x1->2x1 the weights would be 1:1:0 and 0:0:1 for a total of 1:1:1.