[Перевод] Как рендерится кадр Rise of the Tomb Raider

1k5ps6qnv2soku_ymxsemz7wnxa.gif


Rise of the Tomb Raider (2015 год) — это сиквел превосходного перезапуска Tomb Raider (2013 год). Лично я нахожу обе части интересными, потому что они отошли от стагнирующей оригинальной серии и рассказали историю Лары заново. В этой игре, как и в приквеле, центральное место занимает сюжет, она предоставляет увлекательные механики крафтинга, охоты и скалолазания/исследований.

В Tomb Raider использовался разработанный Crystal Dynamics движок Crystal Engine, также применявшийся в Deus Ex: Human Revolution. В сиквеле использовали новый движок под названием Foundation, ранее разрабатывавшийся для Lara Croft and the Temple of Osiris (2014 год). Его рендеринг можно в целом описать как тайловый движок с предварительным проходом освещения, и позже мы узнаем, что это означает. Движок позволяет выбирать между рендерерами DX11 и DX12; я выбрал последний, по причинам, которые мы обсудим ниже. Для захвата кадра использовался Renderdoc 1.2 на Geforce 980 Ti, в игре включены все функции и украшательства.

Анализируемый кадр


254df90bbbfd5498710f37624e12d1f4.jpg


Чтобы не было спойлеров, скажу, что в этом кадре плохие парни преследуют Лару, потому что она ищет артефакт, который разыскивают и они. Этот конфликт интересов никак не разрешить без оружия. Лара ночью пробралась на вражескую база. Я выбрал кадр с атмосферным и контрастным освещением, при котором движок может показать себя.

Предварительный проход глубины


Здесь выполняется привычная для многих игр оптимизация — небольшой предварительный проход глубины (примерно 100 вызовов отрисовки). Игра рендерит самые крупные объекты (а не те, которые занимают больше места на экране), чтобы воспользоваться преимуществом функции видеопроцессоров Early-Z. Подробнее о ней рассказывается в статье Intel. Если вкратце, то GPU способны избегать выполнения пиксельного шейдера, если могут определить, что он перекрыт предыдущим пикселем. Это довольно малозатратный проход, предварительно заполняющий Z-буфер значениями глубин.

На этом этапе я обнаружил интересную технику уровней детализации (level of detail, LOD) под названием «fizzle» или «checkerboard». Это распространённый способ постепенного отображения или сокрытия объектов на расстоянии, чтобы в дальнейшем или заменить их мешем более низкого качества, или полностью скрыть их. Посмотрите на этот грузовик. Похоже, что он рендерится дважды, но на самом деле он рендерится с высоким LOD и низким LOD в одной и той же позиции. Каждый из уровней рендерит те пиксели, которые не отрендерил другой. Первый LOD имеет 182226 вершин, а второй LOD — 47250. На большом расстоянии они неразличимы, но один из них в три раза менее затратен. В этом кадре LOD 0 почти исчезает, а LOD 1 рендерится почти полностью. После полного исчезновения LOD 0 будет рендериться только LOD 1.

c31d8d24e58229c27fb4edf6f635974d.png


LOD 0

7bf3b45bbe13521b4bae010f72a702a7.png


LOD 1

jy7rit_bwqm4n0lwpzzlydyx5rs.gif


Псевдослучайная текстура и коэффициент вероятности позволяет нам отбрасывать пиксели, не прошедшие порогового значения. Эта текстура используется в ROTR. Можно задаться вопросом, почему же не используется альфа-смешение. У альфа-смешения есть множество недостатков по сравнению с fizzle fading.

  1. Удобство для предварительного прохода глубин: благодаря рендерингу непрозрачного объекта с проделанными в нём отверстиями, мы можем выполнять рендеринг в предварительном проходе и пользоваться early-z. Объекты с альфа-смешением на столь раннем этапе не рендерятся в буфер глубин из-за проблем с сортировкой.
  2. Необходимость дополнительных шейдеров: если используется отложенный рендерер, то шейдер непрозрачных объектов не содержит никакого освещения. Если вам нужно заменить непрозрачный объект прозрачным, то необходим отдельный вариант, в котором есть освещение. Кроме повышения объёма нужной памяти и сложности из-за по крайней мере одного дополнительного шейдера для всех непрозрачных объектов, они должны быть точными, чтобы избежать выступания объектов вперёд. Это сложно по многим причинам, но всё сводится к тому, что рендеринг теперь выполняется по другому пути кода.
  3. Больший объём перерисовки: альфа-смешение может создавать большую перерисовку и при определённом уровне сложности объектов может потребоваться большая доля полосы пропускания для затенения LOD.
  4. Z-конфликты: z-конфликты — это эффект мерцания, когда два полигона рендерятся на очень близкой друг к другу глубине. При этом неточность вычислений с плавающей запятой заставляет их рендериться по очереди. Если мы отрендерим два последовательных LOD, постепенно скрывая один и показывая второй, то они могут вызвать z-конфликт, потому что очень близки друг к другу. Всегда есть способы обойти это, например, предпочитая один полигон другому, но такая система оказывается сложной.
  5. Эффекты Z-буфера: многие эффекты наподобие SSAO используют только буфер глубин. Если бы мы рендерили прозрачные объекты в конце конвейера, когда ambient occlusion уже выполнено, то не могли бы его учесть.


Недостаток этой техники в том, что она выглядит хуже, чем альфа-смешение, но хороший паттерн шума, размытие после fizzle или временной антиалиасинг могут почти полностью скрыть это. В этом отношении ROTR не делает ничего особо необычного.

Проход нормалей


В своих играх Crystal Dynamics использует довольно необычную схему освещения, которую мы рассмотрим в проходе освещения. Пока же достаточно сказать, что движке нет прохода G-буфера; по крайней мере, в той степени, которая привычна в других играх. На этом проходе объекты передают на выход только информацию о глубине и нормалях. Нормали записываются в render target формата RGBA16_SNORM в мировом пространстве. Любопытно, что этот движок использует схему Z-up, а не Y-up (вверх направлена ось Z, а не Y), которая более часто применяется в других движках/пакетах моделирования. В альфа-канале содержится глянцевость (glossiness), которая в дальнейшем распаковывается как exp2(glossiness * 12 + 1.0). Значение glossiness может быть и отрицательным, потому что знак используется как флаг, показывающий, является ли поверхность металлической. Это можно заметить и самостоятельно, потому что все тёмные цвета в альфа-канале относятся к металлическим объектам.

R G B
Normal.x Normal.y Normal.z Glossiness + Metalness


1e8209484fe730d178c4afffef4c8f93.jpg


Нормали

8fbdd3b2d2ddbce3111a4c017e7daef3.jpg


Glossiness/Metalness

Преимущества предварительного прохода глубин

aa1b1552b0f81c2122d094441e531f3f.jpg


Помните, что в разделе «Предварительный проход глубин» мы говорили об экономии затрат на пиксели? Я немного вернусь назад, чтобы проиллюстрировать её. Возьмём следующее изображение. Это рендеринг детализированной части горы в буфер нормалей. Renderdoc любезно выделил пиксели, прошедшие тест глубины, зелёным цветом, а не прошедшие его — красным (они не рендерятся). Общее количество пикселей, которые бы отрендерились без этого предварительного прохода, примерно равно 104518 (подсчитано в Photoshop). Общее количество пикселей, рендерящихся на самом деле, равно 23858 (вычислено Renderdoc). Экономия примерно в 77%! Как мы видим, при умном использовании этот предварительный проход может дать большой выигрыш, а требует он всего около ста вызовов отрисовки.

Запись многопоточных команд

9195d06236fd6247cea6e6065f05e9e8.png


Стоит отметить один интересный аспект, из-за которого я выбрал рендерер DX12 — запись многопоточных команд. В предыдущих API, например в DX11, рендеринг обычно выполняется в одном потоке. Графический драйвер получал от игры команды отрисовки и постоянно передавал запросы GPU, но игра не знала, когда это произойдёт. Это приводит к неэффективности, потому что драйвер должен каким-то образом догадываться, что пытается сделать приложение, и не масштабируется на несколько потоков. Более новые API, такие как DX12, передают управление разработчику, который сам может решать, как записывать команды и когда отправлять их. Хотя Renderdoc и не может показать, как выполняется запись, вы увидите, что есть семь проходов цвета, помеченные как Color Pass N, и каждый из них обёрнут в пару ExecuteCommandList: Reset/Close. Она обозначает начало и конец списка команд. На список приходится примерно 100–200 вызовов отрисовки. Это не означает, что они были записаны с помощью нескольких потоков, но намекает на это.

Следы на снегу

ca6d212e89b61f52738dfbd01363e366.jpg


Если посмотреть на Лару, то можно заметить, что при перемещении перед скриншотом она оставила на снегу следы. В каждом кадре выполняется вычислительный шейдер (compute shader), записывающий деформации в определённых областях и применяющий их на основании типа и высоты поверхности. Здесь к снегу применяется только карта нормалей (т.е. геометрия не изменяется), но в некоторых областях, где толщина снега больше, деформация выполняется на самом деле! Можно также увидеть, как снег «падает» на место и заполняет оставленные Ларой следы. Гораздо подробнее эта техника описана в GPU Pro 7. Текстура деформации снега — это своего рода карта высот, отслеживающая движения Лары и склеенная по краям так, чтобы шейдер сэмплирования мог воспользоваться этим сворачиванием.

Атлас теней


При создании Shadow mapping используется довольно распространённый подход — упаковка как можно большего количества карт теней в общую текстуру теней. Такой атлас теней на самом деле является огромной 16-битной текстурой размером 16384×8196. Это позволяет очень гибко многократно использовать и масштабировать карты теней, находящиеся в атласе. В анализируемом нами кадре в атлас записано 8 карт теней. Четыре из них относятся к основному источнику направленного освещения (луне, ведь дело происходит ночью), потому что они используют каскадные карты теней — достаточно стандартную технику теней дальних расстояний для направленного освещения, которую я уже немного объяснял ранее. Более интересно то, что в захват этого кадра также включены несколько прожекторных и точечных источников освещения. То, что в этом кадре записано 8 карт теней, не означает, что в нём есть только 8 источников отбрасывающего тени освещения. Игра может кэшировать вычисления теней, то есть освещение, у которого не изменилась или позиция источника, или геометрия в области действия, не должно обновлять свою карту теней.

f008d9169bd3490c26f923bcc4ea8e85.jpg


Похоже, что рендеринг карт теней тоже выигрывает от записи многопоточных команд в список, и в данном случае для рендеринга карт теней записано целых 19 списков команд.

Тени от направленного освещения

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

cef209845628db5e68588f07880542eb.jpg


Ambient Occlusion


Для ambient occlusion ROTR позволяет использовать на выбор или HBAO, или его разновидность HBAO+ (эту технику изначально опубликовала NVIDIA). Существует несколько вариаций этого алгоритма, поэтому я рассмотрю тот, который я обнаружил в ROTR. Сначала буфер глубин разделяется на 16 текстур, каждая из которых содержит 1/16 от всех значений глубин. Разделение выполняется таким образом, что каждая текстура содержит одно значение из блока 4×4 оригинальной текстуры, показанной на рисунке ниже. Первая текстура содержит все значения, отмеченные красным (1), вторая — значения, отмеченные синим (2), и так далее. Если вы хотите узнать подробности об этой технике, то вот статья Louis Bavoil, который также был одним из авторов статьи про HBAO.

1c92dc60fe369caed766efe18dd2739d.png


Следующий этап вычисляет для каждой текстуры ambient occlusion, что даёт нам 16 текстур AO. Генерируется ambient occlusion следующим образом: несколько раз сэмплируется буфер глубин, воссоздавая позицию и накапливая результат вычислений для каждого из сэмплов. Каждая текстура ambient occlusion вычисляется с использованием разных координат сэмплирования, то есть в блоке пикселей 4×4 каждый пиксель рассказывает свою часть истории. Это сделано по причине производительности. Каждый пиксель уже сэмплирует буфер глубин 32 раз, а полный эффект потребует 16×32 = 512 сэмпла, что является перебором даже для самых мощных GPU. Затем они рекомбинируются в одну полноэкранную текстуру, которая получается довольно шумной, поэтому для сглаживания результатов сразу после этого выполняется проход полноэкранного размытия. Очень похожее решение мы видели в Shadow of Mordor.

image


Части HBAO

image


Полное HBAO с шумом

image


Полное HBAO с горизонтальным размытием

image


Готовое HBAO

Тайловый предварительный проход освещения


Предварительный проход освещения (Light Prepass) — довольно необычная техника. Большинство команд разработчиков использует сочетание отложенного + прямого расчёта освещения (с вариациями, например, с тайловым, кластерным) или полностью прямой для некоторых эффектов экранного пространства. Техника предварительного прохода освещения настолько необычна, что заслуживает объяснения. Если концепция традиционного отложенного освещения заключается в отделении свойств материалов от освещения, то в основе предварительного прохода освещения заложена идея отделения освещения от свойств материалов. Хотя такая формулировка выглядит немного глупо, отличие от традиционного отложенного освещения в том, что мы храним все свойства материалов (такие как albedo, specular color, roughness, metalness, micro-occlusion, emissive) в огромном G-буфере, и используем его позже как входные данные для последующих проходов освещения. Традиционное отложенное освещение может представлять большую нагрузку на пропускную способность; чем сложнее материалы, тем больше информации и операций необходимо в G-буфере. Однако в предварительном проходе освещения мы сначала накапливаем всё освещение отдельно, используя минимальный объём данных, а затем применяем их в последующих проходах к материалам. В данном случае освещению достаточно только нормалей, roughness и metalness. Шейдеры (здесь используются два прохода) выводят данные в три render target формата RGBA16F. Один содержит диффузное освещение, второй — зеркальное освещение, а третий — окружающее освещение. На этом этапе учитываются все данные теней. Любопытно, что в первом проходе (диффузное + зеркальное освещение) для полноэкранного прохода используется четырёхугольник (quad) из двух треугольников, а в других эффектах используется один полноэкранный треугольник (почему это важно, можно узнать здесь). С этой точки зрения весь кадр не является целостным.

image


Диффузное освещение

image


Зеркальное освещение

image


Окружающее освещение

Тайловая оптимизация

Тайловое освещение — это техника оптимизации, предназначенная для рендеринга большого количества источников освещения. ROTR разделяет экран на тайлы 16×16, а затем сохраняет информацию о том, какие источники пересекают каждый тайл, то есть вычисления освещения будут выполняться только для тех источников, которые касаются тайлов. В начале кадра запускается последовательность вычислительных шейдеров, определяющих какие источники касаются тайлов. На этапе освещения каждый пиксель определяет, в каком тайле он находится и обходит в цикле каждый источник освещения в тайле, выполняя все вычисления освещения. Если привязка источников к тайлам выполнена качественно, то можно сэкономить много вычислений и большую часть полосы пропускания, а также повысить производительность.

Увеличение масштаба с учётом глубины

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

ROTR делает хитрый ход с помощью раннего отбрасывания стенсила. После прохода нормалей буфер глубин полностью заполнен, поэтому движок выполняет полноэкранный проход, помечающий все прерывающиеся пиксели в стенсил-буфере. Когда настаёт время воссоздания буфера окружающего освещения, движок использует два шейдера: один очень простой — для областей без разрывов глубин, другой — более сложный для пикселей с разрывами. Ранний стенсил отбрасывает пиксели, если они не принадлежат соответствующей области, то есть затраты есть только в нужных областях. На следующих изображениях гораздо понятнее:

image


Окружающее освещение в половинном разрешении

image


Повышение масштаба глубин внутренних частей

image


Окружающее освещение в полном разрешении, без рёбер

image


Повышение масштаба глубин рёбер

image


Готовое окружающее освещение

image


Приближенный вид половинного разрешения

image


Приближенный вид воссозданного изображения

После предварительного прохода освещения геометрия передаётся в конвейер, только на этот раз каждый объект сэмплирует текстуры освещения, текстуру ambient occlusion и другие свойства материалов, которые мы не записали в G-буфер с самого начала. Это хорошо, потому что здесь сильно экономится пропускная способность благодаря тому, что не нужно считывать кучу текстур, чтобы записывать их в большой G-буфер, чтобы потом их снова считать/декодировать. Очевидный недостаток такого подхода в том, что всю геометрию нужно передавать заново, а текстуры предварительного прохода освещения сами по себе представляют большую нагрузку на пропускную способность. Я задавался вопросом, почему для текстур предварительного прохода освещения не использовать более лёгкий формат, например R11G11B10F, но в альфа-канале есть дополнительная информация, поэтому это было бы невозможно. Как бы то ни было, это интересное техническое решение. На этом этапе вся непрозрачная геометрия уже отрендерена и освещена. Заметьте, что в неё включены такие испускающие свет объекты, как небо и экран ноутбука.

2eb3cb0a3f58c521079fc2bbd1b3fd67.jpg


Отражения


Эта сцена — не особо хороший пример для демонстрации отражений, поэтому я выбрал другую. Шейдер отражений — это довольно сложное сочетание циклов, которые можно свести к двум частям: одна сэмплирует кубические карты, а другая выполняет SSR (Screen space reflection — вычисления отражений в экранном пространстве); всё это делается в одном проходе и в конце смешивается с учётом коэффициента, определяющего, обнаружило ли SSR отражение (вероятно, коэффициент не двоичный, а является значением в интервале [0, 1]). SSR работает стандартным для многих игр образом — многократно трассирует буфер глубин, пытаясь найти наилучшее пересечение между лучом, отражённым затеняемой поверхностью, и другой поверхностью в каком-нибудь месте экрана. SSR работает с mip-цепочкой ранее уменьшенного масштаба текущего HDR-буфера, а не со всем буфером.

Тут также есть такие факторы корректировки, как яркость отражения, а также своеобразная френелевская текстура, которая вычислена до этого прохода, на основании нормалей и roughness. Я не полностью уверен, но после изучения ассемблерного кода мне кажется, что ROTR может вычислять SSR только для гладких поверхностей. В движке нет mip-цепочки размытия после этапа SSR, которые существуют в других движках, и нет даже ничего наподобие трассировки буфера глубин с помощью лучей, которая варьируется на основании roughness. В целом, более шероховатые поверхности получают отражения от кубических карт, или не получают их вовсе. Тем не менее, там, где работает SSR, его качество очень высоко и стабильно, с учётом того, что он не накапливается по времени и для него не выполняется пространственное размытие. Альфа-данные тоже поддерживают SSR (в некоторых храмах можно увидеть очень красивые отражения в воде) и это хорошее дополнение, которое не часто увидишь.

image


Отражения до

image


Буфер отражений

image


Отражения после

Освещённый туман


578565d2af38eac19d8aff86b82cf710.jpg


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

image


Туман до

image


Туман после

Объёмное освещение


На ранних этапах кадра происходит несколько операций для подготовки к объёмному освещению. Из ЦП в GPU копируются два буфера: индексов источников освещения и данных источников освещения. Оба считываются вычислительным шейдером, который выводит 3D-текстуру 40×23x16 вида из камеры, содержащую количество источников освещения, пересекающих эту область. Текстура имеет размеры 40×23, потому что каждый тайл занимает 32×32 пикселя (1280/32 = 40, 720/32 = 22.5), а 16 — это количество пикселей в глубине. В текстуру включаются не все источники освещения, а только те, которые помечены как объёмные (в нашей сцене их три). Как мы увидим ниже, есть и другие фальшивые объёмные эффекты, создаваемые плоскими текстурами. Выводимая текстура имеет большее разрешение — 160×90x64. После определения количества источников освещения на тайл и их индекса последовательно запускаются три вычислительных шейдера, выполняющих следующие операции:

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


Когда всё это завершится, у нас получается 3D-текстура, сообщающая, сколько света получает конкретная позиция относительно камеры. Всё, что остаётся сделать полноэкранному проходу — определить эту позицию, найти соответствующий воксель текстуры и прибавить его к HDR-буферу. Сам шейдер освещения очень прост и содержит всего около 16 инструкций.

image


Объёмное освещение до

image


Объёмное освещение после

Рендеринг волос


Если функция PureHair не включена, то поверх друг друга рендерятся стандартные слои волос. Это решение по-прежнему выглядит отлично, но я хотел бы сосредоточиться на самых современных технологиях. Если функция включена, то кадр начинается с симуляции волос Лары последовательностью вычислительных шедеров. Первая часть Tomb Raider использовала технологию под названием TressFX, а в сиквеле Crystal Dynamics реализовала улучшенную технологию. После первоначальных вычислений получается целых 7 буферов. Все они используются для управления волосами Лары. Процесс выполняется следующим образом:

  1. Запуск вычислительного шейдера для вычисления значений движения на основе предыдущих и текущий позиций (для motion blur)
  2. Запуск вычислительного шейдера для заполнения кубической карты светимости размером 1×1 на основе зонда отражений и информации светимости (освещения)
  3. Создание примерно 122 тысяч вершин в режиме полос треугольников (Triangle Strip) (каждая прядь волос — это полоса). Здесь нет никакого буфера вершин, как можно было бы ожидать при обычных вызовах отрисовки. Вместо них есть 7 буферов, содержащих всё необходимое для построения волос. Пиксельный шейдер выполняет ручную обрезку, если пиксель находится за пределами окна, то отбрасывается. Этот проход помечает стенсил как «содержащий волосы».
  4. Проход освещения/тумана рендерит полноэкранный quad со включенным тестированием стенсила, чтобы вычислялись только те пиксели, в которых действительно видны волосы. По сути, он считает волосы непрозрачнми и ограничивает затенение только теми прядями, которые видны на экране.
  5. Также есть финальный проход наподобие этапа 4, который выводит только глубину волос (выполняет копирование из текстуры «глубины волос»)


Если вам интересно узнать об этом подробнее, то у AMD есть множество ресурсов и презентаций, потому что это созданная компанией библиотека общего пользования. Меня сбил с толку этап перед этапом 1, в котором выполняется тот же вызов отрисовки, что и на этапе 3, при этом говорится, что он рендерит только в значения глубины, но на самом деле содержимое не рендерится, и это любопытно; возможно, Renderdoc мне чего-то недоговаривает. Я подозреваю, что он, возможно, пытался выполнить условный запрос рендеринга, но я не вижу никаких вызовов прогнозирования.

image


Волосы до

image


Видимые пиксели волос

image


Волосы с затенением

Тайловый рендеринг альфа-данных и частицы


Прозрачные объекты снова используют потайловую классификацию источников освещения, вычисленную для тайлового предварительного прохода освещения. Каждый прозрачный объект вычисляет собственное освещение в одном проходе, то есть количество инструкций и циклов становится довольно пугающим (именно поэтому для непрозрачных объектов и использовался предварительный проход освещения). Прозрачные объекты могут даже выполнять отражения в экранном пространстве, если они включены! Каждый объект рендерится в порядке сортировки сзади вперёд непосредственно в HDR-буфер, в том числе стекло, пламя, вода в колеях и т.д. Альфа-проход также рендерит подсветку краёв, когда Лара фокусируется на каком-то объекте (например, на бутылке с горючей смесью на ящике слева).

4454a83115f4118ab092fc1ff3bcaaf1.jpg


Однако частицы рендерятся в буфер половинного разрешения, чтобы сгладить огромную нагрузку на полосу пропускания, создаваемую их перерисовкой, особенно когда множество крупных, закрывающих экран частиц используется для создания тумана, мглы, пламени и т.д. Поэтому HDR-буфер и буфер глубин уменьшаются в половину по каждой из сторон, после чего начинается рендеринг частиц. Частицы создают огромный объём перерисовки, некоторые пиксели затеняются примерно 40 раз. На тепловой карте видно, что я имею в виду. Так как частицы рендерились в половинном разрешении, здесь используется тот же умный трюк с повышением масштаба, что и в окружающем освещении (в стенсиле помечаются разрывы, первый проход выполняет рендеринг во внутренние пиксели, второй воссоздаёт края). Можно заметить, что частицы рендерятся до некоторых других альфа-эффектов, таких как пламя, сияние и т.д. Это необходимо, чтобы альфу можно было правильно сортировать относительно, например, дыма. Также можно заметить, что здесь появляются «объёмные» лучи света, идущие от охранных прожекторов. Они добавляются здесь, а не создаются на этапе объёмного освещения. Это малозатратный, но реалистичный способ создания их на большом расстоянии.

image


Только непрозрачные объекты

image


Первый альфа-проход

image


Частицы половинного разрешения 1

image


Частицы половинного разрешения 2

image


Частицы половинного разрешения 3

image
Увеличение масштаба внутренних частей

image


Увеличение масштаба краёв

image


Полные альфа-данные

Выдержка и тональная коррекция


ROTR выполняет выдержку и тональную коррекцию за один проход. Однако хотя мы обычно считаем, что при тональной коррекции происходит гамма-коррекция, здесь это не так. Есть множество способов реализации выдержки, как мы это видели на примере других игр. Вычисление яркости (luminance) в ROTR выполняется очень интересно и не требует почти никаких промежуточных данных или проходов, поэтому мы объясним этот процесс подробнее. Весь экран разделяется на тайлы 64×64, после чего запускается вычисление групп (20, 12, 1) по 256 потоков в каждой для заполнения всего экрана. Каждый поток по сути выполняет следующую задачу (ниже представлен псевдокод):

for(int i = 0; i < 16; ++i)
{
    uint2 iCoord = CalculateCoord(threadID, i, j); // Obtain coordinate
    float3 hdrValue = Load(hdrTexture, iCoord.xyz); // Read HDR
    float maxHDRValue = max3(hdrValue); // Find max component
    float minHDRValue = min3(hdrValue); // Find min component
    float clampedAverage = max(0.0, (maxHDRValue + minHDRValue) / 2.0);
    float logAverage = log(clampedAverage); // Natural logarithm
    sumLogAverage += logAverage;
}


Каждая группа вычисляет логарифмическую сумму всех 64 пикселей (256 потоков, каждый из которых обрабатывает 16 значений). Вместо того, чтобы хранить среднее значение, она сохраняет сумму и количество действительно обработанных пикселей (не все группы обработают ровно 64×64 пикселя, потому что, например, могут выходить за края экрана). Шейдер с умом использует локальное хранилище потока для разделения суммы; каждый поток сначала работает с 16 горизонтальными значениями, а затем отдельные потоки суммируют все эти значения по вертикали, и наконец управляющий поток этой группы (поток 0) складывает результат и сохраняет их все в буфер. Этот буфер содержит 240 элементов, по сути давая нам среднюю яркость множества областей экрана. Последующая команда запускает 64 потока, которые обходят в цикле все эти значения и складывают их, чтобы получить окончательную яркость экрана. Также он возвращается обратно от логарифма к линейным единицам измерения.

У меня нет большого опыта работы с техниками выдержки, но чтение этого поста Кшиштофа Нарковича прояснило некоторые вещи. Сохранение в массив из 64 элементов необходимо для вычисления скользящего среднего, при котор

© Habrahabr.ru