Рендеринг в 3D-играх: введение
Вы играете в свежую Call of Mario: Deathduty Battleyard на своём совершенном игровом ПК. Смотрите на прекрасный сверширокий 4K-монитор, любуясь великолепными пейзажами и замысловатыми деталями. Вас когда-нибудь интересовало, как графика попадает на экран? Задумывались, как игра заставляет компьютер всё это показать вам?
Добро пожаловать в наш тур по рендерингу в 3D-играх: путешествие для начинающих, из которого вы узнаете, как создаётся один базовый кадр на экране.
Каждый год выходят сотни новых игр для смартфонов, приставок и ПК. Разнообразие форматов и жанров очень велико, но один из них, пожалуй, освоен лучше всего — это 3D-игры. Какая игра была первой — вопрос дискуссионный, и быстрый просмотр базы Книги рекордов Гиннесса дал несколько результатов. Можно считать первой игрой Knight Lore компании Ultimate, вышедшую в 1984-м, но строго говоря, изображения в этой игре были двумерными — никакая информация не использовалась в полноценных трёх измерениях.
Так что если мы действительно хотим разобраться, как современные 3D-игры формируют изображение, то придётся начать с другого примера: Winning Run компании Namco, вышедшей в 1988-м. Пожалуй, это была первая полностью трёхмерная игра, использующая технологии, которые не слишком сильно отличаются от современных. Конечно, любые игры, возраст которых перевалил за 30 лет, совсем не то же самое, что и, скажем, F1 компании Codemasters, вышедшая в 2018-м. Но принципиальные схемы похожи.
В этой статье мы рассмотрим процесс генерирования 3D-игрой базового изображения для монитора или телевизора. Начнём с финального результата и спросим себя: «На что я смотрю?»
Затем мы проанализируем каждый этап формирования картинки, которую мы видим. По ходу действия рассмотрим такие понятия, как вершины и пиксели, текстуры и проходы, буферы и шейдинг, ПО и инструкции. Узнаем, как участвует в процессе видеокарта и для чего она вообще нужна. После этого вы сможете взглянуть на свои игры и ПК в новом свете и начнёте больше ценить видеографику.
Параметры кадры: пиксели и цвета
Запустим 3D-игру, в качестве образца возьмём выпущенную в 2007-м Crysis компании Crytek. Ниже — фотография дисплея, на котором отображается игра:
Такую картинку обычно называют кадром. Но на что именно мы смотрим? Воспользуемся макрообъективом:
К сожалению, блики и внешняя подсветка монитора портят фото, но если его немного улучшить мы получим
Мы видим, что кадр в мониторе состоит из сетки отдельных раскрашенных элементов, а если увеличим их ещё больше, то заметим, что каждый элемент представляет собой блок из трёх кусочков. Такой блок называется пикселем (pixel, сокращение от picture element). В большинстве мониторов пиксели раскрашиваются с помощью трёх цветов: красного, зелёного и синего (RGB, red-green-blue). Чтобы отобразить на дисплее новый кадр, нужно обработать список из тысяч, если не миллионов, RGB-значений и сохранить их во фрагменте памяти, к которой у монитора есть доступ. Такие фрагменты памяти называют буферами, то есть монитор получает содержимое буфера кадров.
Это финальная точка всего процесса, так что теперь будем двигаться в обратном направлении к его началу. Процесс часто описывают термином рендеринг (отрисовка), но на самом деле это последовательность связанных друг с другом отдельных шагов, которые по своей сути сильно различаются. Представьте себе, что вы шеф-повар в ресторане, удостоенном звезды Мишлен: конечный результат — это тарелка со вкусной едой, но сколько всего нужно сделать, чтобы этого добиться. Как и в приготовлении пищи, для рендеринга нужны основные ингредиенты.
Необходимые строительные элементы: модели и текстуры
Основными кирпичиками любой 3D-игры являются визуальные ресурсы, заполняющие мир, который будет отрисован. Фильмам, ТВ-шоу и театрам нужны актёры, костюмы, бутафория, декорации, освещение — список довольно велик. То же самое и с 3D-играми. Всё, что вы видите в сгенерированном кадре, было разработано художниками и специалистами по моделированию. Чтобы было понятнее, обратимся к старой школе и взглянем на модель из игры Quake II компании id Software:
Игра вышла больше 20 лет назад. В то время Quake II был технологическим шедевром, хотя, как и в любой игре тех лет, модели тут выглядят весьма примитивно. Зато легко продемонстрировать, из чего они состоят.
На предыдущей картинке мы видим угловатого чувака, состоящего из соединённых друг с другом треугольников. Каждый угол называется вершиной, или вертексом (vertex). Каждый вертекс действует как точка в пространстве и описывается как минимум тремя числами: координатами x, y, z. Однако 3D-игре этого мало, поэтому у каждого вертекса есть дополнительные значения: цвет, направление лицевой стороны (да, у точки не может быть лицевой стороны… просто читайте дальше!), яркость, степень прозрачности и т. д.
У вертексов всегда есть набор значений, связанный с текстурными картами. Это картинки «одежды», которую носит модель. Но поскольку изображение плоское, карта должна содержать вид с любого направления, откуда мы можем посмотреть на модель. Примере с Quake II иллюстрирует простой подход: изображения спереди, сзади и с боков (руки). А современные 3D-игры уже оперируют для моделей многочисленными текстурными картами, каждая из которых содержит множество деталей, без каких-либо пустых мест между ними. Некоторые карты не выглядят как материалы или свойства, вместо этого они предоставляют информацию о том, как свет отражается от поверхности. У каждого вертекса есть набор координат в связанной с моделью текстурной карте, так что она может быть «натянута» на вертекс. Это означает, что при перемещении вертекса текстура переместится вместе с ним.
В отрисованном трёхмерном мире всё, что вы видите, начинается с набора вершин и текстурных карт. Они загружаются в связанные друг с другом буферы памяти. Вертексный буфер (vertex buffer) содержит текстуры и выделенные для последующей отрисовки порции памяти. Буфер команд (command buffer) содержит список инструкций, что нужно делать с этими ресурсами.
Всё это формирует необходимый фреймворк, который будет использоваться для создания финальной сетки раскрашенных пикселей. В некоторых играх это огромный объём данных, потому что слишком долго пересоздавать буферы для каждого нового кадра. Также игры хранят в буферах информацию, необходимую для формирования целого мира, который игрок может увидеть, или достаточную большую его часть, обновляя по мере необходимости. Например, в гоночной игре наподобие F1 2018 всё будет храниться в одной большой коллекции буферов. А в игре с открытым миром, наподобие Skyrim, данные будут подгружаться в буферы и удаляться из них по мере перемещения камеры по миру.
Настройка сцены: вертексы
Имея всю визуальную информацию, игра затем начнёт процесс визуального отображения. Сцена начинается в некой позиции по умолчанию, с базовым расположением моделей, источников света и т.д. Это будет «нулевой» экран — исходная точка для графики. Он часто не отображается, просто обрабатывается системой. Чтобы проиллюстрировать, что происходит на первом этапе рендеринга, мы воспользуемся онлайн-инструментом Real-Time Rendering. Создадим самую простую «игру»: параллелепипед, стоящий на земле.
Этот объект содержит 8 вершин, каждая из которых описывается списком чисел. Вершины формируют модель, состоящую из 12 треугольников. Каждый треугольник, и даже сам объект называется примитивом. По мере движения, вращения и масштабирования примитивов числа проходят через цепочки математических операций и обновляются.
Обратите внимание, что числа model point не изменяются. Эти числа показывают, где именно расположена модель в виртуальном мире. Рассмотрение соответствующих математических расчётов выходит за рамки статьи, скажем лишь, что сначала все объекты помещаются туда, где они должны быть. А затем начинается окрашивание.
Возьмём другую модель, имеющую в 10 раз больше вершин, чем предыдущий параллелепипед. В ходе самого простого процесса окрашивания берётся цвет каждого вертекса, а затем вычисляются изменения цвета от поверхности к поверхности. Это называется интерполяцией.
Увеличение количества вершин в модели не только позволяет создавать более реалистичные объекты, но и улучшает результат цветовой интерполяции.
На этом этапе рендеринга может подробно вычисляться влияние источников света в сцене. Например, как материалы модели отражают цвет. Такие вычисления должны учитывать положение и направление камеры, а также положение и направление источников света.
Для этого есть целый ряд разных математических методик, одни простенькие, другие очень сложные. На иллюстрации выше мы видим, что объект справа выглядит гораздо приятнее и реалистичнее, но для его отрисовки требуется больше работы.
Важно отметить, что сейчас объекты с небольшим количеством вершин мы сравниваем с самыми современными играми. Пролистайте вверх и внимательно рассмотрите изображение из Crysis: в одной этой сцене отображается больше миллиона треугольников. На примере бенчмарка Unigine Valley можно понять, сколько треугольников используется в современных играх.
Каждый объект на этом изображении состоит из соединённых друг с другом вершин, который образуют состоящие из треугольников примитивы. Бенчмарк можно запускать в каркасном (wireframe) режиме, в котором рёбра каждого треугольника обозначаются белыми линиями.
Как видите, любой объект состоит из треугольников, и для каждого треугольника вычисляется расположение, направление и цвет. При этом учитывается расположение источников света, а также расположение и направление камеры. Все изменения, связанные с вершинами, нужно передать игре, чтобы она имела всю необходимую информацию для отрисовки следующего кадра — это делается с помощью обновления вертексного буфера.
Удивительно, но это не самая сложная часть рендеринга, с подходящим оборудованием все вычисления выполняются за несколько тысячных долей секунды! Идём дальше.
Теряем измерение: растеризация
После обработки всех вершин и завершения размещения всех объектов в нашей трёхмерной сцене, процесс рендеринга переходит к очень важному этапу. До этого момента игра была истинно трёхмерной, но финальный кадр таковым уже не является: в ходе ряда изменений просматриваемый мир преобразуется из 3D-пространства, состоящего из тысяч связанных точек, в двумерное изображение, состоящее из раскрашенных пикселей. В большинстве игр эта процедура состоит минимум из двух этапов: проецирования экранного пространства (screen space projection) и растеризации.
Вернёмся к нашему веб-инструменту для рендеринга, он покажет нам, как объём виртуального мира превращается в плоское изображение. Слева изображена камера, исходящие из неё линии создают усечённую пирамиду видимости (frustum), и всё, что в неё попадает, может быть отображено в финальном кадре. Перпендикулярное сечение пирамиды называется областью просмотра (viewport) — это то, что будет показано на мониторе. Для проецирования всего содержимого пирамиды в область просмотра с учётом перспективы камеры используются многочисленные математические вычисления.
Хотя графика в области просмотра двумерная, данные всё ещё истинно трёхмерны, и позднее эта информация будет использована для вычисления, какие примитивы нам видны, а какие скрыты. Это может быть на удивление сложно сделать, потому что примитивы могут отбрасывать видимые нам тени, даже если сами примитивы скрыты от нас. Удаление скрытых от нас примитивов называется отбрасыванием (culling). Эта операция может существенно повлиять на скорость отрисовки всего кадра. После завершения сортировки на видимые и скрытые примитивы, а также удаления треугольников вне пределов пирамиды видимости, завершается последний этап трёхмерности и с помощью растеризации кадр делается полностью двумерным.
На иллюстрации выше показан очень простой пример кадра, содержащего один примитив. Пиксельная сетка накладывается на геометрическую форму и соответствующие пиксели помечаются для последующей обработки. Конечный результат выглядит не слишком похожим на исходный треугольник, потому что мы используем недостаточно пикселей. В связи с этим возникает проблема алиасинга (aliasing, ступенчатость линий), которая решается различными способами. Поэтому изменение в игре разрешения (общего количества пикселей в кадре) так сильно влияет на конечный результат: больше пикселей не только улучшает отображение форм, но и уменьшает влияние нежелательного алиасинга.
После завершения этой части рендеринга мы переходим к следующему большому этапу: финальному раскрашиванию всех пикселей в кадре.
Несите свет: пиксельный этап
Мы подошли к самому сложному этапу рендеринга. Когда-то он сводился к натягиванию на модели одежды (текстур) с использование информации о пикселях (изначально полученной от вершин). Однако дело в том, что хотя текстуры и сам кадр двумерны, однако виртуальный мир на стадии обработки вертексов был искажён, сдвинут и изменён. Для учёта всего этого применяются дополнительные математические вычисления, однако результату могут быть свойственны новые проблемы.
На этой иллюстрации к плоскости применена текстура шахматной доски. Возникает неприятная визуальная рябь, которая усугубляется алиасингом. Для решения этой проблемы применяют уменьшенные версии текстурных карт (множественные отображения, mipmaps), многократное использование информации из этих текстур (фильтрация, filtering) и дополнительные математические вычисления. Эффект заметен:
Для любой игры это было действительно сложным этапом, но сегодня это уже не так, потому что из-за широкого использования других визуальных эффектов, — таких как отражения и тени, — обработка текстур превратилась в относительно небольшой этап процесса рендеринга. При игре на высоких разрешениях нагрузка на этапах растеризации и обработки пикселей возрастает, но на обработку вертексов это влияет относительно мало. Хотя первичное раскрашивание из-за источников света выполняется на вертексном этапе, однако могут применяться и более изощрённые световые эффекты.
На предыдущей иллюстрации мы уже не видим изменений цвета между разными треугольниками, что даёт нам ощущение гладкого бесшовного объекта. Хотя в этом примере сфера состоит из того же количества треугольников, что и зелёная сфера на иллюстрации выше, однако в результате процедуры раскрашивания пикселей нам кажется, что используется гораздо больше треугольников.
Во многих играх пиксельный этап приходится прогонять несколько раз. Например, чтобы зеркало или поверхность воды отражали окружающий мир, это мир нужно сначала отрисовать. Каждый прогон называется проходом (pass), и для получения финального изображения для каждого кадра легко может использоваться четыре и больше проходов.
Также иногда нужно снова прогонять вертексный этап, чтобы перерисовать мир с другой точки и использовать это изображение в сцене, которая показывается игроку. Для этого применяют однобуферную прорисовку (render targets) — используют буферы, которые действуют как финальное хранилище для кадра, но могут выступать и в роли текстур при другом проходе.
Чтобы оценить сложность пиксельного этапа, можете почитать анализ кадра в Doom 2016. Вы будете потрясены количеством операций, которые необходимы для создания одного кадра.
Всю проделанную работу по созданию кадра нужно сохранить в буфер, будь то финальный или промежуточный результат. В целом игра использует на лету минимум два буфера для финального отображения: один для «текущей работы», а второй буфер либо ожидает обращения к нему монитора, либо находится в процессе отображения. Всегда нужен буфер экрана, в котором будут сохраняться результат рендеринга, и когда все буферы заполняются необходимо двигаться дальше и создавать новый буфер. По завершении работы с кадром даётся простая команда, финальные кадровые буферы меняются местами, монитор получает последний отрисованный кадр и запускается процесс рендеринга следующего кадра.
В этом кадре из Assassin's Creed Odyssey мы видим содержимое завершённого кадрового буфера. Это содержимое можно представить в виде таблицы, которая содержит только числа. Они в виде электрических сигналов отправляются в монитор или телевизор, и пиксели экрана меняют свои значения. Наши глаза видят плоское сплошное изображение, однако наш мозг интерпретирует его как трёхмерное. За кулисами всего лишь одного кадра в игре скрывается так много работы, что стоит взглянуть, как же программисты с этим справляются.
Управление процессом: API и инструкции
Придумать, как заставить игру выполнять и управлять всеми вычислениями, вершинами, текстурами, освещением, буферами и т.д. — это огромная задача. К счастью, нам помогают в этом программные интерфейсы (application programming interface, API).
API для рендеринга уменьшают общую сложность, предлагая структуры, правила и программные библиотеки, позволяющие использовать упрощённые инструкции, которые не зависят от оборудования. Возьмите любую 3D-игру, выпущенную для ПК в течение последних трёх лет: она создана с использованием одного из трёх популярных API — Direct3D, OpenGL или Vulkan. Есть и другие подобные разработки, особенно в мобильном сегменте, но в этой статье мы будем говорить об упомянутых трёх.
Несмотря на различия в наименованиях инструкций и операций (например, блок кода для обработки пикселей в DirectX называется пиксельным шейдером (pixel shader), а в Vulkan — фрагментным шейдером (fragment shader)), конечный результат не отличается, точнее, не должен отличаться.
Разница будет проявляться в том, какое оборудование используется для рендеринга. Инструкции, генерируемые API, нужно преобразовать в понятные для оборудования команды, которые обрабатываются драйверами устройства. И производителям оборудования приходится тратить много ресурсов и времени на то, чтобы их драйверы выполняли это преобразование как можно быстрее и корректнее.
К примеру, ранняя бета-версия игры The Talos Principle (2014) поддерживала все три упомянутых API. Чтобы продемонстрировать, как могут отличаться результаты работы разных комбинаций драйверов и интерфейсов, мы прогнали стандартный встроенный бенчмарк, выставив разрешение в 1080р и максимальные настройки качества. Процессор Intel Core i7-9700K работал без разгона, видеокарта Nvidia Titan X (Pascal), оперативная память — 32 Гб DDR4 RAM.
- DirectX 9 = в среднем 188,4 кадров/с.
- DirectX 11 = в среднем 202,3 кадров/с.
- OpenGL = в среднем 87,9 кадров/с.
- Vulkan = в среднем 189,4 кадров/с.
Мы не будем анализировать результаты, и они точно не говорят о том, что какой-то API «лучше» другого (не забывайте, тестировалась бета-версия игры). Скажем лишь, что программирование под разные API связано с различными сложностями, и в любой момент времени производительность будет тоже разной. В целом, разработчики игр выбирают тот API, с которым у них больше опыта работы, и оптимизируют под него свой код. Иногда для описания кода, отвечающего за рендеринг, используют термин движок, но строго говоря, движок — это полный набор инструментов, обрабатывающий все аспекты игры, а не только графику.
Не так-то просто создать с нуля программу, которая рендерит 3D-игру. Поэтому сегодня в очень многих играх применяются лицензированные сторонние системы (например, Unreal Engine). Чтобы оценить их сложность, откройте open source-движок для Quake и просмотрите файл gl_draw.c file: он содержит инструкции для разных операций рендеринга и отражает лишь малую часть всего движка. А ведь Quake вышел больше 20 лет назад, и вся игра (включая все визуальные ресурсы, звуки, музыку и т.д.) занимает 55 Мб. Для сравнения, в Far Cry 5 одни только шейдеры занимают 62 Мб.
Время важнее всего: использование правильного оборудования
Всё вышеописанное может вычислить и обработать процессор любой компьютерной системы. Современные процессоры семейства x86-64 поддерживают все необходимые математические операции и даже содержат для этого отдельные подсистемы. Однако задача отрисовки одного кадра требует выполнения многочисленных повторяющихся вычислений и значительного распараллеливания работы. Центральные процессоры для этого не приспособлены, потому что они создаются для решения как можно более широкого круга задач. Специализированные процессоры для графических вычислений называются GPU (graphics processing units). Они создаются для того, чтобы DirectX, OpenGL и Vulkan могли много и быстро параллельно выполнять математические операции.
Воспользуемся бенчмарком, который позволяет рендерить кадр с помощью центрального процессора или специализированного оборудования — V-ray NEXT компании Chaos Group. На самом деле, он выполняет трассировку лучей, а не рендеринг, но большинство числовых операций здесь так же зависят от оборудования.
Прогоним бенчмарк в трёх режимах: только центральный процессор, только графический процессор, и комбинация обоих процессоров:
- Только центральный процессор = 53 млн. лучей
- Только графический процессор = 251 млн. лучей
- Комбинация обоих процессоров = 299 млн. лучей
Единицу измерения можно игнорировать, суть в пятикратной разнице. Но всё-таки этот тест не слишком связан с играми, так что давайте обратимся к олдскульному бенчмарку Futuremark 3DMark03. Запустим простой тест Wings of Fury с принудительным обсчётом всех вертексных шейдеров (то есть с полным набором операций по перемещению и раскрашиванию треугольников) с помощью центрального процессора.
Результат не должен вас удивить:
- Центральный процессор = в среднем 77 кадров/с.
- Графический процессор = в среднем 1580 кадров/с.
Когда все вычисления с вертексами выполняет центральный процессор, на рендеринг и отображение каждого кадра уходит в среднем 13 мс. А при использовании графического процессора этот показатель падает до 0,6 мс — более чем в 20 раз быстрее.
Разница возрастает ещё больше, если прогнать самый сложный тест бенчмарка — Mother Nature. Центральный процессор выдал ничтожные 3,1 кадра/с.! А графический процессор взмыл на 1388 кадра/с.: почти в 450 раз быстрее. Обратите внимание: 3DMark03 вышел 16 назад, а в тесте на центральном процессоре обрабатываются только вершины, графический процессор всё-равно берёт на себя растеризацию и пиксельный этап. Представьте, если бы бенчмарк было современным и большая часть операций выполнялась программно?
Теперь снова попробуем бенчмарк Unigine Valley, обрабатываемая им графика очень похожа на ту, что используется в играх вроде Far Cry 5. Также тут есть полностью программный механизм рендеринга в дополнение к стандартному DirectX 11. При прогоне на видеопроцессоре получили средний результат в 196 кадров/с. А программная версия? После пары падений мощный тестовый ПК выдал в среднем 0,1 кадра/с. — почти в две тысячи раз медленнее.
Причина такого большого различия кроется в математических вычислениях и формате данных, который используется в 3D-рендеринге. Каждое ядро центрального процессора оснащено модулями операций с плавающей запятой. i7-9700K содержит 8 ядер, в каждом по два таких модуля. И хотя архитектура модулей в Titan X другая, оба вида могут выполнять одни и те же вычисления с данными одного и того же формата. Эта видеокарта имеет свыше 3500 модулей для выполнения сравнимых вычислений, и хотя их тактовая частота гораздо ниже, чем в центральном процессоре (1,5 ГГц и 4,7 ГГц), однако видеопроцессор берёт количеством модулей.
Хотя Titan X не является массовой видеокартой, однако даже бюджетная модель обгонит любой центральный процессор. Поэтому все 3D-игры и API спроектированы под специализированное оборудование. Можете скачать V-ray, 3DMark или любой бенчмарк Unigine и протестировать свою систему — сами убедитесь, насколько хорошо видеопроцессоры адаптированы для рендеринга графики в играх.
Заключительные слова
Это был короткий экскурс в процесс создания одного кадра в 3D-играх, от точки в пространстве до красочного изображения на мониторе.
По сути, весь процесс — это лишь работа с числами. Однако очень многое осталось за рамками статьи. Мы не рассматривали конкретные математические вычисления из евклидовой линейной алгебры, тригонометрии и дифференциальных исчислений, выполняемые вертексными и пиксельными шейдерами. Также мы не поговорили о том, как с помощью статистической выборки обрабатываются текстуры. Опустили такие классные визуальные эффекты, как преграждение окружающего света в экранном пространстве, уменьшение помех при трассировке лучей, применение расширенного динамического диапазона и временное сглаживание.
А когда вы в следующий раз запустите современную 3D-игру, надеемся, вы не только посмотрите на графику иными глазами, но и захотите узнать о ней больше.