Tone mapping bright images
There are multiple different ways to do tone-mapping, including both local and global methods. I won't discuss local methods (i.e. methods based on using surrounding information as part of the tone mapping process) here. Global methods can be broken down into two components:


A formula for mapping from $[0,\infty)$ to $[0,1]$.
A method of applying this to the color.


The simplest formula out there is just $out = in / (in + 1)$, but the results are also fairly boring. A fancier curve is the filmic tone mapping function developed by John Hable, which is popular in the video gaming industry:

float hable(float x)
{
    float A = 0.15;
    float B = 0.50;
    float C = 0.10;
    float D = 0.20;
    float E = 0.02;
    float F = 0.30;

    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}


You can also get even fancier if you can constrain the input domain to a known, fixed peak value.

As for how to apply this function to an actual color value, there are again multiple techniques. A very simple way is to do it per-channel, i.e. color.r = map(color.r); color.g = map(color.g); color.b = map(color.b);. But this has the downside of warping the appearance of bright colors towards white (e.g. orange becomes yellowish under tone-mapping). Some in the movie industry tend to like this look, for whatever reason.

To do color-preserving tone mapping, you can perform the calculation on a single value only and then apply the same scale to all three channels. A good choice for the value to use is to choose the “brightest” of the three components:

float sig = max(color.r, max(color.g, color.b));
color.rgb *= vec3(map(sig) / sig);


However, this approach also has downsides, namely that it takes very bright objects to unnaturally saturated versions of
themselves. For example, a bright sun might suddenly appear various shades of yellow and orange - which also looks unnatural on photographic content.

So ultimately, what I do is first desaturate the color based on how bright it is, and then apply tone mapping after the fact, leading to an algorithm that looks something like this:

// Calculate the desaturation coefficient based on the brightness
float sig = max(color.r, max(color.g, color.b));
float luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
float coeff = max(sig - 0.18, 1e-6) / max(sig, 1e-6);
coeff = pow(coeff, 20.0);

// Update the original color and signal
color.rgb = mix(color.rgb, vec3(luma), coeff);
sig = mix(sig, luma, coeff);

// Perform tone-mapping
color.rgb *= vec3(map(sig) / sig);


But YMMV.

References:


https://github.com/haasn/libplacebo/blob/master/src/shaders/colorspace.c#L664
You can also poke at ITU-R Report BT.2390-2 for some more ideas and guidelines.