Уроки Unreal Engine

Unreal Engine 4: Что такое Toon-контуры

В этом уроке, посвященном освоению Unreal Engine 4, вы узнаете, как создавать Toon- контуры с помощью инвертированных сеток и постобработки.

Когда люди говорят «Toon-контуры», они имеют в виду любую технику, которая визуализирует линии вокруг игровых объектов. Как и cel shading, эти контуры делают игру более стилизованной. Они могут создать впечатление, что объекты нарисованы краской или тушью. Вы можете увидеть примеры этого стиля в таких играх, как Okami, Borderlands и Dragon Ball FighterZ.

В этом уроке вы узнаете, как:

  • Создавать контуры с использованием перевернутой сетки
  • Создание контуров с помощью постобработки и свертки
  • Создать и использовать функции различных материалов
  • Соединять находящиеся рядом пиксели

Примечание: в этом уроке подразумевается, что вы уже знаете основы Unreal Engine, однако, если вам необходимо освежить ваши знания, то рекомендуется изучить серию уроков, посвященных Unreal Engine для начинающих.

Это руководство является частью серии, которая состоит из четырех уроков, посвященный шейдерам в Unreal Engine:

Введение

Перед тем, как приступить к работе, скачайте материалы, необходимые для данного урока. Разархивируйте файл, перейдите к ToonOutlineStarter и откройте ToonOutline.uproject. перед вами появится следующая сцена:

Для начала вы создадите контуры с помощью инвертированной сетки.

Контуры инвертированной сетки

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

Если вы сейчас используете дубликат, то он полностью перекроет оригинал.

Чтобы это исправить, вы можете инвертировать нормали дубликата. При включенном параметре backface culling вы сможете вместо внешних увидеть внутренние грани.

Это позволит оригиналу просвечивать через дубликат, а поскольку он меньше копии, то в итоге вы получите контур.

Преимущества метода:

  • У вас всегда будут четкие линии, так как контур состоит из многоугольников
  • Внешний вид и толщина линий легко регулируются путем перемещения вершин
  • При изменении масштаба контур становится тоньше, однако это свойство также можно рассматривать как недостаток.

Недостатки метода:

  • Как правило, не детали внутри сетки пропадают
  • Поскольку контур состоит из многоугольников, они отсекаются.
  • Негативно сказывается на качестве изображения. Однако это также зависит от того, сколько полигонов имеет ваша сетка. Поскольку вы используете дубликаты, количество полигонов удваивается.
  • Лучше работает на гладких и выпуклых сетках. Жесткие края и вогнутые области создадут отверстия в контуре, как показано на изображении ниже.

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

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

Создание материала инвертированной сетки

Для этого метода вы будете маскировать внешние грани, чтобы у вас получились в итоге полигоны с гранями, обращенными внутрь.

Примечание: из-за маскировки этот метод немного более затратный по времени, чем ручной процесс.

Перейдите в папку «Materials» и откройте M_Inverted. Затем перейдите на панель «Подробности» и настройте следующие параметры:

  • Blend Mode: установите для этого параметра значение «Masked». Это позволит вам помечать нужные области как видимые или невидимые. Вы можете отрегулировать порог, отредактировав значение непрозрачности Mask Clip.
  • Shading Model: установите для этого параметра значение Unlit. Это сделает так, чтобы освещение не влияло на вашу сетку.
  • Two Sided: установите для этого параметра значение. По умолчанию Unreal отбраковывает задние грани, но активация этой опции отключит выборку граней задней поверхности изображения. Если оставить отсечение задних граней включенным, то вы не увидите полигоны с гранями внутрь.

Теперь создайте параметр Vector и назовите его OutlineColor – именно он будет контролировать цвет созданного вами контура. Подключите его к Emissive Color, как показано на изображении ниже:

Чтобы замаскировать внешние полигоны, создайте TwoSidedSign, умножьте его на -1 и соедините получившийся результат с Opacity Mask.

TwoSidedSign выводит значение 1 для лицевых сторон и -1 для задних. Это означает, что передние грани остаются видимыми, а задние грани становятся невидимы. Однако, если вы хотите противоположный эффект, то нужно поменять знаки, умножив значения на -1. Теперь frontfaces будет выводить —1, а backfaces отобразит 1.

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

В Unreal вы можете перемещать положение каждой вершины, используя инструмент World Position Offset. Умножая нормаль вершины на OutlineThickness, вы делаете сетку более толстой. Посмотрите, как это выглядит на практике:

На данный момент материал можно считать завершенным. Нажмите Apply, а затем закройте M_Inverted. Теперь вам нужно продублировать сетку и применить к ней материал, который вы только что создали.

Дублирование сетки

Перейдите в папку Blueprints и откройте BP_Viking. Добавьте компонент Static Mesh как дочерний элемент Mesh и назовите его Outline.

Убедитесь, что вы выбрали Outline и установите для Static Mesh значение SM_Viking. После этого установите для его материала значение MI_Inverted.

MI_Inverted является экземпляром M_Inverted и это позволит вам настроить параметры OutlineColor и OutlineThickness без перекомпиляции.

Нажмите Compile, а затем закройте BP_Viking. Теперь у викинга есть контур цвет и толщина которого регулируются при помощи параметров MI_Inverted.

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

Если вы хотите создавать контуры другим способом, вы можете использовать постобработку.

Метод постобработки

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

Преимущества:

  • Может запросто применяться ко всей сцене сразу
  • Фиксированная стоимость производительности, поскольку шейдер всегда работает для каждого пикселя
  • При изменении масштаба ширина линии остается неизменной, однако это свойство может быть недостатком.
  • Линии контура не отсекаются геометрией, поскольку это эффект постобработки

Недостатки:

  • Обычно требуется несколько граней краев для захвата всех точек, а это влияет на производительность.
  • Этот метод создает некоторый шум, поэтому края могут отображаться в областях с большим отклонением.
  • Обычно распознавание рёбер выполняется свёрткой каждого пикселя.

Что такое свертка?

Свертка представляет собой операцию над двумя группами чисел для получения одного значения и используется при обработке изображений. Сначала вы берете сетку чисел (известную также как ядро) и размещаете центр над каждым пикселем. Ниже приведен пример ядра размером 3 × 3 пикселя, перемещающегося по верхним двум строкам изображения:

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

Сначала поместите ядро так, чтобы целевой пиксель был в центре. Затем умножьте каждый элемент ядра на пиксель, на который он накладывается.

Теперь сложите все результаты вместе, чтобы получить новое значение для центрального пикселя. В этом случае новое значение равно 0,5 + 0,5 или 1. Вот как выглядит изображение после выполнения свертки для каждого пикселя:

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

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

Лапласово распознавание рёбер

Вы уже видели подобное ядро в примерах из предыдущего раздела:

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

Посмотрите на пример такого ядра:

Сначала поместите ядро поверх крайнего пикселя, а затем выполните свертку:

Это даст вам значение 1, которое указывает, что произошли большие изменения и целевой пиксель, вероятнее всего является ребром.

Далее, вам нужно сделать свертку с меньшей дисперсией:

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

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

Теперь начинается самое интересное – вам предстоит создать материал постобработки, который будет выполнять лапласово распознавание рёбер в буфере глубин.

Построение лапласова распознавателя рёбер

Перейдите в папку «Maps» и откройте PostProcess — вы должны увидеть черный экран. Это связано с тем, что на карте содержится информация Post Process Volume с использованием пустого материала.

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

Чтобы получить позицию текущего пикселя, вы можете использовать TextureCoordinate. Например, если текущий пиксель находится посередине, то он вернёт (0.5, 0.5). Этот двухкомпонентный вектор называется UV.

Чтобы сэмплировать другой пиксель, вам нужно добавить смещение к координатам текстуры. В изображении 100×100 каждый пиксель имеет размер 0,01 в UV-пространстве. Чтобы выбрать пиксель справа, вам нужно добавить 0,01 по оси X.

Однако на данный при изменении разрешения изображения также изменяется размер пикселя. Если вы используете одно и то же смещение (0,01, 0) в изображении 200×200, оно сэмплирует сразу два пикселя вправо.

Чтобы это исправить, вы можете использовать узел SceneTexelSize, который возвращает размер пикселя:

Так как вы собираетесь делать выборку из нескольких пикселей, вам придется повторить этот процесс несколько раз.

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

Примечание: материальная функция похожа на функцию, которые используются в Blueprints или C++.

Создание примера функции пикселя

Сначала перейдите в папку MaterialsPostProcess и чтобы создать функцию материала, нажмите Add New. Теперь выберите Materials & TexturesMaterial Function.

Переименуйте его в MF_GetPixelDepth и откройте, чтобы график имел только один FunctionOutput.

Теперь вам нужно создать вход, который будет получать данные о смещении. Для этого создайте FunctionInput:

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

Теперь необходимо задать несколько параметров для входа. Выберите FunctionInput и перейдите к панели Details. Измените следующие параметры:

  • InputName: Offset
  • InputType: Function Input Vector 2. Поскольку буфер глубин является 2D-изображением, смещение должно иметь тип Vector 2.
  • Use Preview Value as Default: Если вы не передадите входное значение, то функция будет использовать значение из Preview Value.

Далее вам нужно умножить смещение на размер в пикселях. Для этого нужно добавить результат в координату текстуры и следующие узлы:

В конце вам нужно сэмплировать буфер глубины, используя предоставленные UV. Добавьте SceneDepth и подключите все следующим образом:

Примечание: вы также можете для этих целей использовать SceneTexture со значением SceneDepth.

Резюмируя:

  1. Offset получает Vector 2 и умножает его на SceneTexelSize. Это даёт вам смещение в UV-пространстве.
  2. Прибавляется смещение к TextureCoordinate, чтобы получить пиксель, находящийся на расстоянии (x, y) пикселей от текущего.
  3. SceneDepth использует переданные UV для сэмплирования соответствующего пикселя, а затем выводит его.

На этом работа с функцией материала закончена. Нажмите на Apply и закройте MF_GetPixelDepth.

Примечание: на панели «Stats» может появиться сообщение о том, что из глубины сцены считываются только полупрозрачные или постпроцессные материалы. Вы можете смело игнорировать это сообщение.

Выполнение свертки

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

Откройте PP_Outline и создайте четыре узла Constant2Vector. Задайте им следующие параметры:

  • (-1, 0)
  • (1, 0)
  • (0, -1)
  • (0, 1)

Далее вам нужно сэмплировать сразу пять пикселей в ядре. Создайте пять узлов MaterialFunctionCall и установите для каждого соответствующее значение MF_GetPixelDepth. После этого подключите каждое смещение к их собственной функции, как показано на изображении ниже:

Далее идет стадия умножения. Поскольку множитель для соседних пикселей равен 1, вы можете пропустить этот шаг. Однако вам все равно нужно умножить центральный пиксель (нижняя функция) на -4.

Далее вам необходимо суммировать все значения. Создайте четыре узла Add и соедините их следующим образом:

Если вы помните график значений пикселей, то заметите, что некоторые из них имеют отрицательное значение. При использовании материала как есть, отрицательные пиксели будут отображаться чёрным, потому что они меньше нуля. Чтобы исправить это, вы можете получить абсолютное значение, которое преобразует все входные данные в положительное значение. Добавьте узе Abs и соедините всё следующим образом:

Резюмируя:

  1. Ноды MF_GetPixelDepth получают значение глубины от центрального, левого, правого, верхнего и нижнего пикселей
  2. Каждый пиксель умножается на его соответствующее значение ядра. В данном случае достаточно умножить только центральный пиксель.
  3. Вычисляется сумма всех пикселей
  4. Выводится абсолютное значение суммы. Это не позволит пикселям с отрицательными значениями отображаться в чёрном цвете.

Нажмите Apply и вернитесь к основному редактору. Посмотрите, на изображении теперь появились линии:

Кроме того, также появились некоторые проблемы. Во-первых, существуют рёбра, у которых разница глубин незначительна. Во-вторых, на фоне присутствуют круговые линии, потому что он является сферой. Это не проблема, если ограничить распознавание рёбер только сетками. Однако если вы хотите создавать линии во всей сцене, то присутствие таких окружностей нежелательно.

Использование порогов

Для начала вам нужно исправить линии, которые появились из-за небольших различий в глубине. Вернитесь в редактор материалов и создайте настройки, как показано на изображении ниже. Убедитесь, что вы установили значение для Threshold = 4.

Позже вы соедините полученный результат распознавания рёбер с точкой A. Он будет выводить значение 1, если значение пикселя выше 4. В противном случае он будет выводить 0.

Теперь вам необходимо избавимся от линий на фоне. Создайте схему, показанную ниже и присвойте DepthCutoff значение 9000.

Таким образом, на выход будет передаваться значение 0, если глубина текущего пикселя больше 9000. В противном случае, на выход будет передаваться значение A<B.

Теперь соедините узлы следующим образом:

Обратите внимание – теперь линии будут отображаться только тогда, когда значение пикселя больше 4 (Threshold), а его глубина меньше 9000 (DepthCutoff).

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

Примечание: вы можете создать экземпляр материала PP_Outline для управления Threshold и DepthCutoff.

Создание толстых линий контура

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

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

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

Теперь давайте реализуем расширенную свертку. Вернитесь в редактор материалов и создайте ScalarParameter с именем DilationRate. Установите его значение = 3 и умножьте каждое смещение на DilationRate.

Это сместит каждое смещение на 3 пикселя от центрального пикселя.

Нажмите Apply, а затем вернитесь в главный редактор. Вы увидите, что ваши линии стали намного толще. Вот сравнение между несколькими показателями дилатации:

Добавление линий к исходному изображению

Вернитесь к редактору материалов и создайте схему, как показано на изображении ниже – учтите, порядок очень важен!

Далее соедините всё следующим образом:

Теперь Lerp будет выводить изображение сцены, если альфа достигает нуля. В противном случае он выведет LineColor.

Нажмите Apply, а затем закройте PP_Outline. Теперь у вашей исходной сцены есть контуры!

Что делать дальше?

Вы можете скачать готовый проект тут.

Если вы хотите ещё поработать с распознаванием рёбер, то попробуйте создать распознавание, работающее с буфером нормалей. Это даст вам некоторые рёбра, которые не отображаются в методе распознавателе рёбер по глубинам. Затем вы сможете объединить оба типа распознавания рёбер вместе.

Свёртки — это обширная тема, которая находит активное применение, в том числе в искусственном интеллекте и обработке звука. Посмотрите интерактивное объяснение свёрток в Images Kernels explained visually. http://setosa.io/ev/image-kernels/ Также там описаны ядра для некоторых других эффектов.

Перевод
Оригинал
Показать больше
Закрыть
Закрыть