Геометрические шейдеры в OpenGL 3.3

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

Однако существует целый ряд задач, требующих работы на уровне отдельных геометрических примитивов. Именно такая возможность и предоставляется геометрическими шейдерами (вошедшими в OpenGL 3.2).

Геометрический шейдер получает на вход уже собранный примитив, вершины которого были обработаны вершинным шейдером. Он имеет доступ сразу ко всем вершинам примитива и в результате своей работы он отбрасывает исходный примитив, создавая вместо него ноль или более примитивов (при этом типы входных и выходных примитивов могут не совпадать). Обратите внимание, что если присутствует геометрический шейдер, то вершинный шейдер также должен присутствовать.

Созданные геометрическим шейдером примитивы поступают на следующий блок конвейера рендеринга - transform feedback (если он активирован), отсечение, растеризацию и т.д.

Рис. 1. Место геометрического шейдера в конвейере рендеринга.

Геометрический шейдер может получать на вход один из пяти типов примитивов, при этом входной тип для шейдера явно задается в его тексте. Допустимыми входными типами примитивов являются:

Первые три типа существовали и до появления геометрических шейдеров и для каждого из них геометрический шейдер получает на вход 1, 2 и 3 вершины соответственно.

Последние два типа (lines_adjacency и triangles_adjacency) соответствуют новым типам примитивов - отрезку с двумя соседними точками и треугольники с соседними вершинами (см. рис. 2).

Рис 2. Примитивы с соседями.

Примитив lines_adjacency (отрезок с соседями, рис 2 слева) состоит из четырех вершин v0v1v2v3, где непосредственно отрезком является v1v2, а v0v1 и v2v3 задают предыдущий и последующий отрезки.

Примитив triangles_adjacency (треугольник с соседями, рис. 2 справа) состоит из 6 вершин v0-v5. При этом сам треугольник задается вершинами с четными номерами v0v2v4, а вершины v1, v3 и v5 задают треугольники, имеющие одно общее ребро с v0v2v4.

Эти типы примитивов позволяют передавать в геометрический шейдер не только сам выводимый примитив (отрезок или треугольник), но и соседние с ним примитивы. Таким образом, геометрический шейдер может, например, определять какие из ребер треугольного мэша является контурными (т.е. разделяют лицевые и нелицевые грани).

Если дан обычный массив примитивов, заданный в виде массива вершин и массива индексов, то легко можно получить массив со смежностью - для этого достаточно лишь перестроить массив индексов. Кроме того, для примитивов со смежностью вводятся соответствующие strip-версии (рис 3 и 4).

Рис 3. Примитив GL_LINE_STRIP_ADJACENCY.

Рис 4. Примитив GL_TRIANGLE_STRIP_ADJACENCY.

Обратите внимание, что для примитива GL_TRIANGLE_STRIP_ADJACENCY главными треугольниками являются треугольники с четными номерами.

Для задания типа входного примитива в GLSL служит директива layout:

layout(triangles) in;

Следующий фрагмент кода демонстрирует как получить тип входных примитивов из уже слинкованной программы:

int	type;
	
glGetProgramiv ( program, GL_GEOMETRY_INPUT_TYPE, &type );

Также в тексте геометрического шейдера задается и тип выходного примитивы. При этом поддерживается только три типа - points, line_strip и triangle_strip (соответственно GL_POINTS, GL_LINE_STRIP и GL_TRIANGLE_STRIP). Кроме типа выходного примитива в геометрическом шейдере следует также задать максимальное количество вершин, которое может сгенерировать шейдер за один вызов.

layout(triangle_strip, max_vertices=4) out;

Следующий фрагмент кода показывает, как можно получить тип выходного примитивы и максимальное число выводимых вершин из уже слинкованной программы.

int	type, num;

glGetProgramiv ( program, GL_GEOMETRY_OUTPUT_TYPE,  &type );
glGetProgramiv ( program, GL_GEOMETRY_VERTICES_OUT, &num );

Кроме того, можно в самом шейдере задать сколько раз он должен быть вызван для каждого входного примитива. Для того, чтобы сам шейдер мог различать эти вызовы служит встроенная переменная gl_InvocationID. Ниже приводится пример директивы, задающей, что для каждого входного примитивы необходимо 5 раз вызвать шейдер, при этом переменная gl_InvocationID будет пробегать значения от 0 до 4.

layout(triangles, invocations = 5) in;

Геометрический шейдер получает на вход следующие встроенные переменные (для core profile)

Структура gl_in состоит из следующих полей:

Также геометрический шейдер имеет доступ к uniform-переменным и выходным переменным вершинного шейдера. Геометрический шейдер также может читать данные из текстур (как и вершинный). Поскольку геометрический шейдер имеет доступ ко всем вершинам примитива, то выходные переменные вершинного шейдера должны быть описаны в геометрическом шейдеры как массивы (по одному значению на каждую вершины для каждой выходной переменной). Размер массива можно не указывать.

Пусть в вершинном шейдера имеется следующая выходная переменная

out vec3 normal;		// normal for vertex

Тогда в геометрическом шейдере она должна быть описана следующим образом:

in vec3 normal [];		// arrays of normals for every vertex

Результаты работы геометрического шейдера записываются в следующие переменные - gl_Position, gl_PointSize, gl_ClipDistance [], gl_PrimitiveID, gl_Layer и gl_ViewportIndex.

За счет записи в переменную gl_ViewportIndex можно выбрать в какой именно viewport будет осуществляться вывод примитивы (если поддерживается расширение ARB_viewport_array).

Запись в переменную gl_Layer позволяет выбрать слой из текстурного массива и/или грань из кубической текстурной карты, в которую будет осуществляться рендеринг.

Также геометрический шейдер может ввести свои выходные переменные, которые будут интерполироваться во время растеризации. Если выходная переменная снабжена спецификатором flat, то в этом случае интерполяции происходить не будет, а будет использовано значение, соответствующее первой или последней вершине (так называемый provoking vertex, устанавливается вызовом функции glProvokingVertex с аргументом GL_FIRST_VERTEX_CONVENTION или GL_LAST_VERTEX_CONVENTION).

out      vec3 vertexNormal;         // per-vertex normal
flat out vec3 faceNormal;           // per-primitive normal

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

Вызов функции EndPrimitive создает выходной примитив из ранее созданных вершин. Ниже приводится простейший пример геометрического шейдера, просто копирующего треугольники на выход.

#version 330 core

layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;

void main ()
{
    for ( int i = 0; i < gl_in.length (); i++ )
    {
        gl_Position = gl_in [i].gl_Position;
        EmitVertex ();
    }
	
    EndPrimitive ();
}

Рассмотрим теперь что может и что не может делать геометрический шейдер. Прежде всего - он к сожалению плохо подходит для тесселляции геометрии (для этого лучше использовать специальные тесселляционные шейдеры). Кроме того, в геометрическом шейдере не стоит создавать большое количество вершин и/или примитивов.

Зато с помощью геометрического шейдера можно:

Генерация billboard'ов

Одним из простых примеров использования геометрических шейдеров является построение billboard'ов по центру, размеру и векторам, задающим ее плоскость. При этом достаточно передать в шейдер всего 4 числа - три координаты точки и ее размер. Два базовых вектора передаются как uniform-переменные.

Геометрический шейдер получает на вход по одной точке для каждого billboard'а и строит два треугольника, образующие четырехугольник с центром в переданной точке. При этом мы считаем, что базовые вектора, задающие плоскость для billboard'а уже сразу заданы преобразованными в screen-space.

Ниже приводится шейдеры, обеспечивающие вывод billboard'ов по набору точек. Для каждой точки в ее xyz-координатах задано положение, а в четвертой координате w задан размер. Отдельные шейдеры отделены строками вида "-- название_шейдера".

//
// Shaders to produce billboards from points
//

-- vertex

#version 330 core

uniform mat4 proj;
uniform mat4 mv;

in  vec4  posSize;		// position in xyz, size in w
out float size;

void main(void)
{
    gl_Position  = proj * mv * vec4 ( posSize.xyz, 1.0 );
    size         = 0.5 * posSize.w;
}

-- geometry

#version 330 core

layout (points) in;
layout (triangle_strip, max_vertices = 6) out;

uniform vec3 up;
uniform vec3 right;

in float size [];

out float clr;

void main ()
{
    vec3	u = up * size [0];
    vec3	r = right * size [0];
    vec3	p = gl_in [0].gl_Position.xyz;
    float	w = gl_in [0].gl_Position.w;
	
    gl_Position = vec4 ( p - u - r, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p - u + r, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p + u + r, w );
    EmitVertex   ();
    EndPrimitive ();				// 1st triangle
	
    gl_Position = vec4 ( p + u + r, w );
    EmitVertex   ();
	
    gl_Position = vec4 ( p + u - r, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p - u - r, w );
    EmitVertex   ();
    EndPrimitive ();				// 2nd triangle
}

-- fragment

#version 330 core

in float clr;
out vec4 color;

void main(void)
{
    color = vec4 ( 1.0 );
}

Обратите внимание, что в код легко может быть внесен поворот billboard'а вокруг своего центра на заданный угол.

Построение граней, являющихся бисектором между двумя направлениями

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

Рис 5. Грань строится вдоль вектора h, являющегося бисектором заданного направления s и направления на камеру (v).

Ниже приводятся соответствующие шейдеры.

-- vertex
#version 330 core

uniform mat4 proj;
uniform mat4 mv;
uniform	vec3 eye;		// eye position

in  vec4  posSize;		// position in xyz, size in w
out float size;
out vec3  v;

void main(void)
{
    gl_Position  = proj * mv * vec4 ( posSize.xyz, 1.0 );
    size         = 0.5 * posSize.w;
    v            = -normalize ( gl_Position.xyz );
}

-- geometry

#version 330 core

layout (points) in;
layout (triangle_strip, max_vertices = 6) out;

uniform vec3 sun;		// sun direction

in float size [];
in vec3  v    [];

out float clr;

void main ()
{
    vec3	p  = gl_in [0].gl_Position.xyz;
    vec3	vn = v [0];
    vec3	up = normalize ( cross ( vn, sun ) ) * size [0];
    vec3	f  = normalize ( vn + sun ) * size [0];            // bisector of v and sun
    float	w  = gl_in [0].gl_Position.w;
	
    gl_Position = vec4 ( p - up - f, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p - up + f, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p + up + f, w );
    EmitVertex   ();
    EndPrimitive ();				                   // 1st triangle
	
    gl_Position = vec4 ( p + up + f, w );
    EmitVertex   ();
	
    gl_Position = vec4 ( p + up - f, w );
    EmitVertex ();
	
    gl_Position = vec4 ( p - up - f, w );
    EmitVertex   ();
    EndPrimitive ();				                   // 2nd triangle
}

-- fragment

#version 330 core

in float clr;
out vec4 color;

void main(void)
{
	color = vec4 ( 1.0 );
}

"Вытягивание" контура

В качестве следующего примера мы рассмотрим задачу о "вытягивании" контура. Подобная задача довольно часто возникает при добавлении детализации - внутри объекта можно создать очень убедительно выглядящую детализацию за счет использованию техник типа Parallax Occlusion Mapping, но вот на контуре примитива хорошо видно отсутствие микрорельефа.

Для борьбы с этим обычно контур "вытягивают" наружу, добавляя дополнительные грани. Эти грани выводятся с помощью специального шейдера таким образом, чтобы поддержать иллюзию микрорельефа и на границе (см. например GPU Gems III глава 4).

При этом существует два подхода к решению подобной задачи - можно либо найти одну грань и на ней построить отрезок, вдоль которого проходит контурная линия. С другой стороны можно просто найти пары треугольников, один из которых является лицевым, а другой нет, и использовать ребро между ними.

Мы сначала рассмотрим первый пример - поиск участка контурной линии прямо внутри треугольника.

Одним из самых простых методов вытягивания контура (fins extrusion) является расчет скалярного произведения между нормалью и направлением на камеру для каждой вершины. Далее определяются ребра, на концах которых это скалярное произведение имеет значения разного знака. Путем линейно интерполяции можно определить на этих ребрах точки A и B, для которых соответствующее скалярное произведение обратится в ноль (рис 6).

Рис. 6. Вытягивание контура для треугольника.

После этого в этих точках путем интерполяции находятся значения нормали и по ним вытягивается четырехугольник в направлении нормали. Обратите внимание, что для этого умножение на матрицу проектирования пришлось перенести из вершинного шейдера в геометрический.

-- vertex

#version 330 core

uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye;       // eye position

in  vec3  pos;          // position in xyz, size in w
in  vec3  normal;
out vec3  n;
out vec3  v;
out float s;

void main(void)
{
    vec3    p = vec3 ( mv * vec4 ( pos, 1.0 ) );
    
    gl_Position  = mv * vec4 ( pos, 1.0 );
    n            = nm * normal;
    v            = normalize ( vec3 ( eye ) - p );    // vector to the eye
    s            = dot ( n, v );
}


-- geometry

#version 330 core

#define EPS 0.001

layout (triangles) in;
layout (triangle_strip, max_vertices = 9) out;

uniform mat4  proj;
uniform float finSize;

in  vec3 n [];
in  vec3 v [];
out vec4 clr;

int signWithTolerance ( float v )
{
    if ( v > EPS )
        return 1;
        
    if ( v < -EPS )
        return -1;
        
    return 0;
}

int addPoint ( float f0, int i0, float f1, int i1, inout vec4 points [4], inout vec3 normals [4], int count )
{
    float   t = f1 / ( f1 - f0 );       // parameter across edge, t*f0 + (1-t)*f1 = 0
    
    points  [count]   = mix ( gl_in [i1].gl_Position, gl_in [i0].gl_Position, t );
    normals [count++] = finSize * normalize ( mix ( n [i1], n [i0], t ) );
    
    return count;
}

void main ()
{
    float f0 = dot ( v [0], n [0] );
    float f1 = dot ( v [1], n [1] );
    float f2 = dot ( v [2], n [2] );    
    int   s0 = signWithTolerance ( f0 );
    int   s1 = signWithTolerance ( f1 );
    int   s2 = signWithTolerance ( f2 );
    
                                // emit source triangle
    for ( int i = 0; i < 3; i++ )
    {
        gl_Position = proj * gl_in [i].gl_Position;
        clr         = vec4 ( 1.0 );
            
        EmitVertex ();
    }

    EndPrimitive ();
                                // now check for fins
                                // quick exit for common cases
    if ( s0 == 1 && s1 == 1 && s2 == 1 )
        return;
        
    if ( s0 == -1 && s1 == -1 && s2 == -1 )
        return;

    bool    on01 = s0 * s1 <= 0;
    bool    on02 = s0 * s2 <= 0;
    bool    on12 = s1 * s2 <= 0;
            
                    // locate edges that contain cotour points
    vec4    points  [4];
    vec3    normals [4];
    int     count = 0;
    
    if ( on01 )
        count = addPoint ( f0, 0, f1, 1, points, normals, count );
            
    if ( on02 )
        count = addPoint ( f0, 0, f2, 2, points, normals, count );
            
    if ( on12 )
        count = addPoint ( f1, 1, f2, 2, points, normals, count );
    
    if ( count >= 2 )       // emit quad from store edge
    {
        gl_Position = proj * points [0];
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * ( points [0] + vec4 ( normals [0], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * points [1];
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        EndPrimitive ();
            
        gl_Position = proj * ( points [0] + vec4 ( normals [0], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * ( points [1] + vec4 ( normals [1], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * points [1];
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        EndPrimitive ();
    }
}

-- fragment

#version 330 core

in  vec4 clr;
out vec4 color;

void main(void)
{
    color = clr;
}

Рис 7. Изображение, получаемое путем вытягивания построенного в треугольнике отрезка.

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

Для этого достаточно один раз пройти по всем треугольникам и для каждого ребра треугольника запомнить третью вершину этого треугольника. Поскольку в обычном мэше каждое ребро принадлежит сразу двум треугольникам (но при этом для них имеет противоположные направления, см. рис. 7), то, для того чтобы найти дополнительную вершину для этого ребра, нужно просто найти вершину для "перевернутого ребра".

Рис 8. Поиск смежного ребра.

class   Edge
{
    int from, to;
public:
    Edge ( int f, int t )
    {
        from = f;
        to   = t;
    }
    
    bool    operator == ( const Edge& e ) const
    {
        return from == e.from && to == e.to;
    }
    
    bool    operator < ( const Edge& e ) const
    {
        if ( from < e.from )
            return true;
            
        if ( from > e.from )
            return false;
            
        return to < e.to;
    }
};

unsigned * makeAdjacencyIndex ( int numTris, unsigned * src )
{
    int        n   = 3 * numTris;
    unsigned * res = new unsigned [2*n];    // it will double number of elements
    map<Edge, unsigned> adj;                // for every edge keep vertex of triangle
    
                                            // build adjacence map
    for ( int i = 0; i < numTris; i++ )
    {
        int j = 3 * i;
        
        adj [Edge ( src [j],   src [j+1])] = src [j+2];
        adj [Edge ( src [j+1], src [j+2])] = src [j];
        adj [Edge ( src [j+2], src [j]  )] = src [j+1];
    }
    
                                        // now fill new structure
    for ( int i = 0; i < numTris; i++ )
    {
        int j = 6 * i;
        int k = 3 * i;
        
        res [j+0] = src [k+0];          // source triangle
        res [j+2] = src [k+1];
        res [j+4] = src [k+2];
                                        // now adjacent vertices
        res [j+1] = adj [Edge ( src [k+1], src [k  ] )];
        res [j+3] = adj [Edge ( src [k+2], src [k+1] )];
        res [j+5] = adj [Edge ( src [k],   src [k+2] )];
    }
    
    return res;
}

Ниже приводится соответствующие шейдеры.

-- vertex

#version 330 core

uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye;       // eye position

in  vec3  pos;          // position in xyz, size in w
in  vec3  normal;
out vec3  n;
out vec3  v;

void main(void)
{
    vec4    p = mv * vec4 ( pos, 1.0 );
    
    gl_Position  = p;
    n            = normalize ( nm * normal );
    v            = normalize ( vec3 ( eye ) - p.xyz );                  // vector to the eye
}


-- geometry

#version 330 core

#define EPS 0.001

layout (triangles_adjacency) in;
layout (triangle_strip, max_vertices = 12) out;

uniform mat4 proj;
uniform float finSize;

in  vec3 n [];
in  vec3 v [];
out vec4 clr;

void    emitEdge ( int i0, int i1 )
{
        gl_Position = proj * gl_in [i0].gl_Position;
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * ( gl_in [i0].gl_Position + finSize * vec4 ( n [i0], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * gl_in [i1].gl_Position;
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        EndPrimitive ();
            
        gl_Position = proj * ( gl_in [i0].gl_Position + finSize * vec4 ( n [i0], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * ( gl_in [i1].gl_Position + finSize * vec4 ( n [i1], 0.0 ) );
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        
        gl_Position = proj * gl_in [i1].gl_Position;
        clr         = vec4 ( 0.0, 1.0, 0.0, 1.0 );
        EmitVertex ();
        EndPrimitive ();
}

vec3    calcNormal ( int i0, int i1, int i2 )
{
    vec3    va = gl_in [i1].gl_Position.xyz - gl_in [i0].gl_Position.xyz;
    vec3    vb = gl_in [i2].gl_Position.xyz - gl_in [i0].gl_Position.xyz;
    
    return normalize ( cross ( va, vb ) );
}
    
void main ()
{
                                            // emit source triangle
    for ( int i = 0; i < 3; i++ )
    {
        gl_Position = proj * gl_in [2*i].gl_Position;
        clr         = vec4 ( 1.0 );
            
        EmitVertex ();
    }

    EndPrimitive ();
    
    vec3    n0 = calcNormal ( 0, 2, 4 );    // normal for triangle itself
    
    if ( dot ( n0, v [0] ) < EPS )          // front-facing
    {
        vec3    n1 = calcNormal ( 1, 2, 0 );
        vec3    n2 = calcNormal ( 3, 4, 2 );
        vec3    n3 = calcNormal ( 5, 0, 4 );
                                            // check other triangles for back-facing
        if ( dot ( n1, v [1] ) >= -EPS )
            emitEdge ( 0, 2 );
            
        if ( dot ( n2, v [3] ) >= -EPS )
            emitEdge ( 2, 4 );
            
        if ( dot ( n3, v [5] ) >= -EPS )
            emitEdge ( 0, 4 );
    }
}

-- fragment

#version 330 core

in  vec4 clr;
out vec4 color;

void main(void)
{
    color = clr;
}

Обратите внимание, что все примеры в этой статье строятся уже на основе нового framework'а, поэтому они полностью самодостаточны.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.

Используются технологии uCoz