| Дата последнего изменения : | 24.07.2008 18:31 |
Рейтинг |
|
Рендеринг сложных динамических сцен в реальном времени – довольно сложная задача. Разработчики, как и любители компьютерных игр, предпочитают иметь дело с максимально реалистичной виртуальной средой, ради которой не приходилось бы жертвовать качеством геймплея или скоростью. Примером такой среды, часто используемой в современных играх и рассмотренной в нашей статье, являются открытые природные ландшафты. Природный ландшафт подразумевает рендеринг большого количества геометрических объектов, что сильно сказывается на производительности. Рельеф, вода, облака и растительность – лишь основные составляющие этой сложнейшей задачи. Однако, благодаря изобретательности и творческому подходу при использовании 3D API, например, OpenGL, Direct3D* или любого другого API, разработчик может «выжать» из современного оборудования просто поразительное быстродействие.
Наш пример посвящен рендерингу моделей деревьев и основан на применении Direct3D*, но предлагаемые идеи могут применяться к любой другой трехмерной сцене, содержащей множество объектов сходной геометрии и построенной на основе любого 3D API. Деревья являются любопытным примером, поскольку обладают всеми обязательными атрибутами действительно сложной задачи для рендеринга. Они имеют сложную геометрию, в изобилии присутствуют в природном ландшафте и подвергаются воздействию природных факторов (таких как притяжение, ветер и т.д.). При этом для них может требоваться весьма детальная прорисовка текстур. Поскольку деревья существуют в природе, люди знают, как они выглядят, как ведут себя и как взаимодействуют с окружающей средой. Соответственно, у людей имеются определенные представления о том, как должны выглядеть деревья в игре. Если поставлена задача создания максимально реалистичной среды, разработчикам следует учитывать эти представления при создании виртуальных пространств.
В реальном мире произрастает множество видов деревьев, и моделирование их всех было бы чрезвычайно трудной задачей, которая потребовала бы огромных затрат времени. Однако, большинство деревьев в одном географическом районе относительно похожи, поэтому детальных моделей всего лишь четырех деревьев, которые затем можно поворачивать и масштабировать, будет вполне достаточно, чтобы смоделировать действительно реалистичный лес. Тем более, что для этого не нужно искать талантливого художника или быть им. Каждое дерево в приводимом нами примере динамически генерируется с помощью программных алгоритмов и содержит более 10 000 полигонов. Количество деревьев на нужном участке земли, а также тип, масштаб, угол поворота и положение каждого дерева определяются с помощью функции шума (noise function). Порог, определяющий плотность деревьев в лесу, задается изменяемой константой, и в первой сцене нашего примера ее значение подобрано таким образом, чтобы создать в общей сложности до 400 деревьев.
Предположим, что все деревья в первой сцене определены и характеристики каждого дерева (тип дерева, масштаб, угол поворота и положение) заданы. Возникает вопрос, каким образом их следует рендерить. Ответ на него отнюдь не очевиден и, в сущности, меняется в зависимости от желаемого результата. Мы рассмотрим возможные подходы к решению данной проблемы, взвешивая все «за» и «против», а также расскажем подробнее о некоторых важных особенностях программной реализации. Желаемым результатом в нашем случае является максимально быстрый рендеринг реалистичных моделей деревьев.
Первый способ значительно медленнее остальных, и производительность в данном случае недостаточна для моделирования игровой среды в реальном времени. Его принцип прост: рендеринг полной геометрии всего, что есть в сцене. Четыреста деревьев, по десять с чем-то тысяч полигонов на каждое дерево, не говоря уже о земле, небе, воде – это немалый объем геометрии, более 4 миллионов треугольников на кадр! Конечно, объем вычислений, который должен быть выполнен на программном уровне, весьма незначителен. Но наш движок не так эффективен, как лучшие из существующих сегодня коммерческих игровых движков, и мы смогли получить всего 1 или 2 кадра в секунду, что абсолютно неприемлемо. При таком результате следует вплотную заняться оптимизацией, и, как мы увидим далее, вполне возможно добиться существенных улучшений не снижая количества полигонов в дереве или плотности деревьев в лесу.
Первая очевидная возможность оптимизации – применить один из методов отсечения невидимых объектов сцены (visibility culling). В нашем примере мир делится с помощью 2-мерной сетки, при этом каждая ячейка (quadrant) сетки содержит отдельный участок рельефа, отдельную плоскость на заданном уровне для моделирования воды, и связанный список деревьев. Все те ячейки, которые не попадают в пространство обзора камеры, просто исключаются из рендеринга. Конечно, это несколько упрощенный подход к отсечению невидимых объектов, и он создает дополнительную нагрузку на программную часть, однако он помогает добиться желаемого результата. Далее по тексту мы будем подразумевать, что режим отсечения невидимых объектов включен. Это небольшое, казалось бы, изменение позволяет отрисовывать лишь 40% ячеек сетки 10 х 10, что дает максимальную частоту кадров 3 fps. Более тонкая сортировка (например, деление сцены на большее количество мелких ячеек) позволит сократить процент отрисовываемых ячеек примерно до 25%. Тем не менее, рендеринг полной геометрии видимой сцены все еще является чрезвычайно ресурсоемкой задачей, и вопрос о том, что же все-таки нужно оставить в кадре, пока не решен.
Как выясняется, вовсе необязательно рендерить полную геометрию дерева, которое находится так далеко от камеры, что занимает на экране лишь один пиксель. Пользователь в любом случае едва ли сможет его разглядеть. Более того, нет необходимости в рендеринге каждого дерева, которое не находится настолько близко к воображаемой камере, чтобы пользователь мог разглядеть разницу между трехмерным деревом и похожей на дерево текстурой, или импостером (impostor). Импостер, в данном контексте, представляет собой билборд (billboard, двухмерный прямоугольник), лицевая сторона которого всегда повернута к камере, с текстурой, имитирующей геометрический объект, который он заменяет, как показано на рис. 1 и 2.
Рис. 1: Дерево и импостер в режиме отображения объемных объектов (solid view) (альфа-канал импостера позволяет пропускать темные участки при рендеринге).
Рис. 2: То же дерево и импостер в режиме отображения сетки (wireframe view)
Использование импостеров радикально сокращает количество полигонов, но влечет за собой ряд новых вопросов:
Теперь начинается самое интересное с точки зрения разработчика. Представьте, что вы обходите по кругу дерево в реальном мире. Ветки дерева неподвижны по отношению к стволу. Иными словами, они не вращаются на стволе, чтобы постоянно быть у вас перед глазами. Теперь представьте, что камера в игре движется вокруг дерева – или любого похожего объекта. Если дерево будет выглядеть одинаково с любой точки обзора, цель симулирования реальности достигнута не будет. Чтобы избежать этого, следует отрисовывать полную геометрию любого дерева, находящегося в пределах определенного расстояния от камеры, а дальние деревья можно отображать в виде импостеров. Но и в этом случае остается одна проблема.
Как мы уже упоминали, каждое дерево в сцене обладает уникальными характеристиками, присвоенными ему при генерировании. Если для всех импостеров применять одинаковую текстуру, то все деревья, изображенные импостерами, будут выглядеть абсолютно одинаково, и в итоге ваш лес будет выглядеть совсем не натурально. Кроме того, если дистанция от камеры до дерева сократится до величины, при которой геометрия дерева должна быть отрисована полностью, обязательно проявится весьма заметный эффект внезапного появления. Однако еще не все потеряно, так как в нашем арсенале есть мощный инструмент, который поможет поддержать иллюзию: метод «рендеринг в текстуру» (render-to-texture).
Рендеринг деревьев только с одной точки обзора (вида) нецелесообразен по причинам, упоминавшимся выше; следовательно, необходимо решить, сколько видов для каждого дерева нужно рендерить в текстуру, следует ли использовать одну текстуру для каждого вида или одну текстуру для всех выбранных видов. Секрет выбора количества видов - в тщательном подборе расстояния замены, т.е. расстояния от камеры, на котором вместо импостера рендерится уже полная геометрия дерева. Чтобы сократить объем занимаемой памяти и при этом сохранить иллюзию реальности, в нашем примере используется расстояние замены, при котором отсутствует заметный эффект внезапного появления. Мы используем всего восемь видов: север, северо-восток, восток, и так далее. Обычно этих восьми видов вполне достаточно, но вы можете экспериментировать с большим или меньшим количеством, в зависимости от потребностей вашего приложения. Конечно, при этом важно учитывать, что все зависит от масштабов виртуального мира, где должны расти деревья. Но после ряда экспериментов относительно нетрудно найти подходящую величину.
Рассмотрим вторую проблему: использовать по одной текстуре для каждого вида или одну текстуру, изображающую все восемь видов модели дерева. Подкачка текстур (texture swapping) в современных API может оказаться весьма ресурсоемкой, поэтому рендериг каждого вида дерева в отдельную текстуру может привести к существенным затратам ресурсов. Рендеринг всех восьми видов дерева в одну текстуру значительно сокращает объемы подкачки текстур. Следующий шаг, рендеринг всех восьми видов всех четырех деревьев в одну текстуру, вообще устраняет необходимость в подкачке текстур в интервалах обработки импостеров. В целях сохранения реалистичности при одновременном снижении нагрузки на ресурсы системы и повышении производительности в нашем примере используется именно последний метод.
Хотя данное решение и весьма эффективно, рендеринг всех возможных видов модели в одну текстуру по методу render-to-texture имеет свои недостатки. Первый недостаток связан с рендерингом в текстуру с альфа-каналом и присущ всем методам рендеринга в текстуру (альфа-канал необходим для того, чтобы сделать возможным альфа-тест на фоне импостеров за проход рендера). В приводимом в качестве примера коде создается область памяти для вывода изображения (render target, область рендеринга) с альфа-каналом, который, однако, поддерживается не всеми аппаратными графическими средствами. Мы протестировали наш пример на нескольких графических картах и получили хорошие результаты (с использованием последних доступных версий драйверов), но вовсе не исключаем, что на некоторых конфигурациях наш пример не будет запускаться правильно.
Если необходимо обеспечить совместимость с графическими картами, которые не поддерживают объекты типа «render target» с альфа-каналом, существует способ обойти эту проблему. Вместо того, чтобы создавать область рендеринга с альфа-каналом, создайте базовую (блокируемую) область рендеринга. Также создайте текстуру в системной памяти с тем же размером, что и область рендеринга, текстуру с альфа-каналом в системной памяти, и текстуру с альфа-каналом в видеопамяти. Этот метод проиллюстрирован на приводимой ниже схеме, и мы разберем его более подробно:
Рис. 3: Создание импостера с альфа-каналом для графических карт, прямо не поддерживающих рендеринг в текстуры с альфа-каналом.
Сначала создайте объект в области рендеринга. Заблокируйте область рендеринга, скопируйте данные в текстуру в системной памяти, имеющую тот же размер, и разблокируйте область рендеринга. На этом этапе область рендеринга можно удалить, она больше не понадобится. Скопируйте данные из текстуры в системной памяти в текстуру с альфа-каналом в системной памяти, по необходимости устанавливая при этом значения альфа-канала (непрозрачные пиксели там, где отрисован объект, прозрачные в остальных областях). Наконец, заблокируйте текстуру с альфа-каналом в видеопамяти, скопируйте данные из системной текстуры с альфа-каналом и разблокируйте видео-текстуру с альфа-каналом. В результате должен получиться точный импостер объекта с установленными значениями прозрачности для альфа-теста при рендеринге импостера. Поскольку эти операции выполняются только во время запуска, потери производительности не существенны.
Второй недостаток метода рендеринга всех возможных видов в одну текстуру связан с возможной альтернативой импостерам – точечными спрайтами (point sprites). Если все восемь видов всех четырех деревьев отрисованы в одну текстуру, это означает, что для каждого импостера должен применяться какой-либо метод трансформации текстуры (см. рис. 4). В DirectX* 8.0 точечные спрайты не поддерживают трансформацию текстуры, поэтому, разрешив точечные спрайты, придется рендерить каждый вид каждого дерева в отдельную текстуру. Мы отвергли этот вариант по причинам, описанным выше, поэтому мы вынуждены отказаться от использования точечных спрайтов, хотя они и имеют преимущество в виде необходимости хранения только z-координаты (z-position).
Рис. 4: Трансформации текстуры импостера (выбор координат текстуры)
При этом усложняется реализация текстур с MIP-картами (MIP-mapped textures), поскольку необходимо создавать буферы вокруг вида каждого дерева, чтобы избежать размытости на границах текстур.
После определения схемы текстурирования следует переходить к следующему важному аспекту, который необходимо учитывать при рендеринге сотен (возможно, тысяч) мелких объектов в Direct3D*: правильное использование буферов вершин. Поскольку все импостеры, по сути, представляют собой квадранты (quads) с координатами конкретной текстуры, рассчитываемыми на основе угла поворота каждого дерева, можно использовать
В каждом из этих случаев придется «на лету» выбирать нужные координаты текстуры для каждого импостера, однако это справедливо для любой ситуации, когда одна текстура изображает несколько видов. Снижение производительности происходит из-за сотен вызовов функции DrawIndexedPrimitive(), причем за каждый вызов получается рендерить только 4 вершины.
Большей эффективности можно добиться за счет выполнения трансформаций для всех импостеров на программном уровне с последующим помещением всех трансформированных вершин в единый большой буфер вершин. В примере использован буфер вершин, способный вместить 1000 импостеров деревьев (4000 вершин), хотя потенциально возможна поддержка до 16 000 импостеров деревьев (64 000 вершин). В примере реализована поддержка только 1000 импостеров деревьев на буфер, поскольку прирост производительности при увеличении вместимости буфера свыше 4000 вершин ничтожно мал. Хотя может показаться, что это чересчур сложное решение для того, чтобы использовать лишь одну текстуру, советуем вам попробовать выполнять расчеты трансформаций импостеров (масштабирование, поворот, позиционирование, расчет координат текстуры) на программном уровне и использовать единый большой буфер вершин. Конечный результат будет весьма хорош как визуально, так и с точки зрения производительности. Производительность, измеряемая по частоте кадров, возрастает с жалких 3 fps до целых 90 fps, то есть почти на 1000%!
Сцена, отрисованная без импостеров
Аналогичная сцена, отрисованная с импостерами
Как свидетельствуют факты, быстрый рендеринг простых импостеров представляет собой замечательный инструмент в арсенале разработчика. Сочетание таких методов, как «render-to-texture», с алгоритмическим созданием контента (procedural content) снижает потребность в дополнительных графических ресурсах и сокращает время разработки контента. Тщательное продумывание процессов отсечения невидимых объектов, текстурирования и использования API поможет увеличить общую скорость рендеринга в вашем графическом конвейере и в итоге повысит общее качество визуализации. Надеемся, что представленные здесь идеи пригодятся вам, когда вы в следующий раз столкнетесь со сложной проблемой виртуального представления реального мира.
