НОЛЬ: Инженерная составляющая вызывающего интерактивного повествования | Кодропс

НОЛЬ: Инженерная составляющая вызывающего интерактивного повествования | Кодропс



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

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

// accept the stroke as a "zero" only if it truly closes a round loop
const wound    = totalSignedAngle(points, center);   // ~2π for a full turn
const radiusCV = std(radii) / mean(radii);           // low = round, not a scribble
const closed   = dist(points[0], points.at(-1))  5.76 && radiusCV 

Далее игра разворачивается как единая непрерывная прокрутка, которая переносит вас с экрана загрузки на интерактивную карту города. Созданный за четыре месяца с использованием Three.js, GSAP, Howler и Vite, он преобразует более гигабайта исходных ресурсов в сайт размером менее 10 МБ, который работает со скоростью 60 кадров в секунду даже на бюджетном телефоне Android. В этой статье мы рассмотрим 3D-конвейер, инструменты, шейдеры и оптимизацию производительности, которые сделали это возможным.

Как стартовал проект

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

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

Повествование: шесть стадий, пять врат

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

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

Архитектура: все решает одно число

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

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

{
  scrollVh: 300,            // how much virtual scroll this segment owns
  enter(ctx)        { /* build this segment's Three.js objects (async) */ },
  scrub(ctx, p)     { /* p is LOCAL progress 0..1 within this segment  */ },
  update(ctx, t, dt){ /* runs every frame, scroll or not (idle motion) */ },
  teardown(ctx)     { /* dispose and hand off to the next segment      */ },
}

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

Подготовка 3D для Интернета

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

Вся геометрия поставляется со сжатием DRACO с использованием локальных декодеров. Транскодеры Draco и KTX2/Basis входят в комплект поставки. public/vendor/ а не загружаться из CDN. Мы усвоили этот урок на собственном горьком опыте, когда замедление темпов роста gstatic и unpkg приводило к сбою декодирования каждого сжатого ресурса, хотя сами ресурсы размещались локально. Если ваш декодер находится на чужом сервере, то и ваш конвейер тоже.

Текстуры оказали наибольшее влияние на производительность. Хотя PNG-файл может быть небольшим на диске, он полностью распаковывается в памяти графического процессора. Текстура 2048² по-прежнему занимает около 16 МБ видеопамяти независимо от размера файла. KTX2 со сжатием ETC1S сохраняет сжатие на графическом процессоре, использует гораздо меньше памяти и загружается значительно быстрее.

Средство предварительного просмотра сжатия

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

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

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

Мы также объединили связанные текстуры в общие атласы. Например, все текстуры рук упакованы в один атлас 4×4, где каждая сетка использует UV-смещения и масштабируется для ссылки на свой собственный раздел.

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

Ранние сборки имели размер от 35 до 40 МБ. Финальная сборка весит менее 10 МБ, а интерактивная карта мира изолирована в отдельной группе загрузки, поэтому она никогда не блокирует начальный опыт.

Заикание, которого вы не ожидаете: загрузка текстур

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

Чтобы избежать этого, мы решили загрузку текстур тремя способами.

  1. Декодируйте основной поток. С использованием createImageBitmap() перемещает декодирование изображения из основного потока, оставляя только загрузку графического процессора во время рендеринга.
  2. Загружайте во время простоя. Декодированные текстуры ставятся в очередь и загружаются только тогда, когда у браузера есть свободное время:
// upload queued textures only while there's idle time to spare
function drainUploads(deadline) {
  while (uploadQueue.length && deadline.timeRemaining() > 5) {
    renderer.initTexture(uploadQueue.shift()); // forces the GPU upload now
  }
  if (uploadQueue.length) requestIdleCallback(drainUploads, { timeout: 2000 });
}
  1. Разбейте большие атласы на более мелкие части. Самые большие текстуры разделены на тайлы размером 256² и загружаются по одной на каждый кадр, что предотвращает превышение бюджета кадра при одной загрузке.

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

Адаптивный менеджер по качеству

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

if (avgMs > 22 && tier > LOW  && cooldownElapsed) downgrade();   // ~83fps

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

Большая часть последнего месяца была потрачена на профилирование и оптимизацию бюджетного устройства Android. Мы отслеживали скачки кадров один за другим, пока все не прошло гладко. Худшим нарушителем оказался одиночный кадр длительностью 157 мс.

Шейдеры

Цепочка постобработки

Каждый кадр состоит из серии проходов постобработки, которые применяются последовательно:

  1. Оказывать: Основная 3D сцена.
  2. Фон: Процедурные фоны GLSL вместо графических ресурсов.
  3. Преломление стекла: Рендерит неразбитое стекло так, чтобы оно преломляло фон за ним.
  4. Мороз и тропа: Рисует жест пользователя, распространение инея и эффект таяния.
  5. Глубина резкости: Качество размытия масштабируется в зависимости от активного уровня качества и отключается при низких настройках.
  6. Передний план: Применяет зернистость пленки и окончательное отображение тонов.
  7. Отложенный текст: Объединяет текст после тональной компрессии, чтобы он оставался четким. Этот проход пропускается, если текстовые спрайты не видны.
  8. Разбить: На последнем проходе рисуется разбитое стекло.

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

// boot: the always-on spine, added in order
composer.addPass(renderPass);     // 1. the 3D scene
composer.addPass(bgPass);         // 2. procedural background
composer.addPass(frostingPass);   // 3. draw-zero frost + melt
composer.addPass(lensBlurPass);   // 4. depth-of-field, tier-gated
composer.addPass(fgPass);         // 5. grain + the one tone mapping

// later, off the loader's critical path
composer.addPass(textPass);            // type, composited after tone mapping
composer.insertPass(glassPass, 2);     // slots in right after the background
composer.addPass(shatterPass);         // the break, last over everything

// warmed during an earlier stage's idle time,
// so their first real frame is a cache hit, not a shader-compile stall
glassPass.prewarm(renderer, camera);
shatterPass.prewarm(renderer, camera);

// per frame: don't pay for the text composite when nothing's on screen
textPass.enabled = textPass.hasVisibleSprites();

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

Мороз разблокировать

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

// one of four axis passes → octagonal spread; uSpreadAxis is the...



Источник

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх