Высотные карты для пиксельной игры с видом сверху
Для «движка», разработанного мной для моей игры, я использую карты высот пиксельной графики, чтобы реализовать различные крутые эффекты: динамическую высоту воды, пересекающуюся геометрию, декали, 3D-освещение и даже z-сортировку сцены.
Я использую материалы, состоящие из двух текстур: diffuse-карту и то, что я назвал картой эффектов. Каждый канал цвета на карте эффектов используется для хранения дополнительной информации: красный — для карты высот, зелёный — для карты излучения; синий и альфа-каналы пока не используются.

Рендеринг происходит за два прохода: один для рендеринга буфера эффектов, в котором красный канал — это вручную рендерящийся буфер глубин, а второй для рендеринга в буфер кадров, который отображается на экране. (Маленькая зелёная область в буфере эффектов — это декаль тени в стоящего там NPC.)

В первом проходе рендеринга во фрагментном шейдере происходит следующее:
// Получаем значение этого пикселя в красном канале карты эффектов
float height = texture(effectMap, TexCoord).r;
// Прибавляем позицию сущности по z к базовой высоте
height += min(entityPos.z, 127.0) / 127.0;
// Ручной тест глубин:
// если мы уже отрендерили пиксель в этой позиции на большей высоте, ничего не делаем
if(gl_FragDepth > 1.0 - height)
discard;
// Вручную записываем высоту в буфер глубин
gl_FragDepth = 1.0 - height;
// Также записываем высоту в красный канал буфера эффектов,
// который будет использоваться как текстура на втором проходе рендеринга
FragColor = vec4(heightMapHeight, 0.0, 0.0, 1.0);
Во втором проходе рендеринга во фрагментном шейдере происходит следующее:
// Снова получаем значение этого пикселя в красном канале карты высот
// и вычисляем нормализованную абсолютную высоту
float height = texture(effectMap, TexCoord).r;
height += min(entityPos.z, 127.0) / 127.0;
// Получаем значение высоты буфера эффектов, отрендеренное в первом проходе
float effectBufferHeight = texture(effectBuffer, gl_FragCoord.xy / textureSize(effectBuffer, 0)).r;
// Если высота в буфере эффектов больше, чем высота этого пикселя,
// то этот пиксель перекрывается пикселем другого спрайта, поэтому не должен рендериться
if(effectBufferHeight > height)
discard;
Я не занимаюсь программированием графики профессионально, так что не стоит считать этот код оптимальным решением. Думаю, выполнение этой задачи в два прохода рендеринга может показаться затратным или ненужным, но поскольку оба фрагментных шейдера могут выполнять выход заранее, ещё до выполнения затратных вычислений света/контуров/воды/ambient occlusion (да, в моём 2D-движке есть ambient occlusion!), мне кажется, это вполне приемлемое решение.
Z-сортировка
В 2D-играх обычно требуется выполнять сортировку спрайтов по их координате y и отрисовывать всё сзади вперёд, что теперь не нужно благодаря рендерингу буфера глубин. Всю z-сортировку в игре выполняют лишь эти несколько строк кода (пиксельные карты высот, на создание которых были потрачено сотни часов ручного труда). Четырёхугольники могут находиться в любой координате z и отрисовываться в любом порядке. На самом деле, в этом случае для снижения объёма перерисовки спрайты должны отрисовываться спереди назад.
Но такая система намного мощнее, чем обычная z-сортировка. Представьте ситуацию: персонаж может перемещаться и находиться за стеной или перед ней. Координата y этого персонажа определяет, должен ли он отрисовываться до или после отрисовки стены. Но в других играх не может быть никаких промежуточных состояний, в которых часть персонажа отрисовывается перед стеной, а другая скрыта за стеной. В моём движке это решение принимается на уровне пикселей, а не спрайтов. Объекты могут накладываться друг на друга, как будто это почти 3D-модели или воксели.

Ambient occlusion
Так как края в местах пересечения спрайтов выглядели очень плохо, я реализовал эффект, напоминающий ambient occlusion. Шейдер просто проверяет, имеют ли соседние пиксели в карте эффектов ожидаемое значение спрайта, изучая пиксели в этих позициях на его собственной карте высот. Если они не совпадают, цвет затемняется.

Создаём карты высот
Карты высот приходится рисовать вручную, однако я написал несколько маленьких скриптов Aseprite, чтобы ускорить процесс. Благодаря использованию PNG с 8 битами на пиксель можно хранить 2^8 = 256 значений высот. Проблема здесь в том, что невооружённым глазом различить эти значения почти невозможно.
Я создал градиент с 256 значениями и придал значениям оттенки паттерна столбцов зелёного, жёлтого, красного, синего, сине-зелёного цветов и мадженты. Проблема различения значений с одинаковым оттенком стала менее явной, но разница между соседними значениями одного оттенка всё равно была едва заметна. Поэтому я решил использовать в качестве палитры пиксельных карт высот градиент из 128 цветов. Из-за этого сущности могут иметь высоту не более 128 пикселей, чего более чем достаточно для моей игры, а более высокие объекты при необходимости можно составить из нескольких сущностей.

Я вас всё ещё не убедил?
Преимущества такого рендеринга по максимуму используются в моей игре, например, для анимации вырастающих из земли зданий:
Но можно придумать и ещё множество других эффектов:
-
источники света в 3D-позициях и вычисление расстояния между пикселем и источником света с учётом всех трёх измерений
-
ноги персонажа, скрывающиеся в высокой траве или снегу
-
надевание брони/одежды/оборудования на персонажа без необходимости разбираться, какие пиксели находятся перед/позади спрайта персонажа
-
реальный объёмный туман
-
декали, использующие 3D-текстуры
-
и так далее
У GTA VI появился собственный кофе
Высокие оценки Warhammer 40,000: Boltgun 2 не уберегли её разработчиков от сокращений
Разобрал сохранение Skyrim VR с помощью ИИ-агентов и превратил его в словарь для изучения английского
Режиссёр объяснил, почему звезду «Во все тяжкие» почти целиком вырезали из «Клэйфейса»
Третьего сезона «Бегущих по краю» может не быть, шоураннер рассказал, при каких условиях возможно продолжение
Мобильная The Amazing Spider-Man 2 стала доступна на современных Android-устройствах
Microsoft продлила на месяц поиск покупателя для Arkane Lyon
Konami вела себя манипулятивно: компания намеренно ограничивала обмен информацией между командами, работающими над Silent Hill