Link

我们要绘制第一个三角形,首先需要设置它将使用的着色器。对于三角形,我们只需要 2 个着色器,这是渲染 3D 对象的最小值。我们将有一个片段着色器,它只是将片段着色为红色,以及一个形成三角形的顶点着色器。

着色器是用 GLSL 编写的,它是 OpenGL 着色语言。它类似于 C,但有一些小的差异。

顶点着色器将为每个绘制调用中的顶点执行一次函数(在本例中我们将使用 main(),但它可以有其他名称)。顶点着色器的任务是写入 gl_Position 以输出顶点的最终位置,并将更多变量输出到片段着色器。

片段着色器将使用来自顶点着色器的变量,并且对于每个三角形覆盖的像素,它将执行一次。它的任务是输出最终颜色。

我们将向着色器文件夹添加 2 个新文件。 triangle.frag 和 triangle.vert

顶点着色器

让我们打开 triangle.vert 并编写着色器

//we will be using glsl version 4.5 syntax
#version 450

void main()
{
	//const array of positions for the triangle
	const vec3 positions[3] = vec3[3](
		vec3(1.f,1.f, 0.0f),
		vec3(-1.f,1.f, 0.0f),
		vec3(0.f,-1.f, 0.0f)
	);

	//output the position of each vertex
	gl_Position = vec4(positions[gl_VertexIndex], 1.0f);
}

在我们的第一个顶点着色器中,我们创建了一个由 3 个 vec3 组成的常量数组,这将是三角形每个顶点的位置。

然后我们必须写入 gl_Position 以告诉 GPU 顶点的位置。这是顶点着色器工作的必要条件。在这里,我们将使用 gl_VertexIndex 访问我们的位置数组,gl_VertexIndex 是正在执行此着色器的顶点的编号。然后我们将其转换为 vec4,因为这是 gl_Position 所期望的。

片段着色器

//glsl version 4.5
#version 450

//output write
layout (location = 0) out vec4 outFragColor;

void main()
{
	//return red
	outFragColor = vec4(1.f,0.f,0.f,1.0f);
}

我们的片段着色器甚至更简单。我们将只返回硬编码的红色。

输出行非常重要

layout (location = 0) out vec4 outFragColor;

在这里,我们声明了片段着色器输出的变量。我们声明我们将在位置 0 输出一个 vec4。如果我们同时写入多个图像,例如来自 GBuffer,我们将有更多输出变量,但在这种情况下,一个就足够了。

编译着色器。

如果着色器位于正确的文件夹 project/shaders 中,它们将被 CMake 检测到。重新生成 Cmake visual studio 项目,它们应该被检测到。检查生成的 visual studio 解决方案上的“Shaders”项目,新的 2 个文件应该在那里。如果您重建 Shaders 项目,则应该编译着色器。在构建输出中,如果发生错误,它将给出错误。

如果您查看根项目文件夹下的 CMakeLists.txt,您将看到它正在创建一个自定义的 Shader 目标,该目标通过抓取 shaders/ 文件夹中所有以 *.frag 和 *.vert 结尾的文件来构建。我建议您阅读该部分。它已被注释,解释了它的工作原理。

Vulkan 着色器工作流程

Vulkan 不直接理解 GLSL,它理解 SPIRV。 SPIRV 是 Vulkan 的着色器字节码。将 SPIRV 视为 GLSL 的二进制优化版本。

我们需要将我们刚刚编写的 GLSL 转换为 spirv,以便 Vulkan 可以理解它。这就是我们上面所做的,结果是一些 .spv 文件,我们可以将其加载到 Vulkan 上。

Vulkan SDK 附带了 glslang 着色器编译器的内置版本,我们在这里使用它来离线编译着色器。可以使用相同的编译器作为库,并在您的游戏引擎中即时编译 GLSL 着色器,但我们目前不打算这样做。

在代码中加载着色器

现在我们有了 .spv 文件,我们可以尝试加载它们。

在 Vulkan 中,加载的着色器存储在 VkShaderModule 中。您可以将多个 VkShaderModule 与多个管线一起使用和组合,因此我们将首先创建一个“load_module()”函数,该函数将加载 SPIRV 文件并将其编译为 VkShaderModule

让我们首先向我们的 VulkanEngine 类添加一个新函数


//loads a shader module from a spir-v file. Returns false if it errors
bool load_shader_module(const char* filePath, VkShaderModule* outShaderModule);

并开始其实现

bool VulkanEngine::load_shader_module(const char* filePath, VkShaderModule* outShaderModule)
{
	return false;
}

我们将使用来自 Cpp 的标准文件输出。确保将 <fstream> 添加到 vk_engine.cpp 中的包含列表。

我们在 load_shader_module 函数中要做的第一件事是打开文件

bool VulkanEngine::load_shader_module(const char* filePath, VkShaderModule* outShaderModule)
{
	//open the file. With cursor at the end
	std::ifstream file(filePath, std::ios::ate | std::ios::binary);

	if (!file.is_open()) {
		return false;
	}
}

我们将使用标志 std::ios::binary 以二进制模式打开流,并使用 std::ios::ate 将流光标放在末尾

在 cpp 中,文件操作是在流上完成的,它有一个我们将必须使用的光标。因为光标现在位于文件末尾,我们可以使用它来了解文件有多大,创建一个足够大的 std::vector<uint32_t> 来容纳整个着色器文件,然后将整个文件复制到向量中


//find what the size of the file is by looking up the location of the cursor
//because the cursor is at the end, it gives the size directly in bytes
size_t fileSize = (size_t)file.tellg();

//spirv expects the buffer to be on uint32, so make sure to reserve an int vector big enough for the entire file
std::vector<uint32_t> buffer(fileSize / sizeof(uint32_t));

//put file cursor at beginning
file.seekg(0);

//load the entire file into the buffer
file.read((char*)buffer.data(), fileSize);

//now that the file is loaded into the buffer, we can close it
file.close();

有了这个,我们现在已经将整个着色器文件加载到 buffer std 向量中,并且可以将其加载到 Vulkan 上。


//create a new shader module, using the buffer we loaded
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.pNext = nullptr;

//codeSize has to be in bytes, so multiply the ints in the buffer by size of int to know the real size of the buffer
createInfo.codeSize = buffer.size() * sizeof(uint32_t);
createInfo.pCode = buffer.data();

//check that the creation goes well.
VkShaderModule shaderModule;
if (vkCreateShaderModule(_device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
	return false;
}
*outShaderModule = shaderModule;
return true;

在着色器中出现错误是很常见的,这会导致 vkCreateShaderModule 失败,因此我们在此处不会使用 VK_CHECK 宏。

创建 VkShaderModule 非常简单,我们只需要使用典型的 Vulkan sType 和 pnext 样板填充创建信息,然后将代码指针设置为我们存储文件的向量。然后我们只需调用该函数即可获得结果。

在初始化时加载着色器。

我们将向 VulkanEngine 类添加另一个 init_ 函数 init_pipelines(),我们将在其中初始化我们要渲染的对象的管线。我们尚不打算编写管线,但我们将尝试加载 SPIRV 着色器以查看一切是否顺利。

也将其添加到类中的其他 init_ 声明中

void VulkanEngine::init_pipelines(){

VkShaderModule triangleFragShader;
	if (!load_shader_module("../../shaders/triangle.frag.spv", &triangleFragShader))
	{
		std::cout << "Error when building the triangle fragment shader module" << std::endl;
	}
	else {
		std::cout << "Triangle fragment shader successfully loaded" << std::endl;
	}

	VkShaderModule triangleVertexShader;
	if (!load_shader_module("../../shaders/triangle.vert.spv", &triangleVertexShader))
	{
		std::cout << "Error when building the triangle vertex shader module" << std::endl;

	}
	else {
		std::cout << "Triangle vertex shader successfully loaded" << std::endl;
	}
}

让我们使用相对路径加载两个着色器模块。很容易使其无法正常工作,因此我们将打印到控制台以查看着色器是否已加载。

剩下的唯一事情是从我们的主 init() 函数调用 init_pipelines() 函数。我们可以在我们想要的任何时间点调用它,只要它在 init_vulkan() 之后,但我们只会将其添加到末尾。

void VulkanEngine::init()
{
	// ... other stuff ...


	init_pipelines();

	//everything went fine
	_isInitialized = true;
}

如果您此时运行代码,您应该会在控制台窗口中看到“successfully loaded”输出。

下一步:设置渲染管线