Как сделать туман в юнити
Как сделать туман в юнити
The effect descriptions on this page refer to the default effects found within the post-processing stack.
Fog is the effect of overlaying a color onto objects dependant on the distance from the camera. This is used to simulate fog or mist in outdoor environments and is also typically used to hide clipping of objects when a camera’s far clip plane has been moved forward for performance.
The Fog effect creates a screen-space fog based on the camera’s depth texture. It supports Linear, Exponential and Exponential Squared fog types. Fog settings should be set in the Scene tab of the Lighting window.
Scene with Fog
Scene without Fog.
UI for Fog
Properties
Property: | Function: |
---|---|
Exclude Skybox | Should the fog affect the skybox? |
Details
This effect is only applied in the deferred rendering path. When using either rendering path fog should be applied to forward rendered objects using the Fog found in Scene Settings. The parameters for the Post-processing fog are mirrored from the Fog parameters set in the Scene tab of the Lighting window. This ensures forward rendered objects will always receive the same fog when rendering in deferred.
Requirements
See the Graphics Hardware Capabilities and Emulation page for further details and a list of compliant hardware.
Глобальный туман
Графический эффект Global Fog создаёт эскпотенциальный туман на основе камеры. Все расчеты производятся в мировом пространстве, благодаря чему возможно использование режимов тумана на основе высоты, которые могут быть использованы для сложных эффектов (см пример).
Демонстрация тумана, основанного на дистанции и высоте
Пример “читинга” атмосферных эффектов, использующих глобальный туман
Также как и в случае с другими эффектами изображения, этот эффект доступен только в Unity Pro и перед тем как он станет доступен, вы должны установить стандартные Pro ассеты.
Свойства
Свойство: | Функция: |
---|---|
Fog Mode | Доступные типы тумана, основанные на дистанции, высоте и том и другом одновременно. |
Start Distance | Дистанция, при которой туман начинает рассеиваться в мировых единицах координат. |
Global Density | Угол, при котором Fog Color увеличивается вместе с расстоянием. |
Height Scale | Угол, при котором плотность тумана уменьшается вместе с высотой (если активен туман, основанный на высоте). |
Height | Координата Y в мировом пространстве, где туман начинает рассеиваться. |
#Глобальный туман | Цвет тумана. |
Аппаратная поддержка
Для этого эффекта требуется видеокарта с поддержкой Shader Model 2 и Depth Textures. Для более подробного ознакомления с темой и списком совместимых аппаратных средств, посетите страницу документации графические возможности аппаратных средств и их эмуляция.
Реализация тумана войны из Civilization VI в Unity
Эффект тумана войны из Civilization VI — отличный пример простой структуры вычислительного шейдера (compute shader). Если вы всегда хотели узнать об основах программирования таких шейдеров, то этот туториал для вас. Вы сможете понять его даже без знания шейдеров и программирования на C#; более опытные разработчики могут пропустить введение.
Анализ эффекта
Давайте начнём проект с изучения и анализа эффекта в игре. К счастью, Civilization — пошаговая игра, поэтому мы можем наблюдать эффект столько, сколько нам нужно. Я загрузил своё старое сохранение и сделал пару скриншотов разных областей мира.
Шум Перлина
Шум Перлина — это тип шума, используемый для моделирования органических форм. Обычно на его основе создаются карты высот рельефа и эффекты растворения.
Теперь давайте рассмотрим графическую составляющую скрытой области. Эффект на ней напоминает эффект изображения поверх статических мешей (зданий, растительности…) в сцене (контур границы и нечто, напоминающее тень). Очевидно, что динамические объекты под туманом войны невидимы, потому что это лишило бы его смысла.
Кроме этого присутствуют нарисованные от руки текстуры рельефа, например, травы, которые особенно заметны на пустых тайлах. В своём проекте мы будем использовать только эти текстуры, потому что создание подобного эффекта изображения в дополнение к шейдеру было бы слишком масштабной задачей для одного туториала.
Также есть ещё два аспекта, создающих эффект тумана войны. Первый — это градиент границы области, который чётко можно заметить на тайлах океана. Также можно увидеть, что он не имеет постоянной толщины, то есть он тоже основан на упомянутом выше шуме Перлина.
Во-вторых, поверх эффекта есть небольшой шум, разрушающий монотонность больших пустых тайлов. Его можно увидеть на тайлах океана к югу (на тех, у которых нет волн побережья). Для этого тоже можно использовать текстуру шума Перлина.
Я подготовил шаблон проекта для этого туториала, в котором уже создана предварительная структура. Если у вас его нет, то можете клонировать или скачать его с GitHub.
В следующем разделе я вкратце расскажу о важных частях структуры проекта, особенно о скрипте управления вычислительным шейдером на C#.
Проект Unity
Начнём с открытия «Assets/SampleScene.unity». Как видите, в сцене почти ничего нет — только простая сетка шестиугольников, источник направленного освещения и камера.
Здесь я хочу обратить внимание на два аспекта: настройку постобработки камеры и структуру материалов.
Выбрав основную камеру, вы увидите объём и слой постобработки. Здесь мы используем пакет постобработки Unity. Применяемые эффекты достаточно просты. Присутствует нейтральная тональная коррекция, небольшое виньетирование и глубина резкости. Можете свободно экспериментировать с этими эффектами и добавлять новые, если хотите изменить внешний вид результата.
Если выбрать произвольное поле шестиугольника в сцене, то вы увидите. что ему уже назначен материал. Хотя все они используют одинаковый шейдер, в разных типах тайлов применяются разные спрайты. В случае представленного выше изображения тайл позже станет ветряной мельницей. В нашем проекте используется 9 разных типов тайлов, их материалы находятся в папке «Assets/Materials».
Это подводит нас к текстурам тайлов. В папке «Assets/Textures» находятся цветные текстуры каждого тайла, а также их нарисованные от руки версии с суффиксом «_Map». Цветные текстуры взяты из Hexagon Pack разработчика Kenney.
Kenney Assets
На сайте Kenney.nl есть множество бесплатных ассетов (2D, 3D и звуковых), которые можно использовать в своих проектах. Большинство из них даже имеет лицензию Creative Commons.
Нарисованные от руки версии этих текстур соответствуют цветным текстурам, что важно для эффекта, ведь на границе тумана войны он не должен иметь никаких смещений относительно исходного тайла.
Также в этой папке есть ещё две текстуры. «PerlinNoise» — это текстура шума Перлина.
Генератор текстур
Для создания текстуры шума Перлина я использовал этот онлайн-генератор текстур Кристиана Петри. Если вы хотите поэкспериментировать с другими значениями шума, то можете просто заменить текстуру из проекта новой.
Вторая текстура («MapBackground») используется для тех мест в тумане войны, которые нарушают монотонность больших пустых поверхностей.
Разобравшись с текстурами, перейдём к шейдерам и скриптам. В папке «Assets/Shaders» есть два шейдера: «MaskCompute» — это вычислительный шейдер, используемый для генерации маски видимых и скрытых областей; «Tile Shader» — это шейдер, применяемый к материалам тайлов. Он сэмплирует значение в текстуре маски, созданной вычислительным шейдером, и на основании неё рендерит текстуру тайла или туман войны. Подробнее мы рассмотрим шейдеры в следующих разделах.
Также в «Assets/Scripts» есть два скрипта. Чтобы понять, что происходит в шейдерах, важно понять логику C#. Давайте разберём по порядку каждый из них. Начнём с «GridCell».
Этот скрипт прикреплён к каждой шестиугольной ячейке в сцене. Он предназначен для управления видимостью ячейки и переключает её, когда происходит взаимодействие с мышью.
В начале каждая ячейка добавляет себя в список ячеек в скрипте «MaskRenderer» при помощи вызова функции «RegisterCell». В готовой игре этот список должен просто заполняться при помощи инспектора, однако для прототипирования довольно полезно иметь подобную функцию, потому что она без лишних действий позволяет увеличивать размер карты.
Мы хотим иметь возможность взаимодействия с демо и переключения видимости ячеек. Для этого у каждой ячейки есть коллайдер. При помощи OnMouseDown() и OnMouseEnter() можно перетаскивать курср мыши по экрану и переключать по пути видимость всех ячеек.
Давайте рассмотрим эту корутину. Используемый в ней паттерн стандартен для анимации, управляемой через скрипт на C#. Преимущество самостоятельно выполнения вычислений вместо подготовки и воспроизведения анимации в Unity заключается в возможности приостановки анимации в любой момент времени.
Теперь откроем скрипт «MaskRenderer.cs». Важно, чтобы вы полностью его поняли, ведь он управляет логикой вычислительного шейдера.
Как сказано ранее, каждая ячейка добавляет себя в список ячеек, используемый рендерером масок. Позже мы создадим вычислительный буфер (compute buffer) с удобной для шейдера struct переменных из списка.
Здесь перечислено несколько переменных, открытых для редактора; они задают основные параметры эффекта. «TextureSize» — это размер создаваемой текстуры маски, в идеале он должен быть степенью двойки. «MapSize» — это физический размер ячейки шестиугольников в единицах измерения Unity. Позже нам потребуется это число, чтобы наложить текстуру маски на сетку. «Radius» — это радиус одной ячейки, т.е. расстояние между центром и углом. Вместо того, чтобы определять, находится ли текущий вычисляемый тексел внутри поля шестиугольника, мы проверяем, находится ли он внутри описывающей поле окружности. Последний параметр — это «BlendDistance», определяющий ширину вокруг видимой области, которая используется для смешения с невидимой областью. Внутренний радиус области смешивания вокруг ячейки задаётся переменной «Radius», внешний — значением «Radius» + «BlendDistance».
Это текстура, которую мы записываем в вычислительный шейдер. При работе с render texture нужно учитывать множество аспектов, по сравнению с другими объектами Unity C# они довольно низкоуровневые. В отличие от других объектов, их не очищает сборщик мусора, поэтому чтобы освободить память для другой информации, нам придётся вручную вызывать Release() после того, как они больше не будут нам нужны.
Память Render Texture
Данные Render texture хранятся в памяти GPU, то есть хорошо они работают только тогда, когда все операции выполняются на стороне GPU, как и происходит в нашем случае (мы выполняем в них запись в вычислительном шейдере и считываем их в шейдере тайлов). Однако в момент копирования обратно в память ЦП вы заметите довольно большой пик времени вычисления кадра.
В этом скрипте на C# мы задаём довольно много переменных, и большинство из них задаётся в каждом кадре. Чтобы избежать сравнения строк в каждом вызове, мы кэшируем ID свойств в виде integer. Так следует делать всегда при работе с шейдерами в скрипте на C#. Здесь имя каждой переменной должно оставаться таким же, как в шейдерах, чтобы движок Unity мог их задавать.
Вычислительный буфер используется для парсинга информации ячейки для вычислительного шейдера. Благодаря использованию одинакового типа для каждого из значений struct мы в дальнейшем сможем просто использовать для задания данных каждой ячейки в вычислительном шейдере float3. Позже мы увидим, как работает этот буфер.
Мы выполняем настройку основных параметров шейдера и render texture. Обратите внимание: несмотря на то, что используемый здесь формат текстур является лишней тратой ресурсов и его можно заменить на другой (нам бы хватило формата с одним каналом, поскольку нам нужно только значение маски), необходимо, чтобы была включена произвольная запись. Мы задаём размер текстуры и саму текстуру для вычислительного шейдера как глобальные переменные.
Глобальные переменные шейдера
В масштабных проектах никогда не стоит использовать глобальные переменные. Однако они довольно полезны при прототипировании, так как эти переменные без дальнейшей настройки можно использовать в любом шейдере.
Как говорилось выше, есть низкоуровневые объекты Unity, для которых мы должны самостоятельно заниматься управлением памятью. В нашем случае это вычислительный буфер и render texture.
Последняя часть нашего скрипта — это функция Update(). В ней мы начинаем с создания элементов вычислительного буфера для каждой ячейки и их добавления в список элементов. Мы должны обновлять их в каждом кадре, потому что значения видимости меняются в зависимости от того, видима ли ячейка.
Эта часть понятна сама по себе — мы просто задаём значения всех переменных, которые будут необходимы в вычислительном шейдере. Тут стоит упомянуть два аспекта. Первое: мы передаём функции SetBuffer() значение 0, обозначающее индекс вычислительного ядра, для которого мы задаём буфер. У нас оно только одно, поэтому и индекс 0. Второе: мы делим радиус и расстояние смешения на физический размер карты. Мы должны гарантировать, что все длины имеют одинаковый масштаб; при работе с текстурами простейший масштаб — это масштаб UV [0;1].
Функция dispatch выполняет само вычислительное ядро (compute kernel). Первый параметр — это снова индекс ядра, который в нашем случае равен 0. Другие три параметра — это количество групп потоков в направлении x, y и z. Сейчас вы наверно сбиты с толку, так что давайте немного поговорим о том, как выполняются шейдеры в GPU.
GPU рассчитаны на параллельное выполнение одинаковых инструкций для различных данных. Например, если у нас в вершинном шейдере есть функция «x += 1» то мы параллельно прибавляем 1 к x для множества вершин. Данные не обязаны быть вершинами, они могут быть пикселями или, в случае вычислительного шейдера, практически чем угодно. Размер этих групп можно задавать в вычислительном шейдере; в нашем случае я задал значение 8x8x1. Это можно представить как то, что вычислительный шейдер одновременно рендерит 8×8 текселов текстуры.
Поэтому при диспетчеризации вычислительного шейдера важно вычислить, как часто мы должны запускать его в направлении x, y и z, чтобы покрыть всю render texture. Так как мы работаем в 2D, игнорируем z и присваиваем ей значение 1. Мы можем вычислить количество групп потоков в направлениях x и y, разделив разрешение текстуры на 8 — размер каждой группы.
Если текстура имеет размер 512 х 512, то мы должны запускать вычислительное ядро (512/8) x (512/8) = 64 x 64 = 4096 раз. В будущем я выпущу туториал, где это будет рассматриваться более подробно, но пока будет достаточно такого краткого введения в тему. Давайте начнём писать шейдеры.
Вычислительный шейдер масок
Открыв шейдер «Assets/Shaders/MaskCompute.compute», вы увидите созданную мной заготовку.
Первая строка сообщает Unity название нашего вычислительного ядра; у нас оно одно, так что тут всё просто. Вторая строка сообщает Unity размер группы потоков, в нашем случае это 8x8x1. Основы групп потоков я рассказывал в предыдущем разделе. Последний элемент здесь — это параметр id, который мы парсим в функцию. Эта переменная хранит id потока, над которым мы сейчас работаем, снова в трёх измерениях. В нашем случае id.xy — это пиксель, для которого вычисляет значение текущий поток.
Начнём с добавления в скрипт на C# основных переменных. Здесь не должно быть ничего неожиданного, помните, что радиус и расстояние смешения уже заданы в масштабе UV.
Нам нужна ещё одна переменная для вычислительного буфера, содержащая данные всех ячеек.
Помните, что каждый элемент в буфере содержит три значения float? В шейдере мы можем скомбинировать их во float3, что упростит работу с буфером.
Последняя необходимая нам переменная — это текстура маски. Так как мы должны выполнять запись в неё, для типа должны быть включены чтение/запись, поэтому это RWTexture2D. Каждый пиксель текстуры имеет тип float4, поэтому мы имеем по одному float на канал.
Теперь мы можем написать саму функцию вычислений. Давайте начнём с того, что зададим текущему текселу значение 0.
Мы должны обойти в цикле все ячейки и определить, находится ли ячейка в интервале тексела и видима ли она. Если да, то мы присваиваем маске значение 1, если нет, то оставляем его равным 0. Для этого можно использовать простой цикл.
Чтобы вычислить расстояние между текущим текселом и каждой ячейкой, обе позиции должны находиться в одном пространстве. Выше мы начали преобразовывать значения в пространство UV, поэтому давайте повторим это и для позиции тексела с центром ячейки.
Теперь мы можем вычислить расстояние между ними при помощи length().
Теперь нам осталось только вычислить, меньше ли расстояние радиуса, и если да, то изменить значение маски на 1. Так как нам нужно на границе плавное смешение, зависящее от расстояния смешения, для этого можно использовать smoothstep. Нужно умножить это значение на видимость ячейки, чтобы рендерить в маску только видимые ячейки.
Smoothstep работает следующим образом: если переменная «UVDistance» больше, чем «_Radius + _Blend», то она возвращает 0. Если переменная меньше, чем «_Radius», то она возвращает 1. Между ними значения плавно интерполируются от 0 до 1.
Нужно учесть ещё один аспект. Ячейки внутри буфера не идут в определённом порядке, поэтому тексел может находится в пределах радиуса видимой ячейки, а значение маски будет равно 1; но при этом значение маски может смениться позже в цикле на 0, потому что тексел не находится внутри радиуса следующей ячейки. Мы можем исправить это, сделав так, чтобы значение маски записывалось только тогда, когда оно больше имеющегося.
После того, как мы назначили значение маски, можно двигаться дальше. Всё просто, правда?
Давайте откроем «Assets/Shaders/TileShader.shader» и немного изменим его, чтобы он отображал маску. Этот шейдер представляет собой шаблонный поверхностный шейдер, в нём нет ничего особенного. Структуру простого поверхностного шейдера мы подробно рассматривали в предыдущем туториале о Gears Hammer of Dawn.
Чтобы наложить текстуру маски на сетку, нам нужна позиция вершины в мировом пространстве.
В поверхностных шейдерах мы можем получить её, добавив к входящей struct переменную «worldPos». Также нам нужна переменная «_MapSize» и текстура «_Mask» для масштабирования позиции в мире и сэмплирования текстуры в вычисленных координатах. Помните, как мы использовали для них глобальную переменную шейдера? Их значение должно автоматически задаваться нашим скриптом на C#. Теперь мы можем сэмплировать текстуру и присвоить цвет маски цвету albedo выходной struct поверхности.
Изменив шейдер, мы можем запустить режим Play и нажать мышью на сетку, чтобы увидеть, как маска изменяет своё значение.
Разобравшись с маской, мы можем приступить к шейдеру тайлов.
Шейдер шестиугольных тайлов
Мы начнём с добавления в шейдер нескольких свойств. Давайте разберём, что делает каждое из них.
«_MainTex» — это текстура, содержащая цветное изображение тайла, «_MapTex» — это его нарисованная от руки версия. «_Noise» — это текстура шума Перлина, которую мы используем для границы тумана войны. Значение «_Cutoff» определяет, при каком значении маски мы переходим от цветного тайла к туману войны; мы хотим, чтобы этот переход был резким. «_MapColor» — это базовый цвет карты тумана войны, обычно он светло-коричневый. «_MapEdgeColor» — это цвет, который имеет эффект у границ. Наконец, «_MapBackground» — это прозрачная фоновая текстура, которую мы накладываем поверх эффекта тумана войны, чтобы повысить его разнообразие.
Для сэмплирования «_MainTex» и «_MapTex» нам нужны UV-координаты. Мы можем их получить, добавив во входящую struct переменные float2 с префиксом «uv».
Так как свойства — это конструкции Unity, используемые для отображения переменных в инспекторе, нам нужно добавить в часть файла с кодом шейдера CG переменные с теми же именами.
Теперь мы можем начать работу над самой функцией поверхности. Первое, что мы делаем — сэмплируем две текстуры тайлов. Пока мы этим занялись, давайте сохраним значение маски в отдельную переменную.
Также нам нужно сэмплировать текстуру фона карты и текстуру шума.
Если мы будем использовать текущую маску в таком виде, то получим довольно монотонные границы в виде кругов вокруг видимой области, что будет выглядеть не очень красиво. Мы хотим, чтобы граница была более шумной, и чтобы сделать это, мы можем вычесть значение шума из значения маски. Чтобы избежать странного и неопределённого поведения в будущем, ограничим результат интервалом от 0 до 1.
Однако у этой функции есть проблема. Значение шума может быть любым в интервале от 0 до 1, поэтому так мы можем вычесть 1 из маски в том месте, где она должна быть видимой, что приведёт к пятнам, рендерящимся в виде тумана войны. Мы можем исправить это, умножив шум на значение равное величине, обратное значению маски.
Во время экспериментов я осознал, что эффект шума теперь не так видим, как должна быть вся область смешения, потому что сейчас он оказывает меньшее влияние при приближении к радиусу ячейки. Добавив функцию возведения в степень, мы можем увеличить влияние шума на область смешения.
Теперь мы можем проверить, меньше ли адаптированное значение шума чем указанное значение «_Cutoff», и если да, то отрендерить туман войны. Внешний вид тумана войны является комбинацией «_MapColor», нарисованного от руки тайла и текстуры фона.
И на этом код закончен! В следующем разделе я вкратце разберу параметры материала.
Завершающие штрихи
Все материалы имеют одинаковую структуру, отличаются только текстуры цвета и карты. В моих примерах я использую значение cutoff, равное 0.3, цвет карты в моём случае равен #BCA76E, цвет границы — #574A36.
Если вы не изменяли карту, то размер должен быть равен 26, я использую радиус 1.0 и расстояние смешения 0.8.
Готово!
Этот туториал получился короче предыдущего; тем не менее, вам должно понравиться с ним работать. Если в процессе работы у вас возникнут проблемы, то сверьтесь с готовой версией проекта.