steps3D - Tutorials - Mesh-шейдеры и расширение NV_mesh_shader

Mesh-шейдеры и расширение NV_mesh_shader

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

Рис 1. Традиционный графический конвейер

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

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

Именно поэтому в конвейер был добавлен специальный шаг - тесселляция, позволяющий разбивать входные примитивы на мелкие части. При этом для этой стадии у нас есть два тесселляционных шейдера. Один из них (tessellation control shader) управляет тем, как именно будет происходит разбиение входного примитива на части. А второй (tessellation evaluation shader) отвечает за построение новых примитивов по произведенной тесселляции.

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

Именно такую возможность и дает расширение GL_NV_mesh_shader (у него есть аналог для Vulkan - VK_EXT_mesh_shader, также данный функционал поддерживается и в DX12). Фактически данный функционал вводит новый графический конвейер, показанный на следующем рисунке.

Рис. 2. Новый графический конвейер.

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

Задача mesh-шейдера сгенерировать небольшую группу примитивов и отправить ее на растеризацию. Задача task-шейдера - определить сколько групп нитей mesh-шейдера нужно запустить.

Mesh-шейдеры

Как уже говорилось mesh-шейдер очень похож на вычислительный шейдер, он также запускает группы нитей, каждая нить имеет свой идентификатор в пределах группы (gl_LocalInvocationID) и идентификатор группы (gl_WorkGroupID).

Задачей каждой такой группы нитей является генерация небольшого (обычно не более 64 вершин и 126 примитивов) объема геометрии. При этом эта геометрия создается совместно сразу всеми нитями группы. Созданная рабочей группой нитей геометрия сразу же направляется на растеризацию и далее обработка идет как в традиционном графическом конвейера.

Как и вычислительный шейдер mesh-шейдер устроен довольно просто - задается номер версии GLSL и при помощи директивы layout задается размер рабочей группы, на данный момент поддерживаются только одномерные рабочие группы (local_size_y и local_size_z должны быть равны единице).

Давайте рассмотрим простейший mesh-шейдер.

#version 450                // specs written against OpenGL 4.5
#extension GL_NV_mesh_shader : require
 
layout ( local_size_x = 1 ) in;
layout ( triangles, max_vertices = 3, max_primitives = 1 ) out;
 
    // custom per-vertex attribute
layout ( location = 0 ) out PerVertexData
{
    vec4    color;
} v_out [];                // [max_vertices]

В первых двух строках мы задаем требуемую версию GLSL как 4.5 (расширение GL_NV_mesh_shader написано для OpenGL 4.5) и задаем обязательную поддержку расширение GL_NV_mesh_shader для GLSL.

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

В встроенном массиву gl_MeshVerticesNV содержится массив (его размер задается параметром max_vertices из директивы layout), описывающий стандартные атрибуты вершины:

out gl_MeshPerVertexNV
{
    vec4     gl_Position;
    float    gl_PointSize;
    float    gl_ClipDistance [];
    float    gl_CullDistance [];
} gl_MeshVerticesNV [];

Во встроенный массив gl_MeshPerPrimitiveNV содержится информация по всем сгенерированным примитивам.

perprimitiveNV out gl_MeshPerPrimitiveNV
{
    int        gl_PrimitiveID;
    int        gl_Layer;
    int        gl_ViewportIndex;
    int        gl_VewportMask;
} gl_MeshPrimitiveNV [];

При помощи последней директивы layout мы задаем дополнительный вершинный атрибут - цвет (vec4 color). Приведем ниже остальную часть нашего простейшего mesh-шейдера.

void main ()
{
        // store vertex positions
    gl_MeshVerticesNV [0] = vec4 ( -1, -1, 0, 1 );
    gl_MeshVerticesNV [1] = vec4 (  0,  1, 0, 1 );
    gl_MeshVerticesNV [2] = vec4 (  1, -1, 0, 1 );
    
        // now specify color for every vertex
    v_out [0] = vec4 ( 1, 0, 0, 1 );
    v_out [1] = vec4 ( 0, 1, 0, 1 );
    v_out [2] = vec4 ( 0, 0, 1, 1 );
    
        // now specify primitive as 3 indices - (0,1,2)
    gl_PrimitiveIndicesNV [0] = 0;
    gl_PrimitiveIndicesNV [1] = 1;
    gl_PrimitiveIndicesNV [0] = 0;
    
        // and now specify number of generated primitives
    gl_PrimitiveCountNV = 1;
}

Как видно из приведенного кода мы сперва записываем во встроенный массив gl_MeshVerticesNV координаты трех генерируемых вершин. Далее для для каждой из этих вершин задаем дополнительные атрибуты записывая их в наш массив v_out.

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

Вот и все ! На самом деле есть ограничение на максимальное число создаваемых вершин и максимальное число создаваемых примитивов. NVidia рекомендует использовать на группу не более 64 вершин и не более 126 (действительно 126, это не опечатка) примитивов. Т.е. рабочая группа нитей mesh-шейдера выводит небольшой блок геометрии, получивший название meshlet.

Task-шейдеры

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

Task-шейдер также сильно напоминает вычислительный шейдер, он содержит директиву layout () in, задающую размер рабочей группы в нитях. Как и для mesh-шейдера рабочая группа должна быть одномерной. Основной встроенной переменной task-шейдера является gl_TaskCountNV, в которую рабочая группа нитей записывает число создаваемых рабочих групп mesh-шейдера.

Кроме того, можно создать блок данных, который будет передан создаваемым рабочим группам mesh-шейдера. Для этого используется описатель taskNV вместе с описателем out в task-шейдере и in в mesh-шейдере.

taskNV out Task
{
    uint    baseID;
} task;
 
void main ()
{
    if ( gl_LocalInvocationID.x == 0 )    // 1st thread of the work group
    {
        gl_TaskCountNV = 1;
        task.baseID    = gl_LocalInvocationID.x;
    }
}

Запуск task/mesh-шейдеров на выполнение

Для запуска заданного числа рабочих групп task (или mesh если task-шейдер отсутствует) шейдера в OpenGL были введены следующие команды

void glDrawMeshTasksNV         ( GLuint first, GLuint count );
void glDrawMeshTasksIndirectNV ( GLintptr indirect );
void glMultiDrawMeshTasksIndirectNV      ( GLintptr indirect, GLsizei drawCount, GLsizei stride );
void glMultiDrawMeshTasksIndirectCountNV ( GLintptr indirect, GLintptr drawCount, GLsizei maxDrawCount, GLsizei stride );

Самой простой из них является glDrawMeshTasksNV. Если есть активный task-шейдер, то она запускает count рабочих групп task-шейдера. В противном случае (нет task-шейдера, есть только mesh-шейдер), то она запустит count рабочих групп mesh-шейдера. У запущенных нитей gl_WorkGroupID.x лежит в диапазоне от first до first + count - 1, gl_WorkGroupID.y и gl_WorkGroupID.z равные нулю.

Максимальное число запускаемых рабочих групп можно получить при помощи функции glGetProgramiv с параметром GL_MAX_DRAW_MESH_TASKS_COUNT_NV.

Оставшиеся три команды служат для indirect-рендеринга, при этом данные в indirect-буфере (буфере типа GL_DRAW_INDIRECT_BUFFER) являются одним или несколькими (для multi-draw) экземплярами следующей структуры:

struct
{
    GLuint    count;
    GLuint    first;
} DrawMeshTasksIndirectCommandNV;

Команда glDrawMeshTasksIndirectNV выполняет запуск рабочих групп аналогично команде glDrawMeshTasksNV, но параметры first и count берутся из indirect-буфера по смещению indirect.

Команда glMultiDrawMeshTasksIndirectNV ведет себя аналогично предыдущей, но она берет из indirect-буфера не одну структуру DrawMeshTasksIndirectCommandNV, а целый массив из drawCount таких структур и для каждой из них производит запуск task/mesh-шейдера.

И наконец команда glMultiDrawMeshTasksIndirectCountNV аналогична glMultiDrawMeshTasksIndirectNV, но значение drawCount берется из буфера типа GL_PARAMETER_BUFFER по смещению drawCount.

По этой ссылке можно скачать примеры кода.

Пример на task/mesh-шейдер

Ниже приводится исходный код примера на task-шейдер на С++.

#include    "GlutRotateWindow.h"
#include    "Program.h"
#include    "BasicMesh.h"
#include    "Texture.h"

class   MeshWindow : public GlutWindow
{
    Program     program;
    
public:
    MeshWindow () : GlutWindow ( 100, 50, 900, 900, "Task/Mesh shader" )
    {
        if ( !GLEW_NV_mesh_shader )
            exit ( "GL_NV_mesh_shader not supported" );

        if ( !program.loadProgram ( "task.glsl" ) )
            exit ( "Error building program: %s\n", program.getLog ().c_str () );
    }

    void redisplay () override
    {
        glClear   ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
        glDisable ( GL_DEPTH_TEST );

        program.bind ();
        glDrawMeshTasksNV ( 0, 2 );
    }
};

int main ( int argc, char * argv [] )
{
    GlutWindow::init ( argc, argv, 4, 5 );
    
    MeshWindow  win;
    
    return GlutWindow::run ();
}

Вот соответствующий код на GLSL.

-- task
#version 450
#extension GL_NV_mesh_shader : require

layout ( local_size_x = 1 ) in;

taskNV out Task
{
    float   baseID;
} task;

void    main ()
{
    if ( gl_LocalInvocationID.x == 0 )
    {
        gl_TaskCountNV = 4;
        task.baseID    = gl_WorkGroupID.x;
    }
}

-- mesh
#version 450
#extension GL_NV_mesh_shader : require

layout ( local_size_x = 1 ) in;
layout ( max_vertices = 3, max_primitives = 1 ) out;
layout ( triangles ) out;

out PerVertexData
{
    vec4 color;
} v_out [];   

taskNV in Task
{
    float   baseID;
} task;


const vec3 vertices [3] = { vec3(-1,-1,0),     vec3(0,1,0),       vec3(1,-1,0) };
const vec3 colors   [3] = { vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), vec3(0.0,0.0,1.0) };

void main()
{
    vec3    offs = vec3 ( 0.2 * gl_GlobalInvocationID.x / 2, 0.2*task.baseID, 0 );

    gl_MeshVerticesNV[0].gl_Position = vec4 ( offs + 0.2*vertices[0], 1.0 ); 
    gl_MeshVerticesNV[1].gl_Position = vec4 ( offs + 0.2*vertices[1], 1.0 ); 
    gl_MeshVerticesNV[2].gl_Position = vec4 ( offs + 0.2*vertices[2], 1.0 ); 

    v_out[0].color = vec4(colors[0], 1.0);
    v_out[1].color = vec4(colors[1], 1.0);
    v_out[2].color = vec4(colors[2], 1.0);

    gl_PrimitiveIndicesNV[0] = 0;
    gl_PrimitiveIndicesNV[1] = 1;
    gl_PrimitiveIndicesNV[2] = 2;
  
    gl_PrimitiveCountNV = 1;
}

-- fragment
#version 450

layout(location = 0) out vec4 color;

in PerVertexData
{
    vec4 color;
} fragIn;  

void main()
{
    color = fragIn.color;
}

Использование мешлетов для рендеринга геометрии

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

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

void    createMeshlets ( std::vector<Vertex>& vertices, std::vector<uint32_t>& indices )
{
    std::unordered_map<int, int>    vertexMap;
    std::vector<uint8_t>            indexList;
        
    auto    mapVertex = [&]( uint32_t& i )
    {
    auto it = vertexMap.find ( i );

       if ( it == vertexMap.end () )
       {
            vertexMap [i] = vertexMap.size ();
                i = vertexMap.size ();
       }
    };

    auto addMeshlet = [&]()
    {
    Meshlet m;
    int i = 0;

    for ( auto& vp : vertexMap )
        m.vertices [i++] = vp.first;

    memcpy ( m.indices, indexList.data (), indexList.size () );

    m.vertexCount   = vertexMap.size ();
    m.triangleCount = indexList.size () / 3;

    meshlets.push_back ( m );
    };

    for ( size_t i = 0; i < indices.size (); i += 3 )
    {
        if ( indexList.size () > 3 * maxTriangles || vertexMap.size () > maxVertices )
        {
             addMeshlet     ();
             vertexMap.clear ();
             indexList.clear ();
        }

        auto i1 = indices [i    ];
        auto i2 = indices [i + 1];
        auto i3 = indices [i + 2];
            
        mapVertex ( i1 );
        mapVertex ( i2 );
        mapVertex ( i3 );
            
        indexList.push_back ( uint8_t ( i1 ) );
        indexList.push_back ( uint8_t ( i2 ) );
        indexList.push_back ( uint8_t ( i3 ) );
   }
        
   if ( indices.size () > 0 )
        addMeshlet ();
}

На самом деле приведенный код слишком прост и генерирует далеко не оптимальное разбиение на мешлеты. Гораздо лучше будет воспользоваться готовым средством - подобная функциональность уже содержится в библиотеке meshoptimzier.

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

struct meshopt_Meshlet
{
    /* offsets within meshlet_vertices and meshlet_triangles arrays with meshlet data */
    unsigned int vertex_offset;
    unsigned int triangle_offset;

    /* number of vertices and triangles used in the meshlet; data is stored in consecutive range defined by offset and count */
    unsigned int vertex_count;
    unsigned int triangle_count;
};

size_t meshopt_buildMeshlets(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, 
                   const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, 
                   size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight);
size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles);

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

void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count);

При помощи этих функций можно легко разбить большой мешь на на набор мешлетов и вывести их при помощи mesh-shader-а, как показано ниже.

#include    <vector>
#include    <unordered_map>

#include    "GlutRotateWindow.h"
#include    "Program.h"
#include    "BasicMesh.h"
#include    "Texture.h"
#include    "AssimpConverter.h"
#include    "meshoptimizer.h"

struct  Vertex
{
    glm::vec4   pos;
    glm::vec4   normal;
};

constexpr size_t maxVertices  = 64;
constexpr size_t maxTriangles = 64;
constexpr float  coneWeight   = 0.5f;
    
struct Meshlet
{
    uint32_t    vertices [maxVertices];
    uint32_t    indices  [maxTriangles * 3];
    uint32_t    vertexCount;
    uint32_t    triangleCount;
};
    
static void loadAiMesh ( const aiMesh * mesh, float scale, std::vector<Vertex>& vertices, std::vector<uint32_t>& indices )
{
     const aiVector3D   zero3D (0.0f, 0.0f, 0.0f);
        
    for ( size_t i = 0; i < mesh->mNumVertices; i++ ) 
   {
    aiVector3D&  pos    = mesh->mVertices [i];
    aiVector3D&  normal = mesh->mNormals  [i];
    Vertex       v;
            
    v.pos    = glm::vec4 ( scale * pos.x, scale * pos.y, scale * pos.z, 1.0f );
    v.normal = glm::vec4 ( normal.x, normal.y, normal.z, 0.0f );

    vertices.push_back ( v );
    }

    for ( size_t i = 0; i < mesh->mNumFaces; i++ ) 
    {
    const aiFace& face = mesh->mFaces[i];
            
    assert(face.mNumIndices == 3);
        
    indices.push_back ( face.mIndices[0] );
    indices.push_back ( face.mIndices[1] );
    indices.push_back ( face.mIndices[2] );
   }
}

class   MeshWindow : public GlutRotateWindow
{
    Prograь     program;
    std::vector<Meshlet>        meshlets;
    std::vector<Vertex>         vertices;
    std::vector<uint32_t>   indices;

    VertexArray             vao;            // VAO to hold buffers settings for render
    VertexBuffer            vertexBuffer;   
    VertexBuffer            meshletBuffer;
    
    
public:
    MeshWindow () : GlutRotateWindow ( 100, 50, 900, 900, "Mesh shader" )
    {
        if ( !GLEW_NV_mesh_shader )
            exit ( "GL_NV_mesh_shader not supported" );

        if ( !program.loadProgram ( "meshlet-mesh.glsl" ) )
            exit ( "Error building program: %s\n", program.getLog ().c_str () );

        loadMesh ( "../../Models/teapot.3ds", 0.1f );

        //vao.create             ();
        //vao.bind               ();
        vertexBuffer.create    ();
        vertexBuffer.bindBase  ( GL_SHADER_STORAGE_BUFFER, 0 );
        vertexBuffer.setData   ( vertices );
        vertexBuffer.unbind ();

        meshletBuffer.create   ();
        meshletBuffer.bindBase ( GL_SHADER_STORAGE_BUFFER, 1 );
        meshletBuffer.setData  ( meshlets );
        meshletBuffer.unbind ();

        program.bind ();
    }

    void redisplay () override
    {
        glClear   ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

        glm::mat4   mv = getRotation ();

        program.setUniformMatrix ( "mv",  mv );

        glDrawMeshTasksNV ( 0, meshlets.size () );
    }

    void reshape ( int w, int h ) override
    {
        GlutRotateWindow::reshape ( w, h );
        
        glm::mat4 proj = getProjection ();
        
        program.setUniformMatrix ( "proj", proj );
    }

    void loadMesh ( const std::string& fileName, float scale )
    {
        Assimp::Importer importer;
        const int        flags = aiProcess_FlipWindingOrder | aiProcess_Triangulate | aiProcess_PreTransformVertices | aiProcess_CalcTangentSpace | aiProcess_GenSmoothNormals;
        const aiScene  * scene = importer.ReadFile ( fileName, flags );

        if ( scene == nullptr )
            exit ( "Error loading mesh" );
        
        loadAiMesh ( scene -> mMeshes [0], scale, vertices, indices );
        createMeshlets ( vertices, indices );
    }
    
        // raw create meshlets
    void    createMeshlets ( const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices )
    {
        std::vector<meshopt_Meshlet>    mls               ( meshopt_buildMeshletsBound ( indices.size (), maxVertices, maxTriangles ) );
        std::vector<unsigned int>       meshlet_vertices  ( mls.size() * maxVertices );
        std::vector<unsigned char>      meshlet_triangles ( mls.size() * maxTriangles * 3 );

        mls.resize ( meshopt_buildMeshlets ( mls.data (), meshlet_vertices.data (), meshlet_triangles.data (), indices.data (), indices.size (), (const float *)&vertices[0], vertices.size(), sizeof(Vertex), maxVertices, maxTriangles, coneWeight));
        
        for ( auto& m : mls )
        {
            Meshlet meshlet;
            
            meshlet.vertexCount   = m.vertex_count;
            meshlet.triangleCount = m.triangle_count;
            
            memcpy ( meshlet.vertices, meshlet_vertices.data () + m.vertex_offset, m.vertex_count * sizeof ( uint32_t ) );

            for ( int i = 0; i < m.triangle_count * 3; i++ )
                meshlet.indices [i] = meshlet_triangles [m.triangle_offset + i];

            meshlets.push_back ( meshlet );
        }       
    }
};

int main ( int argc, char * argv [] )
{
    GlutWindow::init ( argc, argv, 4, 5 );
    
    MeshWindow  win;
    
    return GlutWindow::run ();
}

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

-- mesh
#version 450

#extension GL_NV_mesh_shader: require

layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
layout(triangles, max_vertices = 64, max_primitives = 64) out;

uniform mat4 proj;
uniform mat4 mv;

struct  Vertex
{
    vec4   pos;
    vec4   normal;
};

struct Meshlet
{
    uint    vertices [64];
    uint    indices  [64 * 3];
    uint    vertexCount;
    uint    triangleCount;
};

layout (binding = 0) readonly buffer Vertices
{
    Vertex  vertices [];
};

layout(binding = 1) readonly buffer Meshlets
{
    Meshlet meshlets [];
};

out PerVertexData
{
  vec4 color;
} v_out [];   


uint hash ( uint a )
{
   a = (a+0x7ed55d16) + (a<<12);
   a = (a^0xc761c23c) ^ (a>>19);
   a = (a+0x165667b1) + (a<<5);
   a = (a+0xd3a2646c) ^ (a<<9);
   a = (a+0xfd7046c5) + (a<<3);
   a = (a^0xb55a4f09) ^ (a>>16);
   return a;
}

vec3    uint2Vec3 ( uint i )
{
    return vec3 ( float(i & 255), float((i >> 8) & 255), float((i >> 16) & 255) ) / 255.0;
}

void main()
{

    uint mi     = gl_WorkGroupID.x;             // meshlet index
    uint ti     = gl_LocalInvocationIndex;      // vertex index inside meshlet
    uint mhash  = hash ( mi );                  // assign unique color to each meshlet
    vec3 mcolor = uint2Vec3 ( mhash );

            // write vertices
    for ( uint i = ti; i < meshlets [mi].vertexCount; i += 32 )
    {
        uint vi       = meshlets [mi].vertices [i];
        vec4 position = proj * mv * vertices [vi].pos;

        gl_MeshVerticesNV [i].gl_Position = position;
        v_out [i].color                   = vec4 ( mcolor, 1.0 );
    }

            // write indices
    for ( uint i = ti; i < meshlets[mi].triangleCount; i += 32 )
    {
        gl_PrimitiveIndicesNV [i*3]     = meshlets [mi].indices [i*3];
        gl_PrimitiveIndicesNV [i*3 + 1] = meshlets [mi].indices [i*3 + 1];
        gl_PrimitiveIndicesNV [i*3 + 2] = meshlets [mi].indices [i*3 + 2];
    }
        
    gl_PrimitiveCountNV = meshlets [mi].triangleCount;
}

-- fragment
#version 450

layout(location = 0) out vec4 color;

in PerVertexData
{
    vec4 color;
} fragIn;  

void main()
{
  color = fragIn.color;
}

Полезные ссылки

Introduction to Turing Mesh Shaders

MESH SHADERS IN TURING

Quick Introduction to Mesh Shaders (OpenGL and Vulkan)

RGB Triangle with Mesh Shaders in OpenGL

Textured Quad with Mesh Shaders in OpenGL and Vulkan

GL_NV_mesh_shader Simple Mesh Shader Example

Mesh and task shaders intro and basics

Meshlet size tradeoffs

Meshlet triangle locality matters

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