Между этапами вершинного и фрагментного шейдеров существует еще один дополнительный этап графического конвейера под названием геометрический шейдер.
Геометрические шейдеры
В качестве входных данных для геометрического шейдера используется набор вершин, образующих отдельно взятый примитив, например, точку или треугольник. Затем, перед отправкой на следующий этап, шейдер может преобразовать эти вершины по своему усмотрению. Для нас интерес использования геометрического шейдера заключается в том, что он способен преобразовать исходный примитив (набор вершин) в совершенно другие примитивы, добавляя при этом новые вершины к уже существующим.
Давайте рассмотрим следующий пример геометрического шейдера:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#version 330 core layout (points) in; layout (line_strip, max_vertices = 2) out; void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); } |
В начале необходимо объявить тип примитивов входных данных, которые мы получим от вершинного шейдера. Для этого перед ключевым словом in используется спецификатор layout. Аргументы данного спецификатора могут принимать любое из следующих значений:
points
— при визуализации примитивов GL_POINTS
(1);
lines
— при визуализации примитивов GL_LINES
или GL_LINE_STRIP
(2);
lines_adjacency
— GL_LINES_ADJACENCY
или GL_LINE_STRIP_ADJACENCY
(4);
triangles
— GL_TRIANGLES
, GL_TRIANGLE_STRIP
или GL_TRIANGLE_FAN
(3);
triangles_adjacency
— GL_TRIANGLES_ADJACENCY
или GL_TRIANGLE_STRIP_ADJACENCY
(6).
Это практически весь список примитивов, которые мы можем использовать в вызовах функций рендеринга, подобных glDrawArrays(). Если бы мы решили визуализировать вершины в виде GL_TRIANGLES
, то должны были бы выбрать аргумент triangles
. Число в скобках определяет минимальное количество вершин, которое содержит отдельно взятый примитив.
Нам также нужно указать тип примитива, который будет выводиться геометрическим шейдером с помощью спецификатора layout
перед ключевым словом out
. Как и в случае с входными данными, спецификатор выходных данных может принимать несколько различных значений типов примитивов:
points
line_strip
triangle_strip
С помощью всего лишь трех вышеописанных значений спецификатора выходных данных мы можем создать практически любую фигуру из входных примитивов. Например, чтобы создать треугольник, мы воспользуемся параметром triangle_strip
в качестве выходных данных и выведем 3 вершины.
Геометрический шейдер также ожидает, что мы установим максимальное количество вершин, которое ему разрешено выводить (если вы превысите это число, то OpenGL не будет рисовать дополнительные вершины), что мы также можем сделать внутри спецификатора layout
ключевого слова out
. Конкретно в этом случае мы собираемся отобразить объект line_strip
с максимальным количеством вершин, равным 2.
Примечание: Если вам интересно, что такое line_strip
, то это — непрерывная линия (или «полоса»), связывающая вместе набор, состоящий из, как минимум, двух точек. Каждая дополнительная точка приводит к созданию нового участка линии между дополнительной и предыдущей точками. Ниже представлено изображение такой линии, построенной с использованием 5 точек:
Чтобы получить более содержательные для нас результаты, необходимо придумать какой-нибудь способ достучаться до выходных данных предыдущего шейдерного этапа. Можете не ломать голову — GLSL предоставляет нам встроенную переменную под названием gl_in
, внутреннее представление которой (вероятно) будет иметь следующий вид:
1 2 3 4 5 6 |
in gl_Vertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[]; |
В данном примере она объявлена в виде интерфейсного блока, содержащего несколько интересных переменных, из которых наиболее примечательной для нас является переменная gl_Position
, содержащая вектор, определяемый нами в качестве выходного значения вершинного шейдера.
Обратите внимание, что переменная gl_in
объявлена как массив, поскольку большинство примитивов, используемых при рендеринге, содержат более 1 вершины. Стоит отметить, что геометрический шейдер получает все вершины примитива в виде входных данных.
Используя вершинные данные, прошедшие этап вершинного шейдера, мы можем генерировать новые данные с помощью двух функций геометрического шейдера: EmitVertex() и EndPrimitive(). Геометрический шейдер ожидает, что вы создадите или отправите на вывод хотя бы один из примитивов, указанных в качестве выходных данных. В нашем случае мы хотим сгенерировать, по крайней мере, один примитив line_strip
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#version 330 core layout (points) in; layout (line_strip, max_vertices = 2) out; void main() { gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); EmitVertex(); gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0); EmitVertex(); EndPrimitive(); } |
Каждый раз, когда мы вызываем функцию EmitVertex(), вектор, содержащийся в данный момент в gl_Position
, добавляется к выходному примитиву. Затем, при вызове функции EndPrimitive(), все выводимые для этого примитива вершины объединяются в указанный выходной примитив рендеринга. При многократном вызове функции EndPrimitive(), после одного или нескольких вызовов EmitVertex(), может быть сгенерировано несколько примитивов. Конкретно в этом случае генерируются две новые вершины, немного смещенные относительно исходного положения заданной вершины, а затем вызывается функция EndPrimitive(), объединяя две данные вершины в одну линию из двух вершин.
Теперь, когда вы ознакомились с тем, как работают геометрические шейдеры, вы, вероятно, можете догадаться, что делает вышеописанный геометрический шейдер. Он принимает точку в качестве примитива входных данных и создает примитив горизонтальной линии, используя входную точку в качестве центра линии. Если запустить рендер, то результат будет выглядеть следующим образом:
Пока что не очень впечатляюще, но стоит отметить одну интересную деталь — представленный результат был сгенерирован только лишь с помощью следующего вызова рендеринга:
1 |
glDrawArrays(GL_POINTS, 0, 4); |
Хотя это и относительно простой пример, но он демонстрирует то, каким образом можно использовать геометрические шейдеры для (динамического) создания новых фигур «на лету». Позже на этом уроке мы обсудим несколько интересных эффектов, которые мы можем создать с помощью геометрических шейдеров, но сейчас начнем с более простого примера.
Использование геометрических шейдеров
Чтобы продемонстрировать использование геометрического шейдера, мы визуализируем одну небольшую сцену, в которой отобразим 4 точки на z-плоскости в нормализованных координатах устройства. Координаты точек будут следующими:
1 2 3 4 5 6 |
float points[] = { -0.5f, 0.5f, // верхняя-левая 0.5f, 0.5f, // верхняя-правая 0.5f, -0.5f, // нижняя-правая -0.5f, -0.5f // нижняя-левая }; |
Вершинный шейдер должен нарисовать точки на z-плоскости, поэтому для начала мы создадим базовый вершинный шейдер:
1 2 3 4 5 6 7 |
#version 330 core layout (location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } |
Затем во фрагментном шейдере для всех точек установим зеленый цвет:
1 2 3 4 5 6 7 |
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 1.0, 0.0, 1.0); } |
Создадим VAO и VBO для вершинных данных точек, а затем визуализируем их с помощью функции glDrawArrays():
1 2 3 |
shader.use(); glBindVertexArray(VAO); glDrawArrays(GL_POINTS, 0, 4); |
В результате получается сцена с 4 (трудно различимыми) зелеными точками:
А теперь оживим нашу маленькую сцену, добавив к ней магию геометрических шейдеров.
В целях обучения сначала мы создадим так называемый сквозной геометрический шейдер, который в качестве входных данных принимает примитив-точку и без изменений передает её следующему шейдеру:
1 2 3 4 5 6 7 8 9 |
#version 330 core layout (points) in; layout (points, max_vertices = 1) out; void main() { gl_Position = gl_in[0].gl_Position; EmitVertex(); EndPrimitive(); } |
Алгоритм вышеописанного геометрического шейдера не должен вызывать у вас каких-либо трудностей. Он просто выдает неизмененное положение вершины, которое получил в качестве входных данных, и генерирует примитив-точку.
Геометрический шейдер должен быть скомпилирован и связан с программой точно так же, как и вершинный/фрагментный шейдер, но на этот раз мы создадим шейдер, используя GL_GEOMETRY_SHADER
в качестве типа шейдера:
1 2 3 4 5 6 7 8 |
geometryShader = glCreateShader(GL_GEOMETRY_SHADER); glShaderSource(geometryShader, 1, &gShaderCode, NULL); glCompileShader(geometryShader); [...] glAttachShader(program, geometryShader); glLinkProgram(program); |
Этот код компиляции шейдера такой же, как и у вершинного/фрагментного шейдера. Обязательно проверьте наличие ошибок компиляции/линкинга!
Если скомпилировать и запустить пример, то вы должны увидеть следующий результат:
Всё то же самое, что и без геометрического шейдера! Да, это может показаться вам немного скучным, но тот факт, что мы смогли нарисовать точки, означает, что геометрический шейдер работает, так что теперь пришло время для более интересных вещей!
Строим дом
Рисование точек и линий не так уж интересно, поэтому мы снова проявим творческий подход и, используя геометрический шейдер, в месте расположения каждой точки нарисуем дом. Это можно сделать, установив тип выходных данных геометрического шейдера в triangle_strip
и нарисовав в общей сложности три треугольника: два для квадратного дома и один для крыши.
triangle_strip
— это более эффективный способ нарисовать треугольники с небольшим количеством вершин. После того, как будет нарисован первый треугольник, каждая последующая вершина создает еще один треугольник (рядом с предыдущим): каждые 3 соседние вершины образуют треугольник. Если у нас есть в общей сложности 6 вершин, которые образуют triangle_strip
, то мы получим следующие треугольники:
(1,2,3)
(2,3,4)
(3,4,5)
(4,5,6)
Для работы с примитивом triangle_strip
необходимо задать, по крайней мере, 3 вершины. При этом будет сгенерировано N-2
треугольника; таким образом, используя 6 вершин, мы создали 6 - 2 = 4
треугольника. Обратите внимание на следующий рисунок:
Устанавливая тип выходных данных геометрического шейдера как triangle_strip
, мы можем с легкостью нарисовать дом нужной нам формы, создав в необходимой последовательности 3 прилегающих друг к другу треугольника. На следующем рисунке показано, в каком порядке нам требуется нарисовать вершины, чтобы получить искомые треугольники (синяя точка используется в качестве примитива входных данных):
Таким образом, вышеописанный рисунок приводит нас к следующему геометрическому шейдеру:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#version 330 core layout (points) in; layout (triangle_strip, max_vertices = 5) out; void build_house(vec4 position) { gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:нижняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:нижняя-правая EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:верхняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:верхняя-правая EmitVertex(); gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:верхняя EmitVertex(); EndPrimitive(); } void main() { build_house(gl_in[0].gl_Position); } |
Мы по очереди берем каждую из четырех вершин, объявленных ранее в теле программы, и с помощью вышеуказанного геометрического шейдера генерируем по 5 дополнительных вершин, причем каждая дополнительная вершина — это положение точки плюс некоторое смещение для формирования одного большого triangle_strip
. Затем полученный примитив растеризуется, и фрагментный шейдер отрабатывает по всему примитиву, что приводит к созданию зеленого дома для каждой точки, которую мы визуализировали:
Каждый дом состоит из трех треугольников, все они нарисованы с использованием отдельно взятой точки в пространстве. Согласен, зеленые дома выглядят немного скучно, так что давайте немного оживим их, придав им уникальный цвет. Для этого мы добавим в вершинные данные дополнительный атрибут с информацией о цвете для каждой вершины.
Обновленные вершинные данные приведены ниже:
1 2 3 4 5 6 |
float points[] = { -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // верхняя-левая 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // верхняя-правая 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // нижняя-правая -0.5f, -0.5f, 1.0f, 1.0f, 0.0f // нижняя-левая }; |
Затем мы обновим вершинный шейдер, чтобы иметь возможность с помощью интерфейсного блока переслать атрибут цвета в геометрический шейдер:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#version 330 core layout (location = 0) in vec2 aPos; layout (location = 1) in vec3 aColor; out VS_OUT { vec3 color; } vs_out; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); vs_out.color = aColor; } |
А далее нам необходимо объявить тот же самый интерфейсный блок (но с другим именем интерфейса) в геометрическом шейдере:
1 2 3 |
in VS_OUT { vec3 color; } gs_in[]; |
Поскольку геометрический шейдер выполняет работу на множестве вершин, поставляемых для него в качестве входных данных, то они всегда представляются в виде массивов вершинных данных, даже если была передана всего одна вершина.
Примечание: Нам не обязательно использовать интерфейсные блоки для передачи данных в геометрический шейдер. Мы могли бы написать и следующее:
1 |
in vec3 outColor[]; |
Данный прием работает, если вершинный шейдер пересылает цветовой вектор в качестве переменной out vec3 outColor
. Однако в шейдерах, подобных геометрическому, легче работать именно с интерфейсными блоками. На практике входные данные геометрических шейдеров могут быть довольно большими, и группировка их в один большой массив интерфейсного блока имеет гораздо больше смысла.
Мы также должны объявить выходной цветовой вектор для следующего этапа (фрагментного шейдера):
1 |
out vec3 fColor; |
Поскольку фрагментный шейдер ожидает получить только отдельно взятый (интерполированный) цвет, то нет смысла пересылать несколько цветов. Таким образом, вектор fColor
— это не массив, а один вектор. При отрисовке вершины, она сохранит значение того цвета, который в данный момент находился в переменной fColor
. Для раскрашивания домов перед отрисовкой вершины, мы заполним переменную fColor
цветом из вершинного шейдера:
1 2 3 4 5 6 7 8 9 10 11 12 |
fColor = gs_in[0].color; // gs_in[0], так как есть только одна входная вершина gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:нижняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:нижняя-правая EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:верхняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:верхняя-правая EmitVertex(); gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:верхняя EmitVertex(); EndPrimitive(); |
Все вновь созданные вершины будут иметь цвет, определяемый последним сохраненным значением цвета переменной fColor
, равный цвету входной вершины, определенный в атрибутах вершины. Как вы можете видеть на следующем скриншоте, все дома раскрашены в свой уникальный цвет:
Чисто ради удовольствия мы могли бы притвориться, что сейчас — зима, и насыпать на крыши домов немного снега, придав последней вершине свой собственный цвет:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fColor = gs_in[0].color; gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:нижняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:нижняя-правая EmitVertex(); gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0); // 3:верхняя-левая EmitVertex(); gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0); // 4:верхняя-правая EmitVertex(); gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0); // 5:верхняя fColor = vec3(1.0, 1.0, 1.0); EmitVertex(); EndPrimitive(); |
Результат:
GitHub / Урок №27. Геометрические шейдеры в OpenGL — Исходный код №1
Используя геометрические шейдеры, вы можете получить креативный результат даже с самыми простыми примитивами. Поскольку фигуры динамически генерируются на сверхбыстром оборудовании вашего графического процессора, то это может быть намного быстрее, чем самостоятельное определение фигур в вершинных буферах. Геометрические шейдеры — это отличный инструмент для простых (часто повторяющихся) фигур, таких как: кубы в воксельном мире или листья травы на большом открытом поле.
Взрывающиеся объекты
Когда мы говорим про взрывающийся объект, мы, на самом деле, не собираемся взрывать наши драгоценные связанные наборы вершин, мы собираемся за небольшой промежуток времени переместить каждый треугольник вдоль направления его вектора нормали. В результате этого мы получим эффект взрывающегося объекта. Эффект взрывающихся треугольников на модели рюкзака выглядит примерно следующим образом:
Самое замечательное в данном эффекте геометрического шейдера то, что он работает на всех объектах, независимо от их сложности.
Поскольку мы собираемся переместить каждую вершину в направлении вектора нормали треугольника, то нам сначала нужно вычислить этот вектор. Для этого найдем вектор, перпендикулярный поверхности треугольника, используя те 3 вершины, к которым у нас есть доступ. Возможно, вы помните из урока о трансформациях в OpenGL, что вектор, перпендикулярный двум другим векторам, является результатом векторного произведения. Если мы возьмем два вектора a
и b
, параллельные поверхности треугольника, то можно получить и его вектор нормали, применив к векторам a
и b
операцию векторного произведения. Следующая функция геометрического шейдера делает именно то, о чем мы только что говорили:
1 2 3 4 5 6 |
vec3 GetNormal() { vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position); vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position); return normalize(cross(a, b)); } |
Используя векторное вычитание, мы получаем два вектора — a
и b
, параллельные поверхности треугольника. Вычитание одного вектора из другого приводит к вектору, который является разницей этих двух векторов. Поскольку все 3 точки лежат в плоскости треугольника, то вычитание одного его вектора из любого другого его же вектора приводит к вектору, параллельному плоскости. Обратите внимание, что если бы мы при использовании векторного произведения на векторах a
и b
поменяли их местами, то получили бы вектор нормали, указывающий в противоположном направлении. Другими словами, в векторном произведении (функция cross()) важен порядок следования векторов-сомножителей!
Теперь, когда мы знаем, как вычислить вектор нормали, мы можем создать функцию explode(), которая принимает данный нормальный вектор вместе с вектором положения вершины. Функция возвращает новый вектор, перемещающий вектор положения вдоль направления нормального вектора:
1 2 3 4 5 6 |
vec4 explode(vec4 position, vec3 normal) { float magnitude = 2.0; vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; return position + vec4(direction, 0.0); } |
Сама функция не должна быть слишком сложной. Функция sin() получает в качестве аргумента uniform-переменную time
и возвращает значение между -1.0
и 1.0
. Поскольку мы не хотим взрывать объект вовнутрь, то преобразуем диапазон значений sin() в диапазон [0,1]
. Затем полученное значение используется для масштабирования вектора normal
, а результирующий вектор direction
добавляется к вектору положения.
Законченный геометрический шейдер, реализующий эффект взрыва модели, выглядит примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices = 3) out; in VS_OUT { vec2 texCoords; } gs_in[]; out vec2 TexCoords; uniform float time; vec4 explode(vec4 position, vec3 normal) { ... } vec3 GetNormal() { ... } void main() { vec3 normal = GetNormal(); gl_Position = explode(gl_in[0].gl_Position, normal); TexCoords = gs_in[0].texCoords; EmitVertex(); gl_Position = explode(gl_in[1].gl_Position, normal); TexCoords = gs_in[1].texCoords; EmitVertex(); gl_Position = explode(gl_in[2].gl_Position, normal); TexCoords = gs_in[2].texCoords; EmitVertex(); EndPrimitive(); } |
Обратите внимание, что перед отрисовкой вершины мы также выводим соответствующие координаты текстуры.
Кроме того, не забудьте задать значение uniform-переменной time
в вашем OpenGL-коде:
1 |
shader.setFloat("time", glfwGetTime()); |
В результате получается 3D-модель, постоянно взрывающая свои вершины с течением времени, после чего она снова возвращается в нормальное состояние.
GitHub / Урок №27. Геометрические шейдеры в OpenGL — Исходный код №2
Визуализация векторов нормали
Сейчас мы рассмотрим пример действительно полезного использования геометрического шейдера: визуализация векторов нормалей любого объекта. При программировании шейдеров освещения вы, в конечном итоге, столкнетесь со странными визуальными результатами, возникновение которых трудно предугадать. Распространенной причиной ошибок освещения являются некорректные векторы нормалей. Подобные ошибки вызваны либо неправильной загрузкой вершинных данных, либо неправильным указанием их в качестве атрибутов вершин, либо же неправильным управлением ими в шейдерах. Нам необходим какой-нибудь способ определения того, корректно ли заданы векторы нормалей. Самым лучшим вариантом в данном случае будет их визуализация. И так уж получилось, что геометрический шейдер является чрезвычайно полезным инструментом для этой цели.
Идея заключается в следующем: сначала мы рисуем сцену как обычно — без геометрического шейдера, а затем рисуем сцену повторно, но на этот раз только отображая векторы нормалей, которые мы генерируем с помощью геометрического шейдера. Геометрический шейдер в качестве входных данных берет примитив треугольника и генерирует на их основе 3 линии в направлениях нормалей — по одному вектору нормали для каждой вершины. В коде это будет выглядеть примерно так:
1 2 3 4 |
shader.use(); DrawScene(); normalDisplayShader.use(); DrawScene(); |
На этот раз мы создаем геометрический шейдер, который использует нормали вершин, поставляемые моделью, а не генерирует их самостоятельно. Чтобы приспособиться к масштабированию и вращениям (из-за матрицы вида и модели), мы преобразуем нормали с помощью нормальной матрицы. Геометрический шейдер получает свои векторы положения как координаты пространства вида, поэтому мы также должны преобразовать координаты векторов нормалей в то же самое пространство. Всё это можно сделать в вершинном шейдере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; out VS_OUT { vec3 normal; } vs_out; uniform mat4 view; uniform mat4 model; void main() { gl_Position = view * model * vec4(aPos, 1.0); mat3 normalMatrix = mat3(transpose(inverse(view * model))); vs_out.normal = normalize(vec3(vec4(normalMatrix * aNormal, 0.0))); } |
Затем преобразованный вектор нормали пространства вида передается на следующий этап шейдера с помощью интерфейсного блока, после чего геометрический шейдер берет каждую вершину (с координатами и нормальным вектором) и рисует нормальный вектор из каждого вектора положения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#version 330 core layout (triangles) in; layout (line_strip, max_vertices = 6) out; in VS_OUT { vec3 normal; } gs_in[]; const float MAGNITUDE = 0.4; uniform mat4 projection; void GenerateLine(int index) { gl_Position = projection * gl_in[index].gl_Position; EmitVertex(); gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE); EmitVertex(); EndPrimitive(); } void main() { GenerateLine(0); // нормаль первой вершины GenerateLine(1); // нормаль второй вершины GenerateLine(2); // нормаль третьей вершины } |
Содержание геометрических шейдеров, подобных этим, уже должно быть вам понятным. Обратите внимание, что мы умножаем вектор нормали на переменную MAGNITUDE
, чтобы ограничить размер отображаемых нормальных векторов (в противном случае, они были бы слишком большими).
Поскольку визуализация нормалей в основном используется в процессе отладки, то мы можем просто отобразить их в виде одноцветных линий (или супер-причудливых линий, если вам так хочется) с помощью фрагментного шейдера:
1 2 3 4 5 6 7 |
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0, 1.0, 0.0, 1.0); } |
Теперь, отрисовав модель сначала с помощью обычных шейдеров, а затем со специальным шейдером визуализации нормалей, вы увидите что-то вроде следующего:
GitHub / Урок №27. Геометрические шейдеры в OpenGL — Исходный код №3
Помимо того, что наш рюкзак теперь выглядит немного волосатым, он дает нам действительно полезный метод определения правильности нормальных векторов модели. Вы можете себе представить, что геометрические шейдеры, подобные этому, также могут быть использованы для добавления меха к объектам.