Как устроена графика в видеоиграх?

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

Вот эта станция и локомотив в стиле дикого запада из Red Dead Redemption 2, на самом деле состоит из 2,1 миллиона вершин, собранных в 3,5 миллиона треугольников с 976 цветами и текстурами, назначенных для различных поверхностей, и все это с виртуальным солнцем, освещающим данную сцену.

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

Цепочка рендеринга графики в видеоиграх имеет три ключевых этапа: вертекс шейдинг или же шейдинг вершин (Vertex Shading), растеризация (Rasterization) и фрагмент шейдинг (Fragment Shading). Хотя в современных видеоиграх используются дополнительные этапы, эти три основных этапа используются на протяжении десятилетий в тысячах видеоигр для компьютеров и консолей и все еще являются основой алгоритма графики видеоигр практически для каждой игры, в которую вы можете сейчас сыграть.

Давайте начнем с первого шага, называемого шейдингом вершин. Основная идея данного этапа заключается в том, чтобы взять всю геометрию и сетки объектов в 3D пространстве, используя поле зрения камеры для расчета, где каждый объект окажется в 2D окне, называемом экраном просмотра, который является 2D изображением, отправляемым на дисплей игрока.В этой сцене со станцией есть 1100 различных моделей, и поле зрения камеры ограничивает то, что видит игрок, уменьшая количество объектов, которые нужно отрендерить, до 600.

Давайте разберем локомотив в качестве примера. Хотя двигатель поезда имеет округлые поверхности и довольно сложные формы, он фактически собран из 762 тысяч плоских треугольников, используя 382 000 вершин и 9 различных материалов/цветов, нанесенных на поверхности треугольников.

Концептуально весь поезд перемещается как одно целое на экран просмотра, но на самом деле каждая из сотен тысяч вершин поезда перемещается по одной за раз. Итак, давайте сосредоточимся на одной вершине. Процесс перемещения вершины, а следовательно, треугольников и поезда, из 3D мира на 2D экран просмотра выполняется с помощью 3 трансформаций.

Сначала перемещение вершины из модельного пространства (model space) в мировое пространство (world space), затем из мирового пространства в пространство камеры (camera space) и, наконец, из перспективного поля зрения на экран просмотра (view screen).

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

После того как три вершины поезда преобразованы с использованием аналогичной матричной математики, мы получаем один треугольник, перемещенный на экран просмотра. Затем остальные 382 тысячи вершин поезда и 2,1 миллиона вершин всех 600 объектов в поле зрения камеры проходят аналогичный набор трансформаций, таким образом перемещая все 3,5 миллиона треугольников на 2D экран просмотра.

Это невероятное количество матричной математики, но графические процессоры спроектированы так, чтобы быть монстрами в рендеринге треугольных сеток, и таким образом эволюционировали на протяжении десятилетий, чтобы обрабатывать миллионы треугольников каждые несколько миллисекунд.
Теперь, когда все вершины перенесены на 2D-плоскость, следующий шаг — использовать 3 вершины одного треугольника и определить, какие именно пиксели на вашем дисплее покрыты этим треугольником. Этот процесс называется растеризацией. Монитор или телевизор с разрешением 4K имеет разрешение 3840 на 2160, что дает около 8,3 миллиона пикселей. Используя координаты X и Y вершин данного треугольника на экране просмотра, ваш графический процессор вычисляет, где он находится в этой огромной сетке и какие пиксели покрыты этим треугольником.

Затем эти пиксели затеняются с использованием текстуры/цвета, назначенного этому треугольнику. Таким образом, с помощью растеризации мы превращаем треугольники в фрагменты, которые представляют собой группы пикселей, происходящих из одного и того же треугольника и имеющих одинаковую текстуру.
Затем мы переходим к следующему треугольнику и закрашиваем пиксели, которые он покрывает, и продолжаем делать это для каждого из 3,5 миллионов треугольников, которые ранее были перенесены на экран просмотра. Применяя значения красного, синего и зелёного цветов каждого треугольника к соответствующим пикселям, в кадровом буфере формируется изображение с разрешением 4K, которое затем отправляется на дисплей.

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

Определение того, какие треугольники находятся спереди, называется проблемой видимости (Visibility problem) и решается с помощью Z-буфера или буфера глубины (Depth buffer).
Z-буфер добавляет дополнительное значение к каждому из 8,3 миллиона пикселей, соответствующее расстоянию или глубине, на которой находится каждый пиксель от камеры.

На предыдущем шаге, когда мы выполняли vertex transformation, мы получили координаты X и Y, а также значение Z, которое соответствует расстоянию от трансформированной вершины до камеры. Когда треугольник растеризуется, он покрывает набор пикселей, и значение Z или глубина треугольника сравнивается со значениями, хранящимися в Z-буфере. Если значения глубины треугольника меньше тех, что находятся в Z-буфере, то есть треугольник ближе к камере, то мы закрашиваем эти пиксели цветом треугольника и заменяем значения Z-буфера значениями Z этого треугольника.

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

Обратите внимание, что поскольку эти треугольники находятся в 3D-пространстве, вершины часто имеют 3 различных Z-значения, и поэтому для каждого отдельного пикселя треугольника необходимо вычислить его Z-значение, используя координаты вершин.
Это позволяет треугольникам с пересекающимися гранями корректно отображать их пересечения пиксель за пикселем.

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

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

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

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

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

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

Уже в Super Mario 64, которой почти 30 лет, имеется простой шейдинг, где цвета поверхностей изменяются в зависимости от освещения и теней в сцене.

Итак, давайте посмотрим, как работает шейдинг фрагментов.Математика пиксельного шейдинга (Pixel Shading Math) работает так: если поверхность направлена прямо на источник света, она затеняется ярче, тогда как если поверхность направлена перпендикулярно или от света, она затеняется темнее. Чтобы рассчитать шейдинг треугольника, нам нужно знать два ключевых момента. Во-первых, направление света, и, во-вторых, направление, в котором смотрит поверхность треугольника.

Давайте продолжим использовать локомотив в качестве примера и закрасим его ярко-красным цветом вместо черного. Как вы уже знаете, этот поезд состоит из 762 тысяч плоских треугольников, многие из которых направлены в разные стороны.

Направление, в котором смотрит отдельный треугольник, называется его нормалью поверхности (surface normal), что просто означает направление, перпендикулярное плоскости треугольника, как флагшток, торчащий из земли. Для расчета шейдинга треугольника мы берем косинус угла (тета) между двумя направлениями. Значение косинуса тета равно 1, когда поверхность направлена на свет, и 0, когда поверхность перпендикулярна свету. Затем мы умножаем косинус тета на интенсивность света и затем на цвет материала, чтобы получить правильно затененный цвет этого треугольника.
Этот процесс корректирует значения RGB треугольников, и в результате мы получаем диапазон от светлого до темного на поверхности в зависимости от того, как ее отдельные треугольники обращены (повернуты) к свету.

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

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

Одна из ключевых проблем заключается в том, что треугольники внутри объекта имеют только одну нормаль, и, следовательно, каждый треугольник будет иметь одинаковый цвет по всей своей поверхности. Это называется плоским шейдингом (Flat shading) и выглядит довольно нереалистично при просмотре на изогнутых поверхностях, такие как корпус этого паровоза. Таким образом, для создания гладкого шейдинга (Smooth shading) вместо использования нормалей поверхностей мы используем одну нормаль для каждой вершины, вычисленную как среднее значение нормалей соседних треугольников. Затем мы используем метод, называемый барицентрическими координатами, чтобы создать плавный градиент нормалей по поверхности треугольника.

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

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

И так мы рассмотрели основные шаги цепочки графического рендеринга, однако, есть много других шагов и более продвинутых тем.
Например, вам может быть интересно, где в этом процессе находятся трассировка лучей и DLSS (глубокое обучение для суперсэмплинга).

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

А DLSS в свою очередь — это алгоритм, который используется для масштабирования кадров с низким разрешением до кадров разрешением 4K с помощью сверточной нейронной сети.

Поэтому DLSS выполняется после трассировки лучей, а цепь рендера создает кадр с низким разрешением.

Интересно отметить, что у последнего поколения GPU есть три полностью отдельные архитектуры вычислительных ресурсов или ядра. Ядра CUDA или шейдинга выполняют графический рендеринговый конвейер. Ядра трассировки лучей, как следует из названия, предназначены для работы с трассировкой лучей. А DLSS работает на ядрах Tensor.

Поэтому, когда вы играете в AAA видеоигру с трассировкой лучей и DLSS, ваш GPU использует все свои вычислительные ресурсы одновременно, позволяя воспроизводить игры в разрешении 4K и рендерить кадры менее чем за 10 миллисекунд каждый. В то время как если бы вы полагались только на ядра CUDA, то один кадр занимал бы около 50 миллисекунд.

В данной статье мы разобрали, как работает графика в видеоиграх, но мы не уделили внимание таким сложным темам как: тени, отражения, UV, normal mapping.

Источник: www.youtube.com/watch?v=C8YtdC8mxTU

Статья поддерживается командой Serverspace.

Serverspace — провайдер облачных сервисов, предоставляющий в аренду виртуальные серверы с ОС Linux и Windows в 8 дата-центрах: Россия, Беларусь, Казахстан, Нидерланды, Турция, США, Канада и Бразилия. Для построения ИТ-инфраструктуры провайдер также предлагает: создание сетей, шлюзов, бэкапы, сервисы CDN, DNS, объектное хранилище S3.

IT-инфраструктура | Кешбэк 17% по коду HABR


 

Источник

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