В декабре прошлого года мы наконец-то закончили наш проект. В этом видео показана наша последняя работа — четырёхминутная анимация «Immersion». Точнее, это запись того, что обычно называется 64k-интро. Но подробнее об этом чуть позже.
Работа над проектом заняла лучшие свободные часы последних двух лет жизни. Всё это началось во время проведения Revision 2015, большого мероприятия, устраиваемого каждый год в Германии во время пасхальных каникул. Мы вдвоём болтали по дороге из отеля в место проведения мероприятия. Предыдущим вечером уровень конкуренции в области 64kB intro оказался высоким. Очень высоким. Опытная и хорошо известная венгерская группа Conspiracy наконец вернулась с серьёзной, потрясающей работой. Наш лучший враг Approximate идеально успел по времени с завершением цикла выпуска и показал значительные улучшения в сторителлинге. Продуктивная группа Mercury обрела собственный зрелый стиль дизайна в интро, которое не оставляло сомнений в своей победе.
В тот год мы пришли с пустыми руками и не участвовали в соревнованиях, но конечно же хотели вернуться как можно раньше. Однако после демонстрации этих качественных интро мы гадали: красивая графика, отличный сюжет, замечательный дизайн — как мы можем подняться до этого уровня? Я не мог придумать концепцию, которая даже при идеальной реализации смогла бы победить всех этих трёх конкурентов. Не говоря уже о том, что наши технические навыки были ниже, чем у каждой из групп. И так мы шли по Гогенцоллернштрассе, перебрасываясь идеями, пока одна из них не «выстрелила». Город, вырастающий из моря. Эта концепция при правильной реализации, возможно, смогла бы соревноваться на том уровне, которого достигла субкультура интро. Revision 2016, приготовься, мы идём!
Revision 2016 стремительно промчалась мимо нас; удастся ли нам успеть к Revision 2017? Увы, мы не справились и к этому новому дедлайну. Когда нас спрашивали на мероприятии, как идут дела, мы отвечали уклончиво: «На первую часть у нас ушёл год. Уверен, что вторую часть успеем сделать за 24 часа». Но мы не успели. Тем не менее, мы выпустили релиз, но вторая часть делалась в спешке, и это было заметно. Настолько, что мы даже не смогли приблизиться к сцене с победителями. Мы продолжали работать, и вкладывали всю необходимую любовь, а потом, наконец, выпустили показанную выше окончательную версию.
Демо — это творения цифрового искусства, стоящие на перекрёстке короткометражных фильмов, музыкальных видео и видеоигр. Хотя они являются неинтерактивными и часто зависят от музыки, как видеоклипы, но рендерятся в реальном времени, как видеоигры.
64-килобайтные интро, или 64k для краткости, похожи на демо, но в них добавлено ограничение по размеру: интро полностью должны умещаться в один двоичный файл размером не более 65536 байт. Никаких дополнительных ресурсов, никакой сети, никаких лишних библиотек: обычное требование заключается в том, чтобы его можно было запустить на PC с только что установленной Windows и последними версиями драйверов.
Насколько велик этот объём? Вот вам опорные точки для сравнения.
В файле размером 64КБ можно хранить:
- 400 миллисекунд звука в формате WAV с CD-качеством, или
- 3 секунды mp3 с 192Кбит/с, или
- RGB-изображение 200×100 в формате .bmp, или
- JPEG-изображение среднего размера и среднего качества, например, такого как этот скриншот 800×450 из интро:
JPEG-изображение размером 65595 байт, на 59 байт больше ограничения в 64КБ.
Да, всё верно: это видео, показанное в начале поста, полностью умещается в один файл, занимающий меньше места, чем сам скриншот видео.
Когда видишь такие числа, то кажется сложным уместить в двоичный файл все изображения и звуки, которые точно должны быть необходимы. Мы уже говорили о некоторых компромиссах, на которые нам пришлось пойти, и о некоторых хитростях, которые мы использовали, чтобы уместить всё в такой маленький размер. Но этого недостаточно.
На самом деле, из-за таких чрезвычайных ограничений невозможно использовать обычные техники и инструменты. Мы написали собственный тулчейн — эта задача интересна сама по себе: мы создали текстуры, 3D-модели, анимации, пути камеры, музыку и т.д. благодаря алгоритмам, процедурной генерации и сжатию. Скоро мы расскажем об этом.
Вот, на что мы потратили доступные нам 64КБ:
- Музыка: 12,4КБ
- 3D-меши: 12,5КБ
- Текстуры: 4,8КБ
- Данные камеры: 1,3КБ
- Шейдеры: 6,2КБ, из 5 тысяч строк кода
- Движок: 12,9КБ, из 20 тысяч строк кода
- Само интро: 12 тысяч строк кода
- Потраченное время: долгие часы, возможно, даже тысячу часов
На этом графике показано, как 64КБ занято разными типами контента после сжатия.
На этом графике показано изменение двоичного размера (не включая примерно 2КБ распаковщика) до окончательного релиза.
Договорившись, что центральной темой будет погружённый в воду город, мы задали себе первый вопрос: как должен выглядеть этот город? Где он расположен, почему затоплен, какая у него архитектура? Один простой ответ на все эти вопросы: это может быть легендарный затерянный город Атландида. Это также объяснит его появление: по воле богов (в буквальном смысле deus ex machina). На этом мы и порешили.
Ранняя концепция затопленного города. Показанные в статье художественные работы созданы Бенуа Моленда (Benoît Molenda).
При принятии дизайнерских решений мы руководствовались двумя книгами: «Тимей» и «Критий», в которых Платон описывал Атлантиду и её судьбу. В частности, в «Критии», он подробно описывает подробности структуры города, его цвета, изобилие драгоценного орихалка (ставшего существенным элементом сцены с храмом), общую форму города и главный храм, посвящённый Посейдону и Клейто. Поскольку Платон, очевидно, основывает свои описания на известных ему странах, то есть смеси греческого, египетского и вавилонского стилей, мы решили придерживаться этого.
Однако без достаточных знаний тематики создание убедительной античной архитектуры казалось сложной задачей. Поэтому мы решили воссоздать уже существующие здания:
Поиски справочных материалов по храму Артемиды (Артемисиону) оказались неожиданным, обогатившим нас опытом. Сначала мы искали только фотографии, схемы или карты. Но когда мы узнали имя Джона Тёртл-Вуда, всё обрело бОльшую глубину. Вуд был тем самым человеком, который занимался поиском местоположения храма и в результате нашёл его. Надеясь на то, что поиск по его имени даст нам больше результатов, чем просто «Артемисион», мы сразу же наткнулись на его книгу, написанную в 1877 году, в которой он не только приводит описания и зарисовки храма, но и описывает своё одиннадцатилетнее путешествие к затерянному памятнику, переговоры с Британским музеем о финансировании, взаимоотношения с местными рабочими и дипломатические переговоры, без которых невозможно было проводить раскопки в произвольных местах.
Эти книги были очень важны для принятия нами дизайнерских решений, но прежде всего, их чтение заставило нас, как личностей, гораздо сильнее ценить работу над проектом.
И кстати, как должна выглядеть крыша? В некоторых эскизах, в том числе и Вуда, в ней было отверстие, в других же оно отсутствовало; здесь очевидно есть какое-то противоречие. Мы решили выбрать модель с открытой крышей, которая позволит нам осветить интерьер замка лучом света. На показанных выше иллюстрациях показаны архитектурный план и поперечные разрезы здания из книги Discoveries at Ephesus, которые можно сравнить с нашей рабочей моделью храма.
С самого начала мы знали, что критически важным в этом интро будет внешний вид воды. Поэтому мы потратили на неё много времени, начав с просмотра справочных материалов, чтобы понять неотъемлемые элементы подводной графики. Как вы можете догадаться, мы вдохновлялись «Бездной» и «Титаником» Джеймса Кэмерона, 3DMark 11, а освещение изучали в «Бегущем по лезвию» Ридли Скотта.
Чтобы достичь правильного ощущения нахождения под водой, недостаточно было реализовать и включить какую-то эпичную функцию MakeBeautifulWater(). Это было сочетание множества эффектов, которые при правильной настройке могли убедить нас, зрителей, в иллюзии и заставить почувствовать, что мы находимся под водой. Но достаточно одной ошибки, чтобы разрушить иллюзию; этот урок мы усвоили слишком поздно, когда в комментариях после первоначального релиза нам показали, где иллюзия исчезает.
Как видно на иллюстрациях, мы также исследовали различные нереалистичные и иногда чрезмерные палитры, но не знали, как добиться такого внешнего вида, так что в результате вернулись к классической цветовой схеме.
Поверхность воды
Рендеринг поверхности воды подразумевает отражение от плоской поверхности. Отражение и преломление первыми рендерятся на отдельных текстурах с помощью одной камеры, находящейся сбоку, а второй, расположенной над плоскостью воды. В основном проходе повехность воды рендерится как меш с материалом, сочетающим в себе отражение и преломление на основании вектора нормали и взгляда. Хитрость заключается в смещении координат текстур на основании нормали поверхности воды в экранном пространстве. Эта техника является классической и хорошо задокументирована.
На среднем масштабе это работает хорошо, например, во время сцены с лодкой, но при крупном масштабе, например, в финальной сцене с появлением храма из воды, результат выглядит искусственным. Чтобы сделать его убедительным, мы использовали художественный трюк, заключающийся в применении к промежуточным текстурам гауссова шума. Размытие текстуры преломления придаёт воде мрачный вид и большее ощущение глубины. Размытие текстуры отражения помогает морю выглядеть более волнующимся. Кроме того, применение большего размытия в вертикальном направлении имитирует вертикальные следы, которые можно ожидать от поверхности воды.
Размытое изображение храма отражается в поверхности воды.
Анимация выполнена с помощью простых волн Герстнера в вершинном шейдере сложением восьми волн со случайными направлениями и амплитудой (в заданном диапазоне). Более мелкие детали выполняются в фрагментном шейдере, содержащем ещё 16 волновых функций. Искусственный эффект обратного рассеяния, основанный на нормали и высоте, делает вершины волн более светлыми, видимыми на показанном выше изображении как небольшие бирюзовые пятна. Во время сцены запуска добавлено несколько дополнительных эффектов, например, шейдер дождевых капель.
Иллюстрация шейдера. Нажмите на изображение, чтобы перейти к шейдеру в Shadertoy.
Объёмное освещение
Одним из первых технических вопросов стал «Как сделать так, чтобы столбы света погружались в воду?». Возможно, подойдёт просвечивающий биллборд с красивым шейдером? Однажды мы начали экспериментировать с наивным ray marching сквозь среду. Мы с радостью наблюдали, как даже в раннем грубом тесте рендеринга, несмотря на плохо подобранные цвета и отсутствие достойной фазовой функции, объёмное освещение сразу стало убедительным. В этот момент мы отказались от первоначальной идеи с биллбордом и больше никогда к ней не возвращались.
Благодаря этой простой технике мы получили эффекты, о которых даже не осмеливались думать. Когда мы добавили фазовую функцию и поэкспериментировали с ней, ощущения стали походить на реальные. С кинематографической точки зрения это открывало множество возможностей. Но проблема была в скорости.
Столбы света придают этой сцене внешний вид, на который нас вдохновил «Бегущий по лезвию».
Настало время превратить этот прототип в реальный эффект, поэтому мы прочитали туториал Себастьяна Хиллара, его презентацию DICE и изучили другие подходы, например, с эпиполярными координатами. В результате мы остановились на более простой технике, близкой к той, что использовалась в Killzone Shadow Fall (видео) с некоторыми отличиями. Эффект выполняется одним полноэкранным шейдером при половинном разрешении:
- Для каждого пикселя испускается луч и его взаимодействия с каждым конусом света решаются анатилически.
Математические расчёты описаны здесь. С точки зрения производительности, вероятно, будет более эффективно использовать ограничивающий меш объёма света, но для 64КБ нам показалось проще применить аналитический подход. Очевидно, что лучи распространяются не дальше глубины в буфере глубин.
- В случае пересечения луча для объёма внутри конуса выполняется ray marching.
Количество шагов ограничено из соображений скорости и к ним добавляются случайные смещения, чтобы избежать полос. Это типичный случай устранения полос для шума, менее сомнительный визуально.
- На каждом шаге получается карта теней, соответствующая свету, а воздействие света накапливается в соответствии с простой фазовой функцией Хени – Гринштейна.
В отличие от подхода на основе эпиполярных координат, с помощью этой техники можно иметь разнородную плотность среды, что добавляет вариативности, но мы не реализовали такой эффект.
- Разрешение полученного изображения увеличивается с помощью двухпроходного двунаправленного гауссова фильтра и добавляется поверх основного буфера рендеринга. В отличие от техники из туториала Себастьяна, мы не используем временное повторное проецирование; мы только используем достаточно большое количество шагов для снижения видимых артефактов (8 шагов при настройках низкого качества, 32 шагов при настройках высокого качества).
Объёмное освещение позволяет придать нужное настроение и отчётливый кинематографический внешний вид, который сложно реализовать иным образом.
Поглощение света
Мгновенно узнаваемый аспект подводного изображения — поглощение. При отдалении объекта он становится всё менее и менее видимым, его цвета сливаются с фоном, пока он полностью не исчезает. Аналогично, объём, на который влияют источники света, тоже уменьшается, потому что свет быстро поглощается водной средой.
Этот эффект имеет отличный потенциал для создания кинематографического ощущения, а смоделировать его очень просто. Он создаётся двумя этапами в шейдере. Первый этап применяет к яркости света простую функцию поглощения при накоплении источников освещения, влияющих на объект, изменяя таким образом цвет и яркость света при достижении им поверхностей. Второй этап применяет ту же функцию поглощения к окончательному цвету самого объекта, таким образом изменяя воспринимаемый цвет в зависимости от расстояния до камеры.
Код имеет приблизительно такую логику:
vec3 lightAbsorption = pow(mediumColor, vec3(mediumDensity * lightDistance));
vec3 lightIntensity = distanceAttenuation * lightColor * lightAbsorption;
vec3 surfaceAbsorption = pow(mediumColor, vec3(mediumDensity * surfaceDistance));
vec3 surfaceColor = LightEquation(E, N, material) * lightIntensity * surfaceAbsorption;
Тест поглощения света в водной среде. Заметьте, как на цвет влияет расстояние до камеры и расстояние от источников света.
Добавление растительности
Мы совершенно точно хотели использовать водоросли. В списке желательных типичных элементов подводных сцен они стояли на одном из первых мест, но их реализация казалась рискованной. Подобные органические элементы могут быть сложными в реализации, а неправильная реализация может разрушить ощущение погружения в иллюзию. Они должны обладать убедительной формой, хорошо интегрироваться в окружение, а возможно, и требовать дополнительнй модели подповерхностного рассеяния.
Однако в один из дней мы почувствовали, что готовы к эксперименту. Начали с куба, отмасштабировали его и расположили случайное количество кубов по спирали вокруг воображаемого стебля: с достаточно большого расстояния это могло сойти за длинное растение со множеством маленьких ветвей. После добавления большого количества шума для деформирования модели водоросли начали выглядеть почти достойно.
Тестовый кадр с несколькими редкими растениями.
Однако когда мы пытались добавить эти растения в сцену, то осознали, что с увеличением количества объектов скорость быстро снижается. Поэтому мы могли добавить слишком маленькое их число, чтобы это выглядело убедительно. Похоже, что наш новый неоптимизированный движок уже наткнулся на первое «бутылочное горлышко». Поэтому в последнюю минуту мы реализовали грубое отсечение по пирамиде видимости (в окончательной версии используется правильное отсечение), что позволило показать в демо плотные кусты.
С подходящей плотностью и размерами (участки с нормальным распределением), и когда подробности скрыты тусклым освещением, картина начинает выглядеть интересной. При дальнейших экспериментах мы попытались анимировать водоросли: функция шума для модуляции силы воображаемого подводного течения, обратная экспоненциальная функция для того, чтобы растения сгибались, а также синусоида, чтобы их кончики закручивались в потоке. В процессе экспериментов мы наткнулись на настоящее сокровище: испускание подводного света сквозь кусты, отрисовывающее на морском дне паттерны теней, пропадающие при удалении от камеры.
Растительность, отбрасывающая паттерны теней на морское дно.
Придание объёма с помощью частиц
Последний лёгкий штрих — это частицы. Внимательно посмотрите на любую подводную съёмку, и вы заметите всевозможные взвешенные частицы. Перестаньте обращать на них внимание, и они пропадут. Мы настроили частицы так, чтобы они были едва заметны и не попадались на пути. Тем не менее, они придают ощущение объёма, заполненного осязаемой средой, и помогают усилить иллюзию.
С технической точки зрения всё достаточно просто: в интро Immersion частицы — это просто экземпляры четырёхугольников с просвечивающим материалом. Проблему порядка рендеринга, вызванную просвечиванием, мы просто избежали, задав позицию вдоль одной оси в соответствии с идентификатором экземпляра. Благодаря этому все экземпляры всегда отрисовываются вдоль этой оси в правильном порядке. Для каждого кадра затем нужно правильно сориентировать объём частиц. На самом деле, для многих кадров этого мы этого совсем не делали, потому что размер частиц и темнота сцены сделали заметные артефакты достаточно редкими.
На этом кадре частицы дают понятие о глубине и ощущение плотности при увеличении глубины погружения.
Как уместить высококачественную музыку примерно в 16КБ? Эта задача не нова, и большинство 64k-интро, написанных после .the .product 2000 года, используют схожие концепции. Оригинальная серия статей довольно старая, но не теряет актуальности: The Workings of FR-08’s Sound System.
Если вкратце, то идея заключается в том, что нам нужна музыкальная партитура и список инструментов. Каждый из инструментов — это функция, генерирующая звук процедурно (см., например, субтрактивный синтез и синтез физического моделирования). Музыкальная партитура — это список используемых нот и эффектов. Она хранится в формате, похожем на midi, с некоторыми изменениями для уменьшения размера. Генерирование музыки происходит во время выполнения программы.
У синтезатора также есть версия в виде плагина (VSTi), который музыкант может использовать в любимой программе написания музыки. Написав музыку, композитор нажимает на кнопку, экспортирующую все данные в файл. Мы встраиваем данные в демо.
При запуске демо оно запускает поток для генерирования музыки в огромный буфер. Синтезатор активно потребляет ресурсы ЦП и не обязательно выполняется в реальном времени. Поэтому мы запускаем поток перед началом демо, когда генерируются текстуры и другие данные.
Даниэль Линдхолм сочинил музыку с помощью синтезатора 64klang, созданного Домиником Риесом.
При создании демо одним из самых критичных аспектов является время итерации. На самом деле, это относится ко многим творческим процессам. Время итерации — это самое важное. Чем быстрее вы можете выполнять итерации, тем больше можете экспериментировать, тем больше вариаций можете исследовать, тем больше вы можете совершенствовать своё видение и повышать качество в целом. Поэтому мы максимально хотим избавиться от всех препятствий, пауз и небольших трений в процессе творчества. В идеале мы хотим иметь возможность менять что угодно и когда угодно, мгновенно видя результат и получая непрерывную обратную связь в процессе внесения изменений.
Возможное решение, используемое многими демо-группами, заключается в сборке редактора и создании всего контента внутри него. Мы этого не сделали. Изначально мы хотели писать код на C++ и делать всё внутри Visual C++. Со временем мы разработали несколько техник, улучшивших наш рабочий процесс и снизивших время итерации.
Горячая перезагрузка всех данных
Если бы мы могли дать в этой статье только один совет, то он был бы таким: сделайте так, чтобы все ваши данные поддерживали горячую перезагрузку. Все данные. Сделайте так, чтобы вы могли обнаруживать изменение данных, загружать новые данные, когда это происходит, и соответствующим образом изменять состояние программы.
Мало-помалу мы обеспечили возможность горячей перезагрузки всех данных: шейдеров, камеры, монтажа, всех кривых, зависящих от времени, и т.д. На практике у нас обычно был редактор и запущенное дополнительно демо. При модификации файла изменения мгновенно становились видимы в демо.
В таком маленьком проекте, как демо, это реализовать достаточно просто. Наш движок отслеживает, откуда поступают данные, а небольшая функция регулярно проверяет изменение меток времени соответствующих файлов. При их изменении она запускает перезагрузку соответствующих данных.
Такая система может быть гораздо более сложной в крупных проектах, в которых такие изменения затрудняются зависимостями и легаси-структурой. Но влияние этого механизма на процесс производства сложно переоценить, поэтому он полностью стоит вложенных усилий.
Настраиваемые значения
Перезагрузка данных — это, конечно, хорошо, но как насчёт самого кода? С ним всё более сложно, поэтому нам пришлось решать эту задачу поэтапно.
Первым шагом был хитрый трюк, позволивший нам изменять литералы констант. Джоел Дэвис описал это в своём посте: короткий макрос, превращающий константу в переменную с фрагментом кода, который распознаёт событие изменения файла исходника, и соответствующим образом обновляет переменную. Очевидно, что конечном двоичном файле этот вспомогательный код отсутствует и оставлена только константа. Благодаря этому компилятор способен выполнить все оптимизации (например, когда константе присвоено значение 0).
Этот трюк можно применять не во всех случаях, но он очень прост и может быть интегрирован в код за считанные минуты. Более того, хотя подразумевается, что он должен всего лишь изменять константы, его можно использовать и для отладки: для изменения веток кода или включения/отключения параметров с условиями наподобие if(_TV(1)).
Рекомпиляция C++
Наконец, самым последним шагом для обеспечения гибкости кода стало включение в кодовую базу инструмента Runtime Compiled C++. Он компилирует код как динамическую библиотеку и загружает её, а также выполняет сериализацию, что позволяет вносить изменения в этот код и наблюдать за результатами в процессе выполнения, без необходимости перезагрузки программы (в нашем случае — демо).
Этот инструмент пока не идеален: его API слишком внедряется в код и ограничивает его структуру (классы обязаны быть производными от интерфейса), а компилирование и перезагрузка кода всё равно занимают несколько секунд. Тем не менее, возможность внесения изменений в логику кода внутри демо и видеть результат обеспечивает широкую творческую свободу. На текущий момент преимущества инструмента используются только для генераторов текстур и мешей, но в будущем мы хотим расширить его работу на всю совокупность кода, занимающегося контентом.
Здесь заканчивается первая часть того, что задумано как серия статей, посвящённых использованным в разработке H – Immersion техникам. Мы хотели бы поблагодарить Алана Вольфа за вычитку статьи; в его блоге есть множество интересных технических статей. В следующих частях мы подробнее расскажем о том, как создаются текстуры и меши.
Комментариев нет:
Отправить комментарий