Вместо вступления
Так уж вышло, что почти всегда, когда пишется какая-то игра - берётся готовый игровой движок. Расчёт прост: при минимальных входных затратах, в очень короткие сроки, создаётся, как минимум, прототип игры, а то и полноценное приложение. Да, есть косвенные затраты, но, как известно, скорость в бизнесе превыше всего.
Читал, как-то, статью о парне, который сам 5 лет писал игрушку. Классная игра вышла, только вот 5 лет срок немаленький и мало кто пойдёт на это.
Побочным эффектом такого штампования игр становится вымывание специалистов из отрасли: люди учатся каким-то базовым вещам, которые позволяют реализовывать заявленное, однако не понимают, как всё это работает. Получается парадокс: простейшая операция делается очень витиевато и сложно только потому, что просто сделать её специалист не умеет. Но, в силу того, что современные компьютеры и игровые системы очень высокопроизводительны - этого, обычно, никто не замечает. Ровно до тех пор, пока не приходится решать задачу, которая требует значительного количества ресурсов. И вот тут появляются проблемы: простейшая графика (скажем, 2d спрайты) выводят загрузку процессора на 100%, а количество кадров начинает значительно проседать.
Такое поведение было мной замечено в игре Ori and the Blind Forest в некоторых местах.
Нередко, это связано ещё и с тем, что в приложения тянутся библиотеки и механики, которые очень ресурсозатратны, а могли были бы быть решены существенно проще.
Плавно переходя к теме
Собственно тут, я хотел поговорить о графике. Как некоторые знают, мне нравится OpenGL, вернее его web реализация - WebGL. Главным образом потому, что это действительно восхитительный способ с минимальными затратами реализовывать восхитительную графику.
Хотя про "минимальные затраты" я, конечно, погорячился: затраты совсем не минимальны; надо, как минимум, неплохо понимать, что делать, чтобы был какой-то положительный результат.
Есть немало "обёрток" для библиотеки, которые позволяют реализовывать большую часть требуемых функций через удобные методы и вызовы (например https://threejs.org/); однако любое упрощение сводится к двум проблемам:
- Разработчик не понимает как это работает;
- Разработчик всё делает как получится, а не оптимально.
Если вы посмотрите примеры, приведённые в библиотеки three.js, на которую я сослался ранее, то можете заметить, что количество кадров в секунду там весьма невысоко (у меня было примерно 40), притом, что ничего сложного и высокопроизводительного на экране не отображалось.
Ближе к делу
В общем, я решил немного поэкспериментировать с графикой и временными отсечками.
Графику буду делать на WebGL; сперва попробую что-то типа анимации спрайтов, настройка эффектов и тому подобное.
Временные отсечки (или тайминги, как их ещё называют) - это некоторое "тактирование" игрового процесса, чтобы то, что отображалось на экране было синхронизировано: персонаж передвигается достаточно плавно, условные шаги в анимации и условное передвижение, примерно совпадают и тому подобное.
Кратко о WebGL
WebGL, кто не знает, это веб-версия OpenGL для встраиваемых систем. Относительно простая в использовании, но требующая понимания ряда пунктов:
- WebGL - это не про 3d графику, а в том числе и про 3d графику, однако всё строится вокруг изначально концепции: посередине точка 0, а дальше +/- 1 - правый (левый) и верхний (нижний) край, но это можно менять;
- Требуется программа отрисовки - это набор инструкций, как и что будет отрисовываться, отображаться и в какой последовательности (меня поначалу это слово "программа" сильно сбивала с толку, фактически - это просто набор инструкций, как всё должно выглядеть);
- WebGL, чаще всего, рисует треугольниками (то есть все формы, которые она обрабатывает - это, по-сути, фигуры, собранные из треугольников) - это несложно понять: прямоугольник - это два треугольника;
- Обычно, используется две программы: фрагментная (fragment) и вершинная (vertex). Вершинная - создаёт фигуру, фрагментная - закрашивает её. Эти программы называются затенителями или шейдерами (шейдерными программами);
- Библиотека работает примерно так: в буфере первой программой формируется некоторая фигура, которая затем раскрашивается второй программой (под раскраской может быть как наложение цвета, так и текстур);
- Помимо общего рисования, есть правила ранжирования фигур (z-Index), особенностей наложения и обработки;
- Также существует область видимости: всё то, что вне её - отрисовано не будет;
- Рисовать можно и 3d объекты, однако, для этого нужно перейти в специальный режим и это, надо заметить, требует определённой сноровки, т.к. все вычисления трёхмерности - сплошная математика и почти целиком она распределяется между вашим приложением и вершинной шейдерной программой;
- Тени, блики и многое другое - это не магия и не встроенные свойства, это - фрагментная шейдерная программа: фактически, все те интересные визуальные эффекты, что наблюдаются в играх - не какое-то встроенное свойство, а используемые или заново изобретённые наработки;
- Сама по себе WebGL - больше напоминает конечный автомат: вводится некоторое количество входных данных, переключаются состояния - и вуаля, отличный кадр.
Наверное, для вступления достаточно. В качестве первой задачи я решил создать параллакс-эффект. Кто не знает - это когда задний фон шевелится вместе с передним, но медленно так, вальяжно, дескать не царское это дело.
Параллакс в играх
По-сути, параллакс эффект - это довольно тривиальная задача: на экране отображается несколько слоёв, от двух и более (чаще всего более), когда первый - это непосредственно игровое полотно, по которому бегает персонаж, второй - это ближайший слой с "дырками", через которые видны следующие слои. Выглядит, обычно, неплохо.
Для начала напишем вступление: оно будет почти всегда одинаково: скомпилировать программу для вершинного и фрагментного шейдеров, связать их с программой и использовать. Далее, надо будет установить цвет заднего фона, настроить некоторые правила, задать параметры и отрисовать. Звучит сложно, но только в первый раз:
Посмотрели скриншоты и испугались? Правильно, WebGL заставит вас пройти все стадии: от страха и гнева, до принятия.
Но, тем не менее, в том коде, что вы увидели, рисуется всего лишь одна точка. И это вызывает тоску. Хорошая новость состоит в том, что если вам надо будет рисовать нечто вроде эйфелевой башни, составленной из разноцветных голов Тома Харди, то программа станет ненамного больше (чего не скажешь о программах шейдеров).
Собственно, не уверен, что мне надо давать полное обучение по тому, как пользоваться WebGL - для этого немало книг написано; я же перейду к тому, что обещал - созданию параллакс-эффекта.
Для начала это будет просто разноцветные зоны, которые будут двигаться с разной скоростью, однако, для определение факта движения - будет нанесена специальная разметка.
Я тут потратил некоторое количество времени, чтобы написать универсальную шейдерную программу, которая нарисует разного размера шахматную клетку на разных плоскостях:
- красная - самый дальний задний фон;
- зелёная - ближний задний фон;
- синяя - ближайший фон;
Вот тексты шейдерных программ:
То есть всё просто получилось. Относительно. Нетрудно заметить, что во фрагментном шейдере я использую операцию ветвления, а шейдеры её не любят, так как она замедляет работу. Однако именно тут это не особо важно.
Следующий этап - начать двигать плоскости синхронно. Сейчас они выровнены по левому краю, но каждая из них за единицу времени должна сместиться на разное количество пикселей с тем, чтобы в последний момент они выровнялись по правому краю страницы. В принципе, ничего удивительного.
Для данной задачи я заложил переменную (a_Offset в вершинном шейдере), которая будет смещать все три плоскости.
Впрочем, в коде, надо сказать, я несколько погорячился. У меня рисуются одна поверх другой три поверхности, а это можно было сделать одним набором данных. Ну да ладно, для первого раза можно и так сделать.
Движение
При попытке заставить всю эту конструкцию двигаться - вылезли, наверное, все косяки, которые только могли быть: и с раскраской в шахматную клетку, и с параметрами. Всё короче. Но не беда. Какие-то несколько часов недоумевания и проклятий - и всё заработало:
Следующим этапом будет наложить какие-нибудь текстуры и посмотреть, что из этого выйдет.
Текстуры
WebGL довольно непростая для понимания технология, особенно для тех, кто привык к объектно-ориентированному подходу. Но когда проясняешь для себя, что это, по сути, автомат, в который загружаешь данные, меняешь состояния, а потом жмёшь кнопку "нарисовать" - всё становится проще.
Чего не скажешь о текстурах. Они не хотели отображаться, от слова "совсем". То есть вот тебе чёрный прямоугольник и всё. Этого я понять не мог, поначалу, никак, хотя и делал всё по инструкции.
Оказалось всё довольно тривиально: рассчитать верное отображение данных бывает непросто, поэтому разработчики установили маленькое условие для текстур: их размер должен быть кратен степени двойки: 2, 4, 8, 16, 32 и так далее. В принципе, после соблюдения этого условия, всё сразу начинает работать сильно лучше (надо сказать, что есть настройки, как гласят легенды, позволяющие наложить структуру произвольного размера, однако без танцев с бубном, особенно поначалу, обойтись не удастся). В общем, я не стал устраивать себе дополнительный аттракцион - обрезал структуру до чётных размеров и всё заработало (хотя фрагментный шейдер пришлось ещё немного огорчить операцией ветвления).
Вообще, вынужден констатировать, что наложение текстур - это вообще не для слабых духом: надо либо предварительно прочитать подробное руководство по WebGL, не упустив деталей, либо смириться с тем фактом, что всё будет работать "немного не так" или вообще не работать.
Например, как сделать так, чтобы текстура размножилась? Применить какой-нибудь хитрый фильтр? О нет, это было бы слишком тривиально! Нужно указать размер текстуры в N раз больше, чем он есть с тем, потому что если размер (0,0) - (1,1) - текстура будет натянута целиком, а вот если указать (0,0) - (10,10) - то текстура будет натянутся, теоретически, 10 раз, но как она будет отображаться - решат уже другие фильтры. Впрочем, не стоит слишком сильно на них рассчитывать с первого раза, там везде "есть нюанс".
Убрать лишний цвет
Хотя уже давно существует прозрачный цвет, я решил использовать JPG в качестве текстур, а у него есть свои особенности. Дабы не выяснять, можно ли через него прокинуть прозрачность - выбрал неиспользуемый цвет, который должен стать прозрачным. Это будет выполнено в шейдере.
Однако, при таком наложении - вместо прозрачности я получил просто серый цвет. Тут, вероятно, проблема в том, что я не настроил наложение цветов.
Движемся дальше. Добавим дорогу и ограждение. Принцип тот же.
Хотел ещё дорогу немного подправить, чтобы выглядела более элегантно, однако изменение точки обзора не влияло на особенности наложения текстур (что, в принципе, закономерно), так что решил оставить как есть, хотя задел для искажения, конечно, сделал.
Заключение
Не обошлось, к слову, без казусов: firefox обрабатывает текстуры несколько иначе, нежели chrome, что привело к тому, что ядрёно-фиолетовая подложка под вторую текстуру получила иной код цвета и не обрабатывалась как надо; пришлось срочно допиливать, но чуть-чуть.
Результат можно посмотреть здесь, а все проекты тут. Исходники - тут. Дальше, думаю, немного развить эту тему и добавить туда кое-чего ещё. Так что не переключайтесь. Мы вернёмся после технического перерыва!