Всем знаком эффект временной слепоты, когда вы входите в темное помещение из светлого. Согласно распространенному заблуждению, чувствительность зрения регулируется размером зрачка. На самом деле, изменение площади зрачка регулирует количество поступающего света всего лишь в 25 раз, а основную роль в адаптации играют сами клетки сетчатки.

Для имитации этого эффекта в играх используется механизм, называемый tonemapping.
tonemapping — процесс проекции всего бесконечного интервала яркостей (HDR, high dynamic range, от 0 и до бесконечности) на конечный интервал восприятия глаза/камеры/монитора (LDR, low dynamic range, ограничен с обоих сторон).
Для того, чтобы работать с HDR, нам понадобится соответствующий экранный буфер, поддерживающий значения больше единицы. Наша же задача будет состоять в правильной конвертации этих значений в диапазон [0..1].
Первым делом, мы должны как-то узнать общую яркость сцены. Для этого нужно вычислить среднее геометрическое значение яркости всех пикселей.
Впро��ем, для нашей ночной сцены это слегка неразумно, так как большая часть площади изображения темная, даже если присутствует яркий источник света, и поэтому средняя яркость практически не изменяется. Так что возьмем максимальную яркость, и поделим ее пополам.
Ужмем нашу картинку до ближайшего квадрата со стороной, равной степени двойки и обесцветим ее. Затем будем каждый раз сжимать ее вдвое, пока не останется один пиксель:

Для сжатия картинки, будем брать четыре соседних пикселя и выбирать из них средний (для нашего случая — вместо него максимальный). Для ускоренного вычисления среднего геометрического воспользуемся формулой

Заметьте, что при логарифмировании исходной картинки мы прибавляем небольшую константу, чтобы избежать коллапса вселенной в случае полностью черного (0) пикселя.
На каждом шаге уменьшения в нашей текстуре хранится минимальное ( R ), максимальное ( G ) и среднелогарифмическое ( B ) значение яркости.
Далее следует небольшой трюк, который позволит избежать чтения текстуры и производить «адаптацию» глаза полностью на GPU: заведем постоянную текстуру размером в 1 пиксель и на каждом кадре будем накладывать на нее новое значение яркости (тоже 1 пиксель) с небольшим alpha (прозрачностью). Таким образом сохраненное значение яркости будет постепенно приходить к текущему, что и требовалось.
AdaptationCoefficient — коэффициент порядка 0.005, который определяет скорость адаптации к яркости.
Осталось взять наши две текстуры (исходное изображение и яркость) и «подкрутить» экспозицию в первой, используя значение из второй.
Здесь мы восстанавливаем значение яркости из логарифма, вычисляем коэффициент масштабирования (scaled), и делаем поправку на уровень белого (_White).
Используемые параметры:
Результат:
Можно получить интересный результат, уменьшая текстуру яркости не до одного пикселя, а останавливаясь за несколько шагов (увеличив LuminanceGridSize). Тогда отдельные области экрана будут «привыкать» независимо. Кроме того, получится эффект «темного пятная», когда одна область сетчаки засвечивается, если смотреть прямо на лампу. Однако в большинстве случаев мозг автоматически прячет эффект засветки, и на мониторе он смотрится неестественно и непривычно.
Подробнее о дневном tonemapping'e читаем у Рейнхарда
Код
Шейдер

Для имитации этого эффекта в играх используется механизм, называемый tonemapping.
tonemapping — процесс проекции всего бесконечного интервала яркостей (HDR, high dynamic range, от 0 и до бесконечности) на конечный интервал восприятия глаза/камеры/монитора (LDR, low dynamic range, ограничен с обоих сторон).
Для того, чтобы работать с HDR, нам понадобится соответствующий экранный буфер, поддерживающий значения больше единицы. Наша же задача будет состоять в правильной конвертации этих значений в диапазон [0..1].
Первым делом, мы должны как-то узнать общую яркость сцены. Для этого нужно вычислить среднее геометрическое значение яркости всех пикселей.
Впро��ем, для нашей ночной сцены это слегка неразумно, так как большая часть площади изображения темная, даже если присутствует яркий источник света, и поэтому средняя яркость практически не изменяется. Так что возьмем максимальную яркость, и поделим ее пополам.
Ужмем нашу картинку до ближайшего квадрата со стороной, равной степени двойки и обесцветим ее. Затем будем каждый раз сжимать ее вдвое, пока не останется один пиксель:

Для сжатия картинки, будем брать четыре соседних пикселя и выбирать из них средний (для нашего случая — вместо него максимальный). Для ускоренного вычисления среднего геометрического воспользуемся формулой
Почему геометрическое? Потому что геометрическое среднее «тяготеет» к более высоким значениям, а значит будут выбираться более яркие пиксели (что нам и нужно, так как нас интересуют имеющиеся на картинке источники света).
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 { CGPROGRAM #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); } ENDCG } // Prepare pass Pass { CGPROGRAM #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); } ENDCG }
Заметьте, что при логарифмировании исходной картинки мы прибавляем небольшую константу, чтобы избежать коллапса вселенной в случае полностью черного (0) пикселя.
На каждом шаге уменьшения в нашей текстуре хранится минимальное ( R ), максимальное ( G ) и среднелогарифмическое ( B ) значение яркости.
Далее следует небольшой трюк, который позволит избежать чтения текстуры и производить «адаптацию» глаза полностью на GPU: заведем постоянную текстуру размером в 1 пиксель и на каждом кадре будем накладывать на нее новое значение яркости (тоже 1 пиксель) с небольшим alpha (прозрачностью). Таким образом сохраненное значение яркости будет постепенно приходить к текущему, что и требовалось.
if (!lumBuffer.IsCreated ()) { Debug.Log ("Luminance map recreated"); lumBuffer.Create (); // если текстура только что создалась, явно установим ее значение Graphics.Blit (currentTex, lumBuffer); } else { material.SetFloat ("_Adaptation", AdaptationCoefficient); Graphics.Blit (currentTex, lumBuffer, material, PASS_UPDATE); }
AdaptationCoefficient — коэффициент порядка 0.005, который определяет скорость адаптации к яркости.
Осталось взять наши две текстуры (исходное изображение и яркость) и «подкрутить» экспозицию в первой, используя значение из второй.
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 { CGPROGRAM #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; } ENDCG }
Здесь мы восстанавливаем значение яркости из логарифма, вычисляем коэффициент масштабирования (scaled), и делаем поправку на уровень белого (_White).
Используемые параметры:
- Key — регулирует общую яркость сцены, которая считается «нормальной»
- Limit — ограничивает максимальную светочувствительность глаза, не позволяя видеть, как Хищник
- White — регулирует ширину диапазона, указывая, какая яркость будет считаться «белой» на изображении
Результат:
Можно получить интересный результат, уменьшая текстуру яркости не до одного пикселя, а останавливаясь за несколько шагов (увеличив LuminanceGridSize). Тогда отдельные области экрана будут «привыкать» независимо. Кроме того, получится эффект «темного пятная», когда одна область сетчаки засвечивается, если смотреть прямо на лампу. Однако в большинстве случаев мозг автоматически прячет эффект засветки, и на мониторе он смотрится неестественно и непривычно.
Подробнее о дневном tonemapping'e читаем у Рейнхарда
Код
Шейдер
Only registered users can participate in poll. Log in, please.
Стоит ли писать еще статьи по шейдерам и пост-обработке?
96.72%Да1357
3.28%Нет46
1403 users voted. 142 users abstained.