Каскады теней что такое
Почему мы отказались от стандартных теней Unity для мобильных шутеров и вместо этого написали свои
Использование освещения и теней практически в любом игровом проекте добавляет реализма картинке и подчеркивает взаимное расположение объектов в сцене. Без них игры были бы скучными, безжизненными, было бы сложнее ориентироваться в игровом мире.
Сегодня мы расскажем, как в геймдеве делаются тени — в реальном времени и статичные. В своих проектах War Robots и Dino Squad мы используем сразу несколько техник — им и уделим особое внимание.
Предпосылки к написанию кастомных теней
В War Robots объекты в сцене — сами роботы и наполнение локаций — имеют более-менее сопоставимые размеры. Освещение в проекте статическое и не меняется на протяжении боя, а тени включены только для главного источника света. В связи с этим было решено использовать гибридное решение из теневой маски света (light shadow mask) от статических мешей и детализированной теневой карты (shadow map) на всех объектах, включая динамические.
В то же время одна из особенностей проекта Dino Squad — разница в масштабах диносов. При игре за маленького велоцираптора камера находится близко к террейну, а в случае с большим Ти-рексом — далеко от него. Отсюда родилась необходимость использовать каскадные тени, чтобы получить хорошую детализацию как вблизи, так и на расстоянии от камеры.
В Unity используются каскадные тени, которые работают по отложенной (deferred) схеме. Прежде всего формируются теневые карты — по одной на каждый каскад. Затем выполняется проход отрисовки глубины без шейдинга. После этого вычислительный шейдер восстанавливает по значениям глубины позицию и делает выборку из набора теневых карт, формируя полноэкранную черно-белую теневую маску. В проходе освещения эта маска используется для понимания того, где расположен фрагмент — в тени или на свету.
Таким образом, в тенях от Unity есть лишний проход рисования screen-space глубины, а также два переключения контекста: растеризации в вычислительный и обратно. Это хороший подход для отложенных рендереров (где, как правило, уже есть значение глубины), но не самое быстрое решение в прямых (forward) рендерерах для мобильных платформ. Нам же нужна была forward-реализация, когда тени получаются не в маске, а сразу во фрагментном шейдере.
Второе ограничение отложенных теней — теневая маска не позволяет рисовать тени за полупрозрачными объектами. У нас же при наложении различных эффектов динос становится прозрачным, и мы должны видеть тени сквозь него.
Еще одной особенностью при отрисовке теневых карт в Dino Squad и War Robots является то, что отсечение треугольников (triangle culling) меняется на обратное, и рисуются только задние стороны объектов (inverse culling). Эта особенность позволяет на раннем этапе отсекать 70% фрагментов, снижая объем экспорта данных в видеопамять при формировании теневых карт (shadow maps). Также отпадает необходимость подбора отступов и интервалов (depth offset/depth bias) для борьбы с такими нежелательными явлениями, как эффект Питера Пэна (peter-panning) и теневая рябь (shadow acne) на этапе отрисовки геометрии.
Взвесив все преимущества и недостатки встроенных теней Unity, мы решили сделать свою реализацию.
Теперь подробнее остановимся на используемых в проектах техниках и начнем с самой простой и доступной техники построения теней, а именно — с проекционных теней.
Проекционные тени
На картинке ниже можно заметить под роботом затененную область. Этот эффект достигается за счет проективного наложения текстуры, имитирующей затенение на поверхность под роботом. Текстура содержит концентрическую «плашку» с линейным увеличением прозрачности от центра к периферии. До перехода на Scriptable Rendering Pipeline (SRP) в Unity такие эффекты можно было сделать с помощью готового компонента Projector. Компонент определял объекты в сцене, которые затрагивались проектором, и накладывал на них проекционную текстуру. Слабым местом такого подхода являлась производительность: объекты, затронутые проектором, приходилось рисовать дважды. Отсутствие поддержки проекторов в SRP потребовало от нас разработки собственного решения.
Скриншот с War Robots Remastered, качество Ultra Low
Представим, что у нас есть некоторая текстура с рисунком (круглая тень), который необходимо наложить на произвольную рельефную трехмерную поверхность (террейн). В реальном мире для решения этой задачи мы могли бы использовать проектор (как в кинотеатре), отобразив с его помощью кадр с текстурой и направив изображение на неровный экран. В основе метода наложения проекционных текстур лежит именно этот принцип. Для более детальной информации можно обратиться к презентации NVIDIA.
В случае проекционных теней текстура с тенью и поверхность террейна представляют собой две несвязанных между собой системы координат. В каждой из этих систем элементы текстуры (тексели) и элементы поверхности (пиксели) имеют свои позиции в разных системах координат (texture space и screen space). Операция проецирования определяет взаимосвязь между текселем и пикселем и тем самым позволяет перенести цвет текселя из текстуры с тенью на соответствующий пиксель поверхности террейна.
В проекте War Robots наложение проекционных теней производится сразу при отрисовке объектов. Этот подход не требует предварительного отбора геометрии объектов, которые затрагиваются проектором, а также отдельного прохода для повторной отрисовки этой геометрии. В итоге такая реализация проекционных теней показывает хорошую производительность и довольно проста в реализации.
Теневые карты
Когда еще в прошлом веке видеопроцессоры стали программируемыми, и появилась возможность чтения из отрисованных текстур, это открыло возможности для добавления новых динамических эффектов. Например, можно отрисовать модель черным цветом в белую текстуру, а затем наложить отрисованную текстуру таким образом, чтобы получилась проекционная тень. При этом сама тень за счет обновления на каждом кадре будет изменяться вместе с моделью на террейне. Однако такой подход сталкивается с проблемой просвета тени в другую сторону.
Чтобы этого избежать, в текстуру можно записывать значение глубины — расстояние от источника света до поверхности. Эта записанная глубина впоследствии сравнивается с расчетной в каждом фрагменте, и если она оказывается меньше — значит, соответствующая ей точка находится в тени.
Такая текстура называется картой глубины (depth map). При отрисовке сцены в карту глубины камера помещается в позицию источника света и ориентируется согласно его направлению. Для каждого текселя карты производится операция сравнения и записывается наименьшее значение глубины. Таким образом, темные пиксели в карте глубины условно находятся ближе к камере, а светлые — дальше. Белый цвет означает бесконечность, где ничего нет.
Работа рендера происходит в следующем порядке. Первым проходом формируется карта глубины как отдельная текстура. Во втором проходе при отрисовке объектов на экран проверяется расстояние каждой точки объекта до источника света. Если рассчитанное и выбранное из карты глубины расстояния совпадают — значит, пиксель не затенен. Если расстояние в карте глубины окажется меньше — значит, на пути до источника света есть препятствие, и пиксель находится в тени.
Так выглядит отдельно тень:
А так — вместе со светом:
Запекание теней у статических объектов
Для формирования теневых карт требуется отдельный проход с отрисовкой почти всей сцены, что может привести к просадке производительности при выполнении вершинного шейдера (vertex shader) из-за большого количества геометрии. Именно с этим мы столкнулись на проекте War Robots. На скриншоте ниже представлена статистика кадра при рендеринге одной из сцен:
В этом кадре происходит обработка 437 тысяч треугольников. Здесь использованы два каскада теней, которые рисуются очень далеко с плавным переходом между каскадами на ближней и дальней дистанциях. Если для эксперимента полностью убрать тени, то в кадре будет обработана всего 221 тысяча треугольников — то есть, число в два раза меньшее:
Изначально тени в War Robots обновлялись в реальном времени и включали два каскада теневых карт. Такая схема позволяла отображать тени на всей геометрии сцены, попавшей в кадр, и в то же время обеспечивала отчетливую границу между освещенной и затененной областями вблизи камеры. Этот подход требует обработки очень большого количества геометрии на каждом кадре, ведь сцены в War Robots Remastered довольно увесистые в плане геометрии. Учитывая, что на игровом уровне War Robots имеется только один основной глобальный источник света, который фиксирован и не меняется на протяжении боя (например, в проекте нет динамической смены времени суток), считать на каждом кадре два довольно дорогостоящих каскада от статического источника света не очень разумно.
В War Robots используются карты освещенности (light maps) — текстуры, содержащие рассчитанное заранее непрямое освещение (baked indirect lighting) на статических объектах. По аналогии с запеканием непрямого освещения можно запечь и теневую карту для неподвижной геометрии от того же источника света.
Пример карты освещенности, содержащей непрямое освещение (light map):
Также отдельной текстурой лежат заранее просчитанные тени (shadow mask):
Таким образом, в проекте War Robots для статических объектов в сцене на дальних дистанциях от камеры используются запеченные тени (shadow masks), а для теней от динамических и статических объектов вблизи камеры — по-прежнему теневые карты. В результате комбинации заранее рассчитанных теней (shadow masks) и обновляемых в реальном времени теней (shadow maps) получается достаточно хороший компромиссный вариант с визуальной точки зрения и просто отличный с точки зрения производительности.
Каскадные тени
Так выглядят запеченные тени:
Поскольку все сцены запекаются в статическую теневую маску размером 1k, разрешение получается очень низким. Зато даже на самых дальних объектах все равно есть какая-то тень.
Динамические тени выглядят более четко вблизи, но их не хватает на дальние объекты:
Увеличивая расстояние (зеленая область) мы потеряем в разрешении: сразу проявит себя алиасинг и другие проблемы.
Для борьбы с этим существует техника каскадных теней. Выглядит она следующим образом:
В зеленую область попадают динамические тени максимального разрешения. В красной области — динамические тени более низкого разрешения. Синяя область — заранее рассчитанная и запеченная в текстуру теневая маска от статических объектов.
Теневая карта каскадных теней реализована атласом, который выглядит следующим образом:
Левая часть соответствует зеленой области высокого разрешения. Правая — красной области более низкого разрешения. При вычислении тени для фрагмента в зависимости от расстояния до камеры выбирается значение из соответствующей части атласа. Затем оно смешивается со статической маской таким образом, чтобы переход был плавным и незаметным:
В конце аддитивно накладывается рассеянное освещение и получается уже финальная картинка:
В этой статье мы рассказали о наиболее распространенных техниках затенения, которые используются в 3D-играх и в частности — в наших собственных проектах. Разумеется, это далеко не все возможные способы, но самые базовые для понимания того, как производится рендеринг теней и освещения и как различные техники могут взаимодействовать между собой.
Авторы статьи: Павел Кирсанов, Роман Вишняков, Станислав Жучков
Уроки по OpenGL с сайта OGLDev
Давайте всмотримся в тени из урока 47:
Как обычно, у нас есть маленькая ближняя и большая дальняя плоскости. Теперь давайте посмотрим на сцену сверху:
Следующим шагом мы разбиваем расстояние от ближней плоскости до дальней на три части. Мы будем называть их ближней, средней и дальней. И ещё давайте добавим направление света (стрелка справа):
Итак, как же мы собираемся рендерить каждый каскад в отдельную карту теней? Вспомним этап теней в алгоритме карты теней. Мы настраиваем сцену для рендера с позиции источника света. Это заключается в создании матрицы WVP с мировыми преобразованиями объекта, преобразованиями пространства света и матрицы проекции. Так как этот урок основывается на уроке 47, который работает с тенями направленного света, то матрица проекции будет ортогональной. Обычно CSM используется для открытых сцен, где главный источник света это солнце, и использование направленного света здесь естественно. Если вы посмотрите на матрицу WVP выше, то вы заметите, что первые две части (мировая и обзора) одинаковые для всех каскадов. В конце концов, позиция объекта на сцене и параметры камеры относительно источника света не зависят от разбиения пирамиды на каскады. Так что важна здесь только матрица проекции, поскольку она задает область, которая будет отрендерена. А поскольку ортогональная матрица проекции задается параллелепипедом, то нам нужно задать три различных параллелепипеда, которые будут отображены в три разных ортогональных матрицы проекции. Все три матрицы будут использованы для получения трёх матриц WVP для рендера каждого каскада в его отдельную карту теней.
Логичнее всего было бы сделать эти рамки настолько маленькими, насколько это возможно для получения наименьшего коэффициента отношения пикселей пространства сцены к карте теней. Для этого создадим ограничивающую рамку для каждого каскада вдоль вектора света. Давайте добавим её к первому каскаду:
Давайте теперь добавим ограничивающую рамку для второго каскада:
И ещё одну для последнего каскада:
Как вы можете заметить, из-за положения света в пространстве границы рамок слегка пересекаются, и как следствие, некоторые пиксели будут отрендерены сразу на несколько карт теней. Но до тех пор, пока все пиксели одного каскада находятся целиков в одной карте теней, нас это не волнует. Выбор карты теней для вычислений в шейдере будет основан на расстоянии до пикселя от самого зрителя.
Находим восемь вершин в пространстве обзора. Это не сложно, требуется лишь немного тригонометрии:
На изображение выше представлен произвольный каскад (так как каждый каскад является такой же усеченной пирамидой с таким же углом обзора, как и остальные). Заметим, что мы смотрим сверху вниз на плоскость XZ. Нам нужно найти X1 и X2:
Таким образом мы получаем координаты X и Z всех восьми вершин каскада в пространстве обзора. Используя аналогичные вычисления для вертикального угла обзора мы можем найти координату Y.
Теперь нам нужно преобразовать координаты каскада из пространства обзора обратно в мировое пространство. Предположим, что зритель расположен в мировом пространстве таким образом, что пирамида выглядит так (красная стрелка обозначает источник света, но пока что мы можем её проигнорировать):
Для того, что бы перенести из мирового пространства в пространство камеры, мы умножаем вектор позиции в мировом пространстве на матрицу камеры (получаемую из позиции камеры и её угла поворота). Это значит, что если мы уже имеем координаты каскада в пространстве камеры, то мы просто умножаем их на обратную матрицу камеры для переноса в мировое пространство:
Как и любой другой объект, мы можем преобразовать координаты пирамиды из мирового пространства в пространство света. Вспомним, что пространство света абсолютно идентично пространству камеры, разве что вместо камеры используется источник света. Так как в нашем случае используется направленный свет, у которого нет позиции в пространстве, нам требуется только повернуть сцену таким образом, чтобы свет был направлен вдоль положительного направления оси Z. А положение света можно задать в начале координат пространства света (то есть, нам не нужно преобразований смещения). Если мы сделаем это для рисунка выше (где красная стрелка задает источник света), то каскады в пространстве света будут выглядеть следующим образом:
Наконец, получив координаты каскадов в пространстве света, нам остается только найти границы рамок. Для этого возьмем наибольшие и наименьшие значения компонент X/Y/Z для всех восьми вершин. Такой параллелепипед содержит значения, необходимые для ортогональной проекции для рендера каскада на карту теней. Получив для каждого каскада отдельную матрицу проекции, мы можем рендерить каждый каскад в отдельную карту. На световом этапе мы будем вычислять коэффициент теней выбирая карту теней ориентируясь на расстоянии от зрителя.
Прямиком к коду!
В этапе теней добавлена парочка изменений, которые заслуживают внимания. Первое, вызов CalOrthoProjs() в начале этапа. Эта функция отвечает за вычисление ограничивающих рамок, используемых для ортогональной проекции. Следующее отличие это цикл по каскадом. Каждый из них по отдельности должен быть привязан на запись, очищен и отрендерен. Каждый каскад имеет свою проекцию в массиве m_shadowOrthoProjInfo (который заполняет CalcOrthoProjs). Так как мы не знаем в какой каскад попадет каждый меш (а их может быть больше одного), то мы вынуждены рендерить всю сцену для каждого каскада.
Единственное отличие в проходе света в том, что для света вместо одной матрицы WVP их стало три. Они отличаются только проекциями. Мы получаем их в цикле в середине этапа.
Перед тем как мы займемся вычислением ортогональной проекции, нам следует обратить внимание на массив m_cascadeEnd (который инициализируется в конструкторе). Этот массив задает каскады записывая значения ближней и дальней Z в первый и последний слот соответственно и границы каскадов посередине. Таким образом первый каскад заканчивается в значении из первого слота, второй из второго и третий из последнего. А значение ближней Z плоскости в первом слоте позже поможет упростить вычисления.
Выше мы видим первый шаг из блока теории о вычислении ортогональной проекции для каскада. Массив frustumCorners заполнен восемью вершинами каскада в пространсве экрана. Заметим, что так как задан только горизонтальный угол обзора, то вертикальный мы вычисляем вручную (например, если горизонтальный угол обзора равен 90°, а размеры окна 1000×500, то вертикальный улог обзора будет равен 45°).
Код выше выполняет шаги со #2 по #4. Каждая вершина каскада домнажается на обратную матрицу преобразований для перевода в мировое пространство. А после она домнажается на преобразования света для перевода в его пространство. И затем мы несколько раз используем функции min/max для вычисления ограничивающей рамки каскада в пространстве света.
Текущая запись в массиве m_shadowOrthoProjInfo заполняется используя значения обрамляющей рамки.
Ничего нового в вершинном и фрагментном шейдерах этапа теней. Мы по прежнему просто рендерим глубину.
Фрагментный шейдер прохода света содержит некоторые дополнения в основной секции. На вход мы получаем три вершины в пространстве света, которые вычислил вершинный шейдер, а так же значение Z в пространстве клиппера. Вместо одной карты теней их теперь три. Кроме того, приложение должно передавать конец каждого каскада в пространстве клиппера. Чуть позже мы увидим как он вычисляется. А пока просто предположим что значение уже есть.
Для того что бы для пикселя выбрать подходящий каскад мы передаем uniform-массив gCascadeEndClipSpace и сравниваем Z компоненту координаты в пространстве клиппера с каждой записью в массиве. Массив отсортирован по возрастанию удаленности. Мы останавливаемся как только нашли запись, значение которой больше или равно текущей компоненте Z. Затем мы вызываем CalcShadowFactor() и передаем туда индекс найденного каскада. Единственное отличие в самой функции в том, что получаем значение глубины из той карты теней, индекс которой равен найденному. Остальное без изменений.
Если вы посмотрите код урока, то вы увидите, что я добавил индикатор границы каскадов назначив каждому из них свой цвет (красный, зеленый или синий). Это очень полезно при отладке, так как вы явно можете видеть границы каждого каскада. С алгоритмом CSM и цветным индикатором сцена выглядит как-то так:
Динамическое освещение в Unreal Engine 4
Некорректное качество теней
В данной части статьи мы рассмотрим решения, направленные на улучшение качества теней соответствующих геометрии. Это также поможет улучшить качество динамических теней.
Пример некорректного затенения геометрии.
Только! для Directional Light : Настройки Cascaded Shadow Maps:
Dynamic Shadow Distance Movable: Расстояние от камеры, на котором тени будут исчезать.Значение 0 отключает их.
Dynamic Shadow Distance Stationary: Расстояние от камеры, на котором тени будут исчезать.По умолчанию стоит 0 для Directional Stationary Lights.
Num Dynamic Shadow Cascades: Количество каскадов на которое будет делится фрустум.Больше каскакодов- лучше качество, но и ресурсоёмкость возрастает.
Обязательно тыкните в ссылочку ниже, ибо наверняка не все знают что такое фрустум.
Больше информации про фрустум: http://en.wikipedia.org/wiki/Viewing_frustum
Num Dynamic Shadow Cascades :
Cascade Distribution Exponent: Параметр регулирующий, на каком расстоянии от камеры распределение ближе (выше значение) или дальше (меньше значение) от камеры. Значение 1 означает, что переход будет пропорционален разрешению экрана.
Cascade Distribution Exponent:
Cascade Transition Exponent: Параметр регулирующий затухающий регион между каскадами. Низкое значение даст жёсткие грани, высокое — мягкие.
Cascade Transition Exponent:
Cascade Transition Exponent
Shadow Distance Fadeout Fraction: Параметр регулирующий на каком расстоянии тени будут затухать. Высокое значение ослабляет тени, в то время как низкое значение оставляет тени тёмными.
Shadow Distance Fadeout Fraction:
Far Shadow
Возможность установки Far Shadow для статических мешей или ландшафта дает дополнительные преимущества в том, дабы иметь возможность использовать каскадные тени на очень дальних расстояниях, а не в ограниченном диапазоне, ближайшему к камере.
Для включения данной опции достаточно лишь поставить галочку в пунктике Far Shadow во вкладке Lightning в настройках Directional Light.
Вы можете указать количество каскадов для Far Shadow, чтобы объекты у которых включено Far Shadow учитывали данный параметр. По умолчанию значение равно 300 метрам, но должно быть больше чем значение у параметра Cascaded Shadow Map Distance.
Не забывайте, что данную фичу рекомендуется использовать только на больших объектах, а не на всех подряд, т.к. при включении данной опции на всех подряд объектах это приведёт к снижению производительности.
На гифке выше: у объекта слева выключено Far shadow, а объект справа имеет параметры Dynamic Shadow Distance = 5000 и far shadow distance = 50000 при 4 каскадах.
Регулирование Cascades для лучшего качества:
Просачивание теней и их точность, можно настроить с регулированием параметров указанных выше. Данный раздел покажет как лучше настроить параметры, чтобы получить наилучший результат в момент приближения и отдаления камеры. Нахождение баланса для каждой отдельной игры — путь проб и ошибок без которого нельзя обойтись.
Базовая сцена с одними лишь настройками по умолчанию:
Базовая сцена дефолтные настройки
Тут у нас уже есть некоторые проблемы с точностью теней на гранях объектов.
Проблемные зоны выделены красными рамками
Демонстрация проблемы, с близкого ракурса.
Проблемные зоны, вид поближе
Сфокусируем внимание на минимизации просветов теней, в пределах разумного. По умолчанию, дистанция динамических теней установлена в 20000 единиц. Данное значение не всегда обязательно и может быть изменено в большую или меньшую сторону.
В этой сцене настройки распределения, дистанции теней и количество каскадов просветов и их точность — на приемлемом уровне.
Базовая сцена финальный результат
Всё динамическое освещение (панель настройки освещения):
Также имеются ещё 2 настройки которые могут улучшить точность освещения.
Местонахождение этих настроек:
Shadow Bias — контролирует точность теней в сцене, но может выдать артефакты, если значение будет слишком низким.По умолчанию стоит значение 0.5 которое даёт хороший результат между точностью и эффективность.
Shadow Filter Sharpness — помогает скрыть некоторые артефакты при низких значениях и помогает создавать более резкие тени на краях.
Принимая во внимание то, что предыдущие настройки не были применены для текущих примеров, то ниже идут 2 примера с демонстрацией этих параметров (Shadow Bias и Shadow Filter Sharpen):
Shadow Bias = 0.5 (Дефолтное значение)
Shadow Bias = 0, установлено слишком низкое значение и поэтому появляются артефакты
Решение данной проблемы — нахождение баланса между установкой не слишком низкого значения и подбором нужного значения в Cascaded Shadows.
Shadow Filter Sharpen — делает края тени более резкими при высоких значениях.
Высокое значение параметра даёт резкий край тени, низкое- плавный край тени.
Почему мой динамический источник света светится сквозь объекты на отдалении?
Результат который мы хотим. Камера вблизи.
Что получается в итоге. Камера в отдалении.
Чтобы понять что происходит, нужно учитывать, что движок использует глубину сцены, дабы определить, что будет видимым и что будет невидимым при рендере. Тут же мы имеем точечный источник света который испускает свет по всему радиусу до тех пор пока не встретит преграду или пока не столкнётся с границей сферы.
На картинке ниже вы можете наблюдать, как при отдалении камеры появляется halo эффект вокруг внешних граней объекта из-за того, что свет не встречает преград на своём пути и исходит во всех направлениях.
Проход света сквозь объект.
Вы можете заметить, что если вы на дистанции выберете/выделите такой объект, то освещение вернётся в нормальное состояние. Это объясняется тем, что вы сфокусировались в данный момент на данном объекте.
Дабы предохранить объект от пропускания света, вам нужно выделить объект, и перейти на панель Details. Тут вам нужно найти во вкладке Rendering, параметр Bounds Scale.
Местонахождение Bounds Scale
Вы можете увидеть/визуализировать границы вашего меша перейдя во вьюпорте > Show > Advanced > Bounds
Увидите нечто такое:
Визуализированные границы объекта.
Если у вас есть вышеописанная проблема, но вы не хотите увеличивать размеры границ вашего меша из-за проблем с производительностью — попробуйте использовать Spotlight. Данный ИС пускает свет лишь в одном направлении, в отличии от point light который пускает свет по всем направлениям когда меш пропускает свет. Пусть это и возымеет некоторый положительный/требуемый эффект, но лишь методом проб и ошибок вы сможете добиться идеального решения.
Как вариант, мы можете попробовать использовать Стационарное освещение, а не динамическое, это позволит вам запечь тени, что в итоге сохранит производительность, и не потребует каждый раз обновлять информацию о тенях.