(Demo) RGB Triangle with Mesh Shaders in OpenGL


GeeXLab - Mesh Shaders in OpenGL



Downloads

 
Mesh shaders are a feature of NVIDIA Turing GPUs. The mesh shader pipeline replaces the regular VTG pipeline (VTG = Vertex / Tessellation / Geometry) and can be seen as the association of a compute-like shader and a fragment shader. Like with a compute shader, you can set the number of threads for workgroups or use synchronization functions like barrier().

Mesh shaders introduce two new shader stages: the task shader and the mesh shader. In this demo, only the mesh shader stage will be used.

A mesh shader offers many possibilities, one being to generate primitives ex-nihilo (= procedurally). No input data is needed by the mesh shader to create primnitives. A primitive can be a point, a line or a triangle.

With a mesh shader, you can for example, generate a triangle directly in the mesh shader (by filling vertices and face indices arrays) and send it to the rasterizer (and then to the pixel shader).

In OpenGL, mesh shaders are exposed via the GL_NV_mesh_shader extension. You can check this support in a GeeXLab script with:

is_supported = gh_renderer.check_opengl_extension("GL_NV_mesh_shader")
if (is_supported == 1) then
  ...
end

 
Using the new functions gl_get_integeri_1i() and gl_get_integerv_1i() of the gh_renderer lib, you can query some hardware limits:

local GL_MAX_DRAW_MESH_TASKS_COUNT_NV = 38205 -- 0x953D
x = gh_renderer.gl_get_integerv_1i(GL_MAX_DRAW_MESH_TASKS_COUNT_NV)
print("GL_MAX_DRAW_MESH_TASKS_COUNT_NV: " .. x)
 
local GL_MAX_MESH_WORK_GROUP_SIZE_NV = 38203 -- 0x953b
x = gh_renderer.gl_get_integeri_1i(GL_MAX_MESH_WORK_GROUP_SIZE_NV, 0)
y = gh_renderer.gl_get_integeri_1i(GL_MAX_MESH_WORK_GROUP_SIZE_NV, 1)
z = gh_renderer.gl_get_integeri_1i(GL_MAX_MESH_WORK_GROUP_SIZE_NV, 2)
print(string.format("GL_MAX_MESH_WORK_GROUP_SIZE_NV: %d x %d x %d", x, y, z))
 
local GL_MAX_MESH_OUTPUT_VERTICES_NV = 38200 -- 0x9538
x = gh_renderer.gl_get_integerv_1i(GL_MAX_MESH_OUTPUT_VERTICES_NV)
print("GL_MAX_MESH_OUTPUT_VERTICES_NV: " .. x)
 
local GL_MAX_MESH_OUTPUT_PRIMITIVES_NV = 38201 -- 0x9539
x = gh_renderer.gl_get_integerv_1i(GL_MAX_MESH_OUTPUT_PRIMITIVES_NV)
print("GL_MAX_MESH_OUTPUT_PRIMITIVES_NV: " .. x)

 
For a GeForce RTX 2070, we have:

- GL_MAX_DRAW_MESH_TASKS_COUNT_NV: 65535
- GL_MAX_MESH_WORK_GROUP_SIZE_NV: 32 x 1 x 1
- GL_MAX_MESH_OUTPUT_VERTICES_NV: 256
- GL_MAX_MESH_OUTPUT_PRIMITIVES_NV: 512

 
To draw a triangle with mesh shaders, we need two things:
– a GPU program with a mesh shader and a pixel shader.
– a way to execute the mesh shader.

There are several ways to create a GPU program in GeeXLab. I choose the XML + shader files way. The mesh shader GPU program is declared in the main XML file while shaders are stored in files:

  <gpu_program name="nv_mesh_prog" 
               filename_ms="shaders/ms.glsl" livecoding_ms="1" 
               filename_ps="shaders/ps.glsl" livecoding_ps="1" />

Live-coding for mesh and pixel shaders is enabled.

The following GLSL mesh shader (in shaders/ms.glsl) generates a triangle:

#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[];   
 
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)};
 
uniform float scale;
 
void main()
{
  gl_MeshVerticesNV[0].gl_Position = vec4(vertices[0] * scale, 1.0); 
  gl_MeshVerticesNV[1].gl_Position = vec4(vertices[1] * scale, 1.0); 
  gl_MeshVerticesNV[2].gl_Position = vec4(vertices[2] * scale, 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;
}

The main function of the mesh shader is to fill the following built-in variables:

  • gl_MeshVerticesNV: vertices array. A triangle has 3 vertices.
  • gl_PrimitiveIndicesNV: indices array. A triangle has 3 indices, one per vertex.
  • gl_PrimitiveCountNV: number of primitives. A triangle is a primitive made up of three vertices.

 
The pixel shader (shaders/ps.glsl):

#version 450
 
layout(location = 0) out vec4 FragColor;
 
in PerVertexData
{
  vec4 color;
} fragIn;  
 
void main()
{
  FragColor = fragIn.color;
}

 
We have a GPU program made up of a mesh and a pixel shaders. Now to draw the triangle, we run this GPU program using the new draw_mesh_tasks() of gh_renderer, which is a wrapper around glDrawMeshTasksNV, no more no less. This function takes two parameters: an offset and a count. In our demo, offset is set to 0. Count is the number of work groups to launch, each work group generating a primitive. In the demo, one workgroup is launched.

The FRAME script will look like:

gh_renderer.set_viewport(0, 0, winW, winH)
gh_renderer.clear_color_depth_buffers(0.2, 0.2, 0.2, 1.0, 1.0)
 
gh_gpu_program.bind(nv_mesh_prog)
 
local offset = 0
local count = 1
gh_renderer.draw_mesh_tasks(offset, count)

 
The final result:

GeeXLab - Mesh Shaders demo in OpenGL

 
The demo requires GeeXLab 0.32+ and is available in the geexlab-demopack-mesh-shaders/vk/rgb-triangle/ folder of the mesh shader demopack.

 
Here is a shorter way to write the mesh shader. In the previous mesh shader, we set one thread per work group with:

layout(local_size_x=1) in;

Now we’re going to set three threads per work group:

layout(local_size_x=3) in;

We can use the built-in gl_LocalInvocationID.x variable to loop over vertices and indices arrays. gl_LocalInvocationID.x is the index of the current thread (there are 3 threads). The mesh shader can be written as follows:

#version 450
#extension GL_NV_mesh_shader : require
 
// Set the number of threads per workgroup (always one-dimensional).
layout(local_size_x=3) in; 
 
// maximum allocation size for each meshlet
layout(max_vertices=3, max_primitives=1) out;
 
// the primitive type (points,lines or triangles)
layout(triangles) out;
 
out PerVertexData
{
  vec4 color;
} v_out[];   
 
 
uniform float scale;
 
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()
{
  uint thread_id = gl_LocalInvocationID.x;
  gl_MeshVerticesNV[thread_id].gl_Position = vec4(vertices[thread_id]*scale, 1.0);
  gl_PrimitiveIndicesNV[thread_id] = thread_id;
  v_out[thread_id].color = vec4(colors[thread_id], 1.0);
  gl_PrimitiveCountNV = 1;
}

 
References





2 thoughts on “(Demo) RGB Triangle with Mesh Shaders in OpenGL”

Leave a Comment

Your email address will not be published. Required fields are marked *