Помните небезызвестный мем про "корованы"? Наверное, каждый, кто разрабатывает игры (или хотел бы этим заняться) раздумывает о неком "проекте мечты", где можно будет "грабить корованы" и "набигать". А ещё, чтобы погода менялась динамически, и на грязи следы от сапог оставались, и деревья росли в реальном времени. И ещё, чтобы ... Понятно, что в реальном игровом проекте такая погоня за хотелками — смерти подобна. А вот в техно-демке — самое то. Часть первая. Свет.
Часть вторая. Структура.
Часть третья. Глобальное освещение. Напомню, что в прошлой части был опрос: на какую тему написать следующую статью. И динамическая вода показалась сообществу наиболее интересной. Но это долгая история и начать придется со структуры проекта и алгоритмах, которые используются под капотом, а затем рассказать о доработках освещения и постэффектах. Текст получится большой, так что вода переезжает дальше, в одну из ближайших статей. Кстати, эта статья содержит поменьше красивых рендеров, но куда больше технических хинтов. Не скучайте. С момента написания предыдущей статьи было сделано очень многое. Переписан начисто весь код проекта, оптимизированы алгоритмы, добавлены новые источники света, реализовано фоновое освещение, реализована вода с бликами, волнами, кипением и замерзанием. Как вы могли заметить — ни слова о персонажах или геймплейной составляющей, это ещё впереди. Не буду забегать вперед и расскажу всё по порядку. Наш проект про магов, а значит, без старых каменных за́мков не обойтись. Вот только рисовать каждый с нуля — себе дороже. Попробуем собирать их из небольших кусочков, например, вот таких: Аккуратные кусочки, из которых можно сделать ВСЁ. Чтобы у этих кусочков был общий контур, напишем какой-нибудь шейдер, а статический батчинг в Unity3d оптимизирует количество вызовов отрисовки. Вот только чтобы получить общий контур, придется использовать двухпроходный шейдер со stencil буфером: первая часть нарисует контуры, а вторая — заливку. А любые элементы, которые используют материалы с многопроходными шейдерами в батчинге не участвуют. Лучше отрисовывать каждый спрайт дважды, но с разными материалами. Количество вершин увеличится, зато вызовов отрисовки останется всего два. Рендерим сплошной контур. Добавляем текстуру. Таким нехитрым способом можем создать вот такой за́мок: Стены в редакторе. Текстуру и цвет контура можно настраивать отдельно. 
 Автоматизируем перенос. Изначально у меня уже были префабы-спрайты, которые использовались на сцене (несколько сотен элементов). Когда я решил обернуть их в Contour, изменения префабов почему-то не применялись к созданным объектам. К счастью, можно легко написать скрипт, который для каждого существующего элемента найдет соответствующий префаб (по имени), и создаст в нужной позиции элемент из этого префаба. Ключевые методы — UnityEditor.AssetDatabase.LoadAssetAtPath и UnityEditor.PrefabUtility.ConnectGameObjectToPrefab Правильный drag'n'drop. Минус разделения на спрайты — теперь по умолчанию на сцене выбирается и используется в drag'n'drop'е один из спрайтов-наследников. Решается проблема добавлением атрибута  перед классом Contour. Отображение префабов. В меню проекта префабы с контурами теперь не отображаются как спрайты, и, если честно, я не нашел способа самому генерировать иконку. Поэтому в префабы я добавил ещё и SpriteRenderer, спрайт которому задаёт мой Contour. При добавлении на сцену я удаляю из объекта не нужный в геймплее SpriteRenderer. Удаление из OnValidate. При добавлении объекта на сцену вызывается OnValidate и именно там я удаляю SpriteRenderer. Вот только ни Destroy, ни DestroyImmediate в этом методе не работают (без колдовства с собственным редактором класса), поэтому использую такой костыль: Правда, иногда придется делать мешанину из спрайтов: если мы хотим получить большой и сложный объект, как поверхность земли с ямами и пещерами. Вместо этого будем генерировать такие элементы на лету, в редакторе (как в PolygonCollider2D). Рендерим полигоны через стандартный MeshRenderer с двумя материалами (для разделения контура и заливки используются submeshes). Редактор, неотличимый от редактора в PolygonCollider2D. Для максимальной гибкости классы для полигонов выстроены так: Почему-то я очень долго упускал из виду атрибут RequireComponent. Это очень удобный механизм для автоматического добавления нужных компонентов. Например, ShapeRenderer требует EditableShape и MeshRenderer, при добавлении его в GameObject автоматически создаются все зависимости. Используя крупные пиксели мы убиваем целую стаю зайцев: Правда за этих зайцев придется заплатить, реализовав поддержку "целочисленной геометрии", а именно: Помните, в прошлой статье был раздел про построение теней? И больша́я часть была посвящена объединению спрайтов в некие группы для оптимизации финального меша? Так вот, забудьте, это все неправда. :) Изначально все модули писались независимо, в режиме прототипирования. И для каждого придумывались свои алгоритмы, структуры и типы данных. В результате возникло две крупных проблемы (помимо legacy-кода говнокода, будем честны): Вот такие модули требуют какой-то информации о "твердых" спрайтах за́мков: Все это безумие используется вот так: После некоторого анализа и длительного гугления находим решение — Region tree, иногда называемый Volume tree. Разбиваем двумерное пространство на 4 части до тех пор, пока каждый лист не окажется либо полностью пустым, либо полностью заполненным. Выставляем листу бит заполненности (или любым другим способом отличаем пустые узлы от заполненных). Возможности, которые дает это дерево, покрывают все наши потребности: Визуально это выглядит так (ура, картинки!): Визуальная часть. Стены из спрайтов, земля — полигоны. Предрасчитанный region tree. Предрасчитанные поверхности. Как оказалось, эта дерево квадрантов реализует почти все возможности, которые нужны модулям. Теперь нужно связать эти модули друг с другом. В "прототипной" версии все контроллеры/менеджеры сами реализовывали соответствующий функционал (расчет освещения, обработку физики частиц и т.д) и наследовались от MonoBehaviour. Возникало несколько проблем: сложный и разросшийся код, сильная зависимость менеджеров друг от друга, отсутствие какого-то общего потока данных между контроллерами. Например, когда в редакторе я передвигал какой-то элемент, менеджеры не подцепляли эти изменения автоматически. Приходилось сначала тыкнуть галку в менеджере дерева, затем в менеджере света, затем в менеджере воды и т.д. И всё ради того, чтобы посмотреть, хорошо ли выглядит новый за́мок. Так себе, правда? Во-первых, по максимуму избавимся от MonoBehaviour. Все объекты по возможности представляются обычными с# классами. Во-вторых, разнесем код по разным пространствам имён, одно имя — один функционал.
И, в-третьих, для каждого функционала реализуем один MonoBehaviour-менеджер, который будет хранить нужные настройки, управлять генерацией контента и т.д. Менеджеры. Причёсанные и в галстуках. Итак, на сцене лежат контроллеры, у каждого одна сфера влияния, код аккуратно упакован в пространство имён NewEngine.Core (Core.Geom, Core.Illumination, Core.Rendering и т.д.). Двигаем спрайт в редакторе и… никакой реакции. Помните, выше был описан класс PoserListener? Он умеет слушать изменения позиции, спрайта, размера у объектов типа Poser. Все менеджеры, которые зависят соответствующих GameObject'ов наследуем от этого класса. Теперь, когда мы двигаем кусок стены (c тегом "foreground") уведомляется Core.Quad.QuadManager, а когда меняем опорные точки для воды (тег "waterLayer") Core.Water.WaterManager сразу же узнает об изменениях. Осталось связать контроллеры между собой, ведь вышеописанному WaterManager требуется знать, когда будет перестроено дерево квадрантов в QuadManager, а ShadowMeshManager'у важно подхватить изменения в SurfaceManager'е. Для этого воспользуемся очень удобным UnityEvent. Его единственный недостаток — по умолчанию, если мы создаём событие-generic с каким-нибудь своим аргументом, Unity3D не отображает его в редакторе. Это исправляется элементарно: И теперь мы можем связывать менеджеры прямо в редакторе, не загрязняя код странными зависимостями: При необходимости зависимости выполняются и в редакторе. Итак, все модули на старте генерируют необходимый контент, обновляют его, но рендерить не умеют. Время это исправить! На самом деле, рендеринг в этом проекте сильно отличается от того, что показывают на youtube в роликах с говорящими названиями "Запилим свою мега-крутую игру на Unity3D из трёх боксов, одного спрайта и компонента RigidBody2D". На самом деле, просто отрисовать нужно только сами спрайты стен и фона. А вот свет, воду и прочее придется делать через постэффекты. Что это означает? Что рендеринг элементов мы будем делать не на экран, а в буферы, затем с помощью различных шейдеров сводить эти всё одну картинку. И всего-то. Эффектов получается немало. На данный момент это: В общем случае постэффект — это некий скрипт, который умеет генерировать некую текстуру/текстуры и, возможно, дополнительные данные (например, параметры шейдеров). Иногда постэффект работает только с модулями, иногда ему приходится вызывать рендеринг сцены с определенными настройками камеры. Важный момент: постэффекты могут зависеть друг от друга. Поэтому, во-первых, важно вызывать эффекты в правильном порядке, во-вторых, нужно уметь хранить и передавать эффектам разношерстные данные о друг друге. Делаем базовый класс для эффекта. Примерно такой: И ещё делаем класс контекста, для связывания данных из постэффектов. По сути, теперь в менеджере рендеринга нам нужно пройтись в правильном порядке по всем активным эффектам, вызывая у них Apply и собирая данные в PipelineContext. В результате у нас отрисуется красивый кадр. Эффекты попадают в менеджер через аналог Poser, который сообщает слушателю о добавлении/удалении эффектов со сцены. Осталось только правильно их сортировать. И было бы круто сделать красивые атрибуты, которые добавлялись бы в постэффекты и автоматически определяли зависимости и порядок, как-то так: Просто перетаскиваем все постэффекты в общем списке. Список создается автоматически из эффектов на сцене в редакторе. Аккуратные алгоритмы обрабатывают данные и создают траву, воду и свет, контроллеры следят за их обновлением, постэффекты — за рендерингом. Аккуратно, быстро и достаточно чисто. И самое главное, готова основа для написания действительно интересных штук! Например, замерзающей воды или светящейся плесени в подземельях. И ведь это вполне геймплейные фишки: В следующей статье попробую рассказать о новых постэффектах и освещении, а пока, на закуску, немного видео и картинок. Спасибо за ваше внимание. Вот про такую воду я обещал вам рассказать :) Зависимости эффектов до рефакторинга. Ну почему все работают с радианами, а Unity3D с градусами? 0о Небольшой, но красивый косяк в поиске поверхностей. Это не ядерный реактор, просто некие рейкасты прорвались наружу. Программист