Part 2. Vulkan中的顶点缓冲和索引缓冲

转载自最早发布在formu.ziyuesinicization.site的文章

晨魔(Morning Demon)

首先,我们需要写顶点数据和索引数据:


typedef struct Vertex
{
    float position[3];
    float color[3];

} Vertex;
                                

Vertex vertices[4] = {
{{-0.5f,  0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}},
{{ 0.5f,  0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}},
{{ 0.5f, -0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}},
{{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}}
};

unsigned int indices[6] = {
0, 2, 1,
0, 3, 2
};                                   
                                

接着编写着色器

IO.hlsl


#ifndef IO_HLSL
#define IO_HLSL

struct VS_INPUT
{
     float3 position : POSITION0;
     float3 color : COLOR0; 
};

struct VS_OUTPUT
{
     float4 position : SV_POSITION;
     float3 color : COLOR0;
};

#endif
                                

vertex.hlsl


#include "IO.hlsl"

VS_OUTPUT main(VS_INPUT input, uint vertexID : SV_VertexID)
{
    VS_OUTPUT output;

    output.position = float4(input.position, 1.0);
    output.color = input.color;

    return output;
}

                                

pixel.hlsl


#include "IO.hlsl"

float4 main(VS_OUTPUT input) : SV_Target
{
    return float4(input.color, 1.0);
}
                                

有了着色器之后,我们就可以去搞CPU端的事情了。

首先,我们需要先了解Vulkan中,一个缓冲是如何被创建和使用的。
我们需要先声明一个缓冲,就像声明一个变量一样:


VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;                
bufferInfo.usage = usage; 
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;  

vkCreateBuffer(device, &bufferInfo, nullptr, buffer);
                                

然后我们需要为创建的缓冲分配一个内存,为了找到一个合适的内存,我们需要借助一个函数:


uint32_t FindMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties)
{
    VkPhysicalDeviceMemoryProperties memProperties;
    vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

    for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
        if ((typeFilter & (1 << i)) && 
            (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
            return i;
        }
    }
    throw std::runtime_error("No suitable memory type found!");
}
                                

之后我们就可以为缓冲分配内存了:


VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, *buffer, &memRequirements);

VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);

vkAllocateMemory(device, &allocInfo, nullptr, bufferMemory);

vkBindBufferMemory(device, *buffer, *bufferMemory, 0);
                                

由于后面要大量用到这个操作,我们把刚刚的操作整理为一个函数:


void CreateAndAllocateBuffer(VkDevice device,VkPhysicalDevice physicalDevice, 
                  VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties,
                  VkBuffer* buffer, VkDeviceMemory* bufferMemory)
{
    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = size;                
    bufferInfo.usage = usage; 
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;  

    vkCreateBuffer(device, &bufferInfo, nullptr, buffer);

    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(device, *buffer, &memRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);

    vkAllocateMemory(device, &allocInfo, nullptr, bufferMemory);

    vkBindBufferMemory(device, *buffer, *bufferMemory, 0);

}
                                

在渲染管线之前,我们开始创建缓冲,并且把数据复制到缓冲然后上传到GPU

为了把数据填充到缓冲区,直接映射内存时,如果内存类型为VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 则需要vkFlushMappedMemoryRanges 来确保数据可见,但是我们使用一个性能更高的办法,用暂存缓冲区的办法,去填充。

先计算先前数据的大小:


VkDeviceSize verticesSize = 4 * sizeof(Vertex);
VkDeviceSize indicesSize = 6 * sizeof(unsigned int);
                                

接着声明暂存缓冲区:


VkBuffer tempBuffer;
VkDeviceMemory tempBufferMemory;
                                

我们先为顶点缓冲区填充数据,这一步需要建立一个临时的命令缓冲区,然后提交到GPU:
先把数据填充到暂存缓冲区


    CreateAndAllocateBuffer(device,physicalDevice, 
                  verticesSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                  &tempBuffer, &tempBufferMemory);

    void* data;
    vkMapMemory(device, tempBufferMemory, 0, verticesSize, 0, &data);
    memcpy(data, vertices, (size_t)verticesSize);
    vkUnmapMemory(device, tempBufferMemory);
                                

创建顶点缓冲区:


    VkBuffer vertexBuffer;
    VkDeviceMemory vertexBufferMemory;

    //Vertex Buffer
    CreateAndAllocateBuffer(device,physicalDevice, 
                  verticesSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                  &vertexBuffer, &vertexBufferMemory);
                                

然后建立临时命令缓冲区,并提交复制命令:


    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 tempCommandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &tempCommandBuffer);

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

    vkBeginCommandBuffer(tempCommandBuffer, &tempBeginInfo);

    VkBufferCopy copyRegion{};
    copyRegion.size = verticesSize;
    vkCmdCopyBuffer(tempCommandBuffer, tempBuffer, vertexBuffer, 1, ©Region);

    //End Copy Command
    vkEndCommandBuffer(tempCommandBuffer);

    VkSubmitInfo tempSubmitInfo{};
    tempSubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    tempSubmitInfo.commandBufferCount = 1;
    tempSubmitInfo.pCommandBuffers = &tempCommandBuffer;

    VkFence tempFence;
    VkFenceCreateInfo tempFenceInfo{};
    tempFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    vkCreateFence(device, &tempFenceInfo, nullptr, &tempFence);

    vkQueueSubmit(graphicsQueue, 1, &tempSubmitInfo, tempFence);
    vkWaitForFences(device, 1, &tempFence, VK_TRUE, UINT64_MAX);
                                

别忘了销毁刚刚创建的暂存缓冲区和临时命令缓冲区:


    vkDestroyFence(device, tempFence, nullptr);
    vkFreeCommandBuffers(device, commandPool, 1, &tempCommandBuffer);

    vkDestroyBuffer(device, tempBuffer, nullptr);
    vkFreeMemory(device, tempBufferMemory, nullptr);
                                

索引缓冲同样的操作:


    CreateAndAllocateBuffer(device,physicalDevice, 
                  indicesSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                  &tempBuffer, &tempBufferMemory);

    vkMapMemory(device, tempBufferMemory, 0, indicesSize, 0, &data);
    memcpy(data, indices, (size_t)indicesSize);
    vkUnmapMemory(device, tempBufferMemory);

    VkBuffer indexBuffer;
    VkDeviceMemory indexBufferMemory;

    //Index Buffer
    CreateAndAllocateBuffer(device,physicalDevice, 
                  indicesSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                  &indexBuffer, &indexBufferMemory);

    //Begin Copy Command
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    vkAllocateCommandBuffers(device, &allocInfo, &tempCommandBuffer);

    tempBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    tempBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(tempCommandBuffer, &tempBeginInfo);

    copyRegion.size = indicesSize;
    vkCmdCopyBuffer(tempCommandBuffer, tempBuffer, indexBuffer, 1, ©Region);

    //End Copy Command
    vkEndCommandBuffer(tempCommandBuffer);

    tempSubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    tempSubmitInfo.commandBufferCount = 1;
    tempSubmitInfo.pCommandBuffers = &tempCommandBuffer;

    tempFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    vkCreateFence(device, &tempFenceInfo, nullptr, &tempFence);

    vkQueueSubmit(graphicsQueue, 1, &tempSubmitInfo, tempFence);
    vkWaitForFences(device, 1, &tempFence, VK_TRUE, UINT64_MAX);

    vkDestroyFence(device, tempFence, nullptr);
    vkFreeCommandBuffers(device, commandPool, 1, &tempCommandBuffer);

    vkDestroyBuffer(device, tempBuffer, nullptr);
    vkFreeMemory(device, tempBufferMemory, nullptr);
                                

接着,我们修改vertexInputCreateInfo,根据一开始的数据,我们顶点布局为,一个位置向量和一个颜色向量,我们要填写VkVertexInputBindingDescription和VkVertexInputAttributeDescription,示例如下:


VkVertexInputBindingDescription bindingDesc{};
    bindingDesc.binding = 0;
    bindingDesc.stride = sizeof(Vertex);
    bindingDesc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

    VkVertexInputAttributeDescription attrDescs[2];

    attrDescs[0].binding = 0;
    attrDescs[0].location = 0;          
    attrDescs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
    attrDescs[0].offset = offsetof(Vertex, position);

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

接着,修改顶点布局描述:


vertexInputCreateInfo.vertexBindingDescriptionCount = 1;
    vertexInputCreateInfo.pVertexBindingDescriptions = &bindingDesc;
    vertexInputCreateInfo.vertexAttributeDescriptionCount = 2;
    vertexInputCreateInfo.pVertexAttributeDescriptions = attrDescs;
                                

我们来到循环,分别绑定顶点缓冲和索引缓冲,然后调用绘制命令即可:


        VkBuffer vertexBuffers[] = { vertexBuffer };
        VkDeviceSize offsets[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);

         vkCmdDrawIndexed(commandBuffer, 6, 1, 0, 0, 0);