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

Путь каждой сцены игры от тумана войны до интерфейса.

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

DTF публикует перевод статьи.

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

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

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

Для начала — пара слов о доступных нам графических библиотеках. League должна работать максимально эффективно на многих платформах. Например, 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), освещения, отражений или высокореалистичных шейдеров кожи. Все видимые объекты отрисовываются в кадровом буфере и показываются на экране, когда рендеринг завершён.

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

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

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

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

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

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

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

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

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

Этап 1: тени

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Этап 5: трава

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

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

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

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

Этап 6: вода

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3030
15 комментариев

Спасибо за статью, в принципе всё очевидно и понятно ) но когда раскладывают по кирпичикам, смотрится очень интересно и сразу видно какой титанический труд за каждым кадром =_+.

6
Ответить

Как то немного поверхностно.
Все таки хотелось узнать как батчатся миньоны.
Как заскинена трава что бы ее можно было без растяжек и разнообразно гнуть.
Ну и количество самих Drawcalls в пике.

2
Ответить

Я так заметил, что от drawcall отходят, что ли. В том же Unity в последних версиях, емнип, убрали стату DC совсем, как не самую 'правдивую' статистику производительности, вместо этого указывая batches и setpass calls.

Ответить

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

3
Ответить

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

1
Ответить

Ммм, хейтеры с неадекватным мнением?

1
Ответить

Спасибо за перевод!

1
Ответить