OpenGL communityСтатьи

Динамическая тесселяция ландшафта

Автор:

Введение
Описание примеров.
1. Рендеринг ландшафта используя displacement mapping.
2. Стыковка патчей с разным уровнем тесселяции.
3. Использование предварительно сгенерированной текстуры с уровнем тесселяции.
4. Увеличение детализации в зависимости от расстояния до камеры.
5. Увеличение детализации в зависимости от размера патча на экране.
6. Отсечение невидимых патчей.
Заключение
Исходники

Введение


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

Описание примеров.


В каждой главе статьи используются свои шейдеры, для переключения между примерами используются клавиши F1..F6, для переключения режимов служат клавиши 1..8 или ( и ) - переключают на предыдущий и следующий режим соответственно. В качестве дополнительной возможности можно просматривать нормали и уровень тесселяции, для этого используются клавиши C, N, T, M - для вывода текстуры цвета, нормали, уровня тесселяции и смешанный режим: цвет и уровень тесселяции. Клавиша R служит для перезагрузки текущих шейдеров, P переключает между отображением сетки и полигонов.
В примерах, где поддерживается изменение детализации используются следующие клавиши:
- и + изменяет максимальный уровень тесселяции,
< и > изменяет уровень детализации,
[ и ] изменяет высоту ландшафта.
Для перемещения используются клавиши W, S, A, A, для движения по вертикали - Shift, Space.
В заголовке окна отображается выбраная часть примера, соответствующая части статьи, количество кадров в секунду, количество выведенных и сгенерированных вершин.

1. Рендеринг ландшафта используя displacement mapping.


Здесь как и в других примерах рисуется сетка размером 128х128 вершин, используются треугольные или квадратные патчи (в зависимости от примера). Квадратные патчи занимают меньше места - для одного патча используется 4 индекса против 6 для треугольного патча.
Перед отрисовкой сетки устанавливается размер патча:
glPatchParameteri( GL_PATCH_VERTICES, 4 )

По умолчанию размер патча равен 3. Если не установить правильный размер патча, например поставить 3, когда в шейдере определен как 4, то ошибок не возникнет, но рисоваться будет неправильно.
Для избежания подобных ошибок можно получать значение патча из шейдера функцией glGetProgramiv с параметром GL_TESS_CONTROL_OUTPUT_VERTICES, вернется значение определенное в control шейдере в строке:

layout(vertices = 4) out;

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

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

2. Стыковка патчей с разным уровнем тесселяции.


Что бы сохранить качество разбиения поверхности нужно использовать переменную детализацию, но при этом возникает проблема - соседние патчи могут иметь разную детализацию в результате чего между ними получаются разрывы. Поэтому для правильной стыковки патчей с разной детализацией используется параметр gl_TessLevelOuter.
В примере для каждой вершины в вершинном шейдере задается случайное значение уровня тесселяции, переключая
режимы клавишами 1 и 2 можно включать и отключать правильную стыковку. Результат неправильной стыковки: Изображение
На скриншоте видны множественные разрывы между патчами с разной детализацией.

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

max( Input[1].fLevel, Input[2].fLevel )

Можно использовать любые функции расчета, результаты которых для одинаковых ребер будут совпадать.


Треугольные патчи.
Для треугольных патчей используются только три значения из gl_TessLevelOuter.
На нулевой индекс в gl_TessLevelOuter влияют вершины с индексом 1 и 2, на первый - 0 и 2, на второй - 0 и 1.
Пример:

gl_TessLevelOuter[0] = max( Input[1].fLevel, Input[2].fLevel );
gl_TessLevelOuter[1] = max( Input[0].fLevel, Input[2].fLevel );
gl_TessLevelOuter[2] = max( Input[0].fLevel, Input[1].fLevel );


Квадратные патчи.
Для квадратных патчей используются все четыре значения из gl_TessLevelOuter.
На нулевой индекс в gl_TessLevelOuter вилияют вершины с индексом 0 и 3, на первый - 0 и 1, на второй - 1 и 2, на третий - 2 и 3.
Пример:

gl_TessLevelOuter[0] = max( Input[0].fLevel, Input[3].fLevel );
gl_TessLevelOuter[1] = max( Input[0].fLevel, Input[1].fLevel );
gl_TessLevelOuter[2] = max( Input[1].fLevel, Input[2].fLevel );
gl_TessLevelOuter[3] = max( Input[2].fLevel, Input[3].fLevel );


Значение gl_TessLevelInner могут быть любыми - они никак не влияют на правильную стыковку,
но для равномерной тесселяции желательно брать максимальное или среднее значение уровней
тесселяции ребер (gl_TessLevelOuter), например:

float   max_level = max( max( Input[0].fLevel, Input[1].fLevel ),
                         max( Input[2].fLevel, Input[3].fLevel ) );
gl_TessLevelInner[0] = max_level;
gl_TessLevelInner[1] = max_level;

Стоит отметить, что при записи нулей в gl_TessLevelInner и gl_TessLevelOuter патч не создается,
это свойство можно использовать для отсечения патчей, но так же стоит следить, чтобы
отсечение не произошло случайно, поэтому в примере используется функция clamp:

clamp( Rand( inPosition ) * unMaxTessLevel, 1.0, unMaxTessLevel )

unMaxTessLevel - максимальный уровень тесселяции, определяется в приложении.

3. Использование предварительно сгенерированной текстуры с уровнем тесселяции.


В этом примере используется предварительно сгенерированная текстура с уровнем детализации для каждой вершины. Для хранения уровня детализации вполне хватит текстуры формата R8 разрешением 128х128 (один тексель на вершину).

Код фрагментного шейдера gen_normal_and_tesslvl.prg состоит из двух функций:
ReadHeight - считывает текстуру в матрицу 4х4, для этого используются 4 вызова функции textureGatherOffsets.
GenTessLevel - функция генерации уровня детализации, расчитываются перепады высот между точками в два прохода: по вертикали и по горизонтали, результат нормализуется и записывается в текстуру.

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

4. Увеличение детализации в зависимости от расстояния до камеры.


Наибольшая детализация нужна вблизи камеры, а чем дальше от камеры, тем меньше детализация, в этом примере используется линейное изменение детелизации от расстояния до камеры.
Уровень детализации для вершины расчитывается в вершинном шейдере, для этого служит функция Level:

float Level(float dist)
{
    return clamp( unDetailLevel / dist - 2.0, 1.0, unMaxTessLevel );
}

unDetailLevel - уровень детализации.
dist - расстояние от камеры до вершины, расчитывается оно так:

vec4    pos         = unMVPMatrix * vec4( gl_Position.xyz +
                      texture( unHeightMap, Output.vTexcoord1 ).r *
                      Output.vNormal * unHeightScale, 1.0 );
Output.fLevel       = Level( length(pos) );

Для правильного расчета расстояния необходимо переместить вершину на значение определенное в карте высот по нормале к поверхности и спроецировать в пространство экрана. После чего расчитывается расстояние до вершины. Так как камера всегда находится в центре координат, то достаточно узнать длинну вектора.
Это все изменения, которые были внесены в программу, все достаточно просто и количество полигонов уменьшилось более чем в 10 раз.

Изображение

5. Увеличение детализации в зависимости от размера патча на экране.


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

vec4    pos         = unMVPMatrix * vec4( gl_Position.xyz +
                      texture( unHeightMap, Output.vTexcoord1 ).r *
                      Output.vNormal * unHeightScale, 1.0 );
Output.vScrCoords   = pos.xy / pos.w;

В control шейдере расчитывается уровень тесселяции для каждого ребра:

gl_TessLevelOuter[0] = Level( Input[1].vScrCoords, Input[2].vScrCoords );
gl_TessLevelOuter[1] = Level( Input[0].vScrCoords, Input[2].vScrCoords );
gl_TessLevelOuter[2] = Level( Input[0].vScrCoords, Input[1].vScrCoords );
gl_TessLevelInner[0] = max( max( gl_TessLevelOuter[0], gl_TessLevelOuter[1] ),
                                 gl_TessLevelOuter[2] );

Функция Level возвращает уровень детализации в зависимости от размера ребра:

float Level(in vec2 p0, in vec2 p1)
{
    return clamp( distance( p0, p1 ) * unDetailLevel * 0.01, 0.1, unMaxTessLevel );
}

unDetailLevel - уровень детализации, определяемый в приложении, как и в прошлом примере. Его значение
слишком велико, поэтому используется коэффициент 0.01.


У этого подхода есть несколько недостатков:
1. При движении меняется детализация сразу всех патчей - это вызывает мелькание, частично
исправляется использованием высокой детализации и другого типа разбиения:

layout(triangles, fractional_even_spacing) in;

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

Изображение


6. Отсечение невидимых патчей.


В этом примере добавленно отсечение невидимых патчей для двух динамических способов расчета детализации. Изменения в них будут одинаковыми, поэтому разбираться будет только один пример. Клавиши 1 и 2 служат для переключения между примерами.
В вершинный шейдер добавлена функция проверки попадания вершины на экран:
bool InScreen(in vec2 pos)
{
    const float     size = 1.2;
    return ( abs(pos.x) <= size && abs(pos.y) <= size );
}

size - определяет размер экрана, это не совсем константа - может принимать значения от 1.0 и больше.
Чем больше параметр size тем больше полигонов вблизи края экрана будут видны, в том числе те,
которые точно не попадают в экран. Значение 1.0 дает лучшее отсечение и плохую точность - по краям
часто отсекается лишнее. Значение 1.2 дает хорошую точность и минимальный оверхэд. Чтобы посмотреть как происходит отсечение патчей можно установить значение size меньше 1, результат для значения 0.7 паказан на скриншоте:
Изображение

Результат проверки и экранные координаты вершины передаются в control шейдер:

Output.vScrCoords   = pos.xy / pos.w;
Output.bInScreen    = InScreen( Output.vScrCoords );

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

Проверка будет достаточно простая - проверить пересечение патча с экраном в экранных координатах:

bool QuadInScreen()
{
    const float     size = 1.2;
    
    vec4    screen  = vec4( -size, -size, size, size );
    vec4    rect    =  Rect( Input[0].vScrCoords, Input[1].vScrCoords,
                             Input[2].vScrCoords, Input[3].vScrCoords );
    return  ( rect[0] < screen[2] && rect[2] > screen[0] &&
              rect[1] < screen[3] && rect[3] > screen[1] ) ||
            ( screen[2] < rect[0] && screen[0] > rect[2] &&
              screen[3] < rect[1] && screen[1] > rect[3] );
}

Функция Rect расчитывает AABB для вершин патча, константа size та же что и в функции InScreen. Здесь используется алгоритм проверки пересечения прямоугольников.

Ну а дальше все просто:

bool    in_screen = any( bvec4( Input[0].bInScreen, Input[1].bInScreen,
                                Input[2].bInScreen, Input[3].bInScreen ) );
float   max_level = max( max( Input[0].fLevel, Input[1].fLevel),
                         max( Input[2].fLevel, Input[3].fLevel ) );
float   k = ( in_screen || QuadInScreen() ) ? 1.0 : 0.0;
        
gl_TessLevelInner[0] = max_level * k;
gl_TessLevelInner[1] = max_level * k;
gl_TessLevelOuter[0] = max( Input[0].fLevel, Input[3].fLevel ) * k;
gl_TessLevelOuter[1] = max( Input[0].fLevel, Input[1].fLevel ) * k;
gl_TessLevelOuter[2] = max( Input[1].fLevel, Input[2].fLevel ) * k;
gl_TessLevelOuter[3] = max( Input[2].fLevel, Input[3].fLevel ) * k;

Если патч невиден, то k принимает значение ноль и при умножении на уровень тесселяции
получается тоже ноль, в результате патч отсекается тесселятором.

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

Заключение


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

В заключение хочу поблагодарить Кирилла Баженова (aka bazhenovc) за помощь в написании примеров и статьи.

Исходники


Пример с исходниками: скачать.

#displacement mapping, #ландшафт, #тесселяция

12 августа 2012 (Обновление: 14 авг 2012)

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