So I played a bit with HDR this morning, and found a way to vastly improve the quality of Int16 HDR, which has every bit of the quality of FP16, even when using extreme ranges. I had to use maximum light values of 65536 and high exposure to be able to see the first signs of banding.
So basically, there are three ways to use I16 for HDR. The first one is to simply store scaled down values and scale it up in the shader. For instance set maximum value to 32, and get 5 bits of overbrightness and 3 extra bits of precision in the low range compared to RGBA8. This comes at one extra instruction in the shader to decode, so it's cheap.
The second method is to use alpha to store a range, which gives additional precision. To encode it you first find the largest value of red, green and blue, then normalize rgb so the largest value is 1.0, then store maxChannel / maxValue in alpha, where maxValue is the maximum value for the entire texture. Decoding this in the shader is done as rgb * a * maxValue, which comes at a cost of two instructions, so it's cheap too.
The second method has good quality in pretty much all normal lighting conditions. The weakness I have found with this method is that the quality can become poor in dark areas under high exposure when the maximum value for the texture is large. After playing with this a bit this morning I came up with an improvement to the encoding that fixes this and vastly improves quality by distributing the bits better across the components. The problem with small values in a texture with large maximum values is that the value in alpha gets very small, meaning you're probably only using a couple of bits in the dark regions, which ruins it even with high precision in rgb. I solved this by checking if alpha would end up smaller than the minimum rgb value. If so, I scale rgb down and alpha up so that the minimum value and alpha is equal, which essentially shifts over some bits from rgb to alpha, which gives a much better precision after the decoding. This is only a change to the encoding phase, the decoding is still the same as previously, so it's still cheap at two instructions.
Ok, so here's how it performs. I used exposure = 16 for all images. To give a sense of how much exposure that is, here's how it looks with exposure = 1.
Reference FP16 image:
Using a maximum value of 512, this is what I got with the three methods:
Method 1, max = 64
Method 2, max = 64
Method 3, max = 64
Method 1, max = 512
Method 2, max = 512
Method 3, max = 512
Method 1, max = 4096
Method 2, max = 4096
Method 3, max = 4096
Method 1, max = 65536
Method 2, max = 65536
Method 3, max = 65536
All full size png images
So basically, there are three ways to use I16 for HDR. The first one is to simply store scaled down values and scale it up in the shader. For instance set maximum value to 32, and get 5 bits of overbrightness and 3 extra bits of precision in the low range compared to RGBA8. This comes at one extra instruction in the shader to decode, so it's cheap.
The second method is to use alpha to store a range, which gives additional precision. To encode it you first find the largest value of red, green and blue, then normalize rgb so the largest value is 1.0, then store maxChannel / maxValue in alpha, where maxValue is the maximum value for the entire texture. Decoding this in the shader is done as rgb * a * maxValue, which comes at a cost of two instructions, so it's cheap too.
The second method has good quality in pretty much all normal lighting conditions. The weakness I have found with this method is that the quality can become poor in dark areas under high exposure when the maximum value for the texture is large. After playing with this a bit this morning I came up with an improvement to the encoding that fixes this and vastly improves quality by distributing the bits better across the components. The problem with small values in a texture with large maximum values is that the value in alpha gets very small, meaning you're probably only using a couple of bits in the dark regions, which ruins it even with high precision in rgb. I solved this by checking if alpha would end up smaller than the minimum rgb value. If so, I scale rgb down and alpha up so that the minimum value and alpha is equal, which essentially shifts over some bits from rgb to alpha, which gives a much better precision after the decoding. This is only a change to the encoding phase, the decoding is still the same as previously, so it's still cheap at two instructions.
Ok, so here's how it performs. I used exposure = 16 for all images. To give a sense of how much exposure that is, here's how it looks with exposure = 1.
Reference FP16 image:
Using a maximum value of 512, this is what I got with the three methods:
Method 1, max = 64
Method 2, max = 64
Method 3, max = 64
Method 1, max = 512
Method 2, max = 512
Method 3, max = 512
Method 1, max = 4096
Method 2, max = 4096
Method 3, max = 4096
Method 1, max = 65536
Method 2, max = 65536
Method 3, max = 65536
All full size png images