08/09/2020
文章目录
前言
我们将会使用内存顶点缓冲区来替换之前硬编码到顶点着色器中的顶点数据。我们将从最简单的方法开始创建一个CPU可见的缓冲区,并使用memcpy将顶点数据直接复制到缓冲区,之后将会使用暂存缓冲区将顶点数据赋值到高性能的显存中。
着色器使用顶点输入有两个方法,一个由CPU提供内存的顶点缓冲区,还有一个由GPU提供显存的顶点缓冲区,需要用到临时缓冲区。
修改顶点着色器
#version 450
#extension GL_ARB_separate_shader_objects:enable
layout(location = 0) out vec3 fragColor;
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
void main(){
gl_Position = vec4(inPosition,0.0,1.0);
fragColor = inColor;
}
- in 关键字,顶点着色器读取顶点缓冲区的顶点输入/数据
- 描述布局,位置和类型,名字无所谓
准备顶点数据
GLM提供向量和矩阵之类的线性代数数据结构
#include <glm/glm.hpp>
struct Vertex
{
glm::vec2 pos;
glm::vec3 color;
}
const std::vector<Vertex> vertices =
{
{{0.0f,-0.5f},{1.0f,0.0f,0.0f}},
{{0.5f,0.5f},{0.0f,1.0f,0.0f}},
{{-0.5f,0.5f},{0.0f,0.0f,1.0f}}
};
- 确定类型,与顶点着色器中的声明保持一致
顶点输入
一旦数据类型被提交到GPU的显存中,就需要告诉Vulkan传递到顶点着色器中的数据格式。
- 确定位置(location = 0)
- 确定类型(vec2)
struct Vertex
{
glm::vec2 pos;
glm::vec3 color;
static VkVertexInputBindingDescription getBindingDescription()
{
VkVertexInputBindingDescription bindingDescription{};
bindingDescription.binding = 0; //数组中的索引,现在只有一组数组
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; //顶点数据
return bindingDescription;
}
static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescription()
{
std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions{};
attributeDescriptions[0].binding = 0; //与上面的binding相呼应
attributeDescriptions[0].location = 0; //顶点坐标 -- 确定在顶点着色器中的位置
attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; //类型识vec2
attributeDescriptions[0].offset = offsetof(Vertex, pos); //在内存中偏移量,由offsetof计算出来
attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1; //颜色位置
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);
return attributeDescriptions;
}
};
绑定描述
在整个顶点数据从内存加载的速率。即它指定数据条目之间间隔的字节数以及是否每个顶点之后或者每个instance之后移动到下一个条目。即顶点间隔。
- binding 代表数组中的索引
- stride 每个数组元素之间的间隔,以字节数表示
- inputRate:移动到每个顶点后的下一个条目或者每个instance后移动到下一个条目
- 这里使用Vertex表示per-vertex data
属性描述
如何描述结构体中每个元素的属性。pos类型是vec2,是第一个属性,在顶点着色器的位置是0。
- binding: 每个顶点数据的来源
- location:顶点着色器中的位置
- format:类型是vec2
更改管线顶点输入描述
管道需要确定顶点数据的类型,即上面定义好的属性和顶点描述
auto bindingDescription = Vertex::getBindingDescription();
auto attributeDescription = Vertex::getAttributeDescription();
//the format of the vertex data that will be passed to the vertex shader
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 1;
vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; //Optional
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescription.size());
vertexInputInfo.pVertexAttributeDescriptions = attributeDescription.data();
顶点缓冲区
Vulkan 创建顶点缓冲区,在Vulkan中,缓冲区是内存的一块区域,该区域用于向显卡提供预要读取的任意数据。它们可以用来存储顶点数据,也可以用于其他目的。与之前创建的Vulkan对象不同的是,缓冲区自己不会分配内存空间。
创建缓冲区
- 存储数据的大小
- 使用类型
- 分享模式
void createVertexBuffer()
{
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(vertices[0]) * vertices.size();
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS)
{
throw std::runtime_error("failed to create vertex buffer");
}
}
为缓冲区分配内存
首先物理设备GPU查询支持的内存属性,找到适合顶点缓冲区的内存要求的索引。
void createVertexBuffer()
{
//Create part...
//allocate memory
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); //内存类型的索引
if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate vertex buffer memory!");
}
//连接缓冲区和内存
vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);
}
VkMemoryRequirements结构体有三个字段:
- size: 需要的内存字节大小,可能与bufferInfo.size大小不一致。
- alignment: 缓冲区的内存分配区域开始的字节偏移量,它取决于bufferInfo.usage和bufferInfo.flags。
- memoryTypeBits: 适用于缓冲区的存储器类型的位字段。
找匹配的内存类型的索引
显卡可以分配不同类型的内存。每种类型的内存根据所允许的操作和特性均不相同。我们需要结合缓冲区与应用程序实际的需要找到正确的内存类型使用。现在添加一个新的函数完成此逻辑findMemoryType。
uint32_t findMemoryType(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("failed to find suitable memory type!");
}
传数据到顶点缓冲区
void createVertexBuffer()
{
//Create part...
//allocate memeory
//transfer data
void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
memcpy(data, vertices.data(), (size_t)bufferInfo.size);
vkUnmapMemory(device, vertexBufferMemory);
}
- 使用vkMapMemory将缓冲区内存映射(mapping the buffer memory)到CPU可访问的内存中完成。
- memcpy将顶点数据拷贝到映射内存中,并使用vkUnmapMemory取消映射。
命令缓冲区记录顶点缓冲区
类似与声明了一块顶点缓冲区并且开辟了一块内存大小
void createVertexBuffers
{
//前面几步
//记录命令
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdDraw(commandBuffers[i], static_cast<uint32_t>(vertices.size()), 1, 0, 0);
}
知识补充
- 顶点输入的格式:SFLOAT,SINT,SUINT,SFLOAT
- 内存特性:比如从CPU写入数据
- HOST_VISIBLE
- HOST_COHERENT
- HOST_COHERENT_BIT: 由于缓存,驱动程序有可能不会立刻拷贝数据到缓冲区的内存中去
总结
- 设置顶点输入布局(VkPipelineVertexInputStateCreateInfo)
- 创建顶点缓冲区(VkBuffer)和分配显存(VkDeviceBuffer)
- 传输数据(vkMapMemory,memcpy,vkUnmapMemory)
- 在命令缓冲区,绑定顶点缓冲区(vkCmdBindVertexBuffers),可以绑定多个顶点缓冲区