![]() |
Главная
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Уже на протяжении многих лет существует текущий графический конвейер, изображенный на следующем рисунке.
Рис 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-шейдер очень похож на вычислительный шейдер,
он также запускает группы нитей, каждая нить имеет свой идентификатор в пределах группы
(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-шейдер. Обратите внимание, что в то время, как наличие 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-шейдер отсутствует) шейдера в 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-шейдер на С++.
#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
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 triangle locality matters
Скачать исходный код к этой статье можно скачать по следующей ссылке.
Обратите внимание, что для работы этотго примера нужна версия класса Program
, поддерживающая mesh-шейдеры (она прилагается в архиве).