网格着色器剔除

此示例的源代码可以在 Khronos Vulkan 示例 github 存储库中找到。
Mesh Shader Culling

概述

此示例演示如何结合 Vulkan 扩展 VK_EXT_mesh_shader,并介绍网格着色器中的每个图元剔除。

启用网格着色

要启用网格着色功能,需要以下扩展(注意:作为基本要求,需要 VK_API_VERSION_1_1

1) VK_KHR_SPIRV_1_4_EXTENSION_NAME 2) VK_EXT_MESH_SHADER_EXTENSION_NAME 3) VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME

要启用任务着色器和网格着色器,请启用 VkPhysicalDeviceMeshShaderFeaturesEXT 功能中的以下标志。 1) taskShader 2) meshShader

管线创建

使用网格着色器管线时,顶点输入状态和输入汇编状态将被忽略。这是因为网格管线负责定义/创建标准片段管线使用的顶点信息。

网格管线可以像本示例中一样创建自己的顶点。或者,它可以像使用模型时使用计算着色器一样从应用程序接收它们。

因此,我们将 pVertexInputStatepInputAssemblyState 设置为 NULL 来禁用它们。

链接资源

在此示例中,我们使用 UBO(统一缓冲区对象)来设置剔除的设置。

	struct UBO
	{
		float cull_center_x   = 0.0f;
		float cull_center_y   = 0.0f;
		float cull_radius     = 1.75f;
		float meshlet_density = 2.0f;
	} ubo_cull{};
  • cull_center_xcull_center_y 决定剔除掩模的平移

  • cull_radius 定义剔除掩模的大小。

  • meshlet_density 定义示例使用的网格片总数。

任务着色器

任务着色器是可选但推荐的阶段,负责启动网格着色器。它有两个目的

  • 决定在工作组中启动多少个网格着色器。

  • 创建一个任务负载,网格着色器将具有只读访问权限。

    • GLSL 中的 taskPayloadSharedEXT 类型变量最多只能存在一次,它被 EmitMeshTasksEXT 隐式使用,其行为类似于任务着色器中的共享内存,以及网格着色器中的只读 SSBO。

通常,每当使用网格管线时,都应使用任务着色器。虽然它是“可选的”,但强烈建议使用它们以充分利用网格着色管线。唯一不使用它们的情况是用于非常简单的场景,例如渲染 网格着色器示例 中找到的单个三角形。

// Example of the data shared with its associated mesh shader:
// 1) define some structure if more than one variable data sharing is desired:
// Please note: GPU vendors recommend to use as little task payload as possible, eg. by packing the data to fewer bits etc.
struct SharedData
{
    vec4  positionTransformation;
    int   N;
    int   meshletsNumber;
    float subDimension;
    float cullRadius;
};
// 2) use the following variable with a storage class specifier to "establish the connection"
taskPayloadSharedEXT SharedData sharedData;

一旦在任务着色器中使用 taskPayloadSharedEXT 定义了变量,则在调用 EmitMeshTasksEXT 时,该变量将与网格着色器共享。

通常,网格管线指的是一个新的管线,它将片段着色器之前的所有内容替换为一个(可选的)任务着色阶段,该阶段可以调用其他网格着色阶段。

  • 一个任务着色器(可选)用于启动网格着色器工作组

  • 网格着色器负责生成图元和顶点。

有关发射网格任务的更多详细信息,请参阅附加的文章

GPU 制造商在其硬件中设置工作组和网格大小方面推荐了最佳实践。更多阅读可以在这里找到

网格着色器

任务着色器和网格着色器在工作组中执行,类似于计算着色器。每个任务着色器工作组可以启动多个网格着色器工作组。每个网格着色器工作组负责生成顶点和图元。API允许任何逻辑,但典型的应用程序建议围绕meshlet组织,meshlet是一小组顶点和图元。通常,每个任务着色器调用处理一组meshlet,而每个网格着色器工作组处理一个meshlet。顶点和图元的生成过程可以在以下代码中找到

// Vertices:
gl_MeshVerticesEXT[k * 4 + 0].gl_Position = vec4(2.0 * sharedData.subDimension * unitVertex_0, 0.0f, 1.0f) + sharedData.positionTransformation + displacement;
gl_MeshVerticesEXT[k * 4 + 1].gl_Position = vec4(2.0 * sharedData.subDimension * unitVertex_1, 0.0f, 1.0f) + sharedData.positionTransformation + displacement;
gl_MeshVerticesEXT[k * 4 + 2].gl_Position = vec4(2.0 * sharedData.subDimension * unitVertex_2, 0.0f, 1.0f) + sharedData.positionTransformation + displacement;
gl_MeshVerticesEXT[k * 4 + 3].gl_Position = vec4(2.0 * sharedData.subDimension * unitVertex_3, 0.0f, 1.0f) + sharedData.positionTransformation + displacement;
// Indices
gl_PrimitiveTriangleIndicesEXT[k * 2 + 0] = unitPrimitive_0 + k * uvec3(4);
gl_PrimitiveTriangleIndicesEXT[k * 2 + 1] = unitPrimitive_1 + k * uvec3(4);
// Assigning the color output:
vec3 color = vec3(1.0f, 0.0f, 0.0f) * (k + 1) / sharedData.meshletsNumber;
outColor[k * 4 + 0] = color;
outColor[k * 4 + 1] = color;
outColor[k * 4 + 2] = color;
outColor[k * 4 + 3] = color;

有关meshlet生成的更多详细信息,请参见所附文章

逐图元剔除

此示例使用了来自网格着色器的简单逐图元剔除功能。网格着色的目的是仅生成与场景相关的几何图形。

在此示例中,一个圆形可视区域以原点为中心,具有可调节的半径,由GUI控制。当图元移出可视区域时,将跳过其生成过程。

// the actual position of each meshlet:
vec4 position = displacement + sharedData.positionTransformation;
float squareRadius = position.x * position.x + position.y * position.y;
// Cull Logic: only if the meshlet center position is within the view circle defined by the cull radius,
// then the meshlet will be generated.
if (squareRadius < sharedData.cullRadius * sharedData.cullRadius)
{
    // Generating meshlets
}

请注意,每个网格的剔除应在任务着色器中完成,并用于防止网格着色器甚至启动。此处演示的简单剔除方法并非网格着色器中剔除的最佳使用方式,实际上由于收益有限而不建议使用。相反,请选择通过在任务着色器中进行剔除来限制需要启动的网格着色器的数量。

有关更高级的剔除解决方案,请参见以下视频