08/05/2019

3D рендеринг: отрисовка сцены

Всем привет! Команда Fractured Byte снова с вами на UE4 Daily. Сегодня мы рассмотрим на практике, как отрисовывается сцена на GPU, используя знания, полученные в первой части статьи. Рекомендуем освежить в памяти основные моменты, описанные в первой статье, а тем кто не читал ее совсем — рекомендуем сделать это в обязательном порядке, так как в ней мы разбирали основные ресурсы, методы и функции, используемые GPU для отрисовки сцены.

Самой же интересной частью этой статьи является интерактивное демо, в котором буквально можно “пощупать” кадр прямо в браузере.

Вот мы и добрались до отрисовки сцены. В отрисовке сцены участвует как CPU, так и GPU. Задача CPU — подготовить контент и сказать GPU, что отрисовать. GPU просто выполняет команды, которые ему приходят. CPU отправляет команды на GPU через драйвер, используя определенный интерфейс — некий "стандарт общения". На данный момент этих стандартов довольно много — OpenGL, DirectX, Metal, WebGL, Vulkan и т.д., которые предоставляют нам определенные аппаратные возможности для отрисовки.

При создании демки на HTML5 мы столкнулись с проблемой wireframe геометрии — она просто не рисовалась. Оказалось, WebGL (а в браузере в качестве "стандарта общения" с GPU используется именно он) не поддерживает отрисовку "незакрашенных" примитивов, но зато есть поддержка отрисовки других примитивов — линий. Мы создали свой компонент на основе CustomMeshComponent, чтобы преобразовать геометрию в набор линий для демонстрации.

В интерактивной демке мы проиллюстрировали два подхода отрисовки сцены — Forward Rendering и Deferred Rendering. Эти основные методы, все остальные — их вариации или модификации, с учетом новых возможностей (например — LightPrePass).

Первое, что нужно сделать для отрисовки сцены — загрузить модели в память. Наша игра загружает карту, которая содержит ссылки на ассеты с моделями. Всё это хранится в оперативной памяти. Также наша игра отсылает данные о моделях на видеокарту в память GPU.

Когда все ресурсы созданы на GPU, шейдера скомпилированы — можно что-то и отрендерить.

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

Когда все ресурсы загружены, можно приступать к отрисовке сцены. Отсылать на отрисовку всё, что есть в сцене — плохая идея, так как мы можем быстро потратить все вычислительные ресурсы GPU. Что же делать? В действие вступают техники Куллинга (Culling). Мы не расскажем здесь, как работает куллинг и не будем приводить разные варианты его реализации, поговорим только о его разновидностях. В текущем кадре у нас есть камера, которой мы задали позицию, ориентацию в пространстве и собственно она и определяет, что мы увидим на экране. Понятно, что на GPU нужно отсылать только то, что мы можем увидеть. Мы хотели бы остановится на двух видах кулинга: это Frustum Culling, который определяет, какие объекты находятся в пределах кадра:

Frustum Culling. Красные объекты не попадают в камеру, поэтому мы их не отсылаем на отрисовку. Фиолетовым цветом обозначены границы кадра. Анимация камеры приведена исключительно для иллюстрации процесса в динамике.

Второй вид куллинга — Occlusion Culling. Даже когда объект находится в камере, он может быть закрыт другими объектами. На кадре ниже мы можем увидеть, что цилиндр, который в некоторых моментах находится за сферой, можно не отсылать на отрисовку. В то же самое время, маленький цилиндр за конусом — нет, потому что конус — прозрачный объект.

Occlusion Culling

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

Визуализация списка непрозрачной геометрии
Визуализация списка прозрачной геометрии

Одна из техник, которую зачастую используют перед рендерингом кадра — z-prepass. Идея в том, чтобы отрисовать непрозрачную часть сцены сначала в z-buffer, не используя пиксельный шейдер. Таким образом, запись в z-buffer можно будет отключить для следующих пасов. Это позволит избежать ситуации, когда мы вычисляем цвет пикселя, который впоследствии будет перезаписан. В такой простой сцене эта техника не окупит себя, но вот в более сложных сценах без неё не обойтись.

Z-prepass. Визуализация глубины сцены. Анимация камеры приведена исключительно для иллюстрации процесса в динамике.

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

Forward Renderer появился раньше. Его идея была достаточно проста: для каждого объекта на экране выставлялись сразу все параметры (источники света, дополнительные параметры), и каждый объект рисовали за один раз. Всё было хорошо, если бы не сложность отрисовки, разные вариации шейдеров для разных материалов, разные виды источников света. Чтобы хоть как-то ускорить этот процесс, придумали Deferred Renderer. Его идея состоит в том, чтобы отложить процесс шейдинга/освещения на более поздние этапы конвейера. Deferred Renderer происходит в два прохода. В первом проходе мы сохраняем все параметры материалов в массив текстур, который называется GBuffer (когда-то возможность рендеринга в несколько текстур сразу была невозможна, Multiple Render Targets или MRT). Во втором проходе эти параметры декодируются из GBuffer и используются для вычисления освещения.

Какие же плюсы и минусы использования каждой из техники? Для Deferred Renderer требуется возможность рендерить сразу в несколько текстур, он использует больше памяти под GBuffer, например, UE4 использует 7 текстур для GBuffer (GBufferA – GBufferE, GBufferVelocity + еще одна дополнительная текстура), но он должен быть быстрее за счет вынесения освещения в отдельный проход. Тем не менее, при рендеринге прозрачных объектов всегда используется Forward Renderer.

Так что же лучше? Сейчас в пользу использования Forward Renderer указывают только ограничения GPU (если нету поддержки MRT) или ограничение по памяти. Его обычно используют на мобильных устройствах, да и Opengl ES2 не поддерживает MRT. UE4 использует Forward Renderer для мобильных устройств, хотя есть возможность переключиться на Deferred Renderer при использовании Metal или ES3.1.

Продолжаем рендеринг

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

Forward renderer: отрисовка непрозрачной геометрии

Deferred Renderer: записываем всю информацию из материалов в GBuffer. Для удобства в демо мы используем разные проходы, хотя все текстуры рендерятся за один раз. Также мы проиллюстрировали только самые важные текстуры в GBuffer.

Deferred Renderer: GBuffer Base Color
Deferred Renderer: GBuffer Normal
Deferred Renderer: GBuffer Roughness
Deferred Renderer: GBuffer Metallic

Deferred Renderer: после того как наш GBuffer сформирован, мы вычисляем освещение для каждого пикселя.

Deferred Renderer Shading

Одним из недостатков Deferred Renderer является невозможность его работы с прозрачной геометрией, из-за чего приходится рендерить её, используя Forward Renderer. Это связано с тем, что каждый пиксель в GBuffer может хранить информацию только про один материал, а с прозрачной геометрией нам нужно знать еще и цвет пикселя под текущим объектом. Прозрачную геометрию обычно сортируют по дистанции перед рендером (от самой дальней к самой ближней точке) для достижения правильного наложения объектов друг на друга на экране. Хотя такой подход и не гарантирует сортировку на уровне пикселей, но фактически эффективнее пренебречь этой дорогой операцией для прозрачной геометрии.

Transparent geometry

Основная картинка готова, но нужно добавить еще щепотку пост процессов и интерфейса по вкусу.

В нашем демо представлено 2 постпроцесса: Bloom и DoF (Depth Of Field). Оба эффекта отображают процессы, которые происходят в камере и добавляют реалистичности финальной сцене.

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

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

Bloom. Обратите внимание, какой большой засвет появился на самосветящемся логотипе на кубике и солнце на заднем плане.

Глубина резкости — тоже артефакт, который появляется в камерах, и направлен он для достижения реалистичности. Для его реализации движок, так же как и в случае с Bloom, размывает отрендеренный кадр, получая таким образом для каждого пикселя экрана два его состояния: когда он находится в фокусе и когда нет, но это только его самые крайние состояния. Чтобы вычислить финальный цвет пикселя, нужно вычислить, насколько он должен быть размыт в зависимости от расстояния до точки фокуса и размера региона фокуса.

В UE4 все эти постпроцессы уже реализованы и их нужно только настроить по своему усмотрению.

Depth of Field: глубина резкости. Обратите внимание, что объекты в центре кадра — в фокусе, в то время как остальные объекты плавно уходят в расфокус.

Вот мы и рассмотрели все важные этапы конвейера рендера. Надеемся, что вы узнали для себя что-то новое или поняли принцип работы того, о чем уже знали. Начиная со следующей статьи, весь материал будет тесно связан с UE4 и будет требовать понимания написанного в этой статье, так что рекомендуем еще раз почитать те моменты, которые могли остаться непонятными после первого прочтения. Спасибо и до встречи в следующий раз!

Recent Posts

Как в НИУ ВШЭ воспитывают новых Кодзим

11/07/2019
Реально ли попасть в геймдев, отучившись 4 года и получив диплом? Чему предстоит обучаться? Какие подводные камни? Что говорят студенты и преподаватели?

От юриста до разработчика игр при финансовой поддержке Unreal Engine

08/07/2019
Ник Пирс поделился замечательной историей о том, как сменить скучную профессию и стать разработчиком игр. 

Создание ИИ сверхскоростных автомобилей с помощью Unreal Engine

04/07/2019
Ведущий программист Dark Future: Blood Red States рассказывает об изящном решении для создания ИИ правдоподобных противников.