Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, это блог «Технократии». Обычно мы занимаемся цифровой трансформацией бизнеса, но сегодня у нас для вас история, как при помощи библиотеки three.js и шейдеров мы сделали лендинг для нашей промо-кампании. Главный рассказчик — разработчик Артем Ибатуллин.
На старте
В конце прошлого года мы запустили рекламную кампанию, чтобы рассказать о себе и привлечь новые кадры. В центре кампании был наш ролик-манифест, который мы решили разместить на лендинге. На нем же собирали все заявки от заинтересовавшихся.
Вот, кстати, сам ролик:
К дизайну лендинга и технологическим решениям на фронте нас вел визуальный ряд ролика. В нем много «цифровых» эффектов, глитч-арта. Мы добавили немного киберпанка, потому что он выглядит красиво и его просто делать. Уже во время съемок дизайнеры накидывали идеи для вдохновения: темные цвета, эффекты повреждения данных, моноширинные шрифты.
Изначально планировали собрать сайт на тильде, но, когда арт-директор показал предварительные варианты, я предложил отказаться от этой идеи. Во-первых, из-за ограничений платформы, не получилось бы сделать супер-красиво. Во-вторых, мне поднадоели приложения-админки с кучей форм, и я хотел собрать что-нибудь подобное: с эффектами и плюшками. Решили делать сами, и это развязало руки дизайнерам.
Разработка шла параллельно с дизайном, многое обновляли и меняли на ходу. Прямо в Figma дизайнеры оставляли комментарии с ссылками на референсы эффектов, это помогло нам при разработке.
На верстке у нас было два человека, один занимался скелетом и основой лендинга, я же взял на себя эффекты. Верстали последовательно, начиная с верхнего экрана.
3D-модели — основа
Сначала была идея сделать спрайт с кадрами или видео. Эту идею отмели, потому что вес страницы был бы запредельным, и не удалось бы добиться плавности. Тогда решили взять 3D-модели за основу.
Модель можно повернуть как угодно, расставить свет и текстурировать так, чтобы это соответствовало дизайну. Тащить их на сайт мы не стали из-за большого количества полигонов — не все устройства их потянут. Для первого экрана нам было достаточно двумерной картинки. Отрендерили ее в PNG, пережали и пустили вокруг головы цилиндр с наложенной текстурой. Для 3D использовали библиотеку three.js.
Хотелось сделать, чтобы текст подрагивал, как неисправная неоновая лампа. Реализовали так: отрендерили модель два раза, но одну с розовым свечением, а другую без. После наложили две картинки друг на друга и, управляя их прозрачностью, получили нужный эффект.
Для стилизации добавили треугольники — это спрайты, которые вращаются вокруг своей оси. Чтобы добавить интерактивности, привязали их к движению мыши или смартфона.
Для оживления картинки наложили шейдеры из стандартной поставки Three.js:
Bloom для создания эффекта свечения (пришлось его доделать);
Film для создания зернистости и эффекта телевизионной чересстрочной развертки;
Glitch для оживления экрана;
Добавляем глитч на кнопки
Как это сделать? Кнопка в HTML, а глитч накладывается на WebGL. Решение я подглядел на одном сайте: взял канвас размером с экран, нарисовал на нем кнопку и наложил на него нужный эффект.
Работает это так: при наведении на кнопку я делаю ей прозрачный фон в CSS. В этот же момент считаю ее координаты, рисую на канвасе белый прямоугольник и включаю глитч.
Белый прямоугольник можно нарисовать несколькими способами. Например, создать плоскость размером с кнопку и поставить ее перед камерой. У этого подхода есть минус в виде большого количества математики, необходимости правильного позиционирования в пространстве, а из-за перспективы плоскость не в центре экрана при взгляде с камеры будет немного кривой.
Обычно делают по-другому:
Рисуют плоскость так, чтобы, при просмотре с камеры, она занимала ровно весь экран. Ниже есть код, считающий размер плоскости: берем тангенс от угла зрения камеры, умножаем на расстояние от плоскости до камеры, получаем высоту, потом ширину.
Осталось нарисовать белый прямоугольник на получившейся плоскости. Сделаем это материалом с шейдером — это самый быстрый способ.
Про шейдеры
Это программа, в данном случае на языке GLSL, которая запускается на ядре видеокарты. Почему на нем? Все дело в производительности. В процессоре ядер может быть от 1 до 64. В видеокарте же они исчисляются сотнями или даже тысячами. Каждое ядро «принимает в себя» шейдер и выполняет его. Благодаря этому мы получаем значительно большую производительность.
Для примера, посмотрите забавный видео ролик, где «Разрушители мифов» Адам Сэвидж и Джеми Хайнеман сравнивают работу CPU и GPU:
Из чего состоит шейдерный материал в библиотеке three.js?
Вершинный шейдер. Он выполняется для каждой вершины геометрии один раз за отрисовку. В данном случае у нас 4 вершины — 4 угла прямоугольника. Вершинный шейдер преобразует 3D-координаты вершины в 2D координаты на экране. Параллельно с вершиной можно производить другие манипуляции, например, передвинуть ее. Это позволяет с хорошей скоростью анимировать системы частиц (искры, дым или снег).
В нашем случае вершинный шейдер ничего сложного не делает — тут просто код для преобразования координаты вершины из пространства объекта в мировое пространство через матрицу, скопированную из three.js.
Юниформ-переменные позволяют передавать данные в шейдер. Их можно рассматривать как аргументы функции. Юниформ, потому что значения этих переменных для каждого выполняемого шейдера одинаковы, это позволяет исключить неточности отрисовки.
Фрагментный шейдер, который выполняется для каждого рисуемого пикселя. То есть для экрана FullHD шейдер выполняется около 2 миллионов раз за один цикл отрисовки.
Разберем, как работает наш фрагментный шейдер. При наведении на кнопку в юниформ переменные передаются нормализованные (от 0 до 1) координаты краев кнопки. Точка отсчета — левый нижний угол.
С кодом самого шейдера все просто: если рисуемый пиксель попадает в прямоугольник — рисуем белым, если нет — прозрачным. Осталось немного отредактировать код эффекта глитча, чтобы он работал как нам нужно.
Интерактивность без загрузки устройств
Для дальнейшего оформления лендинга мы нашли удовлетворяющую по стилистике 3D-модель, но вставлять ее простой картинкой не хотелось. Пришла идея проецировать нее наш логотип, создавая анимацией эффект рекламных проекторов. Такие обычно висят над головой на улицах и проецируют рекламу на асфальт.
Попробовали реализовать в 3D-max — вышло интересно. Захотелось добавить эффекту интерактивности, но высокая детализированность модельки все усложняла. Ее притягивание обещало сильно нагружать устройства. Я стал думать, как быть, и в голову пришла идея использовать карту нормалей.
Это текстура, у которой в каналах RGB зашифрованы направления векторов нормалей плоскости.
Нормаль к поверхности в заданной её точке — прямая, перпендикулярная к касательной плоскости в указанной точке поверхности.
Для примера рассмотрим ту, что мы использовали. На картинке видно, что плоскости, направленные вправо, имеют красный оттенок, потому что координата Х мапится в R. Y мапится в G, поэтому плечи и верхняя часть головы зеленоватые. Так как все плоскости, которые мы видим, направлены к камере, это дает синеватый оттенок карте нормалей.
Для чего нам карта нормалей? С ее помощью мы можем добиться эффекта искажения. Также в индустрии 3D-графики и геймдева карты нормалей используют, чтобы имитировать эффекты падения света, не изменяя геометрию.
Вот пример: слева фигуры используют реальную геометрию, а справа — одну плоскость с картой нормалей. Этот подход позволяет во много раз сократить количество полигонов геометрии при том же внешнем виде. Обратите внимание, что на модели справа с использованием карт нормалей полигонов намного меньше.
Как реализовать этот эффект?
Разберем алгоритм по шагам:
Преобразуем UV координаты, чтобы масштабировать, повернуть и сдвинуть логотип. Это происходит путем перемножения матриц.
Узнаем цвет нормали в точке. Напоминаю: шейдер применяется для каждого пикселя.
Считаем вектор, который будет показывать, откуда брать новый цвет пикселя
Достаем цвет пикселя от логотипа, умножаем его на нормаль, смешиваем с отрендеренной картинкой.
С помощью uniforms закидываем в шейдер значения времени (нужны для равномерного вращения логотипа), сдвиги по X и Y (для реакции на движения мышкой / вращения телефона) и остальные параметры.
Последний эффект — дань COVID-19 в форме обратной связи. В 3D max лепим на модельку маску и рендерим два раза — с маской и без. На страничке ставим на картинку глитч, подменяем модельку на версию с надетой маской, выключаем глитч. Потом все то же самое, только наоборот. Готово!
Сборка
Обычно мы все пишем на typescript, но в данном случае проверка типов нам особо не нужна, так что решили писать на чистом JS. Чтобы не возиться со сборкой, использовали parcel — он помог нам без конфигурации импортировать HTML друг в друга (что удобно, когда контента на странице много) и использовать Stylus для стилей.
Все остальное
Чтобы не тратить много времени на анимацию, мы использовали animate.css. Также некоторые эффекты взяли с codepen.
Вместо вывода оставим полезные ссылки.
Примеры работ на threejs: https://threejs.org/examples
Интерактивная книга по основам threejs: https://discoverthreejs.com/book/contents/
Отличный курс по шейдерам на GLSL: https://thebookofshaders.com/
Подборка шейдеров: https://www.shadertoy.com/
Куча бесплатных 3D-моделей: https://sketchfab.com/3d-models/popular
Наш лендинг: https://career.technokratos.com/