Imitate eyes adapt to the darkness in 3D

Everyone is familiar effect of temporary blindness when you walk into a dark room with light. According to popular belief, the sensitivity of regulating the size of the pupil. In fact, the change of the pupil area regulates the amount of incoming light only 25 times, the main role is played by adaptation of retinal cells themselves.

To simulate this effect in games using a mechanism called - tonemapping.
Tonemapping - the process of projection of the entire infinite range of brightness (HDR, high dynamic range, from 0 to infinity) on a finite interval perception of the eye / camera / monitor (LDR, low dynamic range, bounded on both sides).

In order to work with HDR, we need the corresponding screen buffer that supports values greater than one. Our task will be to properly convert these values in the range [0..1].

First of all, we need to know the overall brightness of the scene. To do this, calculate the geometric mean luminance of all pixels.
However, for our night scene is a little unreasonable, since most of the area of the image is dark, even if there is a bright light source, and therefore the average brightness remains almost unchanged. So we take the maximum brightness, and divide it in half.
We indicate our image to the nearest square with sides equal to a power of two and desaturate it. Then, every time we compress it twice, until there is one pixel.

To compress the pictures, we will take four neighboring pixels and choose one medium (in our case - instead, maximum). For the acceleration of the geometric mean we use the formula.

RenderTextureFormat rtFormat = RenderTextureFormat.ARGBFloat;
if (lumBuffer == null) {
    lumBuffer = new RenderTexture (LuminanceGridSize, LuminanceGridSize, 0, rtFormat, RenderTextureReadWrite.Default);

RenderTexture currentTex = RenderTexture.GetTemporary (InitialSampling, InitialSampling, 0, rtFormat, RenderTextureReadWrite.Default);
Graphics.Blit (source, currentTex, material, PASS_PREPARE);

int currentSize = InitialSampling;
while (currentSize > LuminanceGridSize) {
    RenderTexture next = RenderTexture.GetTemporary (currentSize / 2, currentSize / 2, 0, rtFormat, RenderTextureReadWrite.Default);
    Graphics.Blit (currentTex, next, material, PASS_DOWNSAMPLE);
    RenderTexture.ReleaseTemporary (currentTex);
    currentTex = next;
    currentSize /= 2;
// Downsample pass
Pass {
        #pragma vertex vert
        #pragma fragment fragDownsample

        float4 fragDownsample(v2f i) : COLOR 
            float4 v1 = tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * float2(-1,-1));		
            float4 v2 = tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * float2(1,1));		
            float4 v3 = tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * float2(-1,1));		
            float4 v4 = tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * float2(1,-1));	

            float mn = min(min(v1.x,v2.x), min(v3.x,v4.x));
            float mx = max(max(v1.y,v2.y), max(v3.y,v4.y));
            float avg = (v1.z+v2.z+v3.z+v4.z) / 4;

            return float4(mn, mx, avg, 1);

// Prepare pass
Pass {
        #pragma vertex vert
        #pragma fragment fragPrepare

        float4 fragPrepare(v2f i) : COLOR 
            float v = tex2D(_MainTex, i.uv);
            float l = log(v + 0.001);
            return half4(l, l, l, 1);

Note that the logarithm of the original image, we add a small constant to avoid the collapse of the universe in the case of a completely black (0) pixel.

At each step, reducing the texture stored in this minimum (R), the maximum (G) and the mean log (B) luminance value.

What follows is a little trick that will avoid reading texture and produce "adaptation" eyes entirely on GPU: Head constant texture size of 1 pixel on each frame will impose on it a new brightness value (also 1 pixel) with a small alpha (transparency). Thus, the stored value of brightness will gradually come to the current, as required.

if (!lumBuffer.IsCreated ()) {
    Debug.Log ("Luminance map recreated");
    lumBuffer.Create ();
    // if the texture just created explicitly set its value
    Graphics.Blit (currentTex, lumBuffer); 
} else {
    material.SetFloat ("_Adaptation", AdaptationCoefficient);
    Graphics.Blit (currentTex, lumBuffer, material, PASS_UPDATE);

AdaptationCoefficient - the coefficient of the order of 0.005, which determines the speed of adjustment of brightness.

It remains to take our two textures (the original image and brightness) and "twist" in the first exposure, using the value of the second.

material.SetTexture ("_LumTex", lumBuffer);
material.SetFloat ("_Key", Key);
material.SetFloat ("_White", White);
material.SetFloat ("_Limit", Limit);
Graphics.Blit (source, destination, material, PASS_MAIN);
// Main pass
Pass {
        #pragma vertex vert
        #pragma fragment frag

        float4 frag(v2f i) : COLOR 
            half4 cColor = tex2D(_MainTex, i.uv);		
            float4 cLum = tex2D(_LumTex, i.uv);	
            float lMin = exp(cLum.x);
            float lMax = exp(cLum.y);
            float lAvg = exp(cLum.z);
            lAvg = max(lMax / 2, _Limit); // force override for dark scene

            float lum = max(0.000001, Luminance(cColor.rgb));

            float scaled = _Key / lAvg * lum;
            scaled *= (1 + scaled / _White / _White) / (1+scaled);
            return scaled * cColor;

Here we return to the brightness of the logarithm, we calculate the scaling factor (scaled), and make allowances for the white level (_White).

Parameters used:

     Key - adjusts the overall brightness of the scene, which is considered "normal"
     Limit - limits the maximum sensitivity of the eyes, do not you see how Predator
     White - adjusts the width of the range, indicating what the brightness will be considered "white" in the image.

You can get an interesting result, reducing the brightness of the texture is not up to a single pixel, and stopping a few steps (increasing LuminanceGridSize). Then separate area of the screen will "get used" regardless. In addition, you get the effect of "dark spots", where one area setchaki light up, if you look directly at the lamp. However, in most cases, the brain automatically hides the effect of exposure, and the monitor, it looks unnatural and unusual.


All data posted on the site represents accessible information that can be browsed and downloaded for free from the web.


User replies

No replies yet