Рендеринг ландшафта при помощи GPU с использованием технологии LOD(Level Of Detail)

Рендеринг ландшафта при помощи GPU с использованием технологии LOD(Level Of Detail)


Введение

Несколько недель назад я приобрел замечательную книгу «Game Programming Gems 6». Меня заинтересовала глава 5.5 «Рендеринг ландшафта при помощи GPU». Заглянув в Интернет, я обнаружил довольно много вопросов по использованию LOD для наложения поверхности земли и решил реализовать метод, предложенный Гарольдом Вистенсом (Harald Vistens), на XNA Framework.

Почему же именно этот вариант? Потому что он великолепен. Для тех, кому интересно – загляните в книгу, упомянутую ранее по тексту.

Хотелось бы отметить несколько ключевых аспектов:

  1. Высокое качество рендеринга сильно нагружает оборудование. Мы же используем лишь по одному вершинному и индексному буферу на все решение, причем их размеры минимальны. Для блока размерностью 17×17 точек, буфер вершин будет содержать 1059 элементов, а индексный буфер — 2782 элементов типа short.
  2. Простейший алгоритм вычисления LOD использует простой критерий оценки, основанный на расстоянии до текстуры, при этом его можно с легкостью заменить другим. В приведенной реализации при помощи скроллинга мышкой можно изменить порог чувствительности критерия расстояния LOD и количество отображаемых примитивов. Разрывы между частями ландшафта удаляются при помощи вертикальной границы, называемой «юбка» или «бордюр» (skirt).

1 Бордюр

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

2 Квадродерево из ограничивающих параллелепипедов

  1. Подготовка данных для рендеринга не обязательна. В качестве источника использован обыкновенный файл текстур в формате D3DFMT_R32F. Файл в примере содержит изображение с разрешением 2048×2048. Важно отметить, что не все видеокарты способны отображать текстуры с разрешением более 2048 точек. Также используется файл с нормалями, который можно заменить программной реализацией вычисления нормалей.
  2. Вся реализация располагается в одном классе. Она легко добавляется в любое приложение.

3 Рендеринг при стандартыных настройках @400000 полигонов при 85 FPS(кадрах в секунду)

4 Рендеринг 3.5 миллионов полигонов при 61 FPS

Реализация

Основная идея алгоритма выполнена с использованием выборки вершинных текстур (см. описание http://msdn.microsoft.com/library/default.asp?url=/library/enus/directx9_c/directx/graphics/reference/assemblylanguageshaders/vertexshaders/vertextextures.asp), реализованной в Shader Model 3.0. Ландшафт разделяется на определенное количество участков, в каждом из которых число вершин постоянно. Вершины формируют регулярную сетку, которая создается единожды на этапе инициализации, определяющим параметром является ее размер.

Расположение каждого участка определяется смещением по осям:

— BiasU

— BiasV,

а также масштабным коэффициентом:

-Scale

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

private
void Draw(float fBiasU, float fBiasV, float fScale, int iLevel)

{


// Установка уникальных координат

effect.Parameters[«fBiasU»].SetValue(fBiasU);

effect.Parameters[«fBiasV»].SetValue(fBiasV);

effect.Parameters[«fScale»].SetValue(fScale);


// Отрисовка

effect.Begin();


foreach (EffectPass pass
in effect.CurrentTechnique.Passes)

{


pass.Begin();

device.DrawIndexedPrimitives(

PrimitiveType.TriangleStrip,

0,

0,

vertexCount,

0,

indexCount);


pass.End();

}

effect.End();

}

Код вершинного шейдера:

struct VS_OUTPUT

{


float4 position : POSITION0;


float4 uv : TEXCOORD0;

}

VS_OUTPUT Transform(float4 position : POSITION0)

{


// Далее uv будет использоваться как TEXCOORD


float4 uv = float2(position.x * fScale + fBiasU,

position.z * fScale + fBiasV, 0, 0);


// но для преобразования в текстурные координаты используется float4

position.x = uv.x * displacementWidth;

position.z = uv.y * displacementHeight;


// Высота определяется на основе информации, полученной о текстуре


// и отмасштабированной

position.y *= tex2Dlod ( displacementSampler, uv ).x * maxHeight;


//Вычисление окончательной позиции, умножением матрицы пространства, вида и


//проекции

Out.position = mul(position, worldViewProj);

Out.uv = uv;

}

Level of Detail

Алгоритм LOD(Изменение уровня детализации) основан на принципе квадродерева. Все поверхности изначально представлены в форме одного участка, разделенного на заданное число точек. На GPU эти данные отсылаются значениями VertexBuffer и IndexBuffer. Производится расчет положения центра этого сектора и расстояние от центра до камеры. Если полученная величина расстояния удовлетворяет установленному критерию, то все поверхности разделяются на четыре части. Процесс деления продолжается пока расстояние от центра следующего участка до камеры удовлетворяет критерию отбора. Далее производится отображение этого участка. Таким образом, мы имеем квадродерево, рассчитанное по определенному критерию, в данном случае — по расположению камеры к центру участка. Размер поверхности не имеет особого значения, так как мы оперируем лишь со смещением и масштабом участка, а эти значения всегда лежат в пределах от 0 до 1. Изначально смещение равно 0.0, а масштаб 1.0.

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

Для построения квадродерева наиболее эффективно использовать рекурсию:

private
void Render(float fMinU, float fMinV, float fMaxU,


float fMaxV, int iLevel, float fScale)

{


float fHalfU = (fMinU + fMaxU) / 2.0f;


float fHalfV = (fMinV + fMaxV) / 2.0f;


bool criterioResult = evaluateCriterion(fHalfU, fHalfV, fMinU, fMaxU, iLevel);


if (criterioResult)

Draw(fMinU, fMinV, fScale, iLevel);


else

{


// Продолжаем разделение. Уменьшаем масштаб…

fScale = fScale / 2.0f;


// и переходим на следующий кровень.

iLevel—;


// Продолжаем процесс для каждого квадранта

Render(fMinU, fMinV, fHalfU, fHalfV, iLevel, fScale);

Render(fHalfU, fMinV, fMaxU, fHalfV, iLevel, fScale);

Render(fMinU, fHalfV, fHalfU, fMaxV, iLevel, fScale);

Render(fHalfU, fHalfV, fMaxU, fMaxV, iLevel, fScale);

}

}

Разрывы

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

protected
void generateStructures()

{

vertices = new VertexPosition[this.BlockSize * this.BlockSize +

2 * this.BlockSize + 2 * this.BlockSize — 4];


float invScaleX = 1.0f / (this.BlockSize — 1.0f);


float invScaleZ = 1.0f / (this.BlockSize — 1.0f);


int indx = 0;


float s, t;

VertexPosition v;


// Заполнение регулярной сетки


for (int i = 0; i < this.BlockSize; i++)

{

t = i * invScaleZ;


for (int j = 0; j < this.BlockSize; j++)

{

s = j * invScaleX;

v = new VertexPosition();

v.Position = new Vector3(s, 1, t);

vertices[indx++] = v;

}

}


// Заполнение бордюра


// верх. слева направо

t = 1.0f;


for (int j = 0; j < this.BlockSize; j++)

{

s = j * invScaleX;

v = new VertexPosition();

v.Position = new Vector3(s, -1, t);

vertices[indx++] = v;

}


// правая часть. сверху вниз

s = 1.0f;


for (int i = this.BlockSize — 2; i > 0; i—)

{

t = i * invScaleZ;

v = new VertexPosition();

v.Position = new Vector3(s, -1, t);

vertices[indx++] = v;

}


// низ. справа налево

t = 0.0f;


for (int j = this.BlockSize — 1; j >= 0; j—)

{

s = j * invScaleX;

v = new VertexPosition();

v.Position = new Vector3(s, -1, t);

vertices[indx++] = v;

}


// левая часть. снизу вверх. за исключением последнего

s = 0.0f;


for (int i = 1; i < this.BlockSize — 1; i++)

{

t = i * invScaleZ;

v = new VertexPosition();

v.Position = new Vector3(s, -1, t);

vertices[indx++] = v;

}


//

vertexCount = vertices.Length;

vb = new Microsoft.Xna.Framework.Graphics.VertexBuffer(device,

VertexPosition.SizeInBytes * vertices.Length, BufferUsage.WriteOnly);

vb.SetData<VertexPosition>(vertices);

vertexDecl = new VertexDeclaration(device, VertexPosition.VertexElements);


//


short uiNumStrips = (short)(BlockSize — 1);

indices = new
int[2 * this.BlockSize * (this.BlockSize — 1) + 8 * (this.BlockSize — 1)];

indx = 0;


int z = 0;


// Заполнение регулярной сетки


while (z < this.BlockSize — 1)

{


for (short x = 0; x < this.BlockSize; x++)

{

indices[indx++] = x + z * this.BlockSize;

System.Console.WriteLine(indx + «: « + indices[indx — 1]);

indices[indx++] = x + (z + 1) * this.BlockSize;

}

z++;


if (z < this.BlockSize — 1)

{


for (int x = this.BlockSize — 1; x >= 0; x—)

{

indices[indx++] = x + (z + 1) * this.BlockSize;

indices[indx++] = x + z * this.BlockSize;

}

}

z++;

}


// верх. слева направо


int num = this.BlockSize * this.BlockSize;


// top, left to right


for (int i = 0; i < this.BlockSize; i++)

{

indices[indx++] = num++;

indices[indx++] = (this.BlockSize — 1) * this.BlockSize + i;

}


// правая часть. сверху вниз


for (int j = 2; j < this.BlockSize; j++)

{

indices[indx++] = num++;

indices[indx++] = (this.BlockSize — j) * this.BlockSize + this.BlockSize — 1;

}


// низ. справа налево


for (int i = 0; i < this.BlockSize; i++)

{

indices[indx++] = num++;

indices[indx++] = this.BlockSize — i — 1;

}


// левая часть. снизу вверх


for (int j = 1; j < this.BlockSize — 1; j++)

{

indices[indx++] = num++;

indices[indx++] = j * this.BlockSize;

}


//

indexCount = indices.Length — 2;

ib = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly);

ib.SetData(indices);

}

Для минимизации количества данных, отсылаемых на GPU, используется структура VertexPosition, содержащая лишь позицию в Vector3. UV-значения размещаются в XZ переменных. Производится расчет обычной вершины при значении Y равном 1 (или больше 0), и вершины бордюра при Y равном -1 (или меньше 0). Данное решения позволяет автоматически рендерить участок вместе с бордюром без дополнительных временных затрат на вершинном шейдере.

// Заполнение для обычной решетки

v.Position = new Vector3(s, 1, t);

// Заполнение для бордюра

v.Position = new Vector3(s, -1, t);

Отсечение по пирамиде видимости

Данная технология используется для уменьшения количества данных, отправляемых на GPU. Для каждого куска рассчитывается BoundingBox, а также производится проверка на попадание его в область видимости камеры. Если кусок не попадает – происходит выход из рекурсии.

private
void Render(float fMinU, float fMinV, float fMaxU,


float fMaxV, int iLevel, float fScale)

{

Vector3 Min = new Vector3(fMinU * displacementTexture.Width,

0, fMinV * displacementTexture.Height);

Vector3 Max = new Vector3(fMaxU * displacementTexture.Width,

MAX_ELEVATION, fMaxV * displacementTexture.Height);

BoundingBox boundingBox = new BoundingBox(Min, Max);


if (camera.Frustum != null &&

camera.Frustum.Contains(boundingBox) == ContainmentType.Disjoint)

{

NumCulled++;


return;

}


Вычисление нормалей

Получить нормали к поверхности можно несколькими способами – чтением из внешней текстуры или вычислением в VertexShader в реальном времени, что увеличивает нагрузку на GPU. В примере использован первый способ.

Формат файла Карты высот

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

Управление

  • W, A, S, D – движение камеры
  • Q, Z – высота камеры
  • Колесико мыши – изменение LOD
  • R – вкл/выкл отображение BoundingBox (ограничивающего параллелепипеда) для каждого участка
  • T – переключить FillMode (режим заполнения): сплошной или в виде каркаса

Оригинал:

http://www.ziggyware.com/readarticle.php?article_id=254

Исходные тексты примера доступны по адресу: http://www.ziggyware.com/downloads.php?cat_id=4&download_id=137

Реклама
Запись опубликована в рубрике Uncategorized. Добавьте в закладки постоянную ссылку.

3 комментария на «Рендеринг ландшафта при помощи GPU с использованием технологии LOD(Level Of Detail)»

  1. Alex:

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

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s