Имитируем ночное зрение человека в 3D-игре

  • Tutorial
Сегодня мы будем заниматься постпроцессингом изображения в DirectX.

Как известно, в темноте зрение человека обеспечивается клетками-палочками сетчатки, высокая световая чувствительность которых достигается за счет потери цветочувствительности и остроты зрения (хотя палочек в сетчатке и больше, они распределены по гораздо большей площади, так что суммарное «разрешение» выходит меньше).

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

В результате мы получим что-то вроде следующего (смотреть на весь экран!):

До: унылый польский шутер


После: финалист IGF и лауреат всех наград E3



Подготовка


Первым делом нужно определиться, какого эффекта мы хотим достичь. Я разбил всю обработку на следующие части:
  • Потеря цветовосприятия в зонах низкой освещенности
  • Потеря четкости зрения там же
  • Небольшой шум (там же)
  • Потеря четкости зрения на большом расстоянии при средней и низкой освещенности

Реализация


Писать будем под Unity3D Pro, в виде шейдера для постпроцессинга.

Прежде чем приступить непосредственно к шейдеру, напишем небольшой скрипт, выполняющий прогонку экранного буфера через этот самый шейдер:

using UnityEngine;

[ExecuteInEditMode]
public class HumanEye : MonoBehaviour
{
	public Shader Shader;
	public float LuminanceThreshold;
	public Texture Noise;
	public float NoiseAmount = 0.5f, NoiseScale = 2;

	private Camera mainCam;
	private Material material;

	private const int PASS_MAIN = 0;

	void Start ()
	{
		mainCam = camera;
		mainCam.depthTextureMode |= DepthTextureMode.DepthNormals;
		material = new Material (Shader);
	}

	void OnRenderImage (RenderTexture source, RenderTexture destination)
	{
		material.SetFloat("_LuminanceThreshold", LuminanceThreshold);
		material.SetFloat ("_BlurDistance", 0.01f);
		material.SetFloat ("_CamDepth", mainCam.far);
		material.SetTexture ("_NoiseTex", Noise);
		material.SetFloat ("_Noise", NoiseAmount);
		material.SetFloat ("_NoiseScale", NoiseScale);
		material.SetVector("_Randomness", Random.insideUnitSphere);
		Graphics.Blit (source, destination, material, PASS_MAIN);
	}		
}

Здесь мы всего лишь устанавливаем параметры шейдера на заданные пользователем и выполняем перерендеринг экранного буфера через наш шейдер.

Теперь займемся непосредственно делом.

Объявления переменных и констант:
sampler2D _CameraDepthNormalsTexture;
float4 _CameraDepthNormalsTexture_ST;

sampler2D _MainTex;
float4 _MainTex_ST;

sampler2D _NoiseTex;
float4 _NoiseTex_ST;
float4 _Randomness;

uniform float _BlurDistance, _LuminanceThreshold, _CamDepth, _Noise, _NoiseScale;

#define DEPTH_BLUR_START 3
#define FAR_BLUR_START 40
#define FAR_BLUR_LENGTH 20


Вершинный шейдер — стандартный и не выполняет никаких необычных преобразований. Самое интересное начинается в пиксельном шейдере.

Для начала выберем значение текущего пикселя, и в добавку к нему — «размытое» значение для того же пикселя:
struct v2f {
	float4 pos : POSITION;
	float2 uv : TEXCOORD0;
	float2 uv_depth : TEXCOORD1;
};

half4 main_frag (v2f i) : COLOR
{
	half4 cColor = tex2D(_MainTex, i.uv);
	half4 cBlurred = blur(_MainTex, i.uv, _BlurDistance);


Получение «размытого» значения выполняется функцией blur(), которая выполняет выборку нескольких пикселей по соседству с нашим и усредняет их значения:

inline half4 blur (sampler2D tex, float2 uv, float dist) {
	#define BLUR_SAMPLE_COUNT 16
        // сгенерированы абсолютно случайным броском float-кости!
	const float3 RAND_SAMPLES[16] = {
			float3(0.2196607,0.9032637,0.2254677),
                        .... еще 14 векторов ....
			float3(0.2448421,-0.1610962,0.1289366),
	};

	half4 result = 0;
	for (int s = 0; s < BLUR_SAMPLE_COUNT; ++s)
		result += tex2D(tex, uv + RAND_SAMPLES[s].xy * dist);
	result /= BLUR_SAMPLE_COUNT;
	return result;
}


Коэффициент неосвещенности пикселя будем определять через среднюю величину яркости по трем каналам. Коэффициент отсекается по заданному пограничному значению яркости (LuminanceThreshold), т.е. все пиксели светлее этого считаются «достаточно яркими», чтобы их не обрабатывать.

half kLum = (cColor.r + cColor.g + cColor.b) / 3;
kLum = 1 - clamp(kLum / _LuminanceThreshold, 0, 1);


Зависимость kLum от яркости будет выглядеть примерно так:



Значения kLum для нашей сцены выглядят так (белый — 1, черный — 0):



Здесь хорошо видно, что яркие области (гало фонарей и освещенная трава) имеют kLum равный нулю и наш эффект к ним применяться не будет.

Расстояние от поверхности экрана до пикселя в метрах можно получить из текстуры глубины (depth texture, Z-buffer), которая явно доступна при deferred-рендеринге.

float depth;
float3 normal;
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv_depth), depth, normal);
depth *= _CamDepth; // depth in meters


Коэффициент kDepth будет определять степень размытия темных предметов вблизи, а kFarBlur — всех остальных вдали:

#define DEPTH_BLUR_START 3
#define FAR_BLUR_START 40
#define FAR_BLUR_LENGTH 20
half kDepth = clamp(depth - DEPTH_BLUR_START, 0, 1);
half kFarBlur = clamp((depth - FAR_BLUR_START) / FAR_BLUR_LENGTH, 0, 1);


Графики обоих коэффициентов от расстояния выглядят одинаково и различаются только масштабом:


Значения kFarBlur:



А теперь — магия! Рассчитываем общий коэффициент размытия пикселя, исходя из предыдущих трех:

half kBlur = clamp(kLum * kDepth + kFarBlur, 0, 1);


Темные пиксели будут размываться, начиная с расстояния в несколько метров (DEPTH_BLUR_START), а удаленные предметы — независимо от освещенности.



Степень потери цвета у нас будет равна степени «неосвещенности» (half kDesaturate = kLum).

Теперь осталось смешать обычный, размытый и черно-белый пиксель и расчитать итоговый цвет:
half kDesaturate = kLum;

half4 result = cColor;
result = (1 - kBlur) * result + kBlur * cBlurred;

half resultValue = result;
result = (1 - kDesaturate) * result + kDesaturate * resultValue;
return result;




Однако если посмотреть на картинку в динамике — то видно, что чего-то не хватает. Чего? Шумов!

half noiseValue = tex2D(_NoiseTex, i.uv * _NoiseScale + _Randomness.xy);
half kNoise = kLum * _Noise;


Здесь мы выбираем случайную величину из текстуры _NoiseTex (заполненную Гауссовым шумом из фотошопа), используя предоставленный скриптом вектор _Randomness, который будет изменяться на каждом кадре.

Полученное случайное значение подмешиваем в наш пиксель:

result *= (1 - kNoise + noiseValue * kNoise);


В качестве бонуса — небольшое видео и сам шейдер:



Update: правильные, человеческие lens flares:
full

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 57

    +14
    «В результате мы получим что-то вроде следующего (смотреть на весь экран!):»
    Если эти маленькие(800х378) картинки смотреть во весь экран(1920х1080), то ничего хорошого точно не увидеть.
    У Вас даже видео более высокого качества.
      +2
      habrastorage.org ужимает картинки по ширине страницы хабра. Перезалил в высоком разрешении
        +15
        Кстати говоря, с такой продуманностью темноты, эта игра просто обязана быть страшилкой =)
          +6
          Я пока просто экспериментирую с unity и графикой, но действительно хочется слепить страшилку :)
            +2
            Сделайте, обязательно сделайте хороший хоррор. И что бы был очень запоминающийся, атмосферный музон. И сюжет, глубокий, со смыслом, заставляющийся задуматься. И что бы при приближении монстров начинались помехи на радио у главного героя.

            Была одна такая серия игр, но после второй части она скатилась в от «в целом неплохо» до «очень плохо во всем».
              +1
              Я буду стараться, честно.
              P.S.
              что бы при приближении монстров начинались помехи на радио у главного героя
              Slender
                0
                Кажется, это было реализовано еще в Silent Hill, и было одной из ее запоминающихся особенностей
                  +3
                  Одна из интересных плюшек в играх была впервые мной замечена в half-life ep2
                  Там была эмуляция переключения глаза при смене освещенности. Зайдя с солнца в темное помещение нужно было выждать несколько секунд, что бы лучше видеть.
                    0
                    Примерно такая же вещь была в Skyrim и жутко бесила. После того, как дракон выплюнет в тебя очередную тонну пламени, глаза «засвечивает» и несколько долей секунд экран совершенно черный.

                    В общем, главное не переборщить.
                      0
                      В GURPS — Tactical Shooting этот эффект описан и предлагаемые там правила как бы намекают, что так и должно быть. Если в тёмной комнате вам в глаза секунду посветить фонариком, то ночное зрение исчезнет на время.
                        +1
                        Flashbang?:)
                      +1
                      Главное сделайте так, чтобы можно было грабить корованы.
                        +1
                        Ну тогда, чтобы были ещё блекджек и ш… кхм… преферанс и блудницы
                      0
                      Я джва года хочу такую игру.
                      Если серьёзно, то правда было бы здорово.
                      0
                      У меня есть прекрасный сценарий и дизайн уровней для физического 3D-квеста-страшилки, как раз таки основанного на световых эффектах.
                        0
                        Жду от вас сообщения :)
                        0
                        Кстати, Андрей Круз, автор очень хорошей серии книг по зомби-апокалипсису как то высказывал желание написать сценарий и принять активное участие в создании игры в жанре его книг. (ЖЖ andrey-cruz.livejournal.com/34296.html)
                        Вот еще некоторые его идеи по особенностям игры (Опять ЖЖ andrey-cruz.livejournal.com/152702.html). Может Вам стоит ему написать?
                  +44
                  Линзы только сделайте глазные, а не камерные
                    +13
                    На 43 секунде и немного раньше от источника света изходит Lens Flare — такого у человека нет. Это характеристика линз кино- и фотокамер, да еще и форме щели диафрагмы с лепестками. Да, это красиво но никак не имитация человеческого зрения.
                      +3
                      Да, я вот дожидаюсь темноты прямо сейчас, чтобы нарисовать «правильный» flare. Вечная проблема с «реалистичностью» — ее не заснимешь на фото, чтобы потом с него делать ):
                        +19
                        Вот тут про человеческий «lens flare» есть.
                          +3
                          Большое спасибо, то что нужно!
                      0
                      Судя по картинкам, что «до», что «после» — жесть какая-то. Такая игра может награды получить за интересность и атмосферу, но никак не за графику.
                      Вы еще вспомните, как в советском старом кино ночные сцены снимались в темноте. Изображение должно любому зрителю казаться идеально реалистичным, но при этом может на проверку быть физически неточным.
                        +2
                        Согласен, на «финалист-лауреат» пока явно не тянет )
                          +2
                          Если учесть, что автор разбирается с эффектами.То можно сказать, что выглядит шикарно.
                            0
                            На мой вкус — зачетно. Что-то такое в этой вполне реалистичной темноте цепляет.
                            Сюда еще леденящий душу сценарий, и только без перебарщиваний. Тогда может получиться гейм-блокбастер.
                        +4
                        Шейдер зачетный, только на мой вкус небо у вас темновато — такое бывает очень редко, при полностью затянутом небе без луны.
                          +1
                          Если рядом нет освещённого мегаполиса — безоблачное и безлунное небо будет совсем тёмным. Хотя яркие звёзды должны быть.
                          +1
                          Получается очень атмосферно. В ролике смутило два момента: как уже выше сказали lens flare и нереалистичность фонарика, слишком цельный пучок и выход. В целом, конечно, круто. Пишите еще.
                            0
                            Это все зависит от фокусировки фонарика. У меня есть фонарик с TIR-оптикой, он дает очень схожую картинку — яркое абсолютно круглое пятно без боковое засветки.
                              0
                              Но свет от самого обычного фонарика, который дает засветку по бокам, был бы реалистичней, имхо.
                                0
                                Да, с этим полностью согласен, смотрелось бы более живо.
                            +6
                            *изменено*
                            Забавно. Весь мир борется с lens flare и только игроделы их пихают повсюду.
                              0
                              Ответил выше, просто забыл про них. Сейчас рисую «правильные» eye flare
                                0
                                Дык, показывайте!
                                  0
                                  См. update
                                    0
                                    Уже реальнее, немного странно, но реальнее ) Пользовались статьей из комментария выше? И, да, я только сейчас сообразил, что вы тот hardex, который начинал адженту, да?
                                      +1
                                      Я тот самый который ее продолжает :)
                              +10
                              Еще у глаз переменный диапазон восприятия яркости + реактивность. Т.е. глаза постепенно адаптируются к уровню освещения.
                                +7
                                Кстати, да. Если в игре реализуете постепенную (через 1-2 минуты) адаптацию (которая естественно немедленно «сбивается», стоит только глянуть на яркое пятно) зрения к темноте — это будет весьма айс!
                                  +2
                                  Обязательно попробую реализовать, может быть даже потянет на отдельный топик :)
                                    +1
                                    Достаточно 30 секунд, а вообще можно на себе потренироваться.
                                0
                                По-моему падение резкости — это уже перебор, по видео сложилось впечатление, что персонаж пьян )
                                  +3
                                  В качестве пожелания игрока разработчику: прикольно, но сделайте такой эффект отключаемым в настройках, плиз. Непроизвольно начинаешь вглядываться в тёмные места, пытаясь рассмотреть детали, и быстро устают глаза.
                                    +1
                                    Надо добавить в игру eye tracking. Такого еще никто не делал.
                                    +2
                                    Отмечу ещё одну неправильно в линзах — они слишком яркие. Лампочка слепит только если смотреть прямо на неё, если же мимо, она будет гораздо тусклее. А у вас слепят все, даже фары машины, смотрящей в сторону (последняя картинка).

                                    Но в целом очень здорово.
                                      0
                                      Отличная статья, спасибо.
                                      P.S. У вас в игре линзы не красивые и слишком напрягающие.
                                        0
                                        Очень круто! Добавьте приспособление глаз к темноте, хотя бы имитацию глобального освещения (в моих мечтах) и, может, остальное от HDR (для того чтобы сильные источники «слепили» игрока с непривычки)?

                                        Уже давно хотел увидеть блур в условиях недостаточного освещения, наконец-то вы реализовали)
                                        Кстати, я тоже писал подобное, для 2D правда.
                                          +2
                                          Не совсем в тему, но…
                                          <зануда>У вас для фар машины — конический источник освещения (или как это правильно называется). На самом деле они таковыми не являются, + там ещё есть неравномерности от стекла фары и формы отражателя. Весь эффект портит :)</зануда>
                                            –2
                                            А Стас Михайлов в игре будет таким же сексуальным? Сделаете?
                                            0
                                            А то, что в темноте не различаются цвета, добавите?
                                              0
                                              Небольшое замечание: если источников света несколько, то тень падает от каждого из источника, это хорошо видно ночью: встаньте меж двух фонарей и посмотрите.
                                              А в целом очень хорошо, только я со своим зрением в -6.5 вижу несколько по иному размытие — «радужного» ареала не видно, источник света «смазывается», кроме того — объекты окрашиваются в цветом источника цвета, переходя на самых границах в темно-синий (практически иссиня-черный).

                                              Или это специфика именно моего зрения?
                                                0
                                                Поначалу это здорово выглядит. Но после нескольких десятков утыканий в стену с криками «ай бл@ть, нихрена не видно», будет выкручена яркость на максимум, либо просто отключена постобработка. Это я вам говорю, потому что сам так делал ни один раз.
                                                  +8
                                                  Я не игрок. Мне пришлось дооолго разглядывать «до» и «после», чтобы понять, чем вообще изображения отличаются и отличаются ли. Воспринимаются они совершенно одинаково, «до» даже приятнее.
                                                    +1
                                                    При использовании фонаря в темноте глаза пытаются сфокусироваться на освещенном участке. Таким образом при включении фонаря было бы логично замылить все что не в пятне света.
                                                      –2
                                                      Я считаю это не правильным, заморачивать на таких вещах, которые особо то ничем не отличаются, потому что тогда:
                                                      Нужно сделать скорость передвижения персонажа реальную как у человека.
                                                      Нужно сделать хроматическую адаптацию зрения при переходе из яркого в темнок (сужение расширение зрачка)
                                                      Нужно сделать «зайчики » от ярких источников света в глазах.
                                                      Вот еще выше тоже предложения пишут.
                                                      Все это уже пытались сделать и все это уже опробавано на людях. Не нужны геймерам такие вещи — это то что мешает чувстовать себя супер героем (первостепенная задача развлекающих видеоигр всегда)
                                                      Именно потому до сих пор играют в первый counter-strike и не всем нужна эта физика и графика в counter-strike source.

                                                      Именно поэтому игровая карта всегда лучше чем игровая карта построенная на основе реальной местности.
                                                      Я не гамер, но в сталкере уровни гавно пусть и по реальной карте, они тупые длинные скучные и неоправданные ничем кроме «как в реале».
                                                        0
                                                        Крутой туториал! Спасибо! Только ссылки на картинки больше не работают.

                                                        Only users with full accounts can post comments. Log in, please.