High Dynamic Range

In order to answer this question, we must first determine why flashlights appear brighter at night than in the daytime. Much of the answer has to do with our eyes.

The pupil is the hole in our eyes that allows light to pass through it; cameras call this hole the aperture. The hole is small, relative to the world, which helps with resolving an image. However, the quantity of light that passes through the hole depends on how large it is. Our iris's can expand and contract to control the size of the pupil. When the pupil is large, more light is allowed to enter the eye; when the pupil is small, less light can enter.

The iris contracts automatically in the presence of bright light, since too much light can damage the retina (the surface of the eye that detects light). However, in the absence of light, the iris slowly relaxes, expanding the pupil. This has the effect of allowing more light to enter the eye, which adds to the apparent brightness of the scene.

So what we need is not to change the overall light levels, but instead apply the equivalent of an iris to the final lighting computations of a scene. That is, we determine the overall illumination at a point, but we then filter out some of this light based on a global setting. In dark scenes, we filter less light, and in bright scenes, we filter more light.

This overall process is called high dynamic range lighting (HDR). It is fairly simple and requires very few additional math computations compared to our current model.

Note

You may have heard this term in conjunction with pictures where bright objects seem to glow. While HDR is typically associated with that glowing effect, that is a different effect called bloom. It is a woefully over-used effect, and we will discuss how to implement it later. HDR and bloom do interact, but you can use one without the other.

The first step is to end the myth that lights themselves have a global maximum brightness. In all previous examples, our light intensity values lived with the range [0, 1]. Clamping light intensity to this range simply does not mirror reality. In reality, the sun is many orders of magnitude brighter than a flashlight. We must allow for this in our lighting equation.

This also means that our accumulated lighting intensity, the value we originally wrote to the fragment shader output, is no longer on the [0, 1] range. And that poses a problem. We can perform lighting computations with a high dynamic range, but monitors can only display colors on the [0, 1] range. We therefore must map from the HDR to the low dynamic range (LDR).

This part of HDR rendering is called tone mapping. There are many possible tone mapping functions, but we will use one that simulates a flexible aperture. It's quite a simple function, really. First, we pick a maximum intensity value for the scene; intensity values above this will be clamped. Then, we just divide the HDR value by the maximum intensity to get the LDR value.

It is the maximum intensity that we will vary. As the sun goes down, the intensity will go down with it. This will allow the sun to be much brighter in the day than the lights, thus overwhelming their contributions to the scene's illumination. But at night, when the maximum intensity is much lower, the other lights will have an apparently higher brightness.

This is implemented in the HDR Lighting tutorial.

This tutorial controls as the previous one, except that the K key will activate HDR lighting. Pressing L or Shift+L will go back to day or night-time LDR lighting from the last tutorial, respectively.

Figure 12.4. HDR Lighting

HDR Lighting

The code is quite straightforward. We add a floating-point field to the Light uniform block and the LightBlock struct in C++. Technically, we just steal one of the padding floats, so the size remains the same:

Example 12.6. HDR LightBlock

struct LightBlockHDR
{
    glm::vec4 ambientIntensity;
    float lightAttenuation;
    float maxIntensity;
    float padding[2];
    PerLight lights[NUMBER_OF_LIGHTS];
};

We also add a new field to SunlightValue: the maximum light intensity. There is also a new function in the LightManager that computes the HDR-version of the light block: GetLightInformationHDR. Technically, all of this code was already in Light.h/cpp, since these files are shared among all of the tutorials here.

Scene Lighting in HDR

Lighting a scene in HDR is a different process from LDR. Having a varying maximum intensity value, as well as the ability to use light intensities greater than 1.0 change much about how you set up a scene.

In this case, everything in the lighting was designed to match up to the daytime version of LDR in the day, and the nighttime version of LDR at night. Once the division by the maximum intensity was taken into account.

Table 12.1. Scene Lighting Values

 

HDR

LDR Day-optimized

LDR Night-optimized

Noon Sun Intensity1.81.81.8(3.0)0.60.60.60.60.60.6
Noon Ambient Intensity0.60.60.6 0.20.20.20.20.20.2
Evening Sun Intensity0.450.150.15(1.5)0.30.10.10.30.10.1
Evening Ambient Intensity0.2250.0750.075 0.150.050.050.150.050.05
Circular Light Intensity0.60.60.6 0.20.20.20.60.60.6
Red Light Intensity0.70.00.0 0.30.00.00.70.00.0
Blue Light Intensity0.00.00.7 0.00.00.30.00.00.7

The numbers in parenthesis represents the max intensity at that time.

In order to keep the daytime lighting the same, we simply multiplied the LDR day's sun and ambient intensities by the ratio between the sun intensity and the intensity of one of the lights. This ratio is 3:1, so the sun and ambient intensity is increased by a magnitude of 3.

The maximum intensity was derived similarly. In the LDR case, the difference between the max intensity (1.0) and the sum of the sun and ambient intensities is 0.2 (1.0 - (0.6 + 0.2)). To maintain this, we set the max intensity to the sum of the ambient and sun intensities, plus 3 times the original ratio.

This effectively means that the light, as far as the sun and ambient are concerned, works the same way in HDR daytime as in the LDR day-optimized settings. To get the other lights to work at night, we simply kept their values the same as the LDR night-optimized case.

Fork me on GitHub