Динамическая тесселяция ландшафта
Автор: /A\
Введение
Описание примеров.
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. При вытягивании полигона его ребра могут сильно отличаться в размерах.
В результате появляются области с низкой детализацией.
Чтобы не вызвать проблемы со стыковкой патчей, нельзя повышать детализацию этих ребер -
в соседних патчах не будет информации об этом. Решением данной проблемы может быть объединение с предыдущей техникой.