Итак, что такое ray casting? Дословно -- "бросание лучей". Фактически -- это способ сделать якобы трёхмерную картинку из полностью двухмерной карты. Как? Очень просто. Из "камеры" испускаются лучи, по одному на каждый столбец пикселей на экране. Испускаются они в секторе, который есть наш угол зрения, FoV. Как только луч сталкивается с первым препятствием на своём пути, он останавливается и докладывает пройденную дистанцию. После чего на экране рисуется столбик пикселей, и высота сего столбика обратно пропорциональная длине луча. Наглядно процесс можно увидеть на видео выше. Жёлтенькие лучи -- это испускаемые из камеры лучи. Чёрные линии -- стены, рисуемые на карте. А слева-сверху -- вид из камеры.
Казалось бы, что можно придумать в таком простом способе делать красивую "3д" картинку из банальных 2д карт? Оказывается, много чего!
Способ задания стен
Удивительно, но есть два основных способа.
Исторически первым был способ, когда карта состояла из клеток, и лучи определяли столкновение самым банальным образом, по алгоритму DDA-линии.
Плюсы и минусы данного метода:
+ Высокая скорость работы, которая не зависит от размера и сложности карты: Максимальное число проверок столкновения для каждого из лучей = дистанция_обзора * 2
+ Максимально простой формат карт, придумать простую карту можно в клетчатой тетради
-- Минимальная часть карты - это квадрат размером 1х1 условную единицу, причём размещается он строго в своей ячейке.
Второй способ даёт большую гибкость ценой производительности. В нём каждая стена -- это линия, заданная двумя точками. В продвинутом варианте используются не только стены, но и полноценные объекты, которые состоят из ломанной линии. В этом случае пересечение луча и стены определяется более прожорливым алгоритмом поиска пересечения двух отрезков.
Плюсы и минусы данного метода:
+ Высокая гибкость: можно создавать стены любой формы (вернее, граница стен будет любой формы, но все они будут абсолютно вертикальными, без наклонов и прочего)
+ Стены любой формы можно размещать в любом месте карты.
-- Скорость работы стремительно падает с возрастанием сложности карты: Максимальное число столкновений для каждого из лучей = число_отрезков_стен_на_всей_карте.
Стоит отметить, что есть способы оптимизации второго способа, но я их касаться не буду.
В дальнейшем я буду разбирать именно первый способ задания стен.
Разнообразие блоков и возможностей
Нетрудно догадаться, что в текущем виде потенциальная игра будет выглядеть скудно. Одни квадратные блоки... скучно! К счастью, в подобном движке можно сделать ещё многое:
- Текстуры
- (Частично)прозрачные блоки
- Зеркала
- Смещать любой блок по высоте (несмотря на то, что у двухмерной карты нет высоты!)
- Следствие из предыдущих пункта: многоэтажность, полублоки и прыжки
- Пол
- Освещение
Рассмотрим каждый из пунктов чуть подробнее.
Текстуры: до ужаса банальная механика: луч запоминает не только тип блока, на котором остановился, но и конкретное место на его стороне (обозначим его как % от ширины блока). При отрисовке соответствующего столбика на экране просто берётся нужный кусок текстуры нужного блока (находим нужный % от ширины текстуры, и вырезаем его)
Прозрачность, полублоки: когда луч доходит до прозрачного блока, он записывает в стек данные о блоке, но не останавливается, а продолжает двигаться дальше. При отрисовке на экран выводятся блоки в обратном порядке: от последнего к первому. Если нарисовать текстуру блока без верхней или нижней половины, то легко получить нижний/верхний полублок, сквозь прозрачную часть которого будет видно стоящие позади него блоки. Правда без пола это будет выглядеть... странно.
Зеркала: когда луч доходит до зеркала, он не останавливается, а отражается от поверхности (получает новое направление в соответствии с законом отражения), после чего весь процесс аналогичен обработке прозрачности.
Смещение по высоте, многоэтажность, прыжки: высота каждого блока кодируется вещественным числом, где 1.0 соответствует высоте стены во весь экран на дистанции 1 клетки от него.
Верхняя граница второго этажа
= screen_height / 2 - wall_height * (screen_height * 1.5)
Нижняя граница второго этажа
= screen_height / 2 - wall_height* (screen_height * 0.5)
Что это значит? screen_height / 2 это позиция середины экрана (а все обычные блоки при стандартных настройках симметричны относительно середины экрана). Как нетрудно догадаться, (screen_height * k) показывает расстояние от середины экрана до верха/низа стены. Если быть точным, то k для низа стены - это номер этажа минус 1.5. Т.к. вверх этажа k это низ этажа k+1, то верх этажа k это номер этажа k+1 минут 1.5.
Абсолютно предсказуемо это работает и для дробной высоты этажа, что даёт возможность создать парящие на высоте 2/3 блока от земли блоки, или... прыгать! Ага. Прыжок, в лучших традициях bethesda game studio, просто "толкает" весь мир вниз на высоту прыжка.
Пол: на удивление сложная механика. Просто вдумайтесь: если на отрисовку стен уходит x лучей, то для отрисовки пола их нужно x*y/2 штук! А если прибавить потолок, то их число дойдёт до космических x*y штук! Дальнейшая математика пола сложна для обзорной статьи, которую я пишу больше для себя. Подробности можно прочитать, как всегда, здесь.
Освещение:
- Статическое с точечным источником, учитывающее препятствия:
мы сохраняем координату "лампочки", после чего при загрузке карты пускаем из неё по одному лучу в каждый пиксель границы карты. Там, где луч коснулся стены, записываем уровень освещения, который либо равен 1 (свет с бесконечной дальностью освещения), либо 1/длина_луча (свет, который затухает с расстоянием), либо можно подобрать свой коэффициент. Точно также можно проверить каждый пиксель пола. Всё это ОЧЕНЬ долго, поэтому свет нужно "запечь" в карту, и загружать вместе с ней. - Динамическое простое: пока не придумал, как учитывать стены. Но пол осветить легко, если вспомнить математику круга под наклоном. Но есть одна проблема, которую я нарисую:
Способы создания многоэтажности
Как ни странно, но способов два.
Первый способ -- это хранить всю многоэтажность на одной плоской карте! Как? Очень просто, но очень сложно. Для каждого многоэтажного блока задаётся индивидуальное поведение. Выглядит это так:
Блок земляного пола с каменным потолком над проходом
Блок прохода с каменным полком над проходом, и рвом с водой под проходом
...
В общем, муторная дичь. Но для шутера с маленьким разнообразием может подойти.
Второй способ куда лучше. При этом он банален и прост. А именно: каждый этаж записывается... в своей карте! После чего на экран рисуются последовательно все этажи (с нужным смещением из прошлой главы), причём самый далёкий этаж рисуется первым, а этаж, на котором находится игрок -- самым последним.
Оконцовка
Таким образом, в псевдотрёхмерной игре можно реализовать многоэтажные уровни с дырами в полу, зеркалами, окнами, полублоками, лестницами и прыжками. Как по мне -- весьма внушительный набор возможностей.