![](https://habrastorage.org/webt/tm/rb/vp/tmrbvpygkzfu1oiwp9_axrl3l6k.jpeg)
Введение
На мой взгляд, рейкастинг — великолепная концепция, понять которую не так сложно, но качественных ресурсов по ней очень мало. Я расскажу о математике, лежащей в её основе, чтобы вы при желании с лёгкостью могли реализовать её в своих будущих проектах. Постараюсь сделать объяснение как можно более понятным, рассказать о всех тонкостях и проблемах, с которыми вы можете столкнуться. Также мы поговорим об оптимизации и о том, как сильно вам могут помочь пространственные хэш-карты. Ещё я напишу простые интерактивные примеры, с которыми вы сможете поэкспериментировать. Учтите, что демо написаны максимально простым образом, не ожидайте увидеть в них код уровня энтерпрайза — мы изучаем саму концепцию, а не реализацию.
Что такое рейкастинг?
«Рейкастинг (ray casting, „испускание лучей“) — самый простой из множества алгоритмов рендеринга компьютерной графики, использующих геометрический алгоритм трассировки лучей (ray tracing). Алгоритмы рендеринга на основе трассировки лучей работают на уровне изображений, выполняя рендеринг трёхмерных сцен в двухмерные изображения…
Принцип рейкастинга заключается в том, чтобы попиксельно отслеживать испускаемые из глаза лучи, чтобы найти ближайший объект, препятствующий пути каждого луча — представьте, что изображение — это проволочная сетка, а каждый квадрат сетки — это пиксель. Таким образом мы можем определить объект, который видит глаз через этот пиксель.» — Википедия
Ну, звучит не очень понятно, правда? Давайте упростим объяснение. Рейкастинг — это популярная фундаментальная техника, используемая для определения видимости определённых объектов (полигонов) трассировкой лучей из глаза (например, персонажа игрока) до каждого пикселя (ну, в нашем случае это не совсем так, но подробнее об этом позже) и нахождением самых ближайших пересечений с объектами.
Где может использоваться рейкастинг?
Рейкастинг можно использовать по-разному, особенно в трёхмерном пространстве. Я выделю три наиболее важных по моему мнению применения, часто встречающихся в игровых 2D-движках:
Создание 3D-перспективы на 2D-карте
Самой известной игрой, использующей эту технику, является Wolfenstein 3D. Лучи в ней трассировались для определения ближайших объектов, а их расстояние от позиции игрока использовалось для правильного масштабирования.
![](https://habrastorage.org/getpro/habr/post_images/e7b/077/52a/e7b07752abe1a72221344fbbbeee75e3.gif)
Простой рейкастинг с коррекцией фишай Лукаса Виейра
Задача точки в многоугольнике
Задача PIP (point-in-polygon) заключается в определении того, находится ли точка внутри, снаружи или на границе многоугольника. При помощи алгоритма рейкастинга мы можем посчитать, сколько раз точка пересекает края многоугольника. Если количество пересечений чётно, то точка находится снаружи многоугольника. Если количество пересечений нечётно, то точка находится внутри или на границе многоугольника.
![](https://habrastorage.org/getpro/habr/post_images/b2f/bb4/fdf/b2fbb4fdf939bd74c176f0d1ee9b9969.jpg)
Решение задачи точки в многоугольнике при помощи рейкастинга
Видимость объектов и испускание света
В статье мы рассмотрим именно эту задачу — определение видимости объектов для игрока и освещение видимой области.
![](https://habrastorage.org/getpro/habr/post_images/705/187/4fc/7051874fcaf08c725e5f3b74095da460.jpg)
Видимость объектов и испускание света
Видимость объектов и испускание света в 2D-играх
В этом разделе мы рассмотрим такие базовые понятия, как точки пересечения, испускание лучей, сортировка точек пересечения для освещения видимой области, а также несколько слов скажем о кругах. На каждом из этапов я буду писать интерактивные демо, чтобы вы не запутались и могли сразу же увидеть результаты.
Точка пересечения отрезка и луча
Здесь мы поговорим о точках пересечения. Давайте поэтапно разберёмся, как их находить и использовать. Сначала мы выведем параметрическое уравнение прямой, вычислим точки пересечения, а затем узнаем, как эффективно определять ближайшую из них.
Вывод параметрического уравнения прямой
Давайте сначала поговорим о прямых и их параметрическом уравнении.
![](https://habrastorage.org/getpro/habr/post_images/f35/f77/fbd/f35f77fbd2caa312cdff87bed99b8dc3.jpg)
Пример прямой линии
Мы можем выразить вектор
Позвольте продемонстрировать пару примеров:
![](https://habrastorage.org/getpro/habr/post_images/f71/411/66d/f7141166d1efa7fc4e25cfe65a18172c.jpg)
Пример параметров параметрического уравнения прямой
Как вы могли заметить, мы можем использовать
Теперь мы можем легко увидеть, нужно ли нам, чтобы точка находилась на:
- прямой:
,
- луче:
,
- отрезке прямой:
.
Выяснив всё это, мы наконец можем вывести параметрическое уравнение:
Если вы всё ещё не понимаете, что здесь происходит, то рекомендую вам прочитать потрясающую короткую лекцию Норма Прокупа: Parametrizing a Line Segment — Concept.
Вычисление точки пересечения
Допустим, точка
Вычислим
Подставим
Подставим
Вычислив
![](https://habrastorage.org/webt/o4/84/mb/o484mbmsntsghda1mdyp_ckj9c8.jpeg)
Демо 1 — все точки пересечения
[Прим. пер.: в оригинале статьи все демо интерактивны.]
Поиск ближайшей точки пересечения
Для правильной отрисовки видимой области нам нужна только ближайшая точка пересечения. Наивным решением было бы вычисление расстояний между точкой начала луча и точками пересечения при помощи теоремы Пифагора:
![](https://habrastorage.org/webt/1u/tz/ua/1utzua6cflv4jzt7pzqu1j98uao.jpeg)
Демо 2 — ближайшая точка пересечения
Испускание лучей
В этом разделе мы рассмотрим два способа испускания лучей. Мы сравним их и расскажем об их достоинствах и недостатках.
Испускание лучей по углу смещения
Первый способ — это испускание лучей во всех направлениях с заданным углом смещения. Например, мы можем испустить 30 лучей, смещённых на
Пусть
![](https://habrastorage.org/getpro/habr/post_images/85e/4cd/e20/85e4cde20b74870710c6907e764b4e66.jpg)
Линия из заданной точки под неким углом
Мы можем задать
Теперь нам нужно вывести формулы для
В нашем случае
![](https://habrastorage.org/webt/zb/u3/zn/zbu3znsnelwslfplbn-1jghncjy.jpeg)
Демо 3 — испускание лучей на угол смещения
Испускание лучей в вершины
Вероятнее всего, вам больше подойдёт испускание лучей в вершины. Вместо испускания лучей во всех направлениях мы просто можем испускать их в вершины многоугольников. Если количество вершин невелико, мы можем сэкономить вычислительные ресурсы, не испуская бесполезные лучи. В следующих разделах мы увидим, как это влияет на плавность анимаций, а также узнаем, как оптимизировать весь процесс.
![](https://habrastorage.org/webt/6f/6x/bf/6f6xbfs_edzkpkvlkslvn2ebq3y.jpeg)
Демо 4 — испускание лучей в вершины
Освещение видимой области
Сейчас начинается самое интересное. Мы осветим видимую область заливкой огромного многоугольника.
Сортировка точек пересечения
Чтобы создать правильный порядок вершин для построения многоугольника, нужно сначала отсортировать их по углу. Для этого мы используем функцию
Давайте сравним оба способа испускания лучей:
![](https://habrastorage.org/webt/ho/1f/hp/ho1fhpgmljifhvp1ci7xeylkahq.jpeg)
Демо 5 — испускание лучей с углами смещения (заполненная видимая область)
![](https://habrastorage.org/webt/v-/di/j0/v-dij0vnmyglk1l9hgl1m1xxqk4.jpeg)
Демо 6 — испускание лучей в вершины (заполненная видимая область)
Оба способа выглядят ошибочными, скачущими и неточными. Давайте разберёмся, почему.
Испускание лучей с небольшим смещением
Заметьте, что происходит, когда лучи испускаются непосредственно на вершины — они должны идти дальше этой вершины, но мы получаем только ближайшую точку пересечения:
![](https://habrastorage.org/getpro/habr/post_images/696/e28/e7c/696e28e7cf11f74e0e91ecd38f2968db.jpg)
Проблема лучей на вершинах
Самым популярным решением является испускание двух дополнительных лучей со смещением на небольшой угол (в обоих направлениях) для каждого испущенного луча. Рассмотрим луч
Обратите внимание, что для первого способа это делать не нужно — мы просто прибавляем или вычитаем смещение из угла, который вычисляем. Дополнительные объяснения можно прочитать в этой статье.
![](https://habrastorage.org/getpro/habr/post_images/6c3/590/ffc/6c3590ffc1737002922b082cd8032b67.jpg)
Проблема лучей в вершинах решена
Давайте посмотрим, как ведёт себя освещение после этих изменений:
![](https://habrastorage.org/webt/cl/da/yk/cldaykcxy76enx_-khlv9scppui.jpeg)
Демо 7 — испускание лучей с углом смещения (заполненная видимая область с дополнительными лучами)
![](https://habrastorage.org/webt/n8/qq/lz/n8qqlzdh_ifbbu00i3k6hkvuu80.jpeg)
Демо 8 — испускание лучей в вершины (заполненная видимая область с дополнительными лучами)
В случае первого способа это не особо улучшило ситуацию. Мы можем уменьшить смещение углов (увеличив количество лучей), но результат всё равно будет плохим. С другой стороны, второй способ выглядит очень плавным и точным. Далее мы больше не будем говорить о первом способе.
Круг видимости и фонарик
Иногда нам бывает нужно ограничивать видимость для игрока. Этого можно достичь, создав область усечения нужной формы. В показанных ниже демо я использовал метод CanvasRenderingContext2D.clip().
![](https://habrastorage.org/webt/b_/7d/37/b_7d37gfezqsqrxsjhpxj2yyugi.jpeg)
Демо 9 — круг видимости
![](https://habrastorage.org/webt/pt/q5/mb/ptq5mbvhig2bumxo_4ryhbzrpjs.jpeg)
Демо 10 — фонарик
Как вы могли заметить, это не является оптимизацией — мы всё равно вычисляем все точки пересечения. Мы вернёмся к этому после того, как узнаем о пространственных хэш-картах.
А как насчёт окружностей?
Я редко видел случаи испускания лучей на окружности. Считайте этот раздел дополнением, в котором я вкратце о них упомяну. Я покажу уравнения и простые демо, а дальнейшую работу оставлю вам.
Точка пересечения окружности и луча
Пусть
Решаем относительно
Решаем квадратное уравнение:
: луч не пересекается с окружностью,
: луч пересекается с окружностью в одной точке (по касательной),
: луч пересекается с окружностью в двух точках.
Только в случае
Только в случае
Только в случае
Только в случае
![](https://habrastorage.org/webt/rf/fm/pe/rffmpe2piewypncu9tcwvfohcbe.jpeg)
Демо 11 — пересечение луча и окружности
Испускание лучей на окружности
Нам нужно найти две касательные к заданной окружности, проходящие через точку начала луча.
Пусть
Из уравнения окружности:
Из линии перпендикуляра к радиусу окружности:
Решаем систему уравнений:
Подставляем
Решаем квадратное уравнение:
: точек касательных нет (точка начала луча находится внутри окружности),
: только одна точка касательной (точка начала луча),
: две точки касательных.
Только в случае
Только в случае
Но есть ещё одна проблема — это не сработает, если
Решаем систему уравнений:
Подставим
Решим квадратное уравнение:
: точки касательной не существует (точка начала луча находится внутри окружности),
: только одна точка касательной (точка начала луча),
: две точки касательных.
Только в случае
Только в случае
Заметьте, что тоже самое мы можем сделать при
![](https://habrastorage.org/webt/px/as/sq/pxassqvv5vdshvumosr1rpckaqy.jpeg)
Демо 12 — испускание лучей на окружности
Способы оптимизации
В этом разделе мы поговорим об использовании пространственных хэш-карт и модифицированного алгоритма рисования линий Брезенхэма. Я не буду вдаваться в подробности реализации, потому что они широко доступны в Интернете. Однако я напишу несколько демо, чтобы вы могли их тестировать и придумывать новые идеи их применения.
Пространственные хэш-карты
Пока мы отрисовываем все многоугольники и вычисляем все точки пересечения, но это практически бесполезно. В большинстве случаев игровая область гораздо больше окна обзора. При помощи пространственных хэш-карт мы можем быстро определять, какие многоугольники видны игроку и выполнять адекватные вычисления.
По сути, мы хотим разделить игровую область на меньшие ячейки (заданного размера). Каждая ячейка состоит из списка, содержащего фигуры (чаще всего это отрезки прямых). Если одна фигура растянулась на несколько ячеек, то она будет включена во все эти ячейки.
Из названия этой методики понятно, что в ней используются хэш-карты (т.е. ячейки будут иметь названия вида X+":"+Y), но в нашем случае желательней будет простой 2D-массив.
Вот простое демо, показывающее, как это может работать:
![](https://habrastorage.org/webt/d9/af/rq/d9afrq2osklbfs_lbdkoeotb3as.jpeg)
Демо 13 — пространственные хэш-карты
Как говорилось выше, мы можем применять их для окон обзора, а также для окружностей видимости и фонариков.
Линия сверхпокрытия на основе алгоритма Брезенхэма
При помощи пространственных хэш-карт и модифицированного алгоритма отрисовки линий Брезенхэма мы можем эффективным образом обходить сетку, выполняя минимальное количество проверок. Алгоритм должен завершать выполнение после обнаружения первой ячейки с точкой пересечения.
Подробнее об алгоритме отрисовки линий Брезенхэма можно прочитать здесь, а об его модифицированной версии — здесь.
![](https://habrastorage.org/webt/cv/cw/mw/cvcwmw6zogo7jknyruerfvzodj0.jpeg)
Демо 14 — Линия сверхпокрытия на основе алгоритма Брезенхэма
Всё имеет свой конец
Простите, к сожалению, я говорю об этой статье, а не о лучах.
Думаю, мы раскрыли всё необходимое для того, чтобы вы могли приступить к работе. Если я найду что-то, требующее дальнейшего объяснения, то постараюсь дополнить статью. Кроме того, я создал небольшую шпаргалку для напоминания о некоторых темах и выведенных формулах.
Если вам понравилась статья, то поставьте звёздочку и подпишитесь на этот репозиторий на GitHub.