Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Shaders

Создание Toon Water для Интернета: часть 2

by
Difficulty:AdvancedLength:LongLanguages:
This post is part of a series called Creating Toon Water for the Web.
Creating Toon Water for the Web: Part 1
Creating Toon Water for the Web: Part 3

Russian (Pусский) translation by Liliya (you can also view the original English article)

Вернемся к этой серии из трех частей о создании стилизованной воды toon в PlayCanvas с использованием вершинных шейдеров. В части 1 мы рассмотрели вопрос о создании нашей окружающей среды и поверхности воды. Эта часть будет охватывать применение плавучести к объектам, добавление водных линий к поверхности и создание линий пены с буфером глубины вокруг краев объектов, которые пересекают поверхность.

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

  • Добавлен маяк и модель осьминога.
  • Добавлена ​​плоскость земли с цветом #FFA457.
  • Добавлено ясный цвет для камеры #6cc8ff.
  • Добавлен окружающий цвет для сцены #FFC480 (вы можете найти это в настройках сцены).

Ниже показано, то с чего я начинал.

The scene now includes an octopus and a ligthouse

Плавучесть

Самый простой способ создать плавучесть - это просто, нужно создать скрипт, который будет продвигать объекты вверх и вниз. Создайте новый скрипт Buoyancy.js и установите его инициализацию следующим образом:

Теперь, в обновлении, мы увеличиваем время и поворачиваем объект:

Примените этот скрипт к своей лодке и наблюдайте, как он подпрыгивает вверх и вниз в воде! Вы можете применить этот скрипт к нескольким объектам (включая камеру - попробуйте)!

Текстура поверхности

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

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

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

А затем назначьте его в редакторе:

The water texture is added to the water script

Теперь нам нужно передать его нашему шейдеру. Перейдите в Water.js и установите новый параметр в функции CreateWaterMaterial:

Теперь идите в Water.frag и объявите нашу новую форму:

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

Изменение Переменных

Измененная переменная позволяет передавать данные из вершинного шейдера в шейдер фрагмента. Это третий тип специальной переменной, которую вы можете иметь в шейдере (остальные два являются единообразными и атрибутами). Он определяется для каждой вершины и доступен каждому пикселю. Поскольку количество пикселей больше, чем вершин, значение интерполируется между вершинами (это происходит из-за того, что имя «меняется» - оно варьируется от значений, которые вы ему даете).

Чтобы попробовать это, объявите новую переменную в Water.vert как переменную:

И затем установите его в gl_Position после его вычисления:

Теперь вернитесь к Water.frag и объявите ту же переменную. Невозможно получить отладочный вывод из шейдера, но мы можем использовать цвет для визуальной отладки. Вот один из способов сделать это:

Теперь плоскость должна выглядеть черно-белой, где разделительная линия там, где ScreenPosition.x равна 0. Значения цвета идут только от 0 до 1, но значения в ScreenPosition могут находиться вне этого диапазона. Они автоматически зажимаются, поэтому, если вы видите черный, это может быть 0 или отрицательное.

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

Задача № 1: создать новую переменную, чтобы передать позицию мира вместо положения экрана. Визуализируйте его так же, как мы это делали выше. Если цвет не меняется с камерой, вы сделали это правильно.

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

UV являются двумерными координатами для каждой вершины вдоль сетки, нормированной от 0 до 1. Это именно то, что нам нужно, чтобы правильно отобразить текстуру на плоскости, и она уже должна быть настроена из предыдущей части.

Объявите новый атрибут в Water.vert (это имя происходит из определения шейдера в Water.js):

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

Теперь мы объявляем то же, что и в шейдере фрагмента. Чтобы убедиться, что это работает, мы можем визуализировать его по-прежнему, так что Water.frag теперь выглядит так:

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

И Вы должны видеть структуру на поверхности:

Caustics texture is applied to the water surface

Стилизация текстуры 

Вместо того, чтобы просто настроить текстуру как наш новый цвет, давайте объединим ее с синим цветом, который у нас был:

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

Однако это не единственный способ совместить цвета.

Задача № 2: Можете ли вы комбинировать цвета таким образом, чтобы получить более тонкий эффект, показанный ниже?
Water lines applied to the surface with a more subtle color

Перемещение текстуры

В качестве конечного эффекта мы хотим, чтобы линии двигались по поверхности, поэтому они не выглядят настолько статичными. Для этого мы используем тот факт, что любое значение, придаваемое функции texture2D за пределами диапазона от 0 до 1, будет обертываться (например, 1.5 и 2.5 оба становятся равными 0,5). Таким образом, мы можем увеличить наше положение на единую постоянную переменную, которую мы уже установили, и умножить положение, чтобы либо увеличить, либо уменьшить плотность линий на нашей поверхности, сделав наш окончательный шейдер- запрос следующим:

Пена и буфер глубины

Рендеринг пенных линий вокруг объектов в воде позволяет намного легче увидеть, как объекты погружены и где они разрезают поверхность. Это также делает нашу воду гораздо более правдоподобной. Для этого нам нужно как-то выяснить, где края находятся на каждом объекте, и делать это эффективно.

Трюк 

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

Рассмотрите представление ниже.

Lighthouse in water

Какие пиксели должны быть частью пены? Мы знаем, что это должно выглядеть примерно так:

Lighthouse in water with foam

Итак, давайте подумаем о двух конкретных пикселях. Я отметил две звезды внизу. Черный - в пене. Красный - нет. Как мы можем указать их отдельно в шейдере?

Lighthouse in water with two marked pixels

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

Viewing the lighthouse from above

Обратите внимание, что красная звезда не находится сверху тела маяка, как она появилась, но черная звезда. Мы можем указать их отдельно, используя расстояние до камеры, обычно называемое «глубина», где глубина 1 означает, что она очень близка к камере, а глубина 0 означает, что она очень далека. Но это не просто вопрос абсолютного мирового расстояния или глубины камеры. Это глубина по сравнению с пикселем позади.

Посмотрите на первый взгляд. Скажем, тело маяка имеет значение глубины 0,5. Глубина черной звезды была бы очень близка к 0,5. Таким образом, он и пиксель за ним имеют одинаковые значения глубины. С другой стороны, красная звезда имела бы гораздо большую глубину, потому что она была бы ближе к камере, скажем, 0,7. И все же пиксель за ним, все еще находящийся на маяке, имеет значение глубины 0,5, поэтому там большая разница.

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

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

Буфер глубины

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

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

Цель буфера глубины - правильно рисовать нашу сцену, без необходимости сортировки объектов назад. Каждый пиксель, который должен быть нарисован, сначала проверяет буфер глубины. Если его значение глубины больше значения в буфере, оно нарисовано, и его собственное значение перезаписывает значение в буфере. В противном случае он отбрасывается (потому что это означает, что перед ним стоит другой объект).

Фактически вы можете отключить запись в буфер глубины, чтобы посмотреть, как все будет выглядеть без него. Вы можете попробовать это в Water.js:

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

Визуализация буфера глубины

Давайте добавим способ визуализации буфера глубины в целях отладки. Создайте новый скрипт DepthVisualize.js. Прикрепите это к своей камере.

Все, что мы должны сделать, чтобы получить доступ к буферу глубины в PlayCanvas:

Затем это автоматически добавит форму во все наши шейдеры, которые мы можем использовать, объявив ее как:

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

Попробуйте скопировать это и прокомментировать / раскомментировать строку this.app.scene.drawCalls.push (this.command); для переключения глубины. Он должен выглядеть примерно так, как показано ниже.

Boat and lighthouse scene rendered as a depth map
Задача № 3: Поверхность воды не втягивается в буфер глубины. Механизм PlayCanvas делает это намеренно. Можете ли вы понять, почему? Что особенного в водном материале? Иными словами, на основе наших правил проверки глубины, что произойдет, если пиксели воды будут записываться в буфер глубины?

Подсказка: есть одна строка, которую вы можете изменить в Water.js, которая заставит воду записываться в буфер глубины.

Еще одно замечание заключается в том, что я умножаю значение глубины на 30 во встроенном шейдере в функции инициализации. Это просто, чтобы четко это видеть, потому что в противном случае диапазон значений слишком мал, чтобы видеть оттенки цветов.

Осуществление Приема

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

Определите следующую форму для Water.frag:

Определите эти вспомогательные функции над основной функцией:

Передайте некоторую информацию о камере в шейдер в Water.js. Поместите это, когда вы передаете другую форму, такую ​​как uTime:

Наконец, нам нужна мировая позиция для каждого пикселя в нашем флеш-шейдере. Нам нужно получить это из вершинного шейдера. Поэтому определите переменную Water.frag:

Определите то же самое, что и в Water.vert. Затем установите его в искаженное положение в вершинном шейдере, поэтому полный код будет выглядеть так:

На самом деле реализация трюка

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

Задача № 4: одно из этих значений никогда не будет больше другого (при условии, что depthTest = true). Можете ли вы вывести который?

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

Что должно выглядеть примерно так:

A rendering of the depth difference at each pixel

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

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

Мой любимый внешний вид - это цвет, подобный цвету статических линий воды, поэтому моя последняя основная функция выглядит так:

Подведем итоги

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

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

Исходный код

Здесь вы можете найти законченный проект PlayCanvas. Порт Three.js также доступен в этом репозитории.

Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.