OpenGL communityСтатьи

Аппаратная тесселяция и displacement mapping в OpenGL

Автор:

Целью написания данной статьи была реализация простого displacement mapping`а с помощью аппаратной тесселяции и OpenGL.
Статья не претендует на полноту изложения теории.

В конце статьи у нас получится вот такая картинка:
gl4_displacementmapping | Аппаратная тесселяция и displacement mapping в OpenGL

Что это такое?
Тесселяция - это разбиение некоторого примитива на несколько меньших. В современном мире это делает видеокарта.

Работает это следующим образом:
Изображение
Из вершинного шейдера в тесселятор приходят патчи(GL_PATCHES, новый вид примитивов), тесселятор их разбивает и передаёт дальше в геометрический шейдер.

Также введены 2 новых типа шейдеров: tesselation control shader и tesselation evaluation shader, на рисунке показано, когда вызывается каждый.

Как это использовать?

Tesselation control shader (GL_TESS_CONTROL_SHADER) позволяет нам задавать параметры для тесселятора. Он вызывается для каждой вершины патча и имеет доступ ко всем его вершинам. Кроме этого, ему на вход поступают:

gl_in              массив вершин (состоит из gl_Position, gl_PointSize и gl_ClipDistance[])
gl_PatchVerticesIn количество вершин патча
gl_PrimitiveID     индекс примитива, по которому нужно записать выходные данные. Записывать по другому индексу нельзя.
gl_InvocationID    номер вершины

И на выход он отправляет:

gl_out             массив вершин
gl_TessLevelInner  данные для тесселятора, об этом ниже (массив из 4х float)
gl_TessLevelOuter  ещё данные для тесселятора (массив из 2х float)

gl_TessLevelOuter управляет разбиением границ примитива, каждое число отвечает за определённую сторону.
gl_TessLevelInner управляет разбиением внутренностей примитива.

Tesselation evaluation shader (GL_TESS_EVALUATION_SHADER) вызывается для каждой вершины, созданной тесселятором. Это как раз то место, где делается основная работа.
На вход он получает:

gl_in              массив вершин
gl_PatchVerticesIn количество вершин ИСХОДНОГО патча
gl_PrimitiveID     индекс примитива
gl_TessCoord       позиция вершины в патче (x, y, z)
gl_TessLevelInner  массив внутренних уровней тесселяции
gl_TessLevelOuter  массив внешних уровней тесселяции

На выходе у него одна вершина (gl_Position, gl_PointSize и gl_ClipDistance[])

Displacement mapping
Основная идея простая, как валенок: сгенерировать много-много примитивов и поднять каждую их вершину на определённую высоту (из карты высот).

За основу был взят урок №3 от KpeHDeJIb

В него были внесены небольшие изменения, а именно:
1. Добавлены нормали для кубика:

static const float cubeNormals[cubeVerticesCount][3] = {
  { 0, 0, 1}, { 0, 0, 1}, { 0, 0, 1}, { 0, 0, 1}, // front
  { 0, 0,-1}, { 0, 0,-1}, { 0, 0,-1}, { 0, 0,-1}, // back
  { 0, 1, 0}, { 0, 1, 0}, { 0, 1, 0}, { 0, 1, 0}, // top
  { 0,-1, 0}, { 0,-1, 0}, { 0,-1, 0}, { 0,-1, 0}, // bottom
  {-1, 0, 0}, {-1, 0, 0}, {-1, 0, 0}, {-1, 0, 0}, // left
  { 1, 0, 0}, { 1, 0, 0}, { 1, 0, 0}, { 1, 0, 0}  // right
};
...
{
  glBindBuffer(GL_ARRAY_BUFFER, cubeVBO[2]);
  glBufferData(GL_ARRAY_BUFFER, cubeVerticesCount * (3 * sizeof(float)),
    cubeNormals, GL_STATIC_DRAW);
  glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
  glEnableVertexAttribArray(2);
}

2. Добавлена поддержка новых шейдеров:

shaderProgram = ShaderProgramCreateFromFile("data/lesson", ST_VERTEX | ST_FRAGMENT | ST_TESSEVAL | ST_TESSCONTROL);

3. Добавлена карта высот:

colorTexture = TextureCreateFromTGA("data/texture.tga");
heightTexture = TextureCreateFromTGA("data/texture_height.tga");
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, heightTexture);
...
if ((colorTextureLocation = glGetUniformLocation(shaderProgram, "heightmap")) != -1)
  glUniform1i(colorTextureLocation, 1);

4. Изменён тип рисуемого примитива:

glDrawElements(GL_PATCHES, cubeIndicesCount, GL_UNSIGNED_INT, NULL);

Teh shaders!
Приступаем к самому интересному - шейдеры!

Их у нас будет 4 штуки - vertex, tesselation control, tesselation evaluation и fragment.

Первый шейдер - вершинный. Ничего особенного он не делает, только передаёт данные дальше.

#version 410 core

// mesh data
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texcoord;
layout (location = 2) in vec3 normal;

// to control shader
out VertexCS
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
} vertcs;

void main(void)
{
  vertcs.position = position;
  vertcs.texcoord = texcoord;
  vertcs.normal = normal;

  gl_Position = vec4(position, 1.0);
}

Следующий шейдер: tesselation control. Он тоже не очень интересный, принимает данные из вершинного, ставит уровни тесселяции по 64(каждая сторона патча будет разбита 64 раза) и передаёт всё это дальше в evaluation shader.

#version 410 core
#define id gl_InvocationID

layout(vertices = 3) out; // задаёт размер патча в 3 вершины
// from vertex shader
in VertexCS
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
} vertcs[];

// to evaluation shader
out VertexES
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
} vertes[];

void main(void)
{  
  vertes[id].position = vertcs[id].position;
  vertes[id].texcoord = vertcs[id].texcoord;
  vertes[id].normal = vertcs[id].normal;
  
  const int inner = 64;
  const int outer = 64;
  if (0 == id) {
    gl_TessLevelInner[0] = inner;
    gl_TessLevelInner[1] = inner;
    gl_TessLevelOuter[0] = outer;
    gl_TessLevelOuter[1] = outer;
    gl_TessLevelOuter[2] = outer;
    gl_TessLevelOuter[3] = outer;
  }
  gl_out [id].gl_Position = gl_in[id].gl_Position;
}

Далее у нас самый интересный шейдер - tesselation evaluation. Его давайте рассмотрим по кускам.
1. Входные/выходные данные:

#version 410 core

layout(triangles, equal_spacing) in; // указывает способ разбиения треугольников - разбиение ребер на равные части

uniform mat4 modelViewProjectionMatrix;

uniform sampler2D heightmap;

// from control shader
in VertexES
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
}
vertes[];

// to fragment shader
out VertexFS
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
} vertfs;

2. Вычисление данных для передачи во фрагментный шейдер. Из control shader`а нам пришли данные о исходном патче (in VertexES), на основе которых мы должны вычислить их же, но для текущей вершины. Делается это простой интерполяцией:

vec2 interpolate2D(vec2 v0, vec2 v1, vec2 v2)
{
  return vec2(gl_TessCoord.x) * v0 + vec2(gl_TessCoord.y) * v1 + vec2(gl_TessCoord.z) * v2;
}

vec3 interpolate3D(vec3 v0, vec3 v1, vec3 v2)
{
  return vec3(gl_TessCoord.x) * v0 + vec3(gl_TessCoord.y) * v1 + vec3(gl_TessCoord.z) * v2;
}
                  
void main(void)
{
  vertfs.position = interpolate3D(vertes[0].position, vertes[1].position, vertes[2].position);
  vertfs.texcoord = interpolate2D(vertes[0].texcoord, vertes[1].texcoord, vertes[2].texcoord);  
  vertfs.normal = normalize(interpolate3D(vertes[0].normal, vertes[1].normal, vertes[2].normal));

3. Вычисляем позицию вершины, смещаем её и переводим в screen space:

  gl_Position = modelViewProjectionMatrix * (
          vec4(interpolate3D(gl_in[0].gl_Position.xyz, gl_in[1].gl_Position.xyz, gl_in[2].gl_Position.xyz), 1.0) -
          vec4(vertfs.normal, 1) * (texture(heightmap, vertfs.texcoord) / 4.0)
    );
}

Волшебное число 4 было подобрано экспериментальным путём, чтобы выглядело красивее.

И, напоследок, тривиальный фрагментный шейдер:

#version 410 core

uniform sampler2D colorTexture;
uniform sampler2D heightmap;

// from evaluation shader
in VertexFS
{
  vec3 position;
  vec2 texcoord;
  vec3 normal;
} vert;

layout(location = 0) out vec4 color;

void main(void)
{
  color = texture(colorTexture, vert.texcoord) * (1 - texture(heightmap, vert.texcoord).r);
}

Полученный результат можно лицезреть на картинке в начале статьи.

Титры
Полный пример с исходным кодом можно скачать тут: gl4_tesselation (перезалито)
Для дальнейшего просветления советую почитать статью Алексея Борескова тут

Спасибо за внимание!

#3D, #displacement mapping, #графика, #OpenGL

5 августа 2012 (Обновление: 3 дек 2012)

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