Pull to refresh

Как рендерится кадр нового Doom

Reading time 15 min
Views 105K
Original author: Adrian Courrèges


Выпущенный в 1993 году первый DOOM внёс фундаментальные изменения в разработку игр и механик, он стал мировым хитом и создал новых идолов, таких как Джон Кармак и Джон Ромеро.

Сегодня, 23 года спустя, id Software принадлежит Zenimax, все основатели уже покинули компанию, но это не помешало коллективу id продемонстрировать весь свой талант, выпустив отличную игру.

Новый DOOM прекрасно дополняет франшизу. В нём используется новый движок id Tech 6; после ухода Джона Кармака его сменил на должности ведущего рендер-программиста бывший разработчик Crytek Тьяго Соуза (Tiago Sousa).

Традиционно id Software через несколько лет после создания публиковала исходный код своих движков, что часто приводило к возникновению интересных ремейков и аналитических статей. Неизвестно, продолжит ли id Tech 6 эту традицию, но нам не обязательно нужен исходный код, чтобы оценить любопытные графические техники, использованные в движке.

Как рендерится кадр


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



В отличие от большинства современных игр под Windows, в DOOM используется не Direct3D, а OpenGL и Vulkan.

Vulkan — это новая потрясающая технология, к тому же Балдур Карлссон (Baldur Karlsson) только совсем недавно добавил поддержку Vulkan в RenderDoc, так что трудно было удержаться от искушения забраться внутрь движка DOOM. Все представленные ниже наблюдения выполнены в игре, запущенной с Vulkan на GTX 980 со всеми настройками, установленными на Ultra, некоторые догадки взяты из презентации Тьяго Соузы и Джина Джефроя (Jean Geffroy) на Siggraph.

Обновление мегатекстур


Первый этап рендеринга — это обновление мегатекстур; эта технология, представленная ещё в id Tech 5, использовалась в RAGE, а теперь и в новом DOOM.

Если объяснять вкратце, то смысл этой технологии в том, что несколько огромных текстур (в DOOM они имеют размер 16k x 8k) располагаются в памяти видеопроцессора; каждая из них — это коллекция из тайлов 128x128.


Страницы 128 x 128, хранящиеся в текстуре 16k x 8k

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

Когда пиксельный шейдер выполняет считывание из «виртуальной текстуры», он просто считывает некоторые из этих физических тайлов размером 128x128.

Разумеется, в зависимости от того, куда смотрит игрок, набор этих текстур будет изменяться: на экране появляются новые модели, ссылающиеся на другие виртуальные текстуры, требуется загрузка новых тайлов и выгрузка старых…

Итак, в начале кадра DOOM обновляет несколько тайлов с помощью инструкции vkCmdCopyBufferToImage, чтобы записать актуальные текстурные данные в память графического процессора.

Подробно почитать о мегатекстурах можно здесь и здесь.

Атлас теневых карт


Для каждого источника света, отбрасывающего тень, создаётся уникальная карта глубин, сохраняемая в один тайл огромного текстурного атласа размером 8k x 8k. Однако не каждая карта глубин рассчитывается для каждого кадра: DOOM активно повторно использует результаты предыдущего кадра и пересчитывает только те карты глубин, которые требуют обновления.


Буфер глубин 8k x 8k (предыдущий кадр)


Буфер глубин 8k x 8k (текущий кадр)

Когда источник света статичен и отбрасывает тени только на статичные объекты, то разумно будет просто хранить его карту глубин «как есть» и не выполнять ненужных пересчётов. Однако если к этом свету переместится противник, то карту глубин нужно будет создать снова.

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

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

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


Рендеринг всех непрозрачных сеток уже выполнен и информация об их глубине передана в карту глубин. Сначала это оружие игрока, затем статическая геометрия, и, наконец, динамическая геометрия.


Карта глубин: готовность 20%


Карта глубин: готовность 40%


Карта глубин: готовность 60%


Карта глубин: готовность 80%


Карта глубин: готовность 100%

Но на самом деле во время предварительного прохода обработки глубин сохраняется не только информация о глубинах.

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


Карта скоростей

Для хранения скорости нужно всего 2 канала: в красном хранится горизонтальная скорость, а в зелёном — вертикальная.

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

А что же это за жёлтая область (красный и зелёный равны 1)? На самом деле это исходный цвет буфера по умолчанию, означающий, что там нет динамических сеток: это «область статичных сеток».

Почему DOOM не выполняет расчёт скорости для статичных сеток? Потому что скорость статичного пикселя легко найти из его глубины и нового состояния камеры по сравнению с последним кадром; не обязательно рассчитывать её для каждой сетки.

Карта скоростей пригодится нам позже при добавлении motion blur.

Запросы отсечения


Мы стремимся отправить для рендеринга в графическом процессоре как можно меньшее количество геометрических объектов, и лучший способ достичь этого — отсечь все сетки, которые невидимы для игрока. БОльшая часть отсечения невидимых частей в DOOM выполняется промежуточным ПО Umbra, но всё равно движок выполняет запросы отсечения к графическому процессору для дополнительной обрезки видимой области.

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

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


Проверка области. Красное — отсечённое, зелёное — видимое.

Кластерный прямой рендеринг непрозрачных объектов


Теперь рендерятся вся непрозрачная геометрия и декали. Информация об освещении хранится в HDR-буфере с плавающей запятой:


25% освещения


50% освещения


75% освещения


100% освещения

Функции проверки глубины устанавливается значение EQUAL, чтобы избежать лишних вычислений: благодаря предыдущему предварительному проходу обработки глубин мы точно знаем, какое значение глубины должен иметь каждый пиксель. Декали также наносятся прямо при рендеринге сеток, они хранятся в текстурном атласе.

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

Скажу пару слов об этом проходе: в нём используется кластерный прямой рендерер, основанный на работах Эмиля Персона (Emil Person) и Олы Олссона (Ola Olsson).

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

Как же работает кластерный рендерер? Сначала окно просмотра разделяется на тайлы: в DOOM создаётся 16 x 8 областей. Некоторые рендереры останавливаются на этом и вычисляют список источников освещения на тайл, который позволяет снизить объёмы расчётов освещения, однако всё равно имеют определённые проблемы с пограничными случаями.

Кластерный рендеринг развивает эту концепцию глубже, переходя из 2D в 3D: не останавливаясь на разделении двухмерного окна просмотра, он выполняет разбивку в 3D всей пирамиды видимости камеры, создавая срезы вдоль оси Z.

Каждый «блок» называется «кластером», можно также назвать их "вокселями в форме пирамиды видимости".

Ниже представлено простое разделение окна просмотра размером 4 x 2; 5 срезов глубины разделяют пирамиду видимости на 40 кластеров.



В DOOM пирамида видимости камеры разделена на 3072 кластера (размером 16 x 8 x 24), срезы глубины логарифмически распложены вдоль оси Z.

В случае кластерного рендерера типичный алгоритм будет таким:

  • Сначала ЦП вычисляет список элементов, влияющих на освещение в каждом кластере: источники освещения, декали и кубические текстуры…

    Для этого все эти элементы «вокселизируются», так, чтобы их область воздействия могла быть проверена на пересечение с кластерами. Данные хранятся в индексированных списках в буфере графического процессора, чтобы шейдеры могли иметь к ним доступ. Каждый кластер может содержать до 256 источников света, 256 декалей и 256 кубических текстур.

  • Затем графический процессор рендерит пиксель:

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

Применение источников освещения и декалей
Вот как пиксельный шейдер может получить список источников освещения и декалей на этом проходе:












Также существует список зондов (не показан на схемах выше), доступ к которому получается точно таким же образом; однако он не используется на этом проходе и мы вернёмся к нему позже.

Издержки влияния на ЦП предварительного создания списка элементов для кластеров окупаются значительным снижением сложности расчётов рендеринга в графическом процессоре ниже по конвейеру.

На кластерный прямой рендеринг стали в последнее обращать внимание: он имеет хорошее способность обрабатывать большее количество источников света, чем простой прямой рендеринг; к тому же он работает быстрее, чем отложенное затенение, которое должно выполнять запись и считывание из нескольких G-буферов.

Однако я не упомянул кое-что: на этом проходе не просто передаётся одна операция записи в буфер освещения; при его выполнении также с помощью MRT создаются два тонких G-буфера:


Карта нормалей


Карта отражения

Карта нормалей хранится в формате R16G16 с плавающей запятой, карта отражения — в R8G8B8A8, альфа-канал содержит коэффициент сглаживания. Так что в DOOM продуманно используется комбинация прямого и отложенного рендеринга с гибридным подходом. Эти новые G-буферы станут полезными при добавлении дополнительных эффектов, таких как отражения.

Я пропустил ещё одно: в то же время создаётся feedback-буфер размером 160 x 120 для системы мегатекстур. Он содержит информацию для потоковой системы, сообщающую о текстурах и их мип-текстурировании, которые необходимо передавать дальше.

Движок мегатекстур работает по принципу обратной связи: если после прохода рендеринга сообщается об отсутствии каких-то текстур, то движок загружает их.

Частицы в графическом процессоре


Затем запускается compute shader для обновления симуляции частиц: положения, скорости и срока жизни.

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

Преграждение окружающего света в экранном пространстве (SSAO)


На этом этапе генерируется карта SSAO.

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

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

Первый результат получается довольно шумным.


Карта SSAO

Отражения в экранном пространстве


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

Глубины

Нормали

Отражение

Предыдущий кадр

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

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

Но у неё возникают собственные артефакты, так как она работает только в экранном пространстве и ей не хватает «глобальной» информации. Например, вы можете видеть красивые отражения в сцене, но когда вы начинаете опускать взгляд, количество отражения уменьшается, и глядя на свои ноги, вы не увидите почти никаких отражений. Мне кажется, что SSR хорошо интегрированы в DOOM, они улучшают качество изображения, но при этом они довольно неброские, и вы не заметите их исчезновения, если не следите за ними специально.

Отражения статичных кубических текстур


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

Эта техника основана на использовании сгенерированных кубических текстур 128 x 128, представляющих собой информацию об освещении среды в различных местах карты (они также называются «зонды среды»). Точно так же, как и источники освещения с декалями на этапе кластеризации пирамиды видимости, зонды тоже индексируются для каждого кластера.

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


Карта статичных отражений

Смешивание карт


На этом этапе compute shader комбинирует все сгенерированные ранее карты. Он считывает карты глубин и отражений и смешивает освещение прямого прохода:

  • с информацией SSAO
  • с SSR для рассматриваемого пикселя, когда она становится доступной
  • если информация SSR отсутствует, в качестве замены используются данные карты статичных отражений
  • также рассчитывается эффект тумана


Смешивание и туман: до


Смешивание и туман: после


Fog — туман, Probe Reflection — отражение зондов

Освещение частиц


В нашей сцене есть частицы дыма и освещение рассчитывается для каждого спрайта. Каждый спрайт рендерится так, как будто он находится в пространстве мира: из его положения мы получаем список источников освещения и соответствующие им теневые карты, после чего рассчитывается освещение четырёхугольника (частицы). Затем результат сохраняется в тайл атласа размером 4k; тайлы могут быть различного размера, зависящего от расстояния от частицы до камеры, настроек качества и т.д. Атлас имеет выделенные области для спрайтов одного разрешения, вот как выглядят спрайты 64 x 64:


Атлас освещения частиц

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

Здесь DOOM отделяет вычисление освещения частиц от основного рендеринга игры: при каком бы разрешении вы ни играли (720p, 1080p, 4k…), освещение частиц всегда вычисляется и хранится в таких небольших тайлах фиксированного размера.

Уменьшение масштаба и размытие


Сцена несколько раз уменьшается в размерах до 40 пикселей. Самые маленькие уровни масштабирования размываются с помощью отдельных вертикальных и горизонтальных проходов (создаётся «цепочка размытия»).



Зачем же так рано выполнять размытие? Этот процесс обычно происходит в конце, во время постпроцессинга для создания эффекта bloom в ярких областях.

Но все эти различные уровни размытия пригодятся нам в следующем проходе для рендеринга преломлений в стёклах.

Прозрачные объекты


Все прозрачные объекты (стёкла, частицы) рендерятся поверх сцены:


Прозрачные объекты: до


Прозрачные объекты: после

Стёкла очень красиво рендерятся в DOOM, особенно стёкла, покрытые изморозью или грязью: декали влияют только на часть стекла, чтобы сделать преломления более или менее размытыми. Пиксельный шейдер вычисляет коэффициент «размытости» преломления и выбирает из комплекта «цепочки размытия» две карты, наиболее близкие к коэффициенту размытости. Он считывает эти две карты и линейно интерполирует два значения, чтобы аппроксимировать конечный цвет размытия преломления. Благодаря этому процессу стёкла могут создавать попиксельно красивые преломления на различных уровнях размытия.

Карта искажений



Карта искажений

Очень горячие области могут создавать на изображении тепловые деформации. В нашей сцене немного искажает изображение гнездо крови.

Искажения рендерятся относительно буфера глубин для создания карты искажений низкого разрешения.

Красный и зелёный каналы представляют собой значение искажений по горизонтальной и вертикальной осям. Синий канал содержит количество применяемого размытия.

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

В этой сцене искажения совсем небольшие и практически незаметны.

Пользовательский интерфейс




UI рендерится в другой буфер рендеринга в режиме заранее умноженного на альфа-канал (premultiplied alpha mode), хранящегося в формате LDR.

Преимущество хранения всего UI в отдельном буфере вместо отрисовки непосредственно поверх завершённого кадра в том, что игра может применить фильтрацию/постпроцессинг, например, хроматическую аберрацию или оптическое искажение для всех виджетов UI за один проход.

Рендеринг не использует каких-то техник батчинга и просто отрисовывает один за одним элементы интерфейса, примерно за 120 вызовов отрисовки.

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

Временной анти-алиасинг (TAA) и motion blur


TAA и motion blur применяются с использованием карты скоростей и результатов рендеринга предыдущего кадра.

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


TAA и motion blur: до


TAA и motion blur: после

Результат получается очень хорошим: не только сетка становится сглаженной, но и искажения отражений (при которых в кадре могут возникать отдельные яркие пиксели) также уменьшаются. Качество гораздо лучше того, которое можно было достичь с помощью метода постпроцессинга FXAA.

Яркость сцены


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

Буфер HDR-освещения циклично уменьшается в два раза от своего разрешения, пока не становится текстурой 2 x 2, при каждой итерации рассчитывается значение цвета пикселя как среднее от яркости четырёх его «родительских» пикселей из карты большего размера.

Bloom



Bloom

Применяется фильтр яркости для приглушения самых тёмных областей изображения. Результат использования фильтра яркости затем циклично уменьшается и размывается способом, похожим на описанный выше.

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

Затем размытые слои соединяются для создания эффекта bloom, который является HDR-текстурой в четыре раза меньше исходного разрешения.

Окончательный постпроцессинг


Весь этот этап выполняется в одном пиксельном шейдере:

  • применяется тепловая деформация с использованием данных карты искажений
  • текстура bloom добавляется поверх буфера HDR-освещения
  • применяются такие эффекты, как виньетирование, грязь/блики
  • вычисляется средняя яркость при помощи сэмплирования карты яркости 2x2, а также дополнительных параметров выдержки, применяются тональная компрессия и грейдинг.


Тональная компрессия: до


Тональная компрессия: после

Тональная компрессия берёт буфер HDR-освещения, содержащий цвета, изменяющиеся в широком диапазоне яркости, и преобразует его в цвет с 8 битами на компонент (LDR), чтобы кадр можно было отобразить на мониторе.

Оператор кинематографической тональной компрессии основан на уравнении (x(Ax+BC)+DE) / (x(Ax+B)+DF) - (E/F), это тональная компрессия Uncharted 2, также применённая в GTA V.

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

UI и зернистость плёнки


И наконец UI располагается поверх кадра игры; в то же время добавляется небольшой эффект зернистости плёнки.


UI и зернистость: до


UI и зернистость: после

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

DOOM удаётся создать высококачественную картинку при высокой скорости игры, потому что он с умом использует старые данные, рассчитанные в предыдущих кадрах. Всего у нас получились 1331 вызов отрисовки, 132 текстуры и 50 буферов рендеринга.

Бонусная информация


Подробнее о стёклах


Результат рендеринга стекла очень хорош; при этом он достигнут довольно простыми способами, которые мы рассматривали выше:

  • подготовка нескольких уровней размытия рендеринга непрозрачных сеток
  • отрисовка просвечивающих элементов в порядке «сзади вперёд» в прямом режиме с применением декалей/освещения/отражения зондов с помощью предыдущей цепочки для различных значений размытия преломления стекла; при этом каждый пиксель может получить собственное значение преломления.


Стекло: до


Стекло: после

Глубина резкости


На изученном нами кадре не видно глубины резкости, так что давайте рассмотрим следующую сцену до и после её применения:


Глубина резкости: до


Глубина резкости: после

Не все игры правильно применяют глубину резкости: наивный подход часто заключается в использовании гауссова размытия и выполнении размывания за один проход в зависимости от глубины пикселя. Такой подход прост и экономен, но имеет некоторые проблемы:

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

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

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

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

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

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


Большая глубина резкости


Большая глубина резкости и размытие 1


Большая глубина резкости и размытия 1 и 2

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

Дополнительные источники


Если вы хотите глубже погрузиться в технологию idTech 6, то к счастью на эту тему есть множество лекций и публикаций:

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+154
Comments 94
Comments Comments 94

Articles