D:\sideБлогGameMaker Studio: шейдеры, что дальше

⚠️ Обращайте внимание на даты.
Этот блог больше не ведётся с 17 января 2023, и на тот момент с написания этой страницы (13.07.2013) прошло 9 лет.

Я тут упомянул, что намереваюсь при помощи шейдеров реализовать 2D-освещение. Это одна из основных причин, по которой я вообще за них взялся. Вода кажется не настолько сложной задачей, как освещение. Я долгое время просто не находил способов это сделать. Затем нашёл способ, реализованный в XNA. Там используется HLSL, способный на большее количество вещей, чем GLSL ES, с которым работаем мы в GameMaker Studio. Строго говоря, HLSL там тоже поддерживается, но только на Windows. К тому же, я не знаю HLSL, хоть он и выглядит очень похоже на GLSL.

Но код понятный. Непонятно только, для чего и зачем. Долгое время (недельку где-то) я просто не мог понять, как всё это работает, не то чтобы понять, как перенести его механику в GMS. Сегодня до меня это, наконец-то, дошло. 

Кто неплохо знает английский, может почитать оригинальный пост, в котором объясняется суть системы. И начиная с сегодняшнего дня, я буду пробовать переносить её в GMS. Посмотрим, что из этого выйдет.
Оригинальный пост про шейдерное 2D-освещение

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

Сразу оговорюсь, что мы тут будем вычислять освещённость одного источника в квадрате 512х512. Сложность? Всё это должно происходить за один шаг, то есть очень быстро. Здесь мы не говорим о том, чтобы сделать нетормозящую систему. Но скорее систему, которая будет слабо тормозить.

Что мы знаем о лучах света? Они с хорошей точностью распространяются по прямой, не особенно огибая препятствия (это не совсем правда, но нам так проще). Поэтому о световой маске мы можем заранее сказать, что между источником света и любой освещённой им точкой нет ни единого препятствия. Сводя данные о световой маске к минимуму - можно сказать, что нам нужно знать лишь о расстоянии до ближайшего препятствия в каждом направлении.

Хитрость первая: разбить всю освещаемую область на отдельные “лучи”, количество которых фиксировано. В нашем случае, пусть это будет 2048 лучей, так же сделал и автор. Это (512 \cdot 4), периметр нашего квадрата, чуть больше количества всех точек на максимальном удалении от источника света. То есть, вычислять освещение ещё точнее нет никакого смысла. Более того - даже такая точность может быть избыточной, и её снижение в 2 раза может быть не так заметно на картинке. Возможно, при реализации это придётся сделать.

Следующий шаг - получить картинку со всеми непрозрачными объектами. Создаём белый сюрфейс 512х512 и рисуем все непрозрачности вокруг источника света (который находится в центре сюрфейса), смешав с чёрным цветом, чтобы на нём были только чёрные силуэты. Автор исходной системы делает здесь следующий трюк: он для каждого непрозрачного пикселя записывает в “красный” канал каждого пикселя расстояние от него до центра, а в каждый прозрачный - что-нибудь большое (позже будет работа с минимумом, и прозрачные сразу отпадут). Зачем это надо, мы узнаем позже, пока нужно просто привыкнуть к тому факту, что в шейдерах мы будем обрабатывать текстуры.

Хитрость вторая: вывернуть пространство таким образом, чтобы все лучи от источника света стали горизонтальными. Просто потому что их так легче обрабатывать. Вы когда-нибудь видели панорамы улиц, расположенные не вокруг зрителя, а на одной длинной прямой стене? Издали выглядит очень странно. Вот здесь мы будем проворачивать нечто подобное, и выглядеть это будет настолько же странно.

То есть, в итоге у нас две точки, расположенные прямо в источнике, растянутся на все 512 пикселей, а точки на краю обрабатываемой зоны останутся там же, где они есть сейчас. Гляньте иллюстрации оригинала, там это объяснено. Тут есть некоторая свобода, можно покреативить с разворачиванием.

Автор оригинала соригинальничал вдвойне - он разбил плоскость на 4 “квадранта” (область, ограниченная двумя лучами), и результат их “натягивания” записал: верхнего и нижнего - в тот же, красный канал, левого и правого - в зелёный (и повернул их набок, чтобы лучи стали горизонтальными). Снова смотрим иллюстрации оригинала. Это просто экономия памяти. На самом деле, записать мы можем ещё больше, у нас есть ещё канал “синий” и “альфа” (прозрачность), им применение мы тоже найдём.

Имейте в виду, что это только названия для данных. Писать мы туда можем всё, что угодно. Можете это считать массивом из 4 чисел, только вместо 0123 там rgba. Но чем удобнее называть именно так - мы можем отображать значения в каждой точке, просто сделав из этих значений картинку. Автор так и делает с иллюстрациями.

Хитрость третья: сворачиваем полученные картинки до двух столбиков из пикселей. Результатом должен стать минимум расстояния, зафиксированный на каждом отдельном луче. Это расстояние, которое проходит отдельный луч до столкновения с препятствием. Технически автор делает это просто - он делает 8 проходов шейдером, который сплющивает картинку вдвое. Вот это, как он говорит “узкое место алгоритма”, и хорошо бы найти способ его сократить. Как? Ну, можно брать минимум из четырёх пикселей, к примеру. Получится 512 -> 128 -> 32 -> 8 -> 2, итого 4 прохода. Какой из способов использовать, станет ясно после тестов производительности.

Каждый пиксель новой картинки - минимум от двух пикселей старой. Отметьте - по красному и зелёному каналу по отдельности! Итого мы получим данных: два канала по два столбика по 512 значений - ровно 2048, количество лучей. А это значит… что задача решена!

Остаётся восстановить картину тени по полученным данным. Фрагментный шейдер получает на вход нашу “текстуру” с расстояниями, и вычисляет:

Дальше можно применить постобработку - приглушать цвет с расстоянием, применять размытие, что хотите.

Мне сейчас всё это кажется кошмаром. Я привык к программированию на ЦП, и мне кажется нереальным, что весь этот объём вычислений поместится в один шаг. Ну что ж. Проверим! Напишем что-нибудь попроще, для начала.