Введение в интро
Демосцена — это о создании классных штук, которые работают в реальном времени (как бы «крутятся в вашем компьютере»). Их называют демки. Некоторые из них по-настоящему маленькие, скажем, 64k или меньше — такие называются интро. Название происходит от рекламирования или представления взломанных программ (crack intro). Итак, интро — это просто маленькая демка.
Я заметил, что многим интересны произведения демосцены, но они не имеют понятия, как в реальности делаются демки. В этой статье — мозговой дамп и посмертное вскрытие нашего свежего интро Guberniya. Надеюсь, будет интересно и новичкам, и опытным ветеранам. Статья затрагивает практически все техники, которые используются в демках, и должна дать хорошее представление о том, как их делать. В этой статье я буду называть людей по никам, потому что именно так принято на сцене.
Бинарник под Windows: guberniya_final.zip (61.8 kB) (немного ломается на картах AMD)
Guberniya в двух словах
Это 64k интро, выпущенное на демопати Revision 2017. Некоторые цифры:
- C++ и OpenGL, dear imgui для GUI
- 62976 байт бинарник под Windows, упакован kkrunchy
- в основном, рейкастинг (raymarching)
- группа из 6 человек
- один художник :)
- сделано за четыре месяца
- ~8300 строк C++, не считая кода библиотеки и пробелов
- 4840 строк шейдеров GLSL
- ~350 коммитов git
Разработка
Демки обычно выпускаются на демопати, где зрители смотрят их и голосуют за победителя. Выпуск для демопати даёт хорошую мотивацию, потому что у вас твёрдый дедлайн и страстная аудитория. В нашем случае это была Revision 2017, большая демопати, которая традиционно проходит в пасхальные выходные. Можете глянуть несколько фотографий, чтобы получить представление о мероприятии.
Количество коммитов в неделю. Самый большой всплеск — это мы срочно хакаем прямо перед дедлайном. Два последних столбца — изменения для финальной версии, после демопати
Мы начали работать над демкой в начале января и выпустили её на Пасху в апреле во время мероприятия. Можете посмотреть запись всего соревнования, если желаете :)
Наша команда состояла из шести человек: cce (это я), varko, noby, branch, msqrt и goatman.
Дизайн и влияние
Песня была готова на довольно ранней стадии, так что я попробовал нарисовать что-то по её мотивам. Было ясно, что нам нужно нечто большое и кинематографическое с запоминающимися частями.
Первые визуальные идеи вращались вокруг проводов и их использования. Мне действительно нравятся работы Виктора Антонова, так что первые наброски во многом скопированы из Half-Life 2:
Первые наброски башен цитадели и амбициозных человеческих персонажей. Полный размер.
Концептуальная работа Виктора Антонова для Half-Life 2: Raising the Bar
Сходства вполне очевидны. В ландшафтных сценах я также пытался передать настроение Eldion Passageway Энтони Шимеса.
Ландшафт создан под вдохновением этого славного видео об Исландии, а также «Койяанискаци», наверное. У меня имелись большие планы насчёт истории, изображённой на раскадровке:
Эта раскадровка отличается от окончательной версии интро. Например, брутальную архитектуру вырезали. Полная раскадровка.
Если я бы делал заново, то ограничился бы просто парой фотографий, которые задают настроение. Так меньше работы и больше пространства для воображения. Но по крайней мере рисование помогло мне упорядочить мысли.
Корабль
Космический корабль спроектировал noby. Это сочетание многочисленных фракталов Мандельброта, пересекающихся с геометрическими примитивами. Дизайн корабля остался немного незавершённым, но нам показалось, что лучше не трогать его в финальной версии.
Космический корабль представляет собой рейкастинг поля расстояний, как и всё остальное.
У нас был ещё один шейдер корабля, который не вошёл в интро. Сейчас я смотрю на дизайн, он очень крут, и очень жаль, что для него не нашлось места.
Дизайн космического корабля от branch. Полный размер.
Реализация
Мы начали с кодовой базы нашего старого интро Pheromone (YouTube). Там была базовая функциональность кадрирования и библиотека стандартных функций OpenGL вместе с утилитой файловой системы, которая паковала файлы из директории с данными в исполняемый файл, используя bin2h
.
Рабочий процесс
Для компиляции проекта мы использовали Visual Studio 2013, потому что он не скомпилировался в VS2015. Наша замена стандартной библиотеки не очень хорошо работала с обновлённым компилятором и выдавала забавные ошибки вроде таких:
Visual Studio 2015 не ужился с нашей кодовой базой
По какой-то причине мы всё равно застряли на VS2015 как редакторе и просто скомпилировали проект, используя инструментарий платформы v120.
Большая часть моей работы с демкой выглядело так: шейдеры открыты в одном окне, а конечный результат с консольной выдачей — в других. Полный размер.
Мы сделали простой глобальный перехват нажатий клавиш, который перезагружал все шейдеры, если обнаруживал комбинацию CTRL+S:
// Listen to CTRL+S.
if (GetAsyncKeyState(VK_CONTROL) && GetAsyncKeyState('S'))
{
// Wait for a while to let the file system finish the file write.
if (system_get_millis() - last_load > 200) {
Sleep(100);
reloadShaders();
}
last_load = system_get_millis();
}
Это работало по-настоящему классно, и редактирование шейдеров в реальном времени стало гораздо интереснее. Не нужны какие-то перехваты событий файловой системы и тому подобное.
GNU Rocket
Для анимации и постановки мы использовали Ground Control, форк GNU Rocket. Rocket — это программа для редактирования анимационных кривых, она подключается к демке через сокет TCP. Опорные кадры отправляются по запросу демки. Это очень удобно, потому что вы можете редактировать и перекомпилировать демку, не закрывая редактор и не рискуя потерять позицию синхронизации. Для окончательной версии опорные кадры экспортируются в бинарный формат. Впрочем, там есть некоторые досадные ограничения.
Инструмент
Изменять точку зрения мышкой и клавиатурой очень удобно, чтобы выбирать углы камеры. Даже простой GUI сильно помогает, когда мелочи имеют значение.
В отличие от некоторых, у нас не было инструмента для демок, так что пришлось создавать его по мере работы. Великолепная библиотека dear imgui позволяет легко добавлять функции по мере надобности.
Например, нужно добавить несколько ползунков для управления параметрами цветности — достаточно всего лишь внести эти строчки в цикл рендеринга (не в отдельный код GUI).
imgui::Begin("Postprocessing");
imgui::SliderFloat("Bloom blur", &postproc_bloom_blur_steps, 1, 5);
imgui::SliderFloat("Luminance", &postproc_luminance, 0.0, 1.0, "%.3f", 1.0);
imgui::SliderFloat("Threshold", &postproc_threshold, 0.0, 1.0, "%.3f", 3.0);
imgui::End();
Конечный результат:
Эти ползунки было легко добавить.
Позицию камеры можно сохранить в файл .cpp
, нажав F6
, так что после следующей компиляции она будет в демке. Это устраняет необходимость отдельного формата данных и соответствующего кода сериализации, но такое решение тоже может оказаться довольно неаккуратным.
Делаем маленькие бинарники
Главное для минимизации бинарника — выбросить стандартную библиотеку и сжать скомпилированный бинарник. В качестве базы для нашей собственной реализации библиотеки мы использовали Tiny C Runtime Library от Mike_V.
Сжатием бинарников занимается kkrunchy — инструмент, сделанный именно для этой цели. Он работает с отдельными исполняемыми файлами, так что вы можете написать свою демку на C++, Rust, Object Pascal или чём угодно ещё. Если честно, размер для нас не был особой проблемой. Мы не хранили много бинарных данных вроде изображений, так что было пространство для манёвра. Даже не пришлось удалять комментарии из шейдеров!
Плавающие запятые
Код с плавающей запятой доставил некоторую головную боль, осуществляя вызовы к функциям несуществующей стандартной библиотеки. Большинство из них удалось устранить, отключив векторизацию SSE ключом компилятора /arch:IA32
и удалив вызовы к ftol
с помощью флага /QIfst
, который генерирует код, не сохраняющий флаги FPU для режима усечения. Это не проблема, потому что вы можете установить режим усечения с плавающей запятой в начале своей программы с помощью такого кода от Питера Шоффхаузера:
// set rounding mode to truncate
// from http://www.musicdsp.org/showone.php?id=246
static short control_word;
static short control_word2;
inline void SetFloatingPointRoundingToTruncate()
{
__asm
{
fstcw control_word // store fpu control word
mov dx, word ptr [control_word]
or dx, 0x0C00 // rounding: truncate
mov control_word2, dx
fldcw control_word2 // load modfied control word
}
}
Можете почитать больше о подобных вещах на benshoof.org.
POW
Вызов pow
по-прежнему генерирует вызов к внутренней функции __CIpow
, которая не существует. Я никак не мог сам выяснить её сигнатуру, но нашёл реализацию в ntdll.dll из Wine — стало ясно, что она ожидает в регистрах два числа двойной точности. После этого стало возможным сделать враппер, который вызывает нашу собственную реализацию pow
:
double __cdecl _CIpow(void) {
// Load the values from registers to local variables.
double b, p;
__asm {
fstp qword ptr p
fstp qword ptr b
}
// Implementation: http://www.mindspring.com/~pfilandr/C/fs_math/fs_math.c
return fs_pow(b, p);
}
Если знаете лучший способ, как бороться с этим, пожалуйста, сообщите.
WinAPI
Если не можете рассчитывать на SDL или нечто подобное, то приходится использовать чистый WinAPI для необходимых операций по выводу окна на экран. Если возникли проблемы, вот что может помочь:
Обратите внимание, что в последнем примере мы загружаем указатели функций только для тех функций OpenGL, которые реально используются в деле. Хорошей идеей может быть автоматизировать это. К функциям нужно обращаться вместе со строковыми идентификаторами, которые хранятся в исполняемом файле, так что чем меньше функций загружается — тем больше экономия места. Опция Whole Program Optimization может убрать все неиспользуемые строковые литералы, но мы не будем её использовать из-за проблемы с memcpy.
Техники рендеринга
Рендеринг производится, в основном, методом рейкастинга, и для удобства мы использовали библиотеку hg_sdf. Иньиго Куилез (с этого момента именуемый просто iq) многое написал об этой и многих других техниках. Если вы когда-нибудь посещали ShaderToy, то должны быть знакомы с этим.
Вдобавок, у нас была выдача рейкастера — значение буфера глубины, так что мы могли совместить знаковые поля расстояний с геометрией в растере, а также применить эффекты пост-обработки.
Шейдинг
Мы применили стандартный шейдинг Unreal Engine 4 (вот большой pdf с описанием) с капелькой GGX. Это не очень заметно, но имеет значение в основным моментах. С самого начала мы планировали сделать одинаковое освещение как для рейкастинга, так и для растеризированных форм. Идея была в использовании отложенного рендеринга и теневых карт, но это совершенно не получилось.
Один из первых экспериментов с наложением теневых карт. Заметьте, что обе башни и провода отбрасывают тень на рейкастинговую землю и также правильно пересекаются. Полный размер.
Невероятно сложно правильно провести рендеринг больших территорий с теневыми картами из-за дико скачущего соотношения экрана-к-теневой карте-текселю и других проблем с точностью. У меня также не было желания начинать эксперименты с каскадными теневыми картами. К тому же, рейкастинг одной и той же сцены с разных углов зрения реально медленный. Так что мы просто решили отдать на слом всю систему одинакового освещения. Это оказалось огромной проблемой позже, когда мы пытались соотнести освещение растеризированных проводов и рейкастинговой геометрии сцены.
Местность
Рейкастинг местности производился численным шумом с аналитическими производными.1 Конечно, сгенерированные деривативы использовались для наложения теней, но также для управления шагом лучей для ускорения обхода лучами плавных контуров, как в примерах iq. Если хотите узнать больше, то читайте старую статью об этой технике или поиграйтесь с классной сценой тропического леса на ShaderToy. Карта высот ландшафта стала более реалистичной, когда msqrt реализовал экспоненциально распределённый шум.
Первые тесты моей собственной реализации численного шума.
Реализация местности от branch, которую решили не использовать. Не помню почему. Полный размер.
Эффект ландшафта рассчитывается очень медленно, потому что мы брутфорсим тени и отражения. Использование теней — это небольшой хак с тенями, в котором размер полутени определяется кратчайшим расстоянием, которое встретилось при обходе луча тени. Они выглядят довольно неплохо в действии. Мы также попытались использовать половинчатую трассировку для ускорения эффекта, но она производила слишком много артефактов. С другой стороны, хитрости рейкастинга от Mercury (ещё одна демогруппа) помогли нам немного улучшить качество без потери скорости.
Рендеринг ландшафта улучшенными итерациями с фиксированной запятой (слева) по сравнению с обычным рейкастингом (справа). Обратите внимание на неприятные артефакты ряби на картинке справа.
Небо генерируется практически такими же техниками, как описано в behind elevated от iq, слайд 43. Несколько простых функций вектора направления луча. Солнце выдаёт довольно большие значения в кадровый буфер (выше 100), так что это тоже добавляет некоторой цветовой естественности.
Сцена с переулком
Это вид, созданный под влиянием фотографий Фан Хо. Наши эффекты пост-обработки действительно позволили создать цельную сцену, хотя изначальная геометрия довольно проста.
Безобразное поле расстояния с некоторыми повторяющимися фрагментами. Полный размер.
Добавлено немного тумана с экспоненциальным изменением расстояния. Полный размер.
Провода делают сцену более интересной и реалистичной. Полный размер.
В окончательной версии в поле расстояния добавлено немного шума, чтобы создать впечатление кирпичных стен. Полный размер.
При пост-обработке добавлены цветной градиент, цветность, хроматические аберрации и блики. Полный размер.
Моделирование с полями расстояний
Бомбардироващики B-52 — хороший пример моделирования со знаковыми полями расстояний. Они были гораздо проще на этапе разработки, но мы довели их к финальному релизу. Издали выглядят довольно убедительно:
Бомбардировщики нормально выглядят на расстоянии. Полная версия.
Однако это просто кучка капсул. По общему признанию, было бы легче просто смоделировать их в каком-нибудь 3D-пакете, но у нас под рукой не было какого-нибудь инструмента для редактирования полигональных сеток, так что мы выбрали более быстрый способ. Просто для справки, вот как выглядит шейдер поля расстояния: bomber_sdf.glsl.
Однако они на самом деле очень простые. Полный размер.
Персонажи
Первые четыре кадра анимации козла.
Анимированные персонажи — это просто упакованные 1-битные растровые изображения. При воспроизведении кадры плавно переходят от одного к другому. Материал предоставил таинственный goatman.
Козопас со своими друзьями.
Пост-обработка
Эффекты пост-обработки написал varko. Система следующая:
- Наложить тени из G-буфера.
- Вычислить глубину резкости.
- Извлечь светлые части для цветности.
- Выполнить N отдельных операций гауссовского размытия.
- Вычислить блики фальшивого объектива и блики прожекторов.
- Составить всё вместе.
- Сделать плавные контуры с помощью FXAA (спасибо, mudlord).
- Цветовая коррекция.
- Гамма-коррекция и лёгкая зернистость.
Блики объектива во многом следуют технике, описанной Джоном Чэпмэном. С ними иногда было тяжело работать, но конечный результат доставляет.
Мы попытались эстетично использовать эффект глубины резкости. Полный размер.
Эффект глубины резкости (основанный на технике DICE) делается в три прохода. Первый вычисляет размер кружка нерезкости для каждого пикселя, а два других прохода накладывают на них два пятна из вращающихся областей. Мы также делаем улучшение в несколько итераций (в частности, накладываем многочисленные гауссовские размытия) при необходимости. Такая реализация хорошо работала у нас и с ней было весело играться.
Эффект глубины резкости в действии. На красной картинке показан рассчитанный кружок резкости для пятна DOF.
Цветовая коррекция
В Rocket есть анимированный параметр pp_index
, который используется для переключения между профилями цветовой коррекции. Каждый профиль — просто разные ветки большого оператора ветвления в шейдере окончательной пост-обработки:
vec3 cl = getFinalColor();
if (u_GradeId == 1) {
cl.gb *= UV.y * 0.7;
cl = pow(cl, vec3(1.1));
} else if (u_GradeId == 2) {
cl.gb *= UV.y * 0.6;
cl.g = 0.0+0.6*smoothstep(-0.05,0.9,cl.g*2.0);
cl = 0.005+pow(cl, vec3(1.2))*1.5;
} /* etc.. */
Он очень простой, но работает достаточно хорошо.
Физическое моделирование
В демке есть две моделируемые системы: провода и стая птиц. Их тоже написал varko.
Провода
Провода добавляют сцене реалистичности. Полный размер.
Провода рассматриваются как ряд пружин. Их моделируют на GPU с использованием вычислительных шейдеров. Мы делаем эту симуляцию во много маленьких шажков из-за нестабильности метода численного интегрирования Верле, который здесь используем. Вычислительный шейдер выдаёт также геометрию провода (ряд треугольных призм) в буфер вершин. К сожалению, по какой-то причине симуляция не работает на картах AMD.
Стая птиц
Птицы дают ощущение масштаба.
Модель стаи состоит из 512 птиц, где первые 128 считаются лидерами. Лидеры двигаются по шаблону вихревого шума, а остальные следуют за ними. Думаю, что в реальной жизни птицы следуют за движениями ближайших соседей, но и такое упрощение выглядит достаточно хорошо. Стая рендерилась как GL_POINTs
, у которых модулировался размер, чтобы создать впечатление взмахов крыльев. Я думаю, такая техника рендеринга также использовалась в Half-Life 2.
Музыка
Обычно музыку для 64k интро делают с помощью VST-плагина: так музыканты могут использовать свои привычные инструменты для сочинения музыки. Классический пример такого подхода — V2 Synthesizer от farbrausch.
Это было проблемой. Я не хотел использовать какой-то готовый синтезатор, но из предыдущих неудачных экспериментов мне было известно, что изготовление собственного виртуального инструмента потребует много работы. Помню, как мне действительно понравилось настроение демки element/gesture 61%, которую сделал branch с музыкальной эмбиент-темой, подготовленной в paulstretched. Это натолкнуло меня на мысль реализовать такое в размере 4k или 64k.
Paulstretch
Paulstretch — великолепный инструмент для действительно сумасшедшего растягивания музыки. Если вы о нём не слышали, то вам определённо стоит послушать, что он может сделать из звука приветствия Windows 98. Его внутренние алгоритмы описаны в этом интервью с автором, и он к тому же open source.
Оригинальный звук (сверху) и растянутый звук (снизу), созданный с помощью эффекта Paulstretch для Audacity. Заметьте также, как частоты размазываются по спектру (вертикальная ось).
По существу, вместе с растяжением исходного сигнала он ещё и взбалтывает его фазы в частотном пространстве, так что вместо металлических артефактов вы получаете неземное эхо. Это требует множества преобразований Фурье, и оригинальное приложение использует для этого библиотеку Kiss FFT. Я не хотел зависеть от внешней библиотеки, так что в итоге реализовал простое дискретное преобразование Фурье на GPU. Потребовалось много времени, чтобы правильно это реализовать, но в конце концов оно стоило того. Реализация шейдера GLSL очень компактная и работает довольно быстро, несмотря на свою брутфорсовую природу.
Модуль трекера
Теперь стало возможным наматывать витки эмбиентного гудения, если в качестве исходных данных есть какой-то осмысленный звук. Так что я решил использовать проверенную и протестированную технологию: трекерную музыку. Оно во многом похожа на MIDI2, но упакована в файл вместе с сэмплами. Например, в демке kasparov от elitegroup (YouTube) используется модуль с дополнительной реверберацией. Если это работало 17 лет назад, то почему не будет сейчас?
Я использовал gm.dls
— встроенный в Windows звуковой банк MIDI (опять старый трюк) и сделал песню с помощью MilkyTracker в формате модуля XM. Этот формат использовался ещё для многих демок под MS-DOS в 90-е годы.
Я использовал MilkyTracker для сочинения оригинальной песни. Окончательный файл модуля очищен от сэмплов инструментов, а вместо них поставлены параметры смещения и длин из gm.dls
Подвох с gm.dls
в том, что инструменты Roland от 1996 года звучат очень архаично и некачественно. Но оказалось, что в этом нет никакой проблемы, если погрузить их в тонну ревербераций! Вот пример, в котором сначала играет короткая тестовая песня, а затем растянутая версия:
На удивление атмоферно, согласитесь? Так что да, я сделал песню, которая имитирует голливудскую музыку, и она классно получилась. Это в целом всё, что касается музыкальной стороны.
Благодарности
Спасибо varko за помощь в некоторых технических деталях этой статьи.
Дополнительные материалы
- Феррис из группы Logicoma показывает свой набор инструментов для создания демок 64k
- Не забудьте сначала посмотреть Engage, их работа на том же конкурсе, в котором участвовали мы
- Исходники некоторых демок Ctrl-Alt-Test
- Есть код 4k и 64k.
- У них тоже есть интро: H-Immersion
1. Вы можете вычислить аналитические производные и для градиентного шума: https://mobile.twitter.com/iquilezles/status/863692824100782080 ↑
2. Первой мыслью было просто использовать MIDI вместо трекерного модуля, но не похоже, чтобы был способ простого рендеринга песни в аудиобуфер Windows. Видимо, каким-то образом такое возможно с помощью DirectMusic API, но я не смог найти как. ↑
Автор: m1rko