Link

我们有一种运行计算着色器来显示的方法,以及一种向引擎添加调试 UI 的方法。 让我们使用它来通过 UI 向着色器发送数据,并进行一些交互操作。

我们将使用 PushConstants 向着色器发送数据。 PushConstants 是 Vulkan 独有的功能,允许向 GPU 发送少量数据。 保持数据量小很重要,因为如果数据量低于某个字节数(请查阅 GPU 供应商文档),大多数驱动程序都会有一个快速通道。 它的主要用例是发送一些每个对象的索引或几个每个对象都会更改的矩阵。 如果您拥有的数据大于几个浮点数或整数,则应使用其他系统,我们将在下一章中展示这些系统。

Push constants 在创建管线布局时配置。 为了保持简单且不必更改太多代码,我们将默认计算效果的 pushconstants 为 4 个 vec4 向量。 16 个浮点数足以进行着色器实验。

在项目着色器文件夹中,有多个计算着色器可供您切换。 我们将专注于一个简单的颜色渐变着色器,但您可以尝试项目附带的其他演示着色器。

由于我们所有的计算着色器都将共享相同的布局,因此我们还将在 UI 中添加一个下拉菜单,以选择要使用的管线。 这样我们就可以在运行时在不同的计算着色器之间切换以进行测试。

我们将用于演示 pushconstants 的着色器是这个。 它将通过 Y 坐标在 2 种颜色之间混合,形成垂直渐变。

它位于着色器文件夹中的 gradient_color.comp 下

#version 460

layout (local_size_x = 16, local_size_y = 16) in;

layout(rgba16f,set = 0, binding = 0) uniform image2D image;

//push constants block
layout( push_constant ) uniform constants
{
 vec4 data1;
 vec4 data2;
 vec4 data3;
 vec4 data4;
} PushConstants;

void main() 
{
    ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);

	ivec2 size = imageSize(image);

    vec4 topColor = PushConstants.data1;
    vec4 bottomColor = PushConstants.data2;

    if(texelCoord.x < size.x && texelCoord.y < size.y)
    {
        float blend = float(texelCoord.y)/(size.y); 
    
        imageStore(image, texelCoord, mix(topColor,bottomColor, blend));
    }
}

它与我们上一篇文章中的渐变着色器基本相同。 我们添加了一个包含 4 个 vec4 的 push constant 块,并且我们从中加载顶部和底部颜色。 data3 和 data4 未使用,但我们在其中添加了它们,以避免验证层抱怨我们的 push-constants 范围大于着色器中的范围。

我们现在需要更改管线布局创建以配置 pushconstants 范围。 让我们首先创建一个结构,将这些 pushconstants 直接镜像到 vk_engine.h 中。

struct ComputePushConstants {
	glm::vec4 data1;
	glm::vec4 data2;
	glm::vec4 data3;
	glm::vec4 data4;
};

要设置 push constant 范围,我们需要更改在 init_pipelines 开始时创建管线布局的代码。 新版本如下所示

VkPipelineLayoutCreateInfo computeLayout{};
computeLayout.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
computeLayout.pNext = nullptr;
computeLayout.pSetLayouts = &_drawImageDescriptorLayout;
computeLayout.setLayoutCount = 1;

VkPushConstantRange pushConstant{};
pushConstant.offset = 0;
pushConstant.size = sizeof(ComputePushConstants) ;
pushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

computeLayout.pPushConstantRanges = &pushConstant;
computeLayout.pushConstantRangeCount = 1;

VK_CHECK(vkCreatePipelineLayout(_device, &computeLayout, nullptr, &_gradientPipelineLayout));

我们需要向管线布局信息添加一个 VkPushConstantRange。 PushConstantRange 包含一个偏移量(我们将保持为 0),然后是一个大小加上阶段标志。 对于大小,我们将使用结构的 cpp 版本,因为它匹配。 对于阶段标志,它将是计算,因为它是我们目前唯一的阶段。

之后,只需更改要编译的着色器为新的着色器

VkShaderModule computeDrawShader;
if (!vkutil::load_shader_module("../../shaders/gradient_color.comp.spv", _device, &computeDrawShader))
{
	std::cout << "Error when building the colored mesh shader" << std::endl;
}

这就是向着色器添加 pushconstants 所需的一切。 现在让我们从渲染循环中使用它们

	// bind the gradient drawing compute pipeline
	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _gradientPipeline);

	// bind the descriptor set containing the draw image for the compute pipeline
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _gradientPipelineLayout, 0, 1, &_drawImageDescriptors, 0, nullptr);

	ComputePushConstants pc;
	pc.data1 = glm::vec4(1, 0, 0, 1);
	pc.data2 = glm::vec4(0, 0, 1, 1);

	vkCmdPushConstants(cmd, _gradientPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(ComputePushConstants), &pc);
	// execute the compute pipeline dispatch. We are using 16x16 workgroup size so we need to divide by it
	vkCmdDispatch(cmd, std::ceil(_drawExtent.width / 16.0), std::ceil(_drawExtent.height / 16.0), 1);

要更新 pushconstants,我们调用 VkCmdPushConstants。 它需要管线布局、要写入数据的偏移量(我们只使用偏移量 0)、数据的大小 + 要复制的指针。 它还需要着色器阶段标志,因为可以在不同命令上更新不同阶段的 pushconstants。

这就是全部。 如果您此时运行程序,您将看到从红色到蓝色的渐变。

Imgui 可编辑参数

我们现在正在硬编码颜色,但我们可以通过使用 imgui 添加一个包含这些可编辑颜色的小窗口来做得更好。

我们想要存储我们将要绘制的计算管线数组,以及其中一个 ComputePushConstant 结构体用于它们的值。 这样我们将能够切换不同的计算着色器。

让我们在 vk_engine.h 中添加一个结构体

struct ComputeEffect {
    const char* name;

	VkPipeline pipeline;
	VkPipelineLayout layout;

	ComputePushConstants data;
};

现在让我们向 VulkanEngine 类添加一个它们的数组,并添加一个整数来保存绘制时要使用的索引

std::vector<ComputeEffect> backgroundEffects;
int currentBackgroundEffect{0};

让我们更改 init_pipelines 中的代码以创建 2 个这样的效果。 一个是我们刚刚完成的渐变,另一个是一个漂亮的星夜天空着色器。

天空着色器太复杂,无法在此处解释,但请随意查看 sky.comp 上的代码。 它取自 shadertoy,并稍作修改以在此处作为计算着色器运行。 pushconstant 的 data1 将包含天空颜色 x/y/z,然后 w 可用于控制星星的数量。

对于 2 个着色器,我们需要创建 2 个不同的 VkShaderModule。

VkShaderModule gradientShader;
if (!vkutil::load_shader_module("../../shaders/gradient_color.comp.spv", _device, &gradientShader)) {
	fmt::print("Error when building the compute shader \n");
}

VkShaderModule skyShader;
if (!vkutil::load_shader_module("../../shaders/sky.comp.spv", _device, &skyShader)) {
	fmt::print("Error when building the compute shader \n");
}

VkPipelineShaderStageCreateInfo stageinfo{};
stageinfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stageinfo.pNext = nullptr;
stageinfo.stage = VK_SHADER_STAGE_COMPUTE_BIT;
stageinfo.module = gradientShader;
stageinfo.pName = "main";

VkComputePipelineCreateInfo computePipelineCreateInfo{};
computePipelineCreateInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
computePipelineCreateInfo.pNext = nullptr;
computePipelineCreateInfo.layout = _gradientPipelineLayout;
computePipelineCreateInfo.stage = stageinfo;

ComputeEffect gradient;
gradient.layout = _gradientPipelineLayout;
gradient.name = "gradient";
gradient.data = {};

//default colors
gradient.data.data1 = glm::vec4(1, 0, 0, 1);
gradient.data.data2 = glm::vec4(0, 0, 1, 1);

VK_CHECK(vkCreateComputePipelines(_device, VK_NULL_HANDLE, 1, &computePipelineCreateInfo, nullptr, &gradient.pipeline));

//change the shader module only to create the sky shader
computePipelineCreateInfo.stage.module = skyShader;

ComputeEffect sky;
sky.layout = _gradientPipelineLayout;
sky.name = "sky";
sky.data = {};
//default sky parameters
sky.data.data1 = glm::vec4(0.1, 0.2, 0.4 ,0.97);

VK_CHECK(vkCreateComputePipelines(_device, VK_NULL_HANDLE, 1, &computePipelineCreateInfo, nullptr, &sky.pipeline));

//add the 2 background effects into the array
backgroundEffects.push_back(gradient);
backgroundEffects.push_back(sky);

//destroy structures properly
vkDestroyShaderModule(_device, gradientShader, nullptr);
vkDestroyShaderModule(_device, skyShader, nullptr);
_mainDeletionQueue.push_function([=]() {
	vkDestroyPipelineLayout(_device, _gradientPipelineLayout, nullptr);
	vkDestroyPipeline(_device, sky.pipeline, nullptr);
	vkDestroyPipeline(_device, gradient.pipeline, nullptr);
});

我们更改了 pipelines 函数。 我们保留了之前的管线布局,但现在我们创建了 2 个不同的管线,并将它们存储到 ComputeEffect 向量中。 我们还为效果提供了一些默认数据。

现在我们可以为此添加 imgui 调试窗口。 这将在 run() 函数中进行。 我们将用新的 ui 逻辑替换演示效果调用

		ImGui::NewFrame();
		
		if (ImGui::Begin("background")) {
			
			ComputeEffect& selected = backgroundEffects[currentBackgroundEffect];
		
			ImGui::Text("Selected effect: ", selected.name);
		
			ImGui::SliderInt("Effect Index", &currentBackgroundEffect,0, backgroundEffects.size() - 1);
		
			ImGui::InputFloat4("data1",(float*)& selected.data.data1);
			ImGui::InputFloat4("data2",(float*)& selected.data.data2);
			ImGui::InputFloat4("data3",(float*)& selected.data.data3);
			ImGui::InputFloat4("data4",(float*)& selected.data.data4);
		}
		ImGui::End();

		ImGui::Render();

首先,我们通过索引数组来获取选定的计算效果。 然后我们使用 Imgui::Text 显示效果名称,然后我们有 int 滑块和 float4 输入用于编辑。

最后我们需要做的是更改渲染循环以选择使用其数据选择的着色器

	ComputeEffect& effect = backgroundEffects[currentBackgroundEffect];

	// bind the background compute pipeline
	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, effect.pipeline);

	// bind the descriptor set containing the draw image for the compute pipeline
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _gradientPipelineLayout, 0, 1, &_drawImageDescriptors, 0, nullptr);

	vkCmdPushConstants(cmd, _gradientPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(ComputePushConstants), &effect.data);
	// execute the compute pipeline dispatch. We are using 16x16 workgroup size so we need to divide by it
	vkCmdDispatch(cmd, std::ceil(_drawExtent.width / 16.0), std::ceil(_drawExtent.height / 16.0), 1);

变化不大,我们只是连接到计算效果数组并从那里上传 pushconstants。

尝试现在运行应用程序,您将看到一个调试窗口,您可以在其中选择着色器并编辑其参数。

下一步: 第 3 章:图形管线