Освещение применяют, наверное, во всех играх всех сортов. Во многих современных движках для этого есть встроенные инструменты, однако если хочется просто взять и просто сделать какой нибудь пошаговый Dungeon Crawler, то движок по типу Unity будет только мешать. Применять сложные технологии для освещения ячеек дело неблагородное, уйдет много времени, сил, да и не факт что в таком случае это оправдано. Для написания простой пошаговой игры нужно нечто более простое, чем явно непростые инструменты во главе с шейдером. В общем, как вы могли догадаться, сегодня поговорим о простом алгоритме освещения тайловых карт.  Парадоксально, но реализовывать такое освещения я буду на движке Godot v3.3.2, однако сам алгоритм от движка никак не зависит.  По ходу моего рассказа мы разберем и реализуем способ, способный выдавать подобные тени: Данный алгоритм был выведен мной, поэтому не стоит воспринимать его как научно обоснованный и выверенный годами способ - сегодняшний рассказ не выйдет за рамки школьной программы. Ладно, от лирики ближе к сути. По факту сутью любого алгоритма освещения является определение освещенности пикселя, только вот достигается это по-разному. Кто то использует рейкастинг, кто то тупую проверку для каждого пикселя, а кто то - геометрию 7-8 класса. Эти крайние "кто то" как раз мы. Смотрите, каждый объект отбрасывает тень от своих углов, тем самым образуя две линии: Получается, что все ячейки внутри двух синих линий загорожены этим объектом. Таким образом, для определения видимости точки мы можем просто сравнить углы, образуемые двумя синими линиями и прямой, проходящей через центр ячейки, с осью X. Получается, точка лежит внутри тени, если ее угол больше α и меньше β: Здесь ячейка, помеченная красной точкой в центре - не освещена, т.к. ее угол лежит между углами препятствия. Работать с углами как то не очень, поэтому будем работать с отношениями координат - это как раз будут тангенс (y к x) и котангенс ( x к y).  Давайте считать координаты ячейки как ее центр, тогда тангенс верхнего угла будет иметь значение (y+0.5)/(x-0.5), тангенс нижнего угла - (y-0.5)/(x+0.5). Котангенс, естественно, - наоборот. Значит теперь вместо углов сравниваем значения функций - вуаля! Но, не все так просто, давайте посмотрим на крайние значения углов - 0 и π/2. Как вы наверняка знаете, в этих значениях всегда что то идет не так, потому что в них не существуют котангенс и тангенс соответственно. Эти углы достигаются при координатах относительно источника света y = 0 и x = 0 соответственно.  Давайте для примера возьмем тангенс. Представим препятствие с координатами, ну, скажем, {0, 4} - тогда тангенс его левого верхнего угла будет 4.5/-0.5 = -9, тангенс нижнего правого 3.5/0.5 = 7.  Напомню - чем больше тангенс, тем больше угол (в первой - нашей четверти). Тогда ячейка не видима, если ее тангенс больше 7 и меньше -9. Опа, отношение двух положительных чисел должно быть меньше отрицательного. Получается, что это препятствие тупо игнорируется. С котангенсом тоже самое, только с правым нижним углом.  Тогда вместо только одной функции будем считать сразу две - тангенс и котангенс, а для углов препятствия возьмем по одной функции - главное чтобы они были разные. Например, для верхнего угла котангенс, тогда для нижнего - тангенс. В будущем значения функций в этих местах я буду называть ограничивающими значениями. Теперь снова рассмотрим то же препятствие {0, 4}. Котангенс его верхнего левого угла имеет значение -0.5/4.5 ≈ -0.11 , тангенс правого нижнего - 3.5/0.5 = 7. Теперь все отлично - ячейка не видима, если ее котангенс больше -0.11(больше, т.к. чем больше котангенс - тем меньше угол) и тангенс меньше 7. В таком случае левый верхний угол нам больше не мешает и мы сравниваем только тангенсы. При координате y = 0 получится тоже самое, только выпадет нижний угол. Теперь, разобравшись с принципом работы, можно перейти и к реализации. Наверняка вы задались одним правильным вопросом - ведь если ячейка перед препятствием, но имеет значения неосвещенной ячейки, мы все равно посчитаем ее тенью. Да, это так, если рассматривать все препятствия сразу. Мы же поступим хитрее.  Точка в принципе может отбрасывать тень только в зоне, помеченной на рисунке серым, ведь в других местах прямая видимость с источником света: Тогда мы можем проходить в типичном двойном цикле от нуля до дальности свечения и, натыкаясь на препятствие, записывать ограничивающие значения в массивы, чтобы постоянно их не вычислять. Таким образом, мы будем учитывать только те препятствия, которые потенциально могут отбрасывать тень. Но ведь если ячейка левее, но выше препятствия - мы все равно препятствие учитываем, скажет пытливый читатель. Да, но котангенс этой ячейки будет гораздо меньше котангенса левого верхнего угла препятствия, т.к. координата x меньше, координата y больше, а котангенс - это отношение x к y. Точно также и с ячейкой ниже, но правее препятствия. Давайте ближе к делу. Пусть у нас будет два массива для хранения ограничивающих значений - один с котангенсами левых верхних углов препятствий, другой с тангенсами нижних углов: А за дальность светила будет отвечать переменная distance:int, которая идет параметром для функции рисования света, как и ячейка cell:Vector2, из которой мы хотим излучать свет. Для удобства я записал ее x и y в отдельные переменные x0:int и y0:int. Также понадобится флаг видимости ячейки - назовем его is_visible:bool. Думаю на объявление переменных смотреть не особо интересно, поэтому сразу к сути. Для создания освещения нам так и так придется смотреть на каждую ячейку, поэтому пробегаем в циклах x и y координаты от нуля до distance и смотрим, если наткнулись на препятствие, то добавляем ограничивающие значения в соответствующие массивы: В критических значениях функций (если x или y равны 0) вместо бесконечности (в годо есть INF) я взял distance2, т.к. в наших вычислениях ничего не будет больше него, так что это вполне сойдет за бесконечно большое число. Для определения видимости точки пробегаем по массивам и сравниваем углы, предварительно определив флаг видимости как истину, ведь пока ячейку никто не перекрывает: Здесь за счет того, что мы всегда одновременно добавляем значения в оба массива, нет необходимости как то проверять длины, ведь в таком случае они всегда совпадают. И, если флаг остался истиной, освещаем ячейку: Отлично, теперь у нас готова база для написания полноценной функции освещения. Под спойлером я оставлю полный код функции и результат ее работы: Функция: Работает прекрасно:  Перед написанием полноценной функции давайте немного побалуемся, ведь я же не просто так сказал в начале, что свет может быть любой формы.  У нас цикл двойной, поэтому можно легко сделать зависимость одной переменной от другой. Во внутреннем (x) цикле может быть что угодно, не только distance+1. Давайте, например, поставим tan(float(y)/distance)*distance+1: Получим свет такой формы:  Или можем использовать логарифм (в годо он натуральный): Получим тоже интересный результат:  Ну и наконец сделаем самый обычный, кргуглый источник света, воспользовавшись уравнением окружности x*x + y*y = r*r:  Ну и как вы наверняка поняли, во второй цикл можно запихнуть в принципе любую двумерную функцию x = f(y). У меня получается зависимость x от y, потому что первым циклом я прохожу по вертикали - вы можете во главенство поставить x, тогда у вас будет привычная зависимость y от x. Однако при использовании этого способа имейте в виду, что это может сильно повлиять на производительность. У меня логарифмический свет работал медленно уже при значениях distance > 40 (фпс падал ниже 30), при отсутствии препятствий.  Любая форма это конечно хорошо, но свет в одной четверти как то невесело - поэтому давайте делать свет таким, каким его придумала матушка природа. Чтобы сделать освещение во все стороны вовсе не обязательно как то расширять циклы - только если вы не хотите сделать разные функции в разных четвертях. Этим мы заниматься не будем, а займемся отражением координат.  Разумеется, мы не будем просто отражать координаты рисования, ведь получится так, что одно препятствие имеет тень сразу во всех четвертях. Вместо этого мы будем проверять точку на препятствие не только по координатам {x+x0, y+y0}, но и по {-x+x0, y+y0}, {-x+x0, -y+y0} и {x+x0, -y+y0}. Тогда будет необходим еще один список, в котором будет храниться четверть препятствия с ограничивающими значениями из массивов по тому же индексу: И при проверке препятствия помимо ограничивающих значений добавляем его четверть в этот массив. Получается четыре проверки, в которых уже нельзя пропускать итерацию, ведь мы можем поймать не только одно препятствие: Координатные циклы у нас, напомню, не изменились. Здесь я использовал не типичные математические четверти, ибо нам без разницы - главное как то их различать. Тогда для определения видимости точки одного флага будет мало - необходимо по флагу на четверть. В цикле точно также смотрим на значения из массивов с ограничивающими значениями и выключаем нужный флаг. Если условие верно, это значит что в какой то четверти данная точка не видна, а чтобы определить эту четверть смотрим в новоиспеченный массив и выключаем флаг соответствующей четверти: Здесь точно также нельзя использовать break, т.к. во время цикла может выключиться сразу несколько ячеек. Хорошо, теперь осталось только нарисовать ячейки в тех четвертях, чьи флаги включены: После этого получаем свет, излучающий во все стороны. Опять же, под спойлером функция целиком и пример ее работы: Функция: Четыре четверти это как одна, только в четыре раза лучше:  Естественно можно точно также сделать свет разной формы, вот для примера обычный круглый свет:  За свою жизнь вы точно видели такое, что свет с расстоянием тусклеет, а у нас он везде одинаково белый - так дело не пойдет. За яркость света будет отвечать переменная brightness:float, которую мы будем передавать функции light_cell. Даже для полного освещения достаточно одной переменной, ведь яркость полностью симметрична для всех четвертей. Очевидно, что чем дальше ячейка от источника света, тем она тусклее.  Я буду работать с круглым светом, поэтому вычислять расстояние будем как линейное расстояние от ячейки до центра. В таком случае чем дальше от источника, тем светлее, поэтому отнимем от дистанции свечения расстояние до ячейки, ведь последнее никогда не будет больше дистанции. Также значение должно быть относительным, поэтому разделим разницу на дистанцию - таким образом получим значение от 0 до 1, то что нужно: Тут функция point_distance вычисляет евклидово расстояние между точками и выглядит так: Теперь просто запишем значение brightness во все light_cell: И вот, казалось бы все готово, но вот незадача - при нулевых координатах возникают яркие полосы: Происходит это потому что при, например, x = 0 ячейки {x+x0, y+y0} и {-x+x0, y+y0} имеют одну и ту же координату, поэтому там ячейка освещается два раза. Чтобы от этого избавится просто ограничим рисование условием x != 0 и y != 0 в нужных местах. Я сделал следующим образом, но вы можеет сделать по-своему, это не имеет значения: Тут главное чтобы при нулевых значениях рисовалось только две симметричные точки, ведь другие две будут повторять эти. Смотрите, если x = 0, а y нет, то сработает первое и третье условия, если наоборот то первое и второе, а в общем случае - все четыре. Оставлять код всей функции не буду, думаю сможете сами, поэтому под спойлером только пример работы:  Вот и все, освещение готово. Только работает как то медленновато... Что бы нормально оптимизировать надо для начала понять, где это можно сделать. Давайте посмотрим на код функции:  Очевидно, что чем больше препятствий, тем дольше будут идти проверки по типу Vector2(x+x0, y+y0) in obstacles, причем в этом учавствуют все препятствия - не только те, что хоть как то могут отбрасывать тень. По умолчанию годо использует линейный поиск - оно и понятно, ведь наш массив никак не отсортирован. Это значит, что надо вставлять элементы в нужное место, тем самым получая отсортированный массив, а затем использовать бинарный поиск.  Хорошо, при следующих данных время выполнения снизилось с ~140мс до ~65мс: Причем даже при большем количестве препятствий время практически не меняется, ведь теперь зависимость времени выполнения алгоритма от их количества сведена к минимуму. Что бы вы понимали масштабы, если у нас было 1024 препятствия, то вместо 1024 итераций тратится только 10. Но это для одной ячейки, для четырех естественно в 4 раза больше. На самой функции освещения это никак не отразилось, я просто при добавлении препятствия добавлял его в список на такое место, что бы тот оставался отсортированным, а при проверке точки на препятствие использовал бинарный поиск: В годо функция Array.bsearch возвращает индекс элемента если таковой имеется, иначе индекс, по которому тот бы находился в случае отсортированного массива. И вместо Vector2(x0+x, y0+y) in obstacles я использовал is_obst(Vector2(x0+x, y0+y)). Ну, разумеется, для всех четвертей. Поглазеть на полный код сможете кликнув по ссылке в конце статьи. Также можно заметить, что если препятствие само находится в тени, то нет смысла его рассматривать, т.к. оно не даст новой тени. Однако просто сравнив угол ячейки, в которой находится это препятствие также, как и для обычной, мы получим неверный результат, ведь если не видно ее центра, не факт что не видно какой то из углов, который отбрасывает тень: В таком количестве цветов сложно что то понять, сейчас поясню. Черная линия проходит через центр правого препятствия и сравнив его как обычную ячейку мы получили бы, что оно не дает новой тени, т.к. заслонено другим препятствием, однако нижняя зеленая линия явно видна и потому дает тень. В таком случае для препятствий будем сравнивать его ограничивающие значения с другими препятствиями, и, если под условие не подпадает хотя бы одно из, то добавляем препятствие как обычно.  Разумеется, запускать цикл проверки на видимость в каждом условии слишком жирно, поэтому перенесем этот цикл на место перед добавлением препятствий и добавим еще четыре флага (по одному на четверть), которые показывают, надо ли препятствие добавлять.  Тогда перед основным циклом проверки на видимость проверяем на препятствия точку и поднимаем нужный флаг в случае совпадения, а в цикле проверки на видимость дополнительно сравниваем ограничивающие значения что бы понять, добавляет ли препятствие новую тень. Под спойлером функция целиком:  Время выполнения функции при следующих данных снизилось с типичных ~65мс до ~40мс: Наверняка внимательный читатель заметил, что в затравке к статье препятствия были синего цвета, а в самой статье они всегда красные. Это не просто так. Я думаю многие слыхали легенды о медленности Gdscript, поэтому я просто не мог пройти мимо оболочки GDNative, которая позволяет писать код для ноды не только на C++, но и некоторых других языках, однако я буду использовать именно первый вариант. Алгоритм, разумеется, не поменялся, поэтому я просто переписал код с gdscript на c++ и спустя нескольких неотразимых балетов с бубном мне все таки удалось заставить это работать.Теперь, при следующей конфигурации время выполнения функции всего то около 10 мс: Под спойлером код на c++: tile_lights.h: tile_lights.cpp:  Тут наверняка можно придумать еще кучу вещей для оптимизации, однако я остановился на самых жирных и думаю что этого вполне достаточно, ведь вряд ли вы будете обновлять свет каждый кадр, как это делал я, а делать это только когда меняется положение источника. Также не думаю, что вам понадобится дальность света больше 15, а при меньших значениях скорость c++ реализации просто запредельна. Разумеется, данный алгоритм предназначен только для тайлмапов и его не следует использовать для обычного освещения. В целом я думаю получилось вполне неплохо, на мой взгляд этот алгоритм гораздо больше подходит для освещения тайлмапа, чем какой нибудь рейкастинг. Не смотря на все этот алгоритм просвечивает в соединениях препятствий углами, однако я считаю что это просто особенность дискретности, поэтому на это можно закрыть галаза, но учитывать при проектировании уровней. Я начинаю потихоньку осваивать искусство git и github, поэтому заглядывайте ко мне в репозиторий, в котором лежит не только код с этой статьи, но и парочка других функций для освещения. Что ж, надеюсь вы приятно провели время и приметили что то для себя, читая этот пост.  А так до следующих встреч - да будут они скорыми.  Разработчик-любитель