Спрайты воды


Спрайты воды

Помните небезызвестный мем про "корованы"? Наверное, каждый, кто разрабатывает игры (или хотел бы этим заняться) раздумывает о неком "проекте мечты", где можно будет "грабить корованы" и "набигать". А ещё, чтобы погода менялась динамически, и на грязи следы от сапог оставались, и деревья росли в реальном времени. И ещё, чтобы …

Понятно, что в реальном игровом проекте такая погоня за хотелками — смерти подобна. А вот в техно-демке — самое то.

Предыдущие статьи

Часть первая. Свет.
Часть вторая. Структура.
Часть третья. Глобальное освещение.

Оглавление

  1. Вступление
  2. Спрайты
  3. Полигоны
  4. Pixel perfect и целочисленная геометрия
  5. Старая структура проекта
  6. Region tree
  7. Менеджеры
  8. Постэффекты
  9. Мысли о будущем

Вступление

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

С момента написания предыдущей статьи было сделано очень многое. Переписан начисто весь код проекта, оптимизированы алгоритмы, добавлены новые источники света, реализовано фоновое освещение, реализована вода с бликами, волнами, кипением и замерзанием. Как вы могли заметить — ни слова о персонажах или геймплейной составляющей, это ещё впереди. Не буду забегать вперед и расскажу всё по порядку.

Спрайты

Наш проект про магов, а значит, без старых каменных за́мков не обойтись. Вот только рисовать каждый с нуля — себе дороже. Попробуем собирать их из небольших кусочков, например, вот таких:

Спрайты воды

Аккуратные кусочки, из которых можно сделать ВСЁ.

Чтобы у этих кусочков был общий контур, напишем какой-нибудь шейдер, а статический батчинг в Unity3d оптимизирует количество вызовов отрисовки.


т только чтобы получить общий контур, придется использовать двухпроходный шейдер со stencil буфером: первая часть нарисует контуры, а вторая — заливку. А любые элементы, которые используют материалы с многопроходными шейдерами в батчинге не участвуют. Лучше отрисовывать каждый спрайт дважды, но с разными материалами. Количество вершин увеличится, зато вызовов отрисовки останется всего два.

Спрайты воды

Рендерим сплошной контур.

Спрайты воды

Добавляем текстуру.

Таким нехитрым способом можем создать вот такой за́мок:

Спрайты воды

Стены в редакторе. Текстуру и цвет контура можно настраивать отдельно.

Полигоны

Правда, иногда придется делать мешанину из спрайтов: если мы хотим получить большой и сложный объект, как поверхность земли с ямами и пещерами. Вместо этого будем генерировать такие элементы на лету, в редакторе (как в PolygonCollider2D). Рендерим полигоны через стандартный MeshRenderer с двумя материалами (для разделения контура и заливки используются submeshes).

Не стану утверждать, что написать аккуратный редактор для полигонов просто. Но вся информация доступна в интернете, а в AssetStore есть готовые решения.

Редактор, неотличимый от редактора в PolygonCollider2D.

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


  • Core.Shapes.Shape — основной класс, содержащий точки полигона и нужную математику. Не MonoBehaviour;
  • Core.Shapes.EditableShape — наследник MonoBehaviour для хранения и редактирования Shape;
  • Core.Shapes.ShapeRenderer — отображает полигон из EditableShape с помощью MeshRenderer;
  • Core.Shapes.ShapeCollider2D — создает физический полигон из EditableShape с помощью PolygonCollider2D.

Pixel perfect и целочисленная геометрия

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

  • Можем рендерить небольшое изображение (пиксель в пиксель) и растягивать его под экран;
  • Можем использовать сложные постэффекты (небольшие текстуры увеличат производительность);
  • Можем работать с целочисленной арифметикой (быстрее и приятнее);
  • Можем маскировать недоработки или баги 🙂

Правда за этих зайцев придется заплатить, реализовав поддержку "целочисленной геометрии", а именно:


  • Базовые вещи, такие как:
    • IntVector2 — по сути, копия UnityEngine.Vector2, но с целочисленными координатами. Иногда нужно конвертировать данные из обычного UnityEngine.Vector2, с учётом масштаба или без (в одном юните 32 пикселя, только не нужно использовать магические числа!), поэтому добавляем следующие функции:
      public Vector2 ToPixels(); public Vector2 ToUnits(); public static IntVector2 FromPixels(Vector2 v); public static IntVector2 FromUnits(Vector2 v); public static IntVector2 FromUnitsRound(Vector2 v); public static IntVector2 FromUnitsCeil(Vector2 v);
    • IntRect. Ничем не отличается по функциональности от UnityEngine.Rect. Доступен метод LineCollision для поиска пересечений с отрезками прямых (алгоритм Лианга-Барски);
    • IntLine. Отрезок прямой с целочисленным началом и концом. Используется в некоторых алгоритмах;
    • IntMatrix. Матрица для двумерных преобразований, где повороты кратны 90°, а смещение и масштаб — целочисленны;
    • IntAngle. Небольшой класс для округления градусов до 90°, выбора направления. Представляете, какая там красивая таблица косинусов?
    • Poser. Элемент, позиционирующий GameObject кратно игровому "пикселю", запрещающий повороты, не кратные прямому углу и масштабирование на дробное значение (с учетом того, что в элементе может быть SpriteRender и тогда придется учитывать pivot спрайта).

      от класс работает в редакторе благодаря атрибуту [ExecuteInEditMode]. Важный момент: при изменении состояния (позиции, поворота, масштаба, спрайта и т.д.) Poser уведомляет об этом класс PoserListener, который умеет отслеживать изменения в редакторе всех элементов Poser с определенным тегом. Это понадобится в дальнейшем;
    • CameraManager. Контроллер камер, который умеет выравнивать текущую камеру и позиционировать камеры для постэффектов.

Старая структура проекта

Помните, в прошлой статье был раздел про построение теней? И больша́я часть была посвящена объединению спрайтов в некие группы для оптимизации финального меша? Так вот, забудьте, это все неправда. 🙂

Изначально все модули писались независимо, в режиме прототипирования. И для каждого придумывались свои алгоритмы, структуры и типы данных. В результате возникло две крупных проблемы (помимо legacy-кода говнокода, будем честны):

  1. Дублирование кода. Одних только классов для целочисленных координат было штуки 3-4 и все — вложенные в другие классы (обычная точка, точка с нормалью, точка с какой-то метаинформацией).
  2. Не оптимальные решения. Каждому модулю были нужны одни и те же данные о спрайтах, но чуть чуть-чуть по-разному обработанные. И эти данные копировались туда-сюда со всякими преобразованиями, что не добавляло ни скорости, ни изящества кода.

Вот такие модули требуют какой-то информации о "твердых" спрайтах за́мков:


  • Тени;
  • Ветер;
  • Рассеянное освещение;
  • Трава;
  • Частицы;
  • Физика;
  • Вода;
  • Прочее (нити паутины и цепи светильников).

Region tree

После некоторого анализа и длительного гугления находим решение — Region tree, иногда называемый Volume tree.

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

Возможности, которые дает это дерево, покрывают все наши потребности:

  • Построение дерева. Можно заполнять дерево, указывая твердые точки, прямоугольники или даже другие деревья с неким смещением. Поэтому для спрайтов предрасчитываются собственные Region tree, а затем, при добавлении на сцену, строится общее дерево.
  • Проверка твердости точки. Рекурсивно спускаемся по дереву, пока у узла есть потомки (в моей версии дерева узел пустой, если Node = null, полный — если Node.children == null, в противном случае в Node.children — массив потомков);

  • Raycast. Рекурсивно проверяем пересечения луча с квадратами узлов.
  • Поиск кратчайшего пути до пустого пространства. Находим лист в заданной точке, поднимаемся по дереву вверх, проверяя узлы соседей (если узел пустой, сразу считаем расстояние до него, если есть потомки — спускаемся рекурсивно вниз и снова ищем ближайший пустой).
  • Поиск отрезков, лежащих на границе. Тут сложнее, если кратко — получаем все заполненные квадраты из дерева, убираем стороны, принадлежащие нескольких квадратам, оптимизируем результат.

Визуально это выглядит так (ура, картинки!):

Спрайты воды

Визуальная часть. Стены из спрайтов, земля — полигоны.

Спрайты воды

Предрасчитанный region tree.

Спрайты воды

Предрасчитанные поверхности.

Менеджеры

Как оказалось, эта дерево квадрантов реализует почти все возможности, которые нужны модулям. Теперь нужно связать эти модули друг с другом.

Во-первых, по максимуму избавимся от 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 не отображает его в редакторе. Это исправляется элементарно:

public class TreeManager : MonoBehaviour {  [System.Serializable]  public class UpdateTreeEvent : UnityEvent<TreeManager> {  }   public UpdateTreeEvent onUpdateTree;  ... }  

И теперь мы можем связывать менеджеры прямо в редакторе, не загрязняя код странными зависимостями:

Спрайты воды

При необходимости зависимости выполняются и в редакторе.

Постэффекты.

Итак, все модули на старте генерируют необходимый контент, обновляют его, но рендерить не умеют. Время это исправить!

На самом деле, рендеринг в этом проекте сильно отличается от того, что показывают на youtube в роликах с говорящими названиями "Запилим свою мега-крутую игру на Unity3D из трёх боксов, одного спрайта и компонента RigidBody2D". На самом деле, просто отрисовать нужно только сами спрайты стен и фона. А вот свет, воду и прочее придется делать через постэффекты.

Что это означает? Что рендеринг элементов мы будем делать не на экран, а в буферы, затем с помощью различных шейдеров сводить эти всё одну картинку. И всего-то.

Эффектов получается немало. На данный момент это:


  • Рендеринг сцены. Не совсем постэффект, просто отрисовка основной геометрии сцены.
  • Глобальное освещение. Расчитывается один раз, на старте уровня;
  • Обычное освещение. Включает в себя источники света, тени, каустику в воде;
  • Световые декали. Небольшие спрайты, которые создают эффект люминесценции.
  • Постэффект сведения. Объединяет результаты всех предыдущих постэффектов в одну картинку (например, применяет эффекты освещения к отрендеренной сцене).
  • И, наконец, рендеринг на экран. Который просто выводит получившуюся текстуру с учётом pixel perfect и разницы в размерах у экрана и текстуры.

В общем случае постэффект — это некий скрипт, который умеет генерировать некую текстуру/текстуры и, возможно, дополнительные данные (например, параметры шейдеров). Иногда постэффект работает только с модулями, иногда ему приходится вызывать рендеринг сцены с определенными настройками камеры.

Важный момент: постэффекты могут зависеть друг от друга. Поэтому, во-первых, важно вызывать эффекты в правильном порядке, во-вторых, нужно уметь хранить и передавать эффектам разношерстные данные о друг друге.

Делаем базовый класс для эффекта. Примерно такой:

namespace NewEngine.Core.Render {  public abstract class PostEffect {  public int OrderId { get; set; } // Понадобится для упорядочивания эффектов.   public abstract void Apply(PipelineContext context); // Обработка эффекта. Исходные данные о других эффектах находятся в PipelineContext, результат сохраняется туда же.  public abstract void Clear(); // Удаление временных текстур. Получается быстрее, чем при GC, почему поясню позднее.   protected Camera CreateCamera(); // Некоторым постэффектам нужна внутренняя камера. Например, чтобы отрисовать источники света.  public List<Camera> Cameras { get; }  } }

И ещё делаем класс контекста, для связывания данных из постэффектов.

namespace NewEngine.Core.Render {  public class PipelineContext {  Dictionary<System.Type, PostEffectContext>;  Camera camera;  Geom.IntRect viewRect;   public PipelineContext(CameraManager cameraManager);  public void Set<Context>(Context value) where Context : PostEffectContext;  public Context Get<Context>() where Context : PostEffectContext;   public Camera Camera { get; }  public Geom.IntRect ViewRect { get; }  } }

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

И было бы круто сделать красивые атрибуты, которые добавлялись бы в постэффекты и автоматически определяли зависимости и порядок, как-то так:

[RequiredPostEffect(typeof(WaterPostEffect))] [RequiredPostEffect(typeof(IlluminationPostEffect))] public class MergerPostEffect : PostEffect { }

Но, по правде говоря, мне было лень это делать и я написал очередной PropertyDrawer:

Просто перетаскиваем все постэффекты в общем списке. Список создается автоматически из эффектов на сцене в редакторе.

Мысли о будущем

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

… лучники начали стрелять с противоположного берега. Маг взорвал файрбол над рекой, вызвав настоящее цунами. Но волна не добралась до берега — волшебник заморозил воду и спрятался от стрел за стеной льда.

… десятый поток пламени так накалил каменный пол, что тот начал светится, как будущий меч в руках кузнеца. «Ни одна живая душа не сможет пройти за мной» — решил маг.

В следующей статье попробую рассказать о новых постэффектах и освещении, а пока, на закуску, немного видео и картинок. Спасибо за ваше внимание.

Вот про такую воду я обещал вам рассказать 🙂

Источник: habr.com

Свойства Спрайта

Пищевая ценность | Витамины | Минеральные вещества

Сколько стоит Спрайт ( средняя цена за 1 л.)?

В настоящее время известны сотни (если не тысячи) алкогольных и безалкогольных напитков. Они возникали с момента появления человека, то есть на протяжении последних нескольких десятков тысяч лет. Современная наука и техника позволяет изготавливать напитки, поражающие своим вкусом и запахом. К ним следует отнести Спрайт. Газированный безалкогольный напиток имеет вкус лайма и лимона. Права на его изготовление принадлежат компании The Coca-Cola Company.

Появление продукта относится к октябрю 1961 года. Своё название Спрайт получил в честь персонажа рекламной компании – Спрайт-Боя, который являлся мальчиком-эльфом с серебряными волосами. Он был придуман иллюстратором Хэддоном Сэндбломом и использовался на протяжении 1940-50 годов.

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

Состав напитка

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

Польза и вред Спрайта

Разговоры о пользе и вреде безалкогольного напитка ведутся постоянно. У каждого лагеря имеются свои сторонники. Например, утверждается, что спрайт способствует повышению настроения, а также улучшению общей работоспособности. Следует также заметить, что напиток действительно неплохо справляется с жаждой.

Разумеется, что «химический» состав нашел своё отражение в негативных свойствах Спрайта. Отмечается, что чрезмерное употребление напитка может привести к язве желудка, проблемам с печенью и слизистой. Не меньше страдает эмаль зубов. Пить Спрайт категорически запрещено людям, страдающим сахарным диабетом или имеющим индивидуальную непереносимость. Отметим, что лучше всего использовать безалкогольный газированный напиток в составе с другими ингредиентами, например, в коктейлях.

Источник: foodfor.ru


You May Also Like

About the Author: admind

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.