Вольный перевод главы «Tesselation Example — Terrain Rendering» или коротко о том, как сгенерировать ландшафт с помощью аппаратной тесселяции.

Совушка
Всем прекрасной графики!

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

Чтобы продемонстрировать потенциал использования тесселяции, мы рассмотрим простую систему рендеринга местности, основанную на четырёхугольных патчах и 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 патча и смещения, описанные в шейдере как переменные и вычисляются путём элементарных поразрядных операций. Вершинный шейдер высчитывает на основе смещения текстурные координаты для патча, которые передаются в шейдер управления тесселяцией как 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, но можно использовать любые библиотеки. 

Результат вышел таким:

Аве, ландшафт. Я с тобой знатно за..мучился.

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

Картинки по запросу Графика старые игры

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *