Покадровый разбор: как работает графический движок League of Legends

12 января старший инженер-программист Riot Games Тони Альбрехт (Tony Albrecht) рассказал в техническом блоге компании о том, как работает рендеринг в MOBA League of Legends и почему её движок требует переработки.

Редакция рубрики «Рынок игр» публикует перевод статьи, выполненный изданием DTF.

b9e708353774be.jpg

Привет, я Тони Альбрехт, один из инженеров Render Strike Team — нового начинания в League of Legends. Эта команда должна улучшить движок рендеринга игры, и нам не терпится приступить к работе. Я коротко расскажу о том, как движок работает сейчас.

Для меня это отличный повод пройтись по этапам графического конвейера для того, чтобы команда поняла, над чем нам предстоит работать. Я расскажу, как League of Legends строит и отображает отдельный кадр в игре (напомню, на мощных компьютерах это происходит более 100 раз в секунду). Обсуждение будет насыщено техническими подробностями, но я надеюсь, окажется доступным даже для тех, у кого нет опыта в рендеринге.

Для начала — пара слов о доступных нам графических библиотеках. League of Legends должна работать максимально эффективно на многих платформах. Например, Windows XP сейчас четвёртая по популярности ОС среди наших игроков (после Windows 7, 10 и 8). Каждый месяц пользователи Windows XP участвуют в более чем 10 миллионах матчей, и чтобы сохранить обратную совместимость, мы должны поддерживать DirectX 9 и использовать только его фишки. Мы пользуемся в целом аналогичным набором функций из OpenGL 1.5 на компьютера с OS X, но это скоро изменится.

Рендеринг для начинающих

В большей части компьютеров установлены CPU (центральный процессор) и GPU (графический процессор). Центральный процессор отвечает за логику и вычисления игры, в то время как графический получает от него треугольники (triangles) и текстуры и отображает их на экране как пиксели. Пиксельные шейдеры (pixel shaders) — небольшие программы на GPU, позволяющие нам влиять на то, как проходит рендеринг.

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

Рассмотрим пример. Вот изображение Гарена, демонстрирующее все 6336 треугольников на каркасе — монолитная модель без текстур. Её создали наши художники и экспортировали в формат, который может читать и анимировать движок League of Legends. Можно заметить неплоское затенение: это ограничение приложения, используемого для исследования рендеринга.

401b4f31e770f7.jpg

Изображение модели без текстур не только скучное, но и неясное. Оно не передаёт того Гарена, которого мы все знаем. Чтобы оживить героя, нужны текстуры.

df144e915d5bb0.jpg

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

f3983d40561192.gif

Это уже на что-то похоже. Шейдер, который рендерит наши сетки со скиннингом (skinned meshes), не только накладывает текстуры на модели, но об этом мы ещё поговорим позже.

Это основы, но League of Legends должна рендерить не только модель чемпиона и текстуры. Рассмотрим поэтапно весь рендеринг приведённой ниже сцены.

Этап 0: туман войны

До начала отрисовывания сцены мы должны приготовить туман войны и тени (туман и тени — звучит зловеще). Центральный процессор хранит туман войны в виде сетки (grid) 128 на 128, которая масштабируется до квадратной текстуры 512 на 512. После мы размываем эту текстуру и с её помощью затемняем соответствующие зоны и области на миникарте.

Этап 1: тени

Тени — обязательная часть 3D-сцены, без них будет казаться, что объекты висят в воздухе. Тени следует рендерить относительно источника света, чтобы казалось, что их отбрасывают миньоны или чемпион. Расстояние от источника света до объекта, отбрасывающего тень, учитывается для каждого пикселя в RGB-компонентах, а альфа-компонент обнуляется.

На первом изображении ниже — поле высоты теней (shadow height field) осаждённой башни, миньонов и двух чемпионов в RGB. На втором — только альфа-компонент. Текстуры были обрезаны, чтобы детали теней были виднее: миньоны внизу, а башня и чемпионы — наверху.

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

Этап 2: статичная геометрия

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

10f1bf999eb133.jpg

Обратите внимание на тени миньонов, а также на туман войны, залезающий на границы сцены. На карте Ущелье призывателей (The Summoner’s Rift) для статичной геометрии не рендерятся динамические тени. Главный источник света не движется, поэтому тени статичных сеток (static meshes) «запекаются» на их текстурах. Так художники могут лучше контролировать внешний вид карты, и улучшается производительность (поскольку не нужно рендерить тени от статичных сеток). Тени могут отбрасывать только миньоны, башни и чемпионы.

Этап 3: скиннинг

На рельеф и тени уже можно накладывать объекты. В первую очередь — миньонов, чемпионов и башни, — то есть те, что должны реалистично двигаться и иметь сгибающиеся суставы (bending joints).

Каждая анимированная сетка (animated mesh) содержит скелет (каркас из иерархически выстроенных костей) и сетку треугольников (mesh of triangles), как на изображении Гарена выше. Каждая вершина связана с костями (от одной до четырёх), при движении которых вершины двигаются на манер кожи — отсюда и название «скиннинг». Наши художники создают сетки и анимации для всех объектов и экспортируют их в формат, который загружается в League of Legends в начале игры.

Все кости сетки модели Гарена продемонстрированы на изображениях выше. Слева — его кости с названиями. Справа — вершины (голубые кубы) и жёлтые линии, показывающие их привязку к костям, которые управляют их положением.

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

Этап 4: контуры

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

af47b1d9789cd5.jpg

b59e4e6766d0e1.jpg

Контуры получаются с помощью масштабированной глубины с предыдущего этапа, к которой применяется фильтр Собеля (Sobel filter), извлекающий границы, которые мы рендерим на каждую сетку по отдельности. Если же GPU не может выполнять рендеринг на несколько объектов одновременно, применяется резервный метод с использованием стенсил-буфера.

Этап 5: трава

0c040291c9be7e.jpg

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

903dfb52632653.jpg

Пучки травы — тоже сетки со скиннингом. Так мы можем анимировать их, когда через них проходят персонажи или при дуновении ветра в Ущелье Призывателей (Summoner’s Rift).

Этап 6: вода

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

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

0365eda0218e31.jpg

На следующем изображении выделен каркас всей ряби.

dea4e6226aed96.jpg

Здесь хорошо видны эффекты воды у берегов реки, вокруг камней и кувшинок.

Анимированная и при нормальном рендеринге вода выглядит так:

Этап 7: декали

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

7a55954e9a2532.jpg

Этап 8: особая обводка

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

e141d0eca53958.jpg

Этап 9: частицы

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

Отобразив только частицы (вообще убрав фон), мы увидим следующее:

826aebf94523b8.jpg

Если показать треугольники частиц (без текстур), мы увидим фиолетовую геометрию:

f44e1e34e3ff54.jpg

А так выглядят частицы при обычной отрисовке.

a99c4c76e1d57e.jpg

Этап 10: постобработка

Теперь, когда основная часть сцены отрисована, нужно её «отполировать». Для этого мы сначала делаем проход с антиалиасингом (АА) — сглаживаем границы и делаем кадр «чище». На статичном изображении этот эффект не очень заметен, но он здорово помогает снизить эффект мерцающих пикселей, который возникает при перемещении высококонтрастных границ по экрану. В League of Legends мы используем алгоритм быстрого сглаживания Fast Approximate Anti-Aliasing.

053713f7f08dea.jpg

На картинке слева — миньон без сглаживания, а справа — сглаженный с помощью FXAA. Обратите внимание, как смягчились границы.

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

Этап 11: урон и полоски здоровья

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

76c170d6507d8b.jpg

Этап 12: пользовательский интерфейс (HUD)

Наконец, отрисовывается пользовательский интерфейс. Текст, иконки и предметы накладываются поверх всего остального, как отдельные текстуры. В этой сцене около тысячи треугольников используются для интерфейса, около 300 для мини-карты и 700 — для всего остального.

Объединяем всё

a22a4327f25317.gif

Вся сцена содержит около 200 тысяч треугольников, около 90 тысяч из них используются только для частиц. За 625 вызовов отрисовки (draw calls) рендерится 28 миллионов пикселей. Для комфортной игры это должно происходить максимально быстро. Можно достичь более 60 кадров в секунду, если все этапы проходятся менее чем за 16,66 миллисекунд.

То, что мы описали, происходит со стороны графического процессора. Центральный процессор за это время обсчитывает всю игровую логику, обрабатывает команды ввода пользователя, коллизию, частицы, анимацию и подаёт команды на GPU. Если частота кадров у вас достигает 300, значит всё это происходит менее чем за 3,3 миллисекунды.

Зачем пересобирать движок рендеринга

Теперь вы понимаете, с какими сложностями связан рендер отдельного кадра League of Legends. Но это только со стороны вывода данных. На экране вы видите результат сотен тысяч вызовов функций к движку рендеринга — он постоянно меняется и развивается, чтобы отвечать требованиям времени. В кодовой базе игры есть разные формы рендеринга для поддержки нового и старого оборудования.

Карты работают по-разному: Ущелье призывателей (Summoner’s Rift) выполняет рендеринг немного иначе, чем Воющая бездна (Howling Abyss) или Проклятый лес (Twisted Treeline). Какие-то части движка рендеринга остались от прошлых версий League, другие — так и не раскрыли свой потенциал.

Команда Render Strike Team должна переработать код, чтобы рендеринг выполнялся через один и тот же интерфейс. Если мы с этим справимся — игроки не заметят разницы (кроме, возможно, улучшения производительности в некоторых моментах). Но после завершения работы мы сможем вносить изменения во все режимы игры одновременно.

©  vc.ru