【Vulkan学习记录-基础篇-2】用Vulkan画两个重叠的矩形

在前一篇中完成了对Vulkan的初始化和三角形的绘制,其中很多东西还没有被用到,这一节最终将绘制这样两个重叠的矩形,并且它们会一直绕着屏幕中心点进行旋转。
在这里插入图片描述
将要补充使用的内容有:VertexBuffer、IndexBuffer、StagingBuffer、UniformBuffer的创建和使用,深度缓冲的创建和设定。
代码将基于第一节的内容进行增添和修改。

1. 创建VertexBuffer、IndexBuffer、StagingBuffer

在前一节绘制三角形时,采用的是在Shader中对三角形的屏幕坐标进行硬编码的形式来绘制,而一般的渲染应该是通过输入顶点,让顶点经过一系列的变换来最终获得它们在屏幕上的位置,所以我们需要创建VertexBuffer和IndexBuffer来指定顶点的数据。

首先先修改一下VertexShader的代码:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

顶点的输入数据有inPosition和inColor,注意这里的Location,它们将会与之后创建描述符进行绑定时对应。

然后我们需要提供这样的结构体作为顶点输入:

struct Vertex 
{
	float pos[3];
	float color[3];
}

然后创建这样一组顶点数据:

const std::vector<Vertex> vertices = {
	{
  {-0.2f, -0.2f , 0.5f}, {0.0f, 1.0f, 0.0f}},
	{
  {0.8f, -0.2f , 0.5f}, {0.0f, 1.0f, 0.0f}},
	{
  {0.8f, 0.8f , 0.5f}, {0.0f, 1.0f, 0.0f}},
	{
  {-0.2f, 0.8f , 0.5f}, {0.0f, 1.0f, 0.0f}},

	{
  {-0.5f, -0.5f , 0.4f}, {1.0f, 0.0f, 0.0f}},
	{
  {0.5f, -0.5f , 0.4f}, {1.0f, 0.0f, 0.0f}},
	{
  {0.5f, 0.5f , 0.4f}, {1.0f, 0.0f, 0.0f}},
	{
  {-0.5f, 0.5f , 0.4f}, {1.0f, 0.0f, 0.0f}}
};

和索引数据:

const std::vector<uint16_t> indices = {
	0, 2, 1, 3, 2, 0,
	4, 6, 5, 6, 4, 7
};

现在的目标是将这些数据配置到pipeline上,所以要先创建VertexBuffer和IndexBuffer,在Vulkan中,创建各种不同的Buffer在形式上都是差不多的,只是在参数细节上有所不同,于是添加一个创建Buffer的函数:

void CreateBuffer(
	VkDeviceSize size,
	VkBufferUsageFlags usage,
	VkMemoryPropertyFlags properties,
	VkBuffer& buffer,
	VkDeviceMemory& memory
) {
	VkBufferCreateInfo bufferInfo = {};
	bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
	bufferInfo.size = size;
	bufferInfo.usage = usage;
	bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

	if (vkCreateBuffer(logicalDevice, &bufferInfo, NULL, &buffer) != VK_SUCCESS)
	{
		throw std::runtime_error(" create buffer fault . ");
	}

	VkMemoryRequirements memory_requirements;
	vkGetBufferMemoryRequirements(logicalDevice, buffer, &memory_requirements);

	VkPhysicalDeviceMemoryProperties memProperties;
	vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
	
	int memInd = -1;
	for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++)
	{
		if ( ( memory_requirements.memoryTypeBits & ( 1 << i ) ) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties)
		{
			memInd = i;
			break;
		}
	}
	if (memInd == -1)
	{
		throw std::runtime_error("no suitable memory for vertex buffer . ");
	}

	VkMemoryAllocateInfo allocInfo = {};
	allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
	allocInfo.allocationSize = memory_requirements.size;
	allocInfo.memoryTypeIndex = memInd;

	if (vkAllocateMemory(logicalDevice, &allocInfo, NULL, &memory) != VK_SUCCESS)
	{
		throw std::runtime_error(" allocate memory fault . ");
	}

	vkBindBufferMemory(logicalDevice, buffer, memory, 0);
}

首先要明确一点,在Vulkan中,所有的Buffer或者Image在创建时,都不会分配相应的内存,这点和Direct3D完全不同,在D3D中我们只需要创建一个Buffer,它的内存自动的就被分配好了,而在Vulkan中,必须要手动的去分配与之对应的内存,然后将两者绑定起来(最后一行),因此这个函数要处理一些与内存分配有关的任务。

关于参数:

参数 含义
VkDeviceSize size 所需要创建的Buffer所占内存的大小
VkBufferUsageFlags usage 所要创建的Buffer的用途
VkMemoryPropertyFlags properties 为Buffer分配的内存的特性
VkBuffer& buffer 创建的buffer
VkDeviceMemory& memory 所要分配的内存

其中VkBufferUsageFlags和VkMemoryPropertyFlags比较麻烦,在官方文档中,给出了这两个东西的具体取值:
在这里插入图片描述

在这里插入图片描述
在本程序中只会用到图中标红线的部分。
VkBufferUsageFlags中标红线的一类是Buffer的类型,比如VertexBuffer、IndexBuffer之类的;还有一类跟Transfer有关,表明这些BUFFER中的数据可能会被拷贝到其他的Buffer或者从其他的Buffer拷贝过来。
VkMemoryPropertyFlagBits中标红线的:
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT:使用这个标记的内存能够最快地被GPU访问,但是不能被CPU访问,它不能被映射到CPU上。
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT:使用这个标记的内存可以被映射到CPU(HOST)上,这意味着不带有这个标记的内存,是不能直接被CPU访问的,它必须要先间接地转移到其他可被映射到CPU的内存中,才能再被CPU访问。
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT:使用这个标记的内存在被映射到CPU上后,再CPU上对它进行的操作,不会被Cache,对它做的一切修改,不需要手动地写回

还有一个对应的VK_MEMORY_PROPERTY_CHCHED_BIT,它表示在CPU上所做的一切修改都会先被Cache下来,必须要手动地将修改写回(调用vkFlushMappedMemoryRanges和 vkInvalidateMappedMemoryRanges)才能让GPU得到修改后的内存数据。

然后函数的定义分为两部分,一部分是创建Buffer,这一部分比较简单,填充好CreateInfo,然后进行创建即可。
第二部分是分配内存,它涉及到三个数据类型:VkMemoryRequirements-要为Buffer分配的内存的要求、VkPhysicalDeviceMemoryProperties-GPU所支持分配的各种内存的性质、VkMemoryAllocateInfo-具体的内存分配信息

typedef struct VkMemoryRequirements {
    VkDeviceSize    size;
    VkDeviceSize    alignment;
    uint32_t        memoryTypeBits;
} VkMemoryRequirements;

VkMemoryRequirements中,size表示分配的内存的容量大小,alignment表示内存的偏移,memoryTypeBits是一个掩码,对应的某一位为1表示它支持相应位所代表的的内存类型。

typedef struct VkPhysicalDeviceMemoryProperties {
    uint32_t        memoryTypeCount;
    VkMemoryType    memoryTypes[VK_MAX_MEMORY_TYPES];
    uint32_t        memoryHeapCount;
    VkMemoryHeap    memoryHeaps[VK_MAX_MEMORY_HEAPS];
} VkPhysicalDeviceMemoryProperties;

VkPhysicalDeviceMemoryProperties中记录了GPU所支持的所有内存类型和内存Heap。每一块被分配的内存都来自于一个Heap,VkMemoryType中记录了内存来自的Heap和此前提到的VkMemoryPropertyFlagBits。

通过语句

if ( ( memory_requirements.memoryTypeBits & ( 1 << i ) ) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties)

来筛选符合VkMemoryRequirements所要求的以及满足VkMemoryPropertyFlagBits properties的内存类型编号,将其传入VkMemoryAllocateInfo,随后执行内存分配。

现在有了这样一个创建Buffer的函数,要怎样选择参数来创建需要的VertexBuffer和IndexBuffer呢?
考虑到VertexBuffer和IndexBuffer在管线中频繁地被GPU读取,而不需要被CPU进行读写,因此让GPU读取的越快越好,于是应该让它们的 VkMemoryPropertyFlagBits设为VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
但是这样就没办法将顶点和索引的数据直接放到它们的内存上,因为带有这个标记的内存是不可以被映射到CPU上的。

为了解决这个问题,引入StagingBuffer,这个Buffer是专门用于做CPU传到GPU的一个中介,我们可以先将这个Buffer映射到CPU上,然后将数据传递到它的内存中,再利用一个内存转移命令,将StagingBuffer中的数据转移到VertexBuffer和IndexBuffer中,这样虽然在初始化数据的时候麻烦了一点,但是保证了GPU读取它们的高效性。

因此需要给VertexBuffer和IndexBuffer的VkBufferUsageFlags添加一个VK_BUFFER_USAGE_TRANSFER_DST_BIT,让它们作为内存转移指令的目标Buffer。

于是得到创建这三个Buffer的程序:

	VkDeviceSize vertSize = sizeof(Vertex) * vertices.size();
	VkDeviceSize indexSize = sizeof(indices[0]) * indices.size();

	VkBuffer vertexBuffer;
	VkDeviceMemory vertexBufferMemory;
	CreateBuffer(vertSize,
		VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
		VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		vertexBuffer,
		vertexBufferMemory
	);

	VkBuffer indexBuffer;
	VkDeviceMemory indexBufferMemory;
	CreateBuffer(indexSize,
		VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
		VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		indexBuffer,
		indexBufferMemory);

	VkBuffer stagingBuffer;
	VkDeviceMemory stagingBufferMemory;
	CreateBuffer(vertSize + indexSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
		VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingBuffer,
		stagingBufferMemory);

然后将Vertex和Index的数据先映射到StagingBuffer中:

	void* data;
	vkMapMemory(logicalDevice , stagingBufferMemory , 0 , vertSize , 0 , &data );
	memcpy(data, vertices.data(), vertSize);
	memcpy((char*)data + vertSize, indices.data(), indexSize);
	vkUnmapMemory(logicalDevice, stagingBufferMemory);

接下里要进行内存转移,这一部分比较麻烦,内存转移也是一个Command,我们需要用到第一节中用过的CommandBuffer:

void CopyBuffer(
	VkBuffer srcBuffer,
	VkBuffer dstBuffer,
	VkDeviceSize size,
	VkDeviceSize srcOffset 
) {
	VkCommandBufferAllocateInfo allocInfo = {};
	allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
	allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
	allocInfo.commandPool = commandPool;
	allocInfo.commandBufferCount = 1;

	VkCommandBuffer commandBuffer;
	vkAllocateCommandBuffers(logicalDevice, &allocInfo, &commandBuffer);

	VkCommandBufferBeginInfo beginInfo = {};
	beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
	beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

	vkBeginCommandBuffer(commandBuffer, &beginInfo);
	
	VkBufferCopy copyRegion = {};
	copyRegion.dstOffset = 0;
	copyRegion.srcOffset = srcOffset;
	copyRegion.size = size;
	vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

	vkEndCommandBuffer(commandBuffer);

	VkSubmitInfo submitInfo = {};
	submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
	submitInfo.commandBufferCount = 1;
	submitInfo.pCommandBuffers = &commandBuffer;

	vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE );
	vkQueueWaitIdle(graphicsQueue);

	vkFreeCommandBuffers(logicalDevice, commandPool, 1, &commandBuffer);
}

注意这里设置了beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;,因为这个CommandBuffer只会在程序开始时被执行一次。
命令的主体是:

	VkBufferCopy copyRegion = {};
	copyRegion.dstOffset = 0;
	copyRegion.srcOffset = srcOffset;
	copyRegion.size = size;
	vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

VkBufferCopy指定了拷贝时源数据和目标数据在内存中的偏移以及数据的大小,注意到stagingBuffer中的数据是VertexData+IndexData,因此在传递IndexData时,需要注意指定srcOffset

然后调用:

	CopyBuffer(stagingBuffer, vertexBuffer, vertSize , 0 );
	CopyBuffer(stagingBuffer, indexBuffer, indexSize, vertSize);

来完成内存转移。

这样VertexBuffer和IndexBuffer的数据就已经设置完成,现在需要将它们绑定到Pipeline上。
在渲染用的CommandBuffer中,加入:

		VkBuffer vertex_buffers[] = { vertexBuffer };
		VkDeviceSize offsets[] = { 0 };
		vkCmdBindVertexBuffers(commandBuffer[i], 0, 1, vertex_buffers, offsets);
		vkCmdBindIndexBuffer(commandBuffer[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

2.配置VertexInputInfo

上一节的7.7.2.4创建VertexInputInfo时,并未配置有意义的信息,这里需要对其做修改,目前我们只是将顶点的数据绑定到了Pipeline中,但是没有将其与VertexShader的输入顶点数据做对应。将Vertex结构体修改为:

struct Vertex {
	float pos[3];
	float color[3];

	static VkVertexInputBindingDescription getBindingDescription() {
		VkVertexInputBindingDescription bindingDescription = {};
		bindingDescription.binding = 0;
		bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
		bindingDescription.stride = sizeof(Vertex);
		return bindingDescription;
	}

	static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescription() {
		std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions = {};
		attributeDescriptions[0].binding = 0;
		attributeDescriptions[0].location = 0;
		attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
		attributeDescriptions[0].offset = offsetof(Vertex , pos);

		attributeDescriptions[1].binding = 0;
		attributeDescriptions[1].location = 1;
		attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
		attributeDescriptions[1].offset = offsetof(Vertex, color);
		return attributeDescriptions;
	}
};

加入了两个静态函数,它们分别获取BindingDescription和AttributeDescription
Vertex中的每一个字段对应一个Attribute,一个binding中包含多个Attribute,每一个Binding有InputRate(逐顶点或逐Instance)和stride(包含的所有Attribute的大小),每个Attribute指定它们的绑定号、location(与VertexShader中的Location对应)、格式和偏移量。

然后修改7.7.2.4:

	//7.7.2.4 create vertex input info 
	auto bindingDescription = Vertex::getBindingDescription();
	auto attributeDescription = Vertex::getAttributeDescription();
	VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
	vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
	vertexInputInfo.vertexBindingDescriptionCount = 1;
	vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
	vertexInputInfo.vertexAttributeDescriptionCount = attributeDescription.size();
	vertexInputInfo.pVertexAttributeDescriptions = attributeDescription.data();

这段程序比较好理解,不多做说明。

到这里就完成了顶点配置所需要的全部工作。

3.创建Uniform Buffer

现在有了顶点在模型空间下的数据,我们需要对其做World、View、Proj三个变换得到它们在裁剪空间下的坐标,于是需要配置存储变换矩阵的Buffer,在Direct3D中则为ConstantBuffer,在Vulkan中,由UniformBuffer来完成这项工作。
先修改VertexShader:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
    fragColor = inColor;
}

然后在程序中准备好UniformBuffer的数据:

struct UniformBufferObject
{
	glm::mat4 model;
	glm::mat4 view;
	glm::mat4 proj;
}ubo;

注意Vulkan本身不带数学库,这里使用了GLM来作为数学库。

下面我们要来创建UniformBuffer,不过先考虑一共需要几个UniformBuffer,直观的想法是一个就够了,因为每一帧渲染都只会用到一次这个Buffer。但是需要注意到,如果当渲染某一帧时GPU没有渲染完,这个时候CPU会先进入下一帧进行运算,如果CPU在下一帧时对这个Buffer做了修改,使它与前一帧不一样,那么此时GPU后续的渲染就会出现问题,至少它会出现某种不一致的现象。所以应该对每个Swapchain Image都分别创建一个UniformBuffer,每一个CommandBuffer中要绑定不同的UniformBuffer。

	std::vector<VkBuffer> uniformBuffers;
	std::vector<VkDeviceMemory> uniformBuffersMemory;
	VkDeviceSize bufferSize = sizeof(UniformBufferObject);
	uniformBuffers.resize(swapChainImages.size());
	uniformBuffersMemory.resize(swapChainImages.size());

	for (size_t i = 0; i < swapChainImages.size(); i++)
	{
		CreateBuffer(bufferSize,
			VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
			uniformBuffers[i],
			uniformBuffersMemory[i]
		);
	}

对于UniformBuffer中的每一个矩阵都需要CPU在每一帧进行更新,于是我们在主循环中加入:

		static auto startTime = std::chrono::high_resolution_clock::now();
		auto currentTime = std::chrono::high_resolution_clock::now();
		float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
		UniformBufferObject ubo = {};
		ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
		ubo.view = glm::lookAt(glm::vec3(0.0f, 0.0f, -5.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
		ubo.proj = glm::perspective(glm::radia
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值