• Что можно приготовить из кальмаров: быстро и вкусно

    В Source есть несколько объектов, отвечающих за освещение, а также множество разнообразных настроек в зависимости от игры, для которой вы делаете карту. В этом уроке мы начнём с базового освещения для Half-Life 2 и других игр на Source 2007, прежде чем обращаться к более сложным приёмам.

    Благодаря предыдущим урокам вы создали чудесную карту, но без освещения ваши игроки будут чувствовать себя покинутыми во тьме.

    • Работа с освещением, cubemaps и HDR
    Базовое освещение

    Самый простой способ добавить на карту освещение – это объекты light и light_spot. Они создают «статическое освещение», просчитываемое в компиляторе и хранящееся в особом файле текстур, известном как карта освещения.

    Объект light

    Данный объект представляет из себя точечный источник освещения, светящий во все стороны. Создайте один такой и загляните в его свойства (меню Object Properties). Перед тем как мы продолжим, хочу обратить ваше внимание на один важный момент. Не присваивайте объектам light имя, если только не собираетесь включать и выключать их. Каждому именованному объекту light соответствует отдельный файл карты освещения, что может серьёзно отразиться на размере и производительности карты.

    Выделив поле Brightness, вы увидите четыре значения. Первые три отвечают за красный, зелёный и синий цвета, а четвёртое за интенсивность света. По умолчанию оно установлено на 200, но я заметил, что меньшие значения делают свет мягче и атмосфернее. Следующие несколько опций настраивают эффект HDR.

    В поле Appearance можно добавить эффект мерцающей лампочки или дневного света, но не злоупотребляйте этой опцией! Оставшиеся настройки отвечают за дальность освещения. Меня устраивают значения по умолчанию, но вы можете добавить или убавить половину дистанции, чтобы освещать большее (или меньшее) расстояние.

    Объект light_spot

    Этот объект имитирует направленный свет. Его можно поворачивать, чтобы выбрать направление света. Выделение объекта light_spot покажет конус освещения на 3D-виде.

    Свойства у объекта практически те же самые. Следует помнить, что значение яркости (Brightness) должно быть на порядок выше – для начала я рекомендую 600-800 для внутреннего освещения. Углы освещения (ширину пучка света) тоже можно поменять. Поглядывайте на 3D-вид, чтобы увидеть разницу в значениях внутреннего (Inner) и внешнего (Outer) углов. Для видимости на больших расстояниях дальность света также необходимо повышать.

    • Избегайте насыщенных цветов, если не хотите, чтобы карта была похожа на рейв-дискотеку.
    • С направленным светом будет полезно комбинировать обычные объекты light с малым радиусом освещения. Таким образом края конуса будут не такими резкими и пространство вокруг него не будет слишком тёмным.
    • Метод проб и ошибок – лучший способ сделать хорошее освещение. Вы можете использовать инструмент Cordon Tool для компиляции небольшой части карты, с которой работаете в данный момент – так процесс пойдёт быстрее.
    • Изобретательный подход может добавить уникальности даже совершенно невзрачному месту – располагайте источники освещения так, чтобы они отбрасывали интересные тени.
    • Освещение может быть очень полезным, помогая игроку увидеть важный объект или подсказывая нужное направление. Об этом есть (на английском).
    Наружное освещение

    Стоит отдельно поговорить о том, как сделать освещение «на улице». В Source наружные пространства – это те же комнаты со скайбоксом. Давайте создадим большое пространство и протестируем в нём наружное освещение. Добавьте несколько объектов, чтобы проверить отбрасывание теней. В списке текстур найдите tools/toolsskybox и примените её к «потолку». В игре эта текстура заменяется на скайбокс.

    Чтобы поменять небо, зайдите в меню Map>Map Properties и найдите поле SkyBox Texture Name. Со списком доступных текстур неба можно ознакомиться на этой странице Valve Developer Wiki. Убедитесь, что используете текстуру _hdr.

    Чтобы свет исходил от скайбокса, вам нужно создать объект light_environment. Его расположение роли не играет. Свойства его тоже не сильно отличаются от других объектов освещения. Однако, есть две особенных настройки яркости: Brightness отвечает за направленный свет, идущий от «солнца», а Ambient за свет «в тени» (за пределами углов освещения солнца). Настройки Pitch Yaw Roll отвечают за направление света. Все они достаточно сложны в понимании, но страница на Developer Wiki включает примеры настроек для каждого неба, и вы можете придерживаться их в точности, либо немного изменять под свои нужды.

    Компиляция с освещением

    К этому времени вы уже должны освоиться с основными настройками компиляции. В окне Run Map выберите Expert для дополнительных настроек, связанных с освещением. На этом виде доступны несколько заранее подготовленных конфигураций с балансом производительности и картинки. Эффект настроек VIS будет рассмотрен в уроке по дистанции обзора.

    Сейчас просто запомните, что пропуск настроек VIS намного ускорит компиляцию, но снизит производительность карты (и создаст проблемы с водой). Компиляция HDR сделает свет наиболее красивым, но займёт гораздо больше времени. Последний флажок $game отвечает за запуск игры после компиляции.

    Как вы могли догадаться по названию, на этом урок по освещению не заканчивается. Во второй части мы рассмотрим масштабирование карты освещения, динамические тени, проецируемые текстуры и много других интересных штук с захватывающими названиями!

    Ознакомившись с первой частью урока, вы уже должны иметь представление о базовых возможностях Source в плане освещения. Давайте же рассмотрим и более продвинутые приёмы, которые может предложить движок, включая дополнительные возможности Portal 2, CS:GO и Dota 2.

    Настройки карты тонов

    Взглянув на наружное освещение своей карты вы заметите, что свет слишком яркий и «сияющий». В эту ловушку попадают многие карты, но, к счастью, есть способ её избежать, используя гениальный объект под названием env_tonemap_controller.

    Создайте такой объект. Контроллер карты тонов активируется через входящий сигнал, так что создаём logic_auto, чтобы отсылать сигнал сразу после загрузки карты.

    По сути, этот объект изменяет «экспозицию» в глазах игрока. В его свойствах множество опций, но нам понадобится только входящий сигнал SetBloomScale, отвечающий за HDR-эффект «блум». Взгляните на картинку, чтобы оценить разницу в значениях.

    Масштаб карты освещения

    Мощное сочетание света и тени выведет любую карту на новый уровень, но статичное освещение Source может огорчить вас эффектом «квадратов». Он проявляется из-за низкого разрешения карты освещения – текстуры, на которой сохраняется освещение. К счастью, есть способ увеличить количество таких текстур на поверхностях блоков.

    Для примера создадим комнату с освещением light_spot и несколькими блоками для проверки теней. Выберите Toggle texture application и на 3D-виде выделите несколько поверхностей возле источника освещения. Теперь вы можете изменить параметр Lightmap Scale. Меньшие значения эквиваленты повышенному разрешению. Чтобы разница была более наглядной, измените камеру 3D-вида на 3D Lightmap grid.

    На изображении показано сравнение двух параметров в игре. Слева установлено значение 16 (по умолчанию), справа 2. Во втором случае тени более чёткие и лучше заметны.

    Не увлекайтесь этой опцией, потому что она серьёзно влияет на размер файлов и производительность карты.

    Динамическое освещение и тени

    До сих пор мы работали только со статичным освещением. Его главное ограничение в том, что свет просчитывается заранее и не реагирует на перемещение объектов или источников света в игре. Динамическое освещение и динамические тени, напротив, просчитываются в игре и меняются в зависимости от окружения.

    Благодаря модульной структуре Source поздние игры сильно продвинулись в динамическом освещении, предлагая больше разнообразных настроек при создании карт.

    Тени от моделей и shadow_control

    В модах к Half-Life 2, CS:S, Portal 1 и Source Base модели и NPC отбрасывали тени с помощью одного базового метода «render to texture», работающего, как показано на картинке.

    Все тени отбрасываются в одном направлении, задаваемом объектом shadow_control. Чтобы тени падали под правильным углом, настройки Pitch Yaw Roll в свойствах shadow_control должны совпадать с углами освещения объекта light_environment.

    В закрытых помещениях это создаст проблемы, так как тени будут выглядеть нереалистично. В этом случае будет полезно включить параметр Disable Shadows в свойствах каждой модели.

    В играх Left 4 Dead, Portal 2, и Alien Swarm тени могут зависеть от ближайшего источника освещения, что задаётся в свойствах shadow_control и соответствующих объектах освещения. Не рекомендуется создавать много таких объектов в одной зоне, иначе при движении объектов и игрока тени будут менять направление случайным образом или вовсе не менять его.

    Проецируемые текстуры

    Начиная с Portal 2, Valve стала широко использовать проецируемые текстуры для создания интересных сцен. Как следует из названия, эти источники освещения представляют из себя изображения, проецируемые на поверхности блоков и объектов. Есть серьёзное ограничение, заключающееся в том, что Valve не позволяет включать больше одной такой текстуры за раз (моддинг позволяет избавиться от этого ограничения).

    Для следующего примера я использовал инструменты Portal 2 Authoring Tools. Создаём объект env_projectedtexture, его можно вращать и передвигать, как и любой источник освещения. Загляните в свойства, тут нам кое-что понадобится. Убедитесь, что параметр Enable Shadows выставлен на Yes и увеличьте значение FarZ, чтобы свет покрывал большее расстояние. Для изменения внешнего вида можете задать другое имя текстуры в поле Texture name, но имейте в виду, что большинство материалов не предназначены для этой цели! Также добавьте на карту обычный объект light.

    Поместите на карту несколько prop_weighted_cube; запустив игру, вы должны наблюдать, как их тени отбрасываются динамически вместе с их перемещением. Вы даже можете использовать параметр Parent, чтобы привязать проецируемую текстуру к физической модели и переносить её. В своём примере я также разместил рядом с проецируемой текстурой объект point_spotlight, имитирующий луч прожектора.

    Технология проецируемых текстур работала и в ранних играх на Source, включая Half-Life 2, где она применялась для динамического света от фонарика игрока. Однако, их не рекомендуется использовать из-за конфликта с фонариком при одновременном задействовании.

    Глобальное динамическое освещение

    В Dota 2 и CS:GO есть свои новые объекты для динамического освещения.

    В CS:GO используется env_cascade_lighting. Он просто помещается на карту и работает в паре с light_environment, излучая динамический свет из скайбокса. Объект использует так называемую каскадную карту теней, выдавая куда более впечатляющую и реалистичную картинку по сравнению с возможностями ранних игр.

    В Dota 2 работает похожий метод: env_global_light. Этот объект работает по аналогии с проецируемыми текстурами и размещается рядом с фиксированной камерой, создавая тени от всех объектов окружения. В новом редакторе «Source 2» доступен предварительный просмотр такого освещения в реальном времени.

    Надеюсь, вы узнали кое-что новое об объектах освещения в Source. Каждый из них в подробностях рассмотрен в Valve Developer Wiki. К тому же, карты других моддеров помогут вам лучше разобраться в работе этих инструментов и использовать их эффективнее.

    Кубические текстуры и эффект HDR

    Ваша карта кажется простоватой? Ей не хватает чего-то особенного? В таком случае засиять по-новому ей поможет High Dynamic Range.

    Если объяснять эффект HDR по-простому, то с ним в тёмной комнате ваши виртуальные глаза будут приспосабливаться к освещению после яркого дневного света. Это значит, что тёмные области внутри помещений будут изначально темнее, но постепенно их яркость будет повышаться до комфортной. Динамический баланс яркости добавляет контраста вашей карте, делает её более яркой и живой в целом.

    Что такое кубические текстуры и как они с этим связаны?

    Кубические текстуры – это технология, применяющаяся движком Source для отражений, также они играют важную роль в работе эффектов HDR, так как должны генерироваться для Low Dynamic Range (не-HDR) и High Dynamic Range. Мы вернёмся к ним чуть позже.

    Как устроен HDR

    Технический анализ реализации эффекта HDR на карте можно найти на Valve Developer Wiki .

    Добавить эффект на вашу карту очень просто ввиду того, что большая часть необходимой работы проходит на этапе рендеринга и после него.

    Источники освещения

    Первым делом проверяем освещение – в свойствах отвечающих за него объектов есть значения Brightness, Brightness HDR и BrightnessScale HDR.

    Brightness – это обычный цвет освещения.

    BrightnessHDR – это отдельный цвет для случая, когда задействован HDR, по умолчанию его значение установлено на «-1 -1 -1 1», что значит «Так же, как при LDR».

    BrightnessScale HDR – на это значение умножается интенсивность HDR-освещения, если оно работает на карте.

    Цвет HDR обычно не меняют, но вы можете заметить, что в HDR-режиме всё становится намного ярче, так что можно чуть убавить множитель яркости.

    Скайбокс

    Здесь всё просто: убедитесь, что ваша текстура скайбокса поддерживает HDR. Большинство скайбоксов от Valve либо поддерживают HDR, либо имеют HDR-версию. Список скайбоксов и дополнительную информацию по ним можно найти на Valve Developer Wiki .

    Компиляция HDR

    Здесь ваша карта превращается в HDR-карту: просто включите эффект в настройках. Для этого поставьте флажок «HDR» в настройках рендеринга или выберите HDR-профиль в экспертных настройках.

    После этого предстоит ещё кое-что сделать, но давайте пока вернёмся к кубическим текстурам.

    Кубические текстуры

    Точный просчёт отражений может быть непростой задачей, с кучей вычислений, связанных с трассировкой лучей, но к счастью есть уловка с низким уровнем детализации, которого зачастую прекрасно хватает для игр. Большую часть времени вы не замечаете отражений – человеческий мозг воспринимает сверкающее стекло как норму – но их полное отсутствие сразу будет бросаться в глаза.

    В Source отражающие/блестящие/глянцевые поверхности выглядят очень неплохо благодаря набору изображений, выступающих основой для отражений. Такие изображения генерируются из того места, где расположен объект env_cubemap. Это значит, что если у вас один такой объект в комнате, то на всех отражениях в ней будет одно и то же. Это приведёт к странным графическим артефактам, поэтому кубическая текстура содержит шесть изображений, представляющих развёртку куба, для формирования полной панорамы комнаты.

    Объекты способны вращать кубическую текстуру, выдавая достоверные отражения. Следовательно, вам понадобится отдельная текстура для каждого отдельного пространства, иначе в прицеле снайперской винтовки будет отражаться интерьер помещения, хотя вы находитесь снаружи.

    Поэтому, когда вы не используете кубические текстуры, во всех отражениях виден скайбокс, потому что он работает как кубическая текстура по умолчанию (в некоторых играх используется фиолетовый клетчатый узор).

    Технический анализ реализации кубических текстур на карте можно найти на Valve Developer Wiki .

    Генерирование кубических текстур

    Когда ваша карта готова и кубические текстуры размещены по своим местам, вы компилируете её и запускаете в соответствующей игре. Создание кубических текстур происходит в самой игре (тот простой шаг, который после рендеринга). Консольная команда «buildcubemaps» запускает быстрый процесс снятия скриншотов с позиций объектов env_cubemap и сохранения их в BSP-файл карты. После этого ваша карта может существенно располнеть, в зависимости от числа кубических текстур.

    В теории этого достаточно для выпуска своей карты с крутыми текстурами и HDR, но на практике не всё так гладко. Hammer начинает генерировать кубические текстуры уже во время рендеринга, но в последних сборках редактора это работает совсем плохо и в результате в файл карты пакуются бесполезные пустые текстуры. Прежде чем генерировать HDR- и LDR-текстуры в игре нам нужно сначала удалить повреждённые данные с помощью программы под названием Pakrat.

    Pakrat позволяет открывать файлы BSP и изменять их содержимое. BSP-файлы представляют из себя контейнеры (как.zip, к примеру), где хранятся различные данные – обычно это файлы самой карты, файлы освещения, кубические текстуры и любой дополнительный контент, который вы решили поместить в свою карту.

    Скачав Pakrat, открываем файл своей карты и находим кубические текстуры. Они именуются по шаблонам c123_123_123.vtf и c123_123_123.hdr.vtf. Удаляем их и сохраняем карту. Теперь мы можем генерировать новые текстуры, открыв карту в игре.

    Команды консоли

    Пошаговая инструкция по удалению старых и созданию новых кубических текстур для HDR и LDR:

  • Открыть BSP-файл в Pakrat
  • Удалить повреждённые текстуры. Они относятся к типу «Texture» (НЕ «Material»)
  • Открыть игру
  • Ввести команды «mat_hdr_level 2» и «mat_specular 0»
  • Загрузить свою карту: «map название-вашей-карты.bsp»
  • Загрузить другую карту для очистки кэша (ввести «Maps *» для вывода списка карт)
  • Ввести команду «mat_hdr_level 0»
  • Снова загрузить свою карту
  • Ввести команду «buildcubemaps»
  • Карта готова, если хотите посмотреть на результат, введите «mat_specular 1» и «mat_hdr_level 2». Загрузите другую карту для очистки кэша, затем можете загружать свою.

    Для проверки кубических текстур можно использовать команду «impulse 81», если они отображаются неочевидно.

    Если ошибки в VRAD исправлены, то некоторые шаги будут излишни, но в любом случае будет полезно узнать весь процесс.

    • Не переименовывайте файл карты, хранящиеся в нём текстуры могут быть привязаны к имени файла. Это не всегда создаёт проблемы, но всё же имейте это в виду.
    • Проверяйте содержимое файла карты в Pakrat, чтобы убедиться в успешном сохранении текстур и обнаружить скрытые большие файлы.
    • Pakrat умеет отображать текстуры – если на их месте одна чернота, значит что-то пошло не так.
    • Иногда образец игры, который запускает Hammer, генерирует текстуры некорректно. Попробуйте запустить полную Steam-версию игры и сгенерировать текстуры в ней.

    Как правило, HDR играет второстепенную роль – к нему обращаются, добавляя завершающие штрихи к уже готовой карте. Пускай это и так, но HDR способен существенно преобразить схему освещения на вашей карте, так что будьте готовы переделывать её по нескольку раз. Source прекрасно сохранился для своих лет, но местами уже выглядит совсем неброско. Приукрасив карту эффектом HDR, вы поможете игрокам не вспоминать, что перед ними игра 2006 года.

    Тени дают объектам ощущения контакта с поверхностью, тем самым позволяя ощутить глубину и пространство.Статические тени отображаются настолько далеко, насколько идёт рендеринг, но динамические тени могут сильнее сказатся на производительности.Данный документ покажет базовые виды теней которые есть в Unreal Engine 4.

    Static Lights

    Статическое освещение отбрасывает полностью статические тени и свет, это означает, что такой тип освещения не имеет прямого влияния на динамические объекты (статическое освещение запечено в кеш непрямого освещения, поэтому оно имеет некоторый эффект), как на примере ниже.

    Персонаж на картинке выше, тот что слева, стоит под статическим светом, свет и тени никак не взаимодействуют с ним; а тот что справа, стоит под стационарным источником света.

    Прямое освещение каскадными картами теней(затенение всей сцены)

    Directional Stationary Lights — специальные источники света, т.к. они поддерживают затенение всей сцены посредством Cascaded Shadow Maps , в момент использования статического затенения.Это очень удобно на уровнях с множеством анимированной растительности; вы хотите движущиеся тени вокруг игрока, но не хотите переплачивать за чрезмерное количество каскадов, для покрытия больших дистанций обзора.С увеличением расстояния, динамические тени растворяются среди статических теней настолько, что переход практически незаметен.Чтобы применить данную возможность, просто измените значение Dynamic Shadow Distance StationaryLight в DirectionalLightStationary , чтобы изменить дистанцию растворения.

    Тени Стационарных источников света

    Динамические объекты (такие как StaticMeshComponents и SkeletalMeshComponents с подвижностью установленной в Movable ) должны быть интегрированны в мировое статическое затенение на дистанции полей карт затенения.Это достигается с помощью теней для каждого объекта.Каждый подвижный объект создаёт 2 динамические тени от стационарного источника света: одну, для управления статической тени проецируемой на объект и вторую, для управления тени проецируемую на остальной мир.С такой настройкой, затенение для стационарных источников света происходит от динамических объектов,которое оно затрагивает.Это означает, что стоимость может варьироваться от очень маленькой, до огромной, в зависимости от того, сколько присутствует динамических объектов.При наличии достаточного количества динамических объектов, более эффективным будет использование Movable освещения. На сцене ниже, сферы — подвижный объект, и все они получают тени от статического мира и проецируют собственные тени, которые соединяются с остальными тенями на отдалении.Фруструм Per Object теней для каждого подвижного объекта также показан.

    Per Object тени используются для подвижных компонентов используя теневую карту границ объекта, поэтому границы должны быть точными. Для скелетал мешей это значит, что они должны иметь physics asset . Для частиц — любой фиксированный ограничивающий бокс должен быть настолько велик, чтобы вместить в себя все частицы.

    Динамические тени

    Подвижные источники света проецируют полностью динамические тени (и освещение) на всём.Информация об освещении не будет запекатся в лайтмапы.Статик меши, Скелетал меши, эффекты, прочее — будут получать и проецировать динамические тени от подвижных источников света.

    Динамические тени самые ресурсоёмкие.

    Превью теней

    Когда редактируете стационарное или статическое освещение, тени могут стать «незапечёнными», Preview Shadowing показывает вам как будут выглядеть ваши тени после запекания.

    Такие (имеется ввиду незапечённые) тени показываются в редакторе с наложенным поверх текстом «Preview «, для распознавания их среди других теней.

    Если вы протестируете свою игру в редакторе, до того как перезапечёте освещение, то превью тени исчезнут, так как они существуют только лишь в режиме редактирования, но никак не в режиме игры в редакторе.

    Для того, чтобы получить тени из превью теней, вам необходимо выбрать опцию Build Lighting из меню Build .

    Вы можете отключить превью теней посредством снятия галочки с Preview Shadows Indicator во вьюпорте Show/Visualize меню.

    Если вы хотите изменить текст материал функции освещения, которая проецирует этот текст, то вы можете его найти в: Engine/EditorMaterials/PreviewShadowIndicator.

    Всё вместе

    Когда все тени собраны вместе, каждая из которых привносит свои сильные стороны и компенсирует слабые стороны других — они впечатляют своим видом.

    При создании сайта вы наверняка пользуетесь возможностями CSS, для добавления теней к элементам на странице. Этот способ подходит тогда, когда вам требуются лишь стандартные тени CSS.

    Shine позволяет придать динамику и получить в результате красивые эффекты, связанные с тенями. Написана она на JavaScript, что может также понравиться и не любителям библиотек типа jQuery. Работает данный скрипт только в тех браузерах, которые поддерживают CSS свойства: text-shadow и box-shadow. Если браузеру потребуются префиксы к этим свойствам, то скрипт добавит их сам.

    Теперь давайте подключим shine:

    Добавим CSS и HTML (применять тень мы будем к тексту):

    Html{ background-color: #eef3f8; } #headline{ margin: 60px auto auto; color: #fff; text-align: center; font-size: 8em; font-family: sans-serif; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2em; } сайт

    Осталось только вызвать скрипт:

    Var shine = new Shine(document.getElementById("headline"));

    На этой строке мы передали в shine элемент с id=headline, в котором находится интересующий нас текст. По моей задумке я хочу, чтобы тень меняла своё положение при движении мыши. Это можно сделать написанием следующих строк кода:

    Window.addEventListener("mousemove", function(event){ shine.light.position.x = event.clientX; shine.light.position.y = event.clientY; shine.draw(); }, false);

    Результат выполнения вышеуказанного скрипта можно увидеть в материалах «демо». Думаю, получилось весьма интересно. Но это не единственное, что может shine. У неё есть хороший набор настроек, который поможет вам достичь нужного результата.

    • numSteps - количество нарисованных теней (по умолчанию 8);
    • opacity - прозрачность тени (по умолчанию 0.1);
    • opacityPow - степень прозрачности, применяющейся для каждой тени (по умолчанию 1.2);
    • offset – смещение теней (по умолчанию 0.15);
    • offsetPow – степень смещения, применяющегося для каждой из теней (по умолчанию 1.8);
    • blur - размытие тени (по умолчанию 40);
    • blurPow - степень размытия, применяющейся к каждой тени (по умолчанию 1.4);
    • shadowRGB - цвет тени, указывается в формате RGB (по умолчанию new shinejs.Color(0, 0, 0)).

    Для того чтобы применить эти настройки требуется оформить их в специальном объекте shinejs.Config и передать вторым параметром при создании экземпляра shine:

    Var config = new shinejs.Config({ numSteps: 3, opacity: 0.5, shadowRGB: new shinejs.Color(253, 0, 0) }); var shine = new Shine(document.getElementById("headline"), config);

    Осталось только упомянуть, где обитает эта библиотека. Нашёл я её на просторах github .

    Надеюсь, что вы найдете применение этому скрипту и сможете сделать ваш сайт ещё более интересным и запоминающимся.

  • Разработка игр
    • Перевод

    Я работаю над игрой в жанре стесл-экшн, где в геймплее большую роль будут играть тени. Поэтому я сделал динамическое освещение/затенение, используя WebGL шейдеры.

    Часть первая: Динамическое освещение На его создание меня вдохновил пост на реддите, где aionskull использовал карты нормалей в Unity для динамического освещения своих спрайтов. А пользователь с ником gpillow запостил в комментах что он сделал что-то похожее в Love2D. Вот тут 8-мб гифка с результатами. За неё спасибо jusksmit’у.

    Итак, что такое динамическое освещение? Это техника в 3D графике, где источник света освещает объекты на сцене. Динамическое потому, что обновляется в реальном времени при движении источника. Довольно стандартная штука в 3D мире и легко применимая к 2D, если, конечно, вы можете использовать преимущества шейдеров.

    Сделать динамическое освещение можно зная, что угол падения света на плоскость определяет её освещенность, а определить освещенность можно узнав вектор нормали, который показывает куда «смотрит» плоскость.

    На картинке выше это стрелка, торчащая из центра панели. Вы можете увидеть, что, когда лучи света идут под большим углом (к нормали), панель освещена гораздо хуже. Так вот, в конце концов, алгоритм довольно прост - чем больше угол, тем меньше света получает панель. Самый простой путь вычислить освещенность - вычислить скалярное произведение между вектором от источника света и вектором нормали.

    Ок, всё очень здорово, но как получить вектора нормали в 2d игре? Здесь, вообще-то, нет никаких объемных объектов… Однако, здесь нам могут помочь дополнительные текстуры (те самые карты нормалей), в которых будет записана необходимая информация. Я создал 2 таких карты для двух домов в видео повыше и использовал их чтобы посчитать освещение, вот пример:

    В начале вы видите обычный спрайт домика без затенения. На второй части картинки расположена его карта нормалей, кодирующая вектора нормалей в цвет текстуры. У вектора есть (x,y,z) координаты, а у пикселя текстуры есть r,g и b компоненты, так что закодировать нормаль реально просто: Возьмем фасад дома, который направлен на юг. Его нормаль будет вектором с координатами . (По хорошему, нормаль должна быть равна (0, 1, 0), но так как вектор мы определяем от -1 до +1, а кодировать надо в промежуток от 0 до 1, то, видимо, автор решил не запариваться и сразу считать нормали от -0.5 до +0.5. прим. перев.)

    RGB значения не могут быть отрицательными, так что мы подвинем все значения на 0.5: . Ну и ещё RGB обычно представляется в числе от 0 до 255, так что мы домножим на 255 и получим , или, другими словами, вектор «на юг» будет вот этим светло-зеленым на карте нормалей.

    Теперь, когда у нас есть нормали, мы можем позволить графической карте сделать её магию.
    Я использую ImpactJS , у него неплохая совместимость с WebGL2D . (Он платный, я рекомендую pixi.js или любая другая графическая библиотека с webgl рендерером. Если знаете ещё аналоги - пишите в комменты! прим. перев.) Используя WebGL2D мы можем легко добавить пиксельный шейдер для освещения:

    #ifdef GL_ES precision highp float; #endif varying vec2 vTextureCoord; uniform sampler2D uSampler; uniform vec3 lightDirection; uniform vec4 lightColor; void main(void) { // Берем вектор нормали из текстуры vec4 rawNormal = texture2D(uSampler, vTextureCoord); // Если альфа-канал равен нулю, то ничего не делаем: if(rawNormal.a == 0.0) { gl_FragColor = vec4(0, 0, 0, 0); } else { // Транслируем из RGB в вектор, а именно из 0..1 в -0.5..+0.5 rawNormal -= 0.5; // Вычисляем уровень освещенности float lightWeight = dot(normalize(rawNormal.xyz), normalize(lightDirection)); lightWeight = max(lightWeight, 0.0); // И записываем в пиксель gl_FragColor = lightColor * lightWeight; } }

    Пара заметок: У нас получается попиксельное освещение, которое немного отличается от вершинного освещения (обычного в 3d). Выбора особого нет, так как вершины в 2d бессмысленны (их всего 4 штуки для отображения плоскости на сцене). Но, вообще-то, это не проблема, попиксельное освещение гораздо более точное. Также следует отметить, что шейдер рендерит только освещение, без основного спрайта. Придется признать, я немного жульничаю, ведь на самом деле я не освещаю свой спрайт, а, скорее, затеняю его и в lightColor я передаю темно-серый цвет. Настоящее освещение пикселей, а именно повышение яркости, выглядит хуже, пиксели кажутся «вытертыми». У этой проблемы есть решения, но сейчас это непринципиально.

    Часть вторая: рисование теней. Отбрасывание теней в 3D - хорошо изученная проблема с известными решениями, типа рейтрейсинга или shadow-mapping’а . Однако, я затруднился найти какое-нибудь приемлимое готовое решение для 2d, пришлось делать самому, думаю получилось нормально, хотя и у него есть пара недостатков.

    Вкратце, будем рисовать линию от пикселя на сцене к солнцу и проверять, есть ли какое-нибудь препятствие. Если есть - то пиксель в тени, если нет - на солнце, так что, впринципе, ничего сложного.

    Шейдер принимает xyAngle и zAngle , которые отвечают за то, где находится солнце. Так как оно очень далеко, то лучи света будут параллельны, и, соответственно, эти два угла будут одинаковы для всех пикселей. Также, шейдер будет принимать карту высот мира. Она будет показывать высоту всех объектов, зданий, деревьев и т.д. Если пиксель принадлежит зданию, то значение пикселя будет примерно 10, и означать, что высота здания в данной точке - 10 пикселей.

    Итак, шейдер начнет в пикселе, который надо осветить и, используя вектор xyAngle , будет продвигаться по направлению к солнцу мелкими шажками. На каждом из них мы будем проверять, если в данном пикселе карты высот что-нибудь.


    Как только мы найдем препятствие, мы определим его высоту, и насколько высоким оно должно быть в данной точке чтобы преградить солнце (используя zAngle ).


    Если значение в карте высот будет больше, то всё, пиксель в тени. Если нет - мы продолжим искать. Но рано или поздно мы сдадимся и объявим, что пиксель освещен солнцем. В примере я захардкодил 100 шагов, пока что работает отлично.

    Вот код шейдера в упрощенной/псевдо форме:

    Void main(void) { float alpha = 0.0; if(isInShadow()) { alpha = 0.5; } gl_FragColor = vec4(0, 0, 0, alpha); } bool isInShadow() { float height = getHeight(currentPixel); float distance = 0; for(int i = 0; i < 100; ++i) { distance += moveALittle(); vec2 otherPixel = getPixelAt(distance); float otherHeight = getHeight(otherPixel); if(otherHeight > height) { float traceHeight = getTraceHeightAt(distance); if(traceHeight height) { traceHeight = getTraceHeight(height, zAngle, distance); if(traceHeight height) { float traceHeight = getTraceHeightAt(distance); if(traceHeight height) { traceHeight = getTraceHeight(height, zAngle, distance); if(traceHeight