В предыдущих частях мы уже, в общем-то, реализовали туман войны. Но вот представьте, что теперь захотелось добавить еще одну фичу — чтобы карта плавно уходила в темноту по краям карты. И вроде бы эта штука кажется чем-то похожей на туман войны, не так ли? Давайте разберемся.
Новая задача
По сути, задача сводится к добавлению попиксельного затенения на все объекты, зависящего от положения в плоскости XZ. Именно поэтому на ум сразу приходит туман войны — он тоже затеняет пиксели в плоскости XZ, просто делает это по немного другому закону.
Добавим новую логику прямо в имеющийся шейдер — быстрое и дешевое решение, просто для проверки идеи:
И вот результат:
Вроде бы все ок. Но… слишком искусственно? Давайте добавим рваные края:
_MapBorderNoiseTex – полоска шума в 1 пиксель толщиной. Вот как это выглядит:
Кажется, все здорово? Но, смотрите: ноги девушки будто обрублены снизу. На самом деле обрезается вся картинка, но в месте, где видно фон, это выглядит странно. Решение? Сделаем фон светлее.
Новые сложности
И именно из-за этой маленькой безобидной доработки все пошло наперекосяк. Сделать пиксель черным – тривиально. А вот сделать его серым, с учетом того разнообразия моделей освещения и рендеринга, о которых я писал ранее оказалось не так просто.
В чем, собственно, сложность? Изначально задача шейдера тумана войны формулировалась так: color *= lerp(FowBrigtness, 1, fov). То есть, для полностью сокрытого пикселя домножаем его цвет на FowBrightness (скажем, 0.5), для полностью видимого пикселя — на 1, т.е. не меняем его. Теперь же нам потребовалось кое-что покруче: color = lerp(LevelBackground, color, border). То есть, для пикселя за границами карты взять LevelBackground (серый), для пикселя в пределах карты — его исходный цвет, ну а для промежуточных — что-то среднее.
Принципиальная разница между этими двумя задачами в том, что в первом случае мы не меняем оттенок пикселя, а во втором — меняем. Более конкретно: мы хотим, чтобы все материалы во всех моделях освещения выдавали именно тот серый, который мы задали. К сожалению, на surface shaders сделать такое мне не удалось.
Может все же пост-эффект?
В процессе борьбы с этой трудностью, меня все чаще стала посещать мысль: а, может, все же сделать пост-эффектом? Тогда красить фон можно будет как угодно. А что касается прозрачности, так вышло, что туман войны до сих пор не распространялся на прозрачные объекты. И это полностью устраивало. Потому что из прозрачных у нас были разве что частицы — выстрелы и взрывы, а они обычно возникают там, где игрок видит.
И вот я решил переделать все на пост-эффект. Сделать это получилось быстрее, чем я ожидал. Самым приятным в этом занятии было удаление вкраплений тумана войны из всех шейдеров проекта. Теперь-то туман войны стал по-настоящему автономен!
В таком переходе всплыл один сложный момент: в шейдере эффекта нужно получить позицию пикселя в мировом пространстве. Для этого следует воспользоваться буфером глубины. Но вот как именно это сделать… как получить к нему доступ из шейдера и в каком формате он сэмплится — эту информацию оказалось не так уж просто найти. В итоге я подглядел решение в одном из пост-эффектов из Standard Assets и сделал так же.
Суть решения в том, чтобы передать в шейдер 4 луча, соответствующих ребрам frustum’а камеры, а работу по получению луча, исходящего из каждого пикселя возложить на интерполяторы. Вот выжимка из шейдера эффекта, отвечающая за это:
Полный код эффекта приводить не буду, т.к. там не появилось ничего принципиально нового и интересного по сравнению с прошлой версией, разве что он стал чуть лучше огранизован.
В примере выше можете обратить внимание на занятный трюк, также позаимствованный у Unity: использование z-координаты вершины в качестве индекса в _FrustumCorners. Для отрисовки такого специально подготовленного квада можно применить такую функцию:
Используется по аналогии со стандартным Graphics.Blit(). Да, кстати, как вы могли догадаться, я использую один эффект и для тумана войны, и границ карты. Сделано это в угоду оптимизации. Я бы, конечно, лучше разделил их, но это даст 2 прохода вместо одного. Да, есть Unity Postprocessing, но я еще не успел освоить его и не хотел заниматься этим прямо сейчас.
Это уже пост-эффект версия. Если откроете в полном размере, то обязательно заметите, что между фоном и черными полосами есть заметная разница.
И, что забавно, на прозрачные объекты туман войны все же распространяется. Но в экранном пространстве. Т.е. костер на земле будет затеняться примерно правильно, а костер в воздухе уже начнет обрубаться. Но в нашем случае это совершенно не важно.
“Самым приятным в этом занятии было удаление вкраплений тумана войны из всех шейдеров проекта” до этого момента все же висел вопрос “а как быть со стандартными шейдерами?” но теперь он отпал :)