Реализация тумана войны (1/3)

Однажды для Tanks vs Aliens понадобилось реализовать туман войны. Казалось бы, это такая популярная фича, что для нее в Asset Store непременно найдется с десяток решений. На деле их нашлось всего несколько, а доверие вызвало лишь одно из них. Основная его фича – расчет полей видимостей с учетом перекрытий, вроде как в Heroes of the Storm или других MOBA’х. Нам же нужен был всего лишь старый добрый RTS’ный туман войны с раскрытием карты кружками вокруг юнитов. Это была одна из причин, почему я в итоге отказался от использования этого ассета и решил делать свою реализацию. О том, как я это делал, собственно и хочу рассказать.

Прежде чем браться за реализацию, давайте сначала определимся, какой туман войны мы хотим получить в итоге.

Туманы войны в дикой природе

Реализации туман войны можно классифицировать по куче всяких критериев, но меня в первую очередь интересовало следующее: каким образом плоский туман войны (а нам нужен именно такой) проецируется на объемный мир? От ответа на этот вопрос зависит то, какую реализацию мы сможем применить.

Во-первых, мы можем проецировать карту видимости в направлении взгляда камеры. Это простой и быстрый способ: достаточно просто нарисовать full-screen quad поверх картинки с прозрачными областями для разведанной территории и закрашенными для неразведанной. Пример такого тумана войны можно увидеть в Казаках 3:

Туман войны в Казаках 3: обрубленные здания

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

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

Другой вариант: проецируем карту видимости сверху-вниз. Тогда у нас из тумана могут торчать верхушки деревьев и здания. Но для расчета потребуется знать положение обрабатываемого пикселя в мировом пространстве. А это значит, что либо мы используем буфер глубины в пост-эффекте, либо мы делаем обработку в обычном проходе и подмешиваем соответствующий код во все шейдеры. Но ведь результат того стоит! Посмотрите, например, на Starcraft II:

Туман войны в Starcraft II

Обратите внимание, как граница видимости проходит по верхушкам зданий. На самом деле в SC2 туман войны гораздо сложнее, чем нам было нужно, он скорее похож на то, что предлагал упомянутый ранее ассет. Это, кстати, довольно интересная тема для размышлений: как лучше реализовать такой сложный туман войны? Может быть, хранить в карте видимости еще и высоту, на которую эта видимость распространяется? Или диапазон высот? Впрочем, сейчас не об этом.

В общем, ожидаемо, что мы довольно быстро остановились на втором варианте.

План

Итак, делаем туман войны с простой плоской картой видимости и проекцией сверху-вниз. План вырисовывался довольно простой:

  1. Ортогональной камерой сверху рендерим круги вокруг всех юнитов в текстуру видимости.
  2. Подмешиваем во все шейдеры код, сэмплящий эту текстуру и модифицирующий результирующий цвет.

Как видите, я изначально остановился на варианте с применением тумана войны “прямо на месте”, вместо отдельного пост-процесс прохода. Вариант с пост-процессом кажется очень привлекательным из-за своей не-интрузивности: не нужно трогать никакие шейдеры, просто нашлепнем сверху еще один проход и все готово. Но пост-процесс я отмел из-за проблем с прозрачными объектами. Как потом выяснилось, напрасно. Но об этом позже 🙂

Белые круги

По первому пункту все просто. Напишем код, который выставит нужный render target, пройдется по всем юнитам и нарисует вокруг них круг. Шучу 🙂 Такая мысль может прийти в программистский мозг, но ее надо гнать. Зачем работать на таком уровне абстракции, когда у нас есть более мощные инструменты? Куда лучше просто настроить отдельный слой в Unity, сделать префаб с ортогональной камерой и добавить круги-меши на все юниты.

Но чего-то меня дернуло написать процедурную геометрию для кругов… Да, потом я понял, что мог бы обойтись квадом и шейдером. Но в итоге решил оставить как есть. Вот, собственно, код:

Да, я не стал заморачиваться с переменной детализацией. 64 сегмента при разрешении текстуры до 400 х 400 (самый большой размер карты) дает отличный результат на любых юнитах, встречающихся в игре. Переходить на квад-и-шейдер тоже не стал, т.к. никакого импакта на производительность эта геометрия не дала (в игре относительно мало юнитов, не более 50-60 на уровне).

Итак, вот что мы пока что получили:

Геометрия, используемая для отрисовки раскрытых областей

Туман пикселей

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

С подмешиванием все просто. Сделаем cginc, добавим туда немного функций… и макросов! Как же приятно иногда вернутся в этот дикий мир текстовых макросов, ЗАГЛАВНЫХ букв и настоящей магии! 🙂

Да, так просто. И долгое время реализация тумана войны была именно такой. Да она и сейчас такая же, разве что 0.75f и 0.25f заменились константами.

Конечно, не обошлось и без сложностей. Во-первых, в проекте используются и Lambert и Standard модели освещения. Во-вторых, в проекте используется и Deferred и Forward рендеринг, зависимо от платформы. В итоге я довольно долго провозился, пытаясь добиться единообразного затенения на всех сочетаниях модели освещения и способа рендеринга. В идеале я бы хотел просто взять самый-самый финальный цвет пикселя и немного затенить его. В проекте используются Surface Shaders, которые, вроде бы предоставляют такую возможность – finalcolor и finalgbuffer. Но между ними есть ощутимая разница: finalcolor применяется после освещения, а finalgbuffer – до. Если подумать, это может быть и логично, т.к. в Deferred освещение вообще выполняется отдельным проходом… но это не снимает вопрос: как модифицировать финальный цвет?

Собственно, камнем преткновения была Standard модель освещения. Даже нулевой Albedo в ней дает очень светлые объекты:

Standard-освещение с нулевым Albedo

Все объекты как объекты, но чертовы камни! Но решение, как часто бывает, оказалось очень простым: использовать Occlusion вместо Albedo. Вот пример одного из Standard-шейдеров, использованных в проекте:

В этом шейдере:

  1. Убираем лишний код на уровнях без тумана войны (есть и такие)
  2. Подключаем показанный ранее файл
  3. Прячем нужные для тумана войны входные параметры под макросом (фишка подсмотрена у Unity)
  4. Выставляем SurfaceOutputStandard.Occlusion зависимо от тумана войны
В случае с Lambert-освещением вместо Occlusion просто модулируем Albedo. Для Emissive-материалов также модулируем Emission. С Terrain-шейдерами пришлось повозиться чуть подольше. Дело в том, что стандартные Unity’вские заголовки не дают способа подменить структуру, передаваемую из вершинного шейдера в пиксельный. Поэтому пришлось скопировать заголовок Unity и внести в него нужные изменения (опечатка в комментарии – не моя):

И вот он долгожданный результат:

Первая версия: сильно пикселизированный туман войны

Обратите внимание, как видимая область распространяется вверх на всю высоту здания. Как раз то, чего хотелось! Да вот только эти пиксели…

One thought on “Реализация тумана войны (1/3)

  1. Спасибо за наводку на “Occlusion”, почему-то ранее не принимал это в серьез. “пришлось скопировать заголовок Unity и внести в него нужные изменения” вот это я понимаю уровень кастомизации!

Leave a Reply

Your email address will not be published. Required fields are marked *