За пару недель до Хэллоуина 2021 я просматривал Sketchfab и наткнулся на крутую 3D-модель Grim Reaper от 3DRT. Имеет разумный поликаунт, набор разных цветов и плавную анимацию. Поэтому было принято решение сделать с этой моделью живые обои на тему Хэллоуина. Тем не менее, я не смог закончить его до Хэллоуина, потому что я постепенно добавлял новые эффекты и функции, которые потребовали довольно много времени для реализации и последующей настройки.

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

Как обычно, исходный код доступен на Github.

И, конечно же, вы можете получить приложение Живые обои для Android.

Композиция сцены

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

Во-первых, визуализируются непрозрачные (ткань альфа-маскирована, поэтому она также непрозрачна) геометрия. Эти анимированные объекты используют анимацию вершин с данными, хранящимися в текстурах FP16, поэтому для демонстрации требуется WebGL 2.

После рендеринга непрозрачных геометрий запись в глубину отключается с помощью glDepthMask(false) и затем прозрачные эффекты — дым, пыль и призраки рисуются поверх них с помощью смешивания. На этом этапе также рисуется небо. Поскольку это самый удаленный объект, он не должен вносить вклад в глубину — он в основном рассматривается как дальняя плоскость отсечения.

Последствия

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

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

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

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

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

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

Шейдер неба

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

Разберем код шейдера. Он сочетает в себе три простых эффекта для создания динамичного неба:

  1. Он начинается с применения цвета к довольно пресно выглядящей базовой текстуре неба в оттенках серого:

2. Затем накладываются волны из текстуры небольшого искажения (похожий, но более выраженный эффект можно использовать для водной ряби). Эффект тонкий, но заметно улучшает общий вид:

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

Таймер ударов молнии представляет собой периодическую функцию нескольких объединенных синусоид, зажатых в диапазоне [0…2]. Я использовал очень удобный графический калькулятор Desmos, чтобы визуализировать и настроить коэффициенты для этой функции — вы можете ясно видеть, что всплески положительных значений создают короткие периодические рандомизированные всплески:

Кроме того, небесная сфера медленно вращается, чтобы сделать фон менее статичным.

Шейдер призраков

Призрачные следы, плавающие вокруг мрачного жнеца, вдохновлены этим руководством по Unreal Engine 4 Niagara — https://www.artstation.com/artwork/ba4mNn.

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

Для этого он изменяет геометрию исходного меша в вершинном шейдере. Шейдер изменяет координаты X и Y входной модели, огибая их по окружности заданного радиуса. Координата Z не подвергается дополнительным преобразованиям. Он отвечает за масштабирование конечного эффекта по вертикали. (Мировое пространство Z-вверх). Шейдер заточен под работу с конкретной моделью — тесселированным листом в плоскости XZ (все координаты Y равны нулю):

Позже геометрия была оптимизирована, чтобы точно соответствовать нашей текстуре спрайта, чтобы уменьшить перерисовку:

Основываясь на математике длины хорды, координаты X и Y изогнутой модели:

x = R * sin(theta);
y = R * cos(theta);

где theta = rm_Vertex.x / R, а R — радиус изгиба. Однако в шейдере тета вычисляется иначе:

float theta = rm_Vertex.x * lengthToRadius;

Значение lengthToRadius является универсальным, но оно не является просто обратным значением R — мы можем передавать значения больше 1/R, чтобы масштабировать длину эффекта (поскольку это, по сути, предварительное умножение rm_Vertex.x).
Это небольшое изменение выполнено. чтобы устранить избыточную математику только для юниформ в шейдере. Предварительное деление длины на радиус выполняется на процессоре, и этот результат передается в шейдер через lengthToRadiusuniform.
Я пытался улучшить этот эффект, применяя искажение смещения во фрагментном шейдере, но в шейдере это практически незаметно. движение. Поэтому мы сохранили оригинальную более простую версию со статической текстурой, которая также дешевле для GPU.

Фильтр уменьшенных цветов

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

Он основан на коде из игры Q1K3 WebGL https://github.com/phoboslab/q1k3, и я настоятельно рекомендую прочитать сообщение в блоге о создании, казалось бы, невозможного Q1K3 — https://phoboslab.org/log/ 2021/09/q1k3-сборка.

Сжатие текстур

Живые обои Android ориентированы на OpenGL ES 3.0+ и используют эффективные сжатые текстуры ETC2 и ASTC. Однако демо-версия WebGL оптимизирована только для максимально быстрого времени загрузки. Я действительно ненавижу, когда какая-то простая демонстрация WebGL требует вечности для загрузки своих ресурсов. Из-за этого было принято решение не использовать аппаратно сжатые текстуры. Вместо этого текстуры сжимаются как WebP с потерями. Общий размер всех ресурсов, включая HTML/CSS/JS, составляет всего 2,7 МБ, поэтому он загружается довольно быстро. Недавно наша демонстрация гор также была обновлена ​​с меньшими ресурсами, но она все еще намного больше, чем у Reaper — она загружает 10,8 МБ данных при начальной загрузке.