<p>As it turns out, what was happening is that the number of black pixels was being subtracted from the total number of pixels when calculating the average log luminence per pixel, which meant very dark scenes with a lot of deep black pixels ended up having a miniscule denominator in their average calculation compared to brighter scenes, which made the average much higher despite the numerator, the actual weighted luminosity value, being much smaller. So I just had to remove the <code>- countForThisBin</code> part.</p>

CC BY-SA 4.0
2023-12-21T03:32:35.973Z
2023-12-21T03:32:35.973Z
Alexis Purslane
<p>I've been working on adding HDR rendering, auto exposure, and tone-mapping to my rendering engine, following <a href="https://bruop.github.io/exposure/" rel="nofollow noreferrer">this blog post</a> for doing auto exposure and it sequel as well as a few side resources for tone mapping. I've gotten tone mapping to work, but not auto exposure, although I feel like I have a solid grasp of the
concepts.</p>
<p>First, let's start with my code. Here are the two shaders I'm using to calculate the average luminence:</p>
<pre class="lang-c prettyprint-override"><code>
/////////// CALCULATE HISTOGRAM

#version 430

#define GROUP_SIZE 256

#define EPSILON 0.005
#define RGB_TO_LUM vec3(0.2125, 0.7154, 0.0721)

uniform vec4 params;
// params.x = minimum log_2 luminance
// params.y = inverse of the log_2 luminance range

layout (binding = 0, rgba16f) uniform image2D hdrImage;
layout (binding = 1, std140) buffer Histogram {
    uint histogram[256];
};

shared uint histogramShared[GROUP_SIZE];

uint colorToBin(vec3 hdrColor, float minLogLum, float inverseLogLumRange) {
    float lum = dot(hdrColor, RGB_TO_LUM);

    if (lum &lt; EPSILON) {
        return 0;
    }

    float logLum = clamp((log2(lum) - minLogLum) * inverseLogLumRange, 0.0, 1.0);

    return uint(logLum * 254.0 + 1.0);
}

layout (local_size_x = 16, local_size_y = 16) in;
void main() {
    histogramShared[gl_LocalInvocationIndex] = 0;
    barrier();

    uvec2 dim = imageSize(hdrImage).xy;

    if (gl_GlobalInvocationID.x &lt; dim.x &amp;&amp; gl_GlobalInvocationID.y &lt; dim.y) {
        vec3 hdrColor = imageLoad(hdrImage, ivec2(gl_GlobalInvocationID.xy)).xyz;
        uint binIndex = colorToBin(hdrColor, params.x, params.y);

        atomicAdd(histogramShared[binIndex], 1);
    }

    barrier();

    atomicAdd(histogram[gl_LocalInvocationIndex], histogramShared[gl_LocalInvocationIndex]);
}

/////////// CALCULATE WEIGHTED AVERAGE

#version 430

#define GROUP_SIZE 256

uniform vec4 params;
// params.x = minLogLum
// params.y = logLumRange
// param.z = timeCoeff
// param.w = numPixels

layout (binding = 0, r16f) uniform image1D target;
layout (binding = 1, std140) buffer Histogram {
    uint histogram[256];
};

shared uint histogramShared[GROUP_SIZE];

layout (local_size_x = 256, local_size_y = 1) in;
void main() {
    uint countForThisBin = histogram[gl_LocalInvocationIndex];
    histogramShared[gl_LocalInvocationIndex] = countForThisBin * gl_LocalInvocationIndex;

    barrier();

    histogram[gl_LocalInvocationIndex] = 0;

    #pragma unroll
    for (uint cutoff = (GROUP_SIZE &gt;&gt; 1); cutoff &gt; 0; cutoff &gt;&gt;= 1) {
        if (uint(gl_LocalInvocationIndex) &lt; cutoff)
            histogramShared[gl_LocalInvocationIndex] += histogramShared[gl_LocalInvocationIndex + cutoff];
        barrier();
    }

    if (gl_LocalInvocationIndex == 0) {
        float weightedLogAverage = (histogramShared[0] / max(params.w - float(countForThisBin), 1.0)) - 1.0;
        float weightedAvgLum = exp2(((weightedLogAverage / 254.0) * params.y) + params.x);
        float lumLastFrame = imageLoad(target, 0).x;
        float adaptedLum = lumLastFrame + (weightedAvgLum - lumLastFrame) * params.z;
        imageStore(target, 0, vec4(adaptedLum, 0.0, 0.0, 0.0));
    }
}

</code></pre>
<p>As far as I can tell, these are functionally identical to the code samples from the blog, with the BGFX-specific stuff replaced with pure GLSL/OpenGL stuff.</p>
<p>Next, here's the relevant part of my final fragment shader where I'm applying the auto exposure:</p>
<pre class="lang-c prettyprint-override"><code>layout (binding = 0, r16f) uniform readonly image1D lumAvg;
layout (binding = 1, rgba16f) uniform readonly image2D hdrImage;

// ...

void main() {
    vec4 C_in = imageLoad(hdrImage, ivec2(gl_FragCoord.xy));
    // I'm having to multiply the average luminence produced by the above shaders by *one hundred times* in order to get anything remotely like a difference between the different values that are being produced!
    float L_avg = imageLoad(lumAvg, 0).r * 100.0;
    // Get luminance of this pixel
    float L_in = dot(C_in.rgb, RGB_TO_LUM);
    // Get the new target luminence, determined by a relationship with the average luminence of the scene it's in. This is the core of the auto exposure, and I just copied it from the blog post.
    float L_prime = L_in / (9.6 * L_avg);

    // Make the input color have the desired luminence by multiplying each channel by the ratio between the desired luminence and the current luminence of the channel.
    C_in.rgb = C_in.rgb * vec3(L_prime / L_in);

    // apply tone mapping (irrelevant here)
    C_in.rgb = lottes(C_in.rgb);

    // Gamma correction
    C_in.rgb = pow(C_in.rgb, vec3(1.0 / 2.22));

    FragColor = C_in;
}
</code></pre>
<p>Essentially, what's happening is that the calculated average luminence appears to <em>decrease</em> with the overall brightness of the scene instead of increasing. This in turn is causing my auto exposure calculations to be inverted, so that the darker a scene gets the lower the exposure gets, and the brighter it gets the higher the exposure, which is undesired behavior for me. Additionally, the luminence average seems to be far too small (and vary by very little). I'm getting values of <code>0.0038</code> for a very bright area (14+ luminence), and <code>0.005</code> for pitch blackness, and that's the extent of it.  I've double checked all the equations, but I can't seem to find out where I went wrong. Is this supposed to be correct behavior?</p>

CC BY-SA 4.0
2023-12-21T02:41:55.203Z
After pulling the luminence histograms out of the GPU's memory, dumping them in a file, and doing some analysis on them with python, if I understand correctly how the histogram works --- that one point is one pixel --- what appears to be happening is that the *total number of pixels* listed in the histogram on bright frames is somehow *smaller* than on dark frames. Please help I think I'm in the Twilight Zone
Alexis Purslane
<opengl><shader><color><compute-shader>
Average luminence and auto exposure algorithm not seeming to work properly
Alexis Purslane
# Average luminence and auto exposure algorithm not seeming to work properly

(Asked by: username_0 on 2023-12-20)

I've been working on adding HDR rendering, auto exposure, and tone-mapping to my rendering engine, following [this blog post](https://bruop.github.io/exposure/) for doing auto exposure and it sequel as well as a few side resources for tone mapping. I've gotten tone mapping to work, but not auto exposure, although I feel like I have a solid grasp of the
concepts.

First, let's start with my code. Here are the two shaders I'm using to calculate the average luminence:

```c

/////////// CALCULATE HISTOGRAM

#version 430

#define GROUP_SIZE 256

#define EPSILON 0.005
#define RGB_TO_LUM vec3(0.2125, 0.7154, 0.0721)

uniform vec4 params;
// params.x = minimum log_2 luminance
// params.y = inverse of the log_2 luminance range

layout (binding = 0, rgba16f) uniform image2D hdrImage;
layout (binding = 1, std140) buffer Histogram {
    uint histogram[256];
};

shared uint histogramShared[GROUP_SIZE];

uint colorToBin(vec3 hdrColor, float minLogLum, float inverseLogLumRange) {
    float lum = dot(hdrColor, RGB_TO_LUM);

    if (lum < EPSILON) {
        return 0;
    }

    float logLum = clamp((log2(lum) - minLogLum) * inverseLogLumRange, 0.0, 1.0);

    return uint(logLum * 254.0 + 1.0);
}

layout (local_size_x = 16, local_size_y = 16) in;
void main() {
    histogramShared[gl_LocalInvocationIndex] = 0;
    barrier();

    uvec2 dim = imageSize(hdrImage).xy;

    if (gl_GlobalInvocationID.x < dim.x && gl_GlobalInvocationID.y < dim.y) {
        vec3 hdrColor = imageLoad(hdrImage, ivec2(gl_GlobalInvocationID.xy)).xyz;
        uint binIndex = colorToBin(hdrColor, params.x, params.y);

        atomicAdd(histogramShared[binIndex], 1);
    }

    barrier();

    atomicAdd(histogram[gl_LocalInvocationIndex], histogramShared[gl_LocalInvocationIndex]);
}

/////////// CALCULATE WEIGHTED AVERAGE

#version 430

#define GROUP_SIZE 256

uniform vec4 params;
// params.x = minLogLum
// params.y = logLumRange
// param.z = timeCoeff
// param.w = numPixels

layout (binding = 0, r16f) uniform image1D target;
layout (binding = 1, std140) buffer Histogram {
    uint histogram[256];
};

shared uint histogramShared[GROUP_SIZE];

layout (local_size_x = 256, local_size_y = 1) in;
void main() {
    uint countForThisBin = histogram[gl_LocalInvocationIndex];
    histogramShared[gl_LocalInvocationIndex] = countForThisBin * gl_LocalInvocationIndex;

    barrier();

    histogram[gl_LocalInvocationIndex] = 0;

    #pragma unroll
    for (uint cutoff = (GROUP_SIZE >> 1); cutoff > 0; cutoff >>= 1) {
        if (uint(gl_LocalInvocationIndex) < cutoff)
            histogramShared[gl_LocalInvocationIndex] += histogramShared[gl_LocalInvocationIndex + cutoff];
        barrier();
    }

    if (gl_LocalInvocationIndex == 0) {
        float weightedLogAverage = (histogramShared[0] / max(params.w - float(countForThisBin), 1.0)) - 1.0;
        float weightedAvgLum = exp2(((weightedLogAverage / 254.0) * params.y) + params.x);
        float lumLastFrame = imageLoad(target, 0).x;
        float adaptedLum = lumLastFrame + (weightedAvgLum - lumLastFrame) * params.z;
        imageStore(target, 0, vec4(adaptedLum, 0.0, 0.0, 0.0));
    }
}

```

As far as I can tell, these are functionally identical to the code samples from the blog, with the BGFX-specific stuff replaced with pure GLSL/OpenGL stuff.

Next, here's the relevant part of my final fragment shader where I'm applying the auto exposure:

```c
layout (binding = 0, r16f) uniform readonly image1D lumAvg;
layout (binding = 1, rgba16f) uniform readonly image2D hdrImage;

// ...

void main() {
    vec4 C_in = imageLoad(hdrImage, ivec2(gl_FragCoord.xy));
    // I'm having to multiply the average luminence produced by the above shaders by *one hundred times* in order to get anything remotely like a difference between the different values that are being produced!
    float L_avg = imageLoad(lumAvg, 0).r * 100.0;
    // Get luminance of this pixel
    float L_in = dot(C_in.rgb, RGB_TO_LUM);
    // Get the new target luminence, determined by a relationship with the average luminence of the scene it's in. This is the core of the auto exposure, and I just copied it from the blog post.
    float L_prime = L_in / (9.6 * L_avg);

    // Make the input color have the desired luminence by multiplying each channel by the ratio between the desired luminence and the current luminence of the channel.
    C_in.rgb = C_in.rgb * vec3(L_prime / L_in);

    // apply tone mapping (irrelevant here)
    C_in.rgb = lottes(C_in.rgb);

    // Gamma correction
    C_in.rgb = pow(C_in.rgb, vec3(1.0 / 2.22));

    FragColor = C_in;
}

```

Essentially, what's happening is that the calculated average luminence appears to *decrease* with the overall brightness of the scene instead of increasing. This in turn is causing my auto exposure calculations to be inverted, so that the darker a scene gets the lower the exposure gets, and the brighter it gets the higher the exposure, which is undesired behavior for me. Additionally, the luminence average seems to be far too small (and vary by very little). I'm getting values of `0.0038` for a very bright area (14+ luminence), and `0.005` for pitch blackness, and that's the extent of it. I've double checked all the equations, but I can't seem to find out where I went wrong. Is this supposed to be correct behavior?

**Tags:** opengl, shader, color, compute-shader

> **Comments:**
> **username_0**: After pulling the luminence histograms out of the GPU's memory, dumping them in a file, and doing some analysis on them with python, if I understand correctly how the histogram works --- that one point is one pixel --- what appears to be happening is that the *total number of pixels* listed in the histogram on bright frames is somehow *smaller* than on dark frames. Please help I think I'm in the Twilight Zone  

---

## 1 answers

### Answer by username_0 on 2023-12-21. Score: 0 | Accepted answer

As it turns out, what was happening is that the number of black pixels was being subtracted from the total number of pixels when calculating the average log luminence per pixel, which meant very dark scenes with a lot of deep black pixels ended up having a miniscule denominator in their average calculation compared to brighter scenes, which made the average much higher despite the numerator, the actual weighted luminosity value, being much smaller. So I just had to remove the `- countForThisBin` part.


---

