Анализ графики Europa Universalis 5: недостатки контурных карт

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

Для анализа использовалась версия 1.0.0 с основным Vulkan-рендером. Съёмка GPU-профиля выполнена через Nvidia Nsight при разрешении 2560×1440.

Основы рендеринга

Напомню пять ключевых этапов отрисовки непрозрачных объектов: привязка меша, вершинный шейдер, сборка примитивов и растеризатор, тест глубины/трафарета и пиксельный шейдер. Без LOD производительность вершинного шейдера и сборки примитивов неизменна независимо от видимой площади объекта.

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

Затем выполняется тест глубины. Если фрагмент ближе камеры, пиксельный шейдер запускается для блока 2×2 пикселей, так как для выбора уровня детализации текстур используются операции dFdx и dFdy, вычисляющие производные по соседним фрагментам.

  1. Нельзя задать конкретных соседей для операций dFdx/dFdy: шейдер сравнивает произвольные точки внутри блока 2×2 (обычно верхний левый с остальными).

  2. При треугольниках размером в один пиксель формулы dFdx/dFdy даёт неточное значение, что ухудшает качество текстур, несмотря на рост числа полигонов.

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

Детализированная 3D-карта

Анализ графики Europa Universalis 5: недостатки контурных карт
Рендер сцены для анализа

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

Тень от забора
Мелкий забор участвует в шэдоукастах
Рендер террейна
Отрисовка ландшафта

Далее рендерится террейн. Учитывая объём шейдера, следовало бы сначала рисовать ближайшие объекты (front-to-back), а потом ландшафт. Вместо этого террейн отрисовывается глобально, а остальные вызовы распределены без оптимизации.

Paradox отказалась от разделения мира на регионы, применяя «убершейдер» для всего: в нём задействованы 145 текстурных слоёв террейна BC3 по 512×512. Многие из них имеют низкую детализацию.

Градиенты земли
Слабая детализация текстур террейна

Константные буферы содержат 69 индексов материалов и данные для 199 биомов — до 15 материалов на биом. Затем идут глобальные текстуры: ProvinceColorIndirectionTexture_Texture (R8G8_UNORM, 16384×8192, 256 MB VRAM) и атлас высот VirtualHeightmapPhysicalTexture (R16_UNORM, 8192×8192, 128 MB, заполнено лишь наполовину). В сумме все «глобальные» текстуры занимают 838,3 MB VRAM. Шейдер — 4253 строки SPIR-V или 1282 GLSL.

ProvinceColorIndirectionTexture_Texture
Текстура индексации провинций
VirtualHeightmapPhysicalTexture
Атлас высот (много пустого пространства)

Речные меши насчитывают 2–4 тыс. вершин, особенно много лишней геометрии на прямых участках.

Рендер рек
Слишком детализированные реки
Избыточная геометрия
Пример лишних полигонов на реке

Система LOD не работает должным образом: вызовы для детализированных и упрощённых домов следуют без порядка. Даже «низкий» LOD содержит больше вершин, чем число пикселей, которые он занимает.

Lod0
Дом с LOD0
Lod1
Дом с LOD1
Шум от избыточной геометрии
Избыточные полигоны создают визуальный шум

Крупнейшая нагрузка приходится на деревья с фрагментным шейдером на 2578 SPIR-V строк (779 GLSL) и встроенной логикой «тумана войны». Вместо отдельного прохода каждый объект рассчитывает туман индивидуально.

Меш деревьев
Тяжёлый шейдер для деревьев

Первый vkCmdDrawIndexed рендерит 1979 инстансов, но это лишь часть леса. Всего задействовано 11 draw-вызовов, по 2–4 тыс. мешей в каждом.

Рендер леса

Деревья стоят плотно, без предкастинга глубины, что приводит к катастрофическому overdraw: GPU многократно обрабатывает одни и те же пиксели. Из-за тяжёлого шейдера лесная зона снижает fps примерно вдвое по сравнению с пустыней.

Для дизеринга используется статичный Bayer4×4, который не меняется между кадрами и усиливает паттерн при использовании DLSS в режиме «производительность».

Bayer4x4
Паттерн Bayer4×4
Сравнение дизеров
Сравнение Bayer4×4, Rdither и IGN (код на Shadertoy, подробности здесь)

После леса рисуются животные: олени, лоси, коровы, овцы, волки и даже бобры.

Детализированный бобр
Высокая детализация бобра
Скрытый бобр
Бобра не сразу видно, но он здесь

Даже если фрагментный шейдер пропускается тестом глубины, вершинный шейдер работает для всех вершин: читает две высотные карты, текстуру шума и туман войны. У бобра 256 строк GLSL или 805 SPIR-V, плюс анимация.

Спойлер
Бобр 11×4 пикселя
Бобр занимает всего 11×4 пикселя

Использовано 485 вершин — более 10 вершин на пиксель. Напоминает случай с Cities: Skylines 2, где у персонажей были прорисованы зубы.

Глобальная карта

Глобальная карта работает заметно быстрее, поэтому её оптимизация менее критична.

Сцена глобальной карты
Рендер глобальной карты
Рендер политической карты

Первый проход — лёгкий шейдер (447 GLSL, 1625 SPIR-V) и один квад на весь фон, что эффективно при большом покрытии пикселей.

Квадр для фона

Затем выполняются сотни вызовов для текста (по одному на название страны), после чего границы рисуются мешами — по одному мешу на страну.

До границ Валахии
Перед рендером меша границ Валахии (шейдер уже отметил контуры)
После границ Валахии
После рендеринга меша границ

Контуры Валахии содержат 4621 треугольник. В wireframe видно лишние полигоны, сделанные с запасом для масштабирования.

Контур Валахии в wireframe

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

Пересечения треугольников
Маленькое государство
Ещё одно крошечное государство

Граница — зона между двумя государствами, поэтому пиксели рисуются дважды и более.

Двойной draw call
Два vkCmdDrawIndexed — две отрисовки одной границы

Шейдер карты рисует границы через SDF-текстуру BorderDistanceFieldTexture_Texture (R8_UNORM, 4096×2048, 8 MB). Такая карта хорошо подходит для чётких линий, но её разрешения недостаточно для Империи Римской.

BorderDistanceFieldTexture_Texture
Поле расстояний для границ
Недостаточное разрешение SDF

Стоит разделить мир на регионы (например, 8) и дать каждому собственную SDF-текстуру. Хотя вершинные буферы занимают ~76,84 MB и индексы ~4,8 MB, это снизит нагрузку на растеризатор и сборку примитивов — узкое место GPU.

Заключение

Paradox упростила логику рендеринга ради ускорения разработки: один «убершейдер» вместо набора специализированных, отсутствие сортировки front-to-back, неэффективный overdraw, примитивная обработка линий, недоработанный LOD и неограниченные шэдоукастеры.

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

Плюсы — лёгкий режим политической карты и плавные переходы между видами. Ключевые улучшения: специализированные шейдеры, front-to-back сортировка, исправление LOD и жёсткая фильтрация мелких шэдоукастеров.

Если ваше железо слабое, избегайте детализированной карты и особенно лесных зон.

 

Источник

Читайте также