Глобальное освещение, динамический свет и декали (да, есть такое слово :) ) в действии. Я очень люблю смотреть на белые предметы без текстуры. Недавно в художественном магазине я долго рассматривал гипсовые фигуры, которые художники используют в качестве модельных объектов. Очень приятно видеть все эти плавные переходы света и мягкие тени. Позже, когда я вернулся домой и открыл Unity3D, пришло понимание, что свет в моём проекте по-прежнему скучный и нереалистичный.
С этого момента началась история глобального освещения, которую я сегодня расскажу. Часть первая. Свет.
Часть вторая. Структура.
Часть третья. Глобальное освещение. Самый первый комментарий к начальной статье этого цикла звучал так: "Магия! И прямые руки." Не уверен в полной прямоте моих рук (в конце предыдущей статьи — визуальные баги, которые это подтверждают), но никакой магии тут нет. Поделюсь секретом процедурных эффектов: Минимум треть работы уже сделана, как только вам в голову пришла идея сделать процедурно генерируемый контент. Это может быть что угодно: пятна на крыльях бабочек или атмосфера планеты, деревья и кусты и т.д. Иногда, особенно со светом, сразу понятно, как происходит "генерация" в реальном мире. Чаще всего алгоритм сводится к: "пустить бесконечно много лучей в бесконечное количество направлений и получить реалистичную картинку". И это вторая треть — написать подобный алгоритм (с учетом того, что бесконечность хорошо аппроксимируется тысячей). Он получается простой, как "hello world", но медленный. Руки сразу тянутся что-нибудь оптимизировать, но, поверьте, не стоит. Лучше запустить его в редакторе и пойти пить чай. А после чая понять, что придуманный метод не даст красивой картинки и всё переделать. Если планируется единожды предрассчитать какую-то картинку в редакторе, и потом использовать её в билде — на этом можно остановится. И, наконец, последняя треть — придумать алгоритм, который даст визуально близкий результат, но будет работать быстрее. Обычно тут пригождается знание всяких интересных контейнеров, алгоритмов, деревьев и т.д. За один из таких алгоритмов — большое спасибо Dionis_mgn, который когда-то рассказал, как сделать классные двумерные тени. Планета из предыдущего проекта. Например, небо для планет в одном из проектов предрассчитывалось так: для каждого пикселя неба выпускались по 20-30 лучей до разных частей Солнца, считалось, сколько лучей пересекается с самой планетой, какую часть пути луч прошел в атмосфере (для подобия рассеивания Рэлея). С хорошим качеством расчеты для одной планеты длились около 30-40 секунд и давали на выходе разнообразные атмосферы в зависимости от удаленности Солнца, "состава" и плотности атмосферы. А еще этому алгоритму удавались неплохие закаты. Закат на Земле II. Вся звёздная система. Давайте определимся, какую картинку мы вообще хотим получить. "На свету светло, в темноте — темно" — звучит неплохо для отправной точки. Как в реальном мире: в шкафу темно, в коридоре светлее, в комнате еще светлее, а на крыше совсем ярко. Переформулируем: элементы фона, персонажи и прочие объекты должны получать столько света, сколько фотонов смогло добраться до них от небесной сферы (в нашем 2D случае — небесной окружности). Понятно, что лучше направлять наши "фотоны" не с неба, как в реальном мире, а наоборот, из освещаемой точки в небо: в противном случае нам понадобится слишком много бросков, да и то, многие уйдут "в молоко". Ещё одно из условий: рассчитываем глобальное освещение только для статических объектов: стен, земли. Так мы сможем запускать его при загрузке и пользоваться результатами весь уровень (без влияния на fps).  Кусочек сцены. На самом деле, расчеты идут для всей сцены целиком. Сказано — сделано. Создаём текстуру размером со всё игровое поле. Пробегаемся по каждому пикселю и смотрим, как много прямых лучей можно протянуть от этой точки до "неба". Лучи будем бросать с равными углами по всей окружности, а "небом" считаем ближайшую точку за пределами карты (вполне хватит расстояния диагонали описывающего карту прямоугольника).
Итого, алгоритм прямого освещения: UPD: Демонстрация освещения одного пикселя. Чтобы ускорить процесс, будем работать не с текстурой, а с одномерным массивом яркостей. Да и не обязательно обрабатывать каждый пиксель: введем коэффициент scale, при scale=4 будем работать с каждым четвёртым пикселем. Размер текстуры и скорость работы вырастет в scale^2 раз. Кроме того, нам не нужно обрабатывать "твёрдые" пиксели стен, но они нам понадобятся в дальнейшем. Заведём для них отдельный массив с булевыми значениями "твёрдости". При 25и лучах получаем такую текстуру. Помните, в прошлой части был раздел про Region tree? С его помощью бросать raycast'ы через всю карту оказывается достаточно быстрым делом. Я не использую цикл по всей текстуре, так как больше половины пикселей принадлежат стенам. Вместо этого итерация производится по массиву индексов "нетвёрдых пикселей". Прямых лучей явно недостаточно: слишком темно будет в комнатах замка, да и резкие границы хорошо видны. Вспоминаем умные слова, вроде raytracing'а, и понимаем, как много времени займёт применение этих умных слов. С другой стороны — ведь любой переотраженный луч приходит откуда-то с карты, а всё прямое освещение мы только что построили! Расширяем массив и храним там целую структуру: Переделаем алгоритм прямого освещения, добавляя данные о коллизиях: Теперь у нас достаточно информации, чтобы рассчитать любое количество отражений: если
луч ушел в небо, получаем прямое освещение, в противном случае — непрямое из точки пересечения. Так получается алгоритм непрямого освещения: Демонстрация непрямого освещения. Собираем из коллизий уже рассчитанное прямое освещение. Самое главное, что теперь вместо операции raycast'а по region tree нам достаточно взять значение яркости в массиве: так мы получим одно отражение. Конечно, этот метод подходит только для pixelart'a: не нужно учитывать нормали или заботиться о возникающих артефактах. Посмотрите, какие результаты даёт этот алгоритм: Первое отражение. Третье отражение. Седьмое отражение. Готовый результат для фоновых стен. Довольно шумная картинка получается. На самом деле, после применения такого освещения к реальным текстурированным объектам шумы почти не заметны. К тому же высокочастотный шум исчезнет при использовании scale > 1. Вот только стены в текущей текстуре чёрные. "Конечно", возразит зануда, далёкий от геймдева, пиксельарта и чувства прекрасного — "Ведь это не стены, а срез трехмерных стен в двумерном пространстве. А внутри стен, как известно, темно.". Поблагодарим зануду и продолжим эксперименты. Попробуем вообще не затемнять стены: Стены без применения освещения. В первом случае результат красиво смотрелся только под землёй, во втором — на поверхности. Нужно адаптивно менять яркость стен в зависимости от окружения. Переведём наши массивы в текстуру, где в одном канале будет яркость пикселя, а другом — принадлежность стене. Размоем пиксели стены на GPU с помощью простого шейдера (простое среднее арифметическое с соседями) в цикле. Размытые стены (scale = 2). Вот такое недоразумение получится, если применить освещение. В первой статье цикла я рассказывал про основы пиксельарта. Дополню еще одной важной аксиомой: никаких градиентов в духе photoshop'а! Это превращает аккуратную картинку в мыло и пластилин. На фоне градиенты не так бросаются в глаза, как на стенах. Пройдемся по текстуре с еще одним шейдером: для каждого пикселя стены с помощью простого округления (с коэффициентом из параметров шейдера) получим несколько градаций яркости. Конечно, полученные переходы далеки от идеала — рука художника не двигала пиксели, убирая кривые лесенки, но нам подойдет. Световая маска с низкой дискретизацией (scale = 2). Результат применения маски. Результат применения маски при использовании реальных текстур. Обратите внимание, как хорошо скрываются шумы и недочеты освещения, когда мы применяем его к реальным текстурам. Если бы глобальное освещение было динамическим, человеческий мозг, отлично распознающий движение, сразу же нашел бы косяки. Итак, у нас есть глобальное освещение! Плюсы этого алгоритма: И минусы: Декали ("decal" — "переводная картинка"), это отличный способ сделать игру более живой, не сильно жертвуя производительностью. Идея проста: на определенную поверхность (стена, пол и т.д) накладывается прямоугольник с текстурой, как настоящая переводная картинка. Это может быть след от пули, какой-нибудь мусор, надпись, что угодно. Но мы будем использовать декали немного иначе: в качестве источников света произвольной формы. Раз уж мы генерируем текстуру с освещением, мы можем добавлять в неё объекты произвольной формы. И эти объекты сразу же начнут светиться! Так можно легко реализовать эффекты люминесценции, теплового излучения. Но есть два важных момента: По сути, алгоритм простой: Разделим все декали (например, с помощью тегов Unity3D) на декали переднего и заднего планов: На примере будет понятнее: Находим старый спрайт травы. Позиционируем "траву" так, чтобы она закрывала кончики стен. Рендерим спрайт только в текстуру освещения. Добавляем свечение на стены. Добавляем свечение на фон. И получаем интересную радиоактивную плесень.
А еще можно делать раскаленные стены, уникальные светящиеся предметы и многое другое. Стена светится от счастья. Вышеописанные источники света. Теперь в нашем проекте есть реалистичный свет, эффекты светимости и обновленные динамические источники света. Сравните с изображением из первой статьи, не так уж мало различий, правда? Изображение из начала этой статьи. Изображение из первой части цикла. И самое интересное: теперь когда готово освещение и произведен рефакторинг алгоритмов и структуры проекта, пришло время написать про воду! Спасибо за чтение и комментарии к прошлым частям и до следующей статьи! Программист