[Перевод] Карты высот для пиксельной игры с видом сверху

Для «движка», разработанного мной для моей игры, я использую карты высот пиксельной графики, чтобы реализовать различные крутые эффекты: динамическую высоту воды, пересекающуюся геометрию, декали, 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-текстуры
и так далее
Habrahabr.ru прочитано 5788 раз