
Разбираюсь в этой теме сам и заодно помогу другим. Есть прекрасная статья, демонстрирующая реальное применение аппаратной тесселяции на различных примерах. Привожу вольный пересказ (который никак не относится к самой главе и к её содержанию, все совпадения — случайны) этой статьи о генерации ландшафта + даю некоторые ссылки, которые объясняют те или иные термины. В конце статьи будет ссылка на готовый пример. Всё, что в тексте будет курсивом или оформлено как спойлер — это мои дополнения, которые я разжёвывал прежде всего для себя. Если кто-то читает эту статью и ему важен прежде всего её смысл — не читайте текст курсивом.
Чтобы продемонстрировать потенциал использования тесселяции, мы рассмотрим простую систему рендеринга местности, основанную на четырёхугольных патчах и displayment mapping. Displaysment map (карта сдвигов) — это текстура, которая содержит сдвиги поверхности в каждой точке. [spoiler title=’Что такое displayment mapping на примере’ style=’green’ collapse_link=’true’]Для примера — мы создали некоторую сетку, из координат которой составили некоторую плоскость. Она растянута в нашей 3D-сцене по координатам x и z. Координата y по всей поверхности — занулена. Тем самым мы получаем гладкую 2D-поверхность из квадратов. Координату y нужно как-то обработать и сделать это таким образом, чтобы на этой поверхности возникли горы и овраги, и всё это было более-менее плавно. Для этого используется карта высот (некоторая структура, хранящая значение координаты y для каждой точки плоскости) и displayment mapping, обычно, подразумевает формирование такой карты высот через текстуру, загруженную в вершинный шейдер. [/spoiler]
Каждый патч в данном примере представляет собой небольшую область ландшафта, которая тесселирована в зависимости от её площади на экране (похожий пример есть в предыдущей статье на этом сайте, где тесселяция производилась в зависимости от удаление камеры от объекта, а не от его площади). Каждая тесселированная вершина перемещается вместе с касательной к поверхности по значению, хранящемуся на карте смещения. Это добавляет небольшой геометрический объект к поверхности без необходимости явно сохранять координаты каждой тесселированной вершины. Смещения по координате y (т.е. смещения плоскости) хранятся на карте высот и применяются во время выполнения шейдера оценки тесселяции (TES). Displayment map (так же её можно назвать как карту высот) используется в данном примере в качестве данной текстуры:

К слову говоря, подобные текстуры можно сгенерировать с помощью различных шумов, например — шумом Перлина, который поминался в предыдущей статье. Его я подробно разберу в одной из следующих.
Наш первый шаг — написать простой вершинный шейдер. Для каждого патча будут использоваться 4 CPs, образующих обычный квадрат, поэтому мы можем использовать константные координаты, заданные напрямую в вершинном шейдере (что, к слову, позволит не передавать данные напрямую на GPU — и это хорошо). Готовый код шейдера представлен ниже
#version 430 core out VS_OUT { vec2 tc; } vs_out; void main(void) { const vec4 vertices[] = vec4[](vec4(-0.5, 0.0, -0.5, 1.0), vec4( 0.5, 0.0, -0.5, 1.0), vec4(-0.5, 0.0, 0.5, 1.0), vec4( 0.5, 0.0, 0.5, 1.0)); int x = gl_InstanceID & 63; int y = gl_InstanceID >> 6; vec2 offs = vec2(x, y); vs_out.tc = (vertices[gl_VertexID].xz + offs + vec2(0.5)) / 64.0; gl_Position = vertices[gl_VertexID] + vec4(float(x - 32), 0.0, float(y - 32), 0.0); }
Константные координаты представляют собой патч отображаемый в XZ плоскости, через центр которого проходит начало координат. Шейдер использует номер каждого патча (хранящийся в gl_InstanceID) для вычисления смещения по плоскости. В данном примере мы генерируем сетку 64×64 патча и смещения, описанные в шейдере как переменные x и y вычисляются путём элементарных поразрядных операций. Вершинный шейдер высчитывает на основе смещения текстурные координаты для патча, которые передаются в шейдер управления тесселяцией как vs_out.tc и в последующем будут использованы для изменения y координаты нашей плоскости в текущей точке.
Дальше мы займёмся шейдером управления тесселяцией. Готовый код шейдера приводится ниже:
#version 430 core layout (vertices = 4) out; in VS_OUT { vec2 tc; } tcs_in[]; out TCS_OUT { vec2 tc; } tcs_out[]; uniform mat4 mvp; void main(void) { if (gl_InvocationID == 0) { vec4 p0 = mvp * gl_in[0].gl_Position; vec4 p1 = mvp * gl_in[1].gl_Position; vec4 p2 = mvp * gl_in[2].gl_Position; vec4 p3 = mvp * gl_in[3].gl_Position; p0 /= p0.w; p1 /= p1.w; p2 /= p2.w; p3 /= p3.w; if (p0.z <= 0.0 || p1.z <= 0.0 || p2.z <= 0.0 || p3.z <= 0.0) { gl_TessLevelOuter[0] = 0.0; gl_TessLevelOuter[1] = 0.0; gl_TessLevelOuter[2] = 0.0; gl_TessLevelOuter[3] = 0.0; } else { float l0 = length(p2.xy - p0.xy) * 16.0 + 1.0; float l1 = length(p3.xy - p2.xy) * 16.0 + 1.0; float l2 = length(p3.xy - p1.xy) * 16.0 + 1.0; float l3 = length(p1.xy - p0.xy) * 16.0 + 1.0; gl_TessLevelOuter[0] = l0; gl_TessLevelOuter[1] = l1; gl_TessLevelOuter[2] = l2; gl_TessLevelOuter[3] = l3; gl_TessLevelInner[0] = min(l1, l3); gl_TessLevelInner[1] = min(l0, l2); } } gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; tcs_out[gl_InvocationID].tc = tcs_in[gl_InvocationID].tc; }
В этом примере основной алгоритм вызывается только в первый вызов, для патча с нулевым ID. Как только мы определили, что gl_InvocationID равен нулю — мы определяем уровни тесселяции для всего патча. Сначала мы переводим CPs патча в нормализованные координаты устройства (NDC) — т.е. в диапазон [-1.0; 1.0]. Для этого сначала переводим координаты в пространство отсечения, умножая их на матрицы проекции+вида+модели, а затем разделим их на соответствующие им гомогенные координаты. После чего мы вычисляем длину каждой стороны патча (в голове патч мы представляем как четырехугольник), в проекции на оси XY, координата Z при вычислении длины игнорируется. Затем вычисляем уровни тесселяции для каждой грани патча в зависимости от её длины.
Так же можно заметить проверку z-компонент у каждой из координат на отрицательность. Эта проверка позволяет не отображать патчи, которые находятся за камерой.
После того, как уровни тесселяции рассчитаны, TCH просто копирует позиции вершин и передаёт их из входа на выход. В общем и целом, шейдер управления тесселяцией имеет доступ ко всем входным CPs, но обрабатывает их только в первый раз. Вызывается он для каждой CP и, соответственно, копирует входные данные и передаёт их на выход для каждой CP.
Далее, выходные координаты принимает шейдер оценки тесселяции, код которого приведён ниже:
#version 430 core layout (quads, fractional_odd_spacing) in; uniform sampler2D tex_displacement; uniform mat4 mvp; uniform float dmap_depth; in TCS_OUT { vec2 tc; } tes_in[]; out TES_OUT { vec2 tc; } tes_out; void main(void) { vec2 tc1 = mix(tes_in[0].tc, tes_in[1].tc, gl_TessCoord.x); vec2 tc2 = mix(tes_in[2].tc, tes_in[3].tc, gl_TessCoord.x); vec2 tc = mix(tc2, tc1, gl_TessCoord.y); vec4 p1 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x); vec4 p2 = mix(gl_in[2].gl_Position, gl_in[3].gl_Position, gl_TessCoord.x); vec4 p = mix(p2, p1, gl_TessCoord.y); p.y += texture(tex_displacement, tc).r * dmap_depth; gl_Position = mvp * p; tes_out.tc = tc; }
Шейдер оценки тесселяции сначала высчитывает координаты текстуры линейно интерполируя их с текущей координатой в точке X. Затем интерполирует полученные значения с координатой в точке Y. Затем он интерполирует позиции входных CP с позицией в текущей точке для создания координаты исходящей вершины. После чего он использует координаты текстуры и смещает координату Y в полученном направлении (этот момент весьма сложен для понимания, поэтому если кому-нибудь надо будет — могу его потом зарисовать и прикрепить к переводу). Полученная позиция объявляется выходной и умножается на mvp-матрицу для получения выходной координаты в пространстве отсечения.
После чего, помимо gl_Position во фрагментый шейдер отправляется так же координата в текущей точке, интерполирующая Y по смещению, полученному в TCS. Код фрагментного шейдера представлен ниже:
#version 430 core out vec4 color; layout (binding = 1) uniform sampler2D tex_color; in TES_OUT { vec2 tc; } fs_in; void main(void) { color = texture(tex_color, fs_in.tc); }
Фрагментный шейдер просто накладывает на текущий фрагмент тексель, исходя из переданной в него координате этой текстуры.
В результате все треугольники, отображаемые на экране должны получиться примерно аналогичной площади и резкие переходы на уровне тесселяции не будут отображаться в конечной визуализации.
Что получилось у автора — можно посмотреть в его статье.
Автор использует текстуры формата .ktx , которые у меня навязчиво не хотели загружаться (потому что у автора своя обёртка над загрузкой, а у меня её нет и внедрять я её не хотел), поэтому текстуры я брал самые обычные: .jpg-формата. Текстура для формирования рельефа — можно взять у автора, можно же найти самому или сгенерировать с помощью шума Перлина (об этом в следующей статье). Текстуру для окрашивания объекта — можно взять любую, на ваш выбор. Я взял первую попавшуюся из сети:
Текстура размером 512×512, поэтому при интерполировании её на сетку графика вышла прямиком из 2003. Но для примера и первого опыта — более чем хорошо. Загружал я текстуры через старый (очень старый) и добрый SOIL, но можно использовать любые библиотеки.
Результат вышел таким:

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