Автор: Михаил Фарленков «Mikx»

Опубликовано: 12.01.2007

Изменено: 12.01.2007

Постоянная ссылка

Комментарии [3]

Ландшафты быстрого приготовления


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


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

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

1. Введение

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

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

Данная статья имеет скорее теоретические характер, нежели практический. Подробное описание реализации метода на C# 2.0 и XNA планируется вынести в отдельную статью.

Описываемый метод ориентирован на современные графические процессоры:

  • NVIDIA – карты поддерживающие шейдерную модель версии 3.0 серии
    • GeForce 6000
    • GeForce 7000 и выше
  • ATI – карты поддерживающие шейдерную модель версии 2.0 серии:
    • Radeon 9000 начиная с 9500
    • X100
    • X1000 и выше

2. Описание задачи

При рендеринге ландшафта перед разработчиком стоят 2 стандартные задачи:

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

Обоснованность этих задач вполне очевидна:

  1. В современных играх масштабы виртуальных миров достигли такого уровня что в одном кадре игрок может увидеть не более нескольких процентов от него. Поэтому нужно постараться избавить графическую подсистему компьютера от необходимости рисовать не видимые участки ландшафта.
  2. Глубина прорисовки также очень велика - уже никого не удивить тем, что горизонт скрывается не под плоской 2D-горой, а под настоящей трёхмерной до которой можно дойти/доехать/долететь и подробно изучить её. Но чтобы позволить себе такую глубину прорисовки разработчик должен постараться сделать так чтобы относительно экрана размеры полигонов ближайшей земной поверхности и отдалённых участков ландшафта были примерно одинаковыми.

3. Традиционный подход

Рассмотрим в общих чертах так сказать традиционные алгоритмы применяемые для решения описанных выше задач.

1. Рисовать только то что может увидеть виртуальная камера.

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

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

Суть метода заключается в следующем: всё доступное пространство "вкладывается" в куб. Далее этот куб делится на 8 равных кубов. Каждый из этих кубов делится ещё на 8 равных кубов и т.д. В итоге весь доступный объем сцены будет разделён на множество вложенных кубов. Каждый куб будет иметь свой адрес в пространстве и в иерархии кубов. Все объекты сцены в зависимости от их расположения и размеров должны быть связаны с тем кубом в иерархии в который они полностью входят. Имея такую организацию сцены простой перебор всех участков ландшафта можно заменить рекурсивной проверкой кубов на которые было поделено всё пространство. Т.е. в самом начале проверяются 8 дочерних кубов корневого куба.

  • Если проверка одного из кубов показала что он полностью входит в поле зрения наблюдателя, то все вложенные в него объекты признаются видимыми и добавляются в очередь рендеринга без дополнительной проверки каждого отдельного объекта.
  • Если проверка показала, что куб полностью выпадает из поля зрения, то все его дочерние объекты также считаются не видимыми и их проверка не производится.
  • Если же проверка куба показала что он частично входит в поле зрения, то в цикл проверки попадают 8 его дочерних узлов иерархии. И т.д.

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

2. Рисовать ровно столько полигонов сколько нужно чтобы визуально ландшафт оставался гладким.

Для реализации динамического уровня детализации при рендеринге ландшафтов применяется техника ROAM. Суть её состоит в том что каждый отдельный видимый участок ландшафта подвергается тесселяции (увеличение или уменьшение количества полигонов) при изменении дистанции между ним и наблюдателем. С начала каждый участок представляется как квадрат состоящий из двух треугольников. Далее каждый делится ещё на 2 равных треугольника и т.д. По мере сокращения или увеличения дистанции между участком и наблюдателем отдельные треугольники могут подвергнуться делению или слиянию. Решение о делении или слиянии принимается на основе специальной формулы результат вычисления которой говорит насколько точно текущая структура участка ландшафта соответствует базовой карте высот.

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

4. Альтернативный подход

Прежде чем рассказывать об альтернативном методе синтеза ландшафта позвольте мне покритиковать традиционный подход к решению задачи.

  • Во-первых у меня есть претензии к сложности его реализации. Октарные деревья и ROAM требуют написать, отладить и поддерживать довольно большой объём кода.
  • Во-вторых меня не устраивает то что при РЕНДЕРИНГЕ ландшафта большую часть работы берёт на себя центральный процессор, а не графический.

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

Метод основывается на возможности которая впервые появилась в шейдерной модели версии 3.0, а именно обращение к текстурам из вершинных шейдеров.

1. Рисовать только то что может увидеть виртуальная камера.

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

Для начала нам нужен плоский меш примерно таких размеров каких обычно бывает видимая часть ландшафта:

При отрисовке каждого кадра данный меш смещается в доль горизонта так чтобы он был как бы привязан к виртуальной камере. Т.е. куда бы не «посмотрела» камера меш должен расположиться под ней в доль направления её взгляда:

Данная операция выполняется в вершинном шейдере для этого в нём должен быть доступен вектор описывающий мировые координаты наблюдателя:

vertexPos.xz = vertexPos.xz + cameraPos.xz;

Далее по мировым координатам вершины можно вычислить её текстурные координаты:

textureCoords.xy = vertexPos.xz * textureScale;

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

float4 heightColor = tex2Dlod(heightMapSampler, textureCoords);

float height = length(heightColor) * heightScale;

vertexPos += mul(normal, height);

Всё. Теперь при рендеринге каждого кадра перед камерой всегда будет виден участок ландшафта форма которого будет будет зависить от того где находится камера.

2. Рисовать ровно столько полигонов сколько нужно чтобы визуально ландшафт оставался гладким.

Для решения этой задачи достаточно лишь изменить топологию нашего единственного участка примерно следующим образом:

5. Комментарии к реализации

1. NVIDIA

Для того чтобы текстура могла быть использована в вершинном шейдере она должна быть определённого формата. Какой именно формат текстуры требуется для использования в вашей видеокарте можно узнать с помощью DirectX Caps Viewer поставляемый в составе DirectX SDK. Например на моей GeForce 6600 можно использовать только 2 формата R32F и A32B32G32R32F. Опытным путём выяснилось что работает только с A32B32G32R32F.

Также важно указать правильный Usage при загрузке тектуры - он должен быть равен QueryVertexTexture. И того код загрузки карты высот должен быть примерно таким (MDX 2.0):

heightMap = new Texture( device,
   cfg.HeightMap,
   1024, 1024, 1,
   Usage.QueryVertexTexture,
   Format.A32B32G32R32F, Pool.Default,
   Filter.Linear,
   Filter.Linear,
   0, false, null);

В самом вершинном шейдере для обращения к текстурам можно использовать функцию:
tex2Dlod(heightMap, textureCoord);

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

2. ATI

К сожалению компания ATI в своих новых видеокартах по неизвестной мне причине решила не реализовывать поддержку вершинных текстур. В место этого они предложили альтернативную технологию под названием Render To Vertex Buffer (R2VB). Суть данной технологии заключается в том что на уровне видеокарты массивы вершин и текстуры представляются единообразно. Т.е. графическому процессору нет большой разницы что обрабатывать в вершинном шейдере – настоящий массив вершин или текстуру вместо него.

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

Подробное описание данной технологии и её применения выходит за рамки данной статьи. Я предлагаю всем желающим ознакомиться с материалами доклада о R2VB c КРИ2006.

6. Расширение метода

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

Ещё один пример использования в данном методе двух проходов - объединение нескольких глобальных карт высот в одну локальную. Максимальное разрешение текстур которое поддерживается современными видеокартами - 4096х4096. Это довольно много для карты высот. Но что если Вы захотите больше? :) В таком случае можно воспользоваться временем первого прохода чтобы собирать локальную карту высот для каждого кадра из нескольких глобальных карт (для тех случаев когда наблюдатель пересекает границу между несколькими глобальными картами).

Заключение

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

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

За рамки данной статьи вышло подробное описание реализации представленного метода. Это отдельная тема материалов по которой хватит на самостоятельную статью. Для опытных разработчиков чувствующих себя уверенно при работе с шейдерами в своих программах я приложил к статье код шейдера для NVIDIA. Для остальных пообещаю вторую статью про реализацию представленного метода на C# 2.0 и XNA, включая рендеринг в два прохода для фильтрации карты высот и объединения нескольких глобальных карт высот в одну локальную, автоматический выбор метода рендеринга (NVIDIA или ATI) при запуске приложения, а также некоторые подсказки по оптимизации и исправлению деффектов.

 

 
   Файл эффекта с шейдерами для NVIDIA