在前一篇中完成了对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, ©Region);
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, ©Region);
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