
Привет, это блог «Технократии». Обычно мы занимаемся цифровой трансформацией бизнеса, но сегодня у нас для вас история, как при помощи библиотеки 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
