绘制一个矩形可以通过绘制两个三角形来实现,如果不共享顶点,就需要 6 个顶点。共享顶点的话,只需要 4 个顶点就可以。可以想象对于更加复杂的三维网格,通过共享顶点可以节约大量内存资源。
索引缓冲是一个包含了指向顶点缓冲中顶点数据的索引数组的缓冲。使用索引缓冲,我们可以对顶点数据进行复用。上图演示了索引缓冲的原理,顶点缓冲中包含每一个独一无二的顶点的数据,索引缓冲使用顶点缓
冲中顶点数据的索引来引用顶点数据。
示例:
//定义顶点数据--交叉顶点属性 (interleaving vertex attributes)。
const std::vector<Vertex> vertices = {
{{-0.5f ,-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}} ,
{{-0.5f , 0.5f} , {1.0f , 1.0f , 1.0f}}
};
//定义索引数据
const std::vector<uint16_t> indices = {
0,1,2,2,3,0
};
VkBuffer indexBuffer ;//存储创建的索引缓冲的句柄
VkDeviceMemory indexBufferMemory ;//索引缓冲的内存句柄
//创建指令缓冲对象--11
void createCommandBuffers(){
commandBuffers.resize(swapChainFramebuffers.size());
//指定分配使用的指令池和需要分配的指令缓冲对象个数
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
/**
level 成员变量用于指定分配的指令缓冲对象是主要指令缓冲对象还是辅助指令缓冲对象:
VK_COMMAND_BUFFER_LEVEL_PRIMARY:
可以被提交到队列进行执行,但不能被其它指令缓冲对象调用。
VK_COMMAND_BUFFER_LEVEL_SECONDARY:
不能直接被提交到队列进行执行,但可以被主要指令缓冲对象调用执行
在这里,我们没有使用辅助指令缓冲对象,但辅助治理给缓冲对象的
好处是显而易见的,我们可以把一些常用的指令存储在辅助指令缓冲对象,
然后在主要指令缓冲对象中调用执行
*/
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffers.size();
//分配指令缓冲对象
if(vkAllocateCommandBuffers(device,&allocInfo,
commandBuffers.data())!= VK_SUCCESS){
throw std::runtime_error("failed to allocate command buffers!");
}
//记录指令到指令缓冲
for(size_t i=0;i<commandBuffers.size();i++){
//指定一些有关指令缓冲的使用细节
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
/**
flags 成员变量用于指定我们将要怎样使用指令缓冲。它的值可以是下面这些:
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:
指令缓冲在执行一次后,就被用来记录新的指令.
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:
这是一个只在一个渲染流程内使用的辅助指令缓冲.
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:
在指令缓冲等待执行时,仍然可以提交这一指令缓冲
*/
beginInfo.flags =
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
//用于辅助指令缓冲,可以用它来指定从调用它的主要指令缓冲继承的状态
beginInfo.pInheritanceInfo = nullptr;
//指令缓冲对象记录指令后,调用vkBeginCommandBuffer函数会重置指令缓冲对象
//开始指令缓冲的记录操作
if(vkBeginCommandBuffer(commandBuffers[i],&beginInfo)!=VK_SUCCESS){
throw std::runtime_error(
"failed to begin recording command buffer.");
}
//指定使用的渲染流程对象
VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType =VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
//指定使用的渲染流程对象
renderPassInfo.renderPass = renderPass;
//指定使用的帧缓冲对象
renderPassInfo.framebuffer = swapChainFramebuffers[i];
/**
renderArea指定用于渲染的区域。位于这一区域外的像素数据会处于未定义状态。
通常,我们将这一区域设置为和我们使用的附着大小完全一样.
*/
renderPassInfo.renderArea.offset = {0,0};
renderPassInfo.renderArea.extent = swapChainExtent;
VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
//指定标记后,使用的清除值
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
/**
所有可以记录指令到指令缓冲的函数的函数名都带有一个 vkCmd 前缀,
并且这些函数的返回值都是 void,也就是说在指令记录操作完全结束前,
不用进行任何错误处理。
这类函数的第一个参数是用于记录指令的指令缓冲对象。第二个参数
是使用的渲染流程的信息。最后一个参数是用来指定渲染流程如何提供绘
制指令的标记,它可以是下面这两个值之一:
VK_SUBPASS_CONTENTS_INLINE:
所有要执行的指令都在主要指令缓冲中,没有辅助指令缓冲需要执行
VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:
有来自辅助指令缓冲的指令需要执行。
*/
//开始一个渲染流程
vkCmdBeginRenderPass( commandBuffers[i], &renderPassInfo,
VK_SUBPASS_CONTENTS_INLINE) ;
//绑定图形管线,第二个参数用于指定管线对象是图形管线还是计算管线
vkCmdBindPipeline(commandBuffers[i],VK_PIPELINE_BIND_POINT_GRAPHICS,
graphicsPipeline ) ;
/**
至此,我们已经提交了需要图形管线执行的指令,以及片段着色器使用的附着
*/
VkBuffer vertexBuffers[] = { vertexBuffer };
VkDeviceSize offset[] = {0};
/**
vkCmdBindVertexBuffers第二,三个参数指定偏移值和我们要绑定的顶点缓冲的数量。
最后两个参数用于指定需要绑定的顶点缓冲数组以及顶点数据在顶点缓冲中的偏移值数组
*/
//绑定顶点缓冲
vkCmdBindVertexBuffers(commandBuffers[i],0,1,vertexBuffers,offset);
/**
只能绑定一个索引缓冲对象.
我们不能为每个顶点属性使用不同的索引,所以即使只有一个顶点属性不同,
也要在顶点缓冲中多出一个顶点的数据
*/
//绑定顶点缓冲到指令缓冲对象--第三个参数为索引数据的类型
vkCmdBindIndexBuffer(commandBuffers[i],indexBuffer,0,
VK_INDEX_TYPE_UINT16);
/**
vkCmdDraw参数:
1.记录有要执行的指令的指令缓冲对象
2. vertexCount:顶点缓冲中的顶点个数
尽管这里我们没有使用顶点缓冲,但仍然需要指定三个顶点用于三角形的绘制。
3.instanceCount:用于实例渲染,为 1 时表示不进行实例渲染
4.firstVertex:用于定义着色器变量 gl_VertexIndex 的值
5.firstInstance:用于定义着色器变量 gl_InstanceIndex 的值
*/
//开始调用指令进行三角形的绘制操作--使用顶点绘制
/*vkCmdDraw( commandBuffers [ i ] ,
static_cast<uint32_t>(vertices.size()) , 1 , 0 , 0);*/
/**
vkCmdDrawIndexed参数:
1.指令缓冲对象
2.指定索引的个数
3.实例的个数 -- 这里没有使用实例渲染,所以将实例个数设置为 1
4.偏移值用于指定显卡开始读取索引的位置,偏移值为1对应索引数据中的第二个索引。
5.检索顶点数据前加到顶点索引上的数值
6.第一个被渲染的实例的 ID -- 这里没有使用
*/
//使用索引绘制
vkCmdDrawIndexed(commandBuffers[i],
static_cast<uint32_t>(indices.size()),1,0,0,0);
//结束渲染流程
vkCmdEndRenderPass( commandBuffers [ i ] ) ;
//结束记录指令到指令缓冲
if(vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS){
throw std::runtime_error("failed to record command buffer!");
}
}
}
//创建索引缓冲--同创建顶点缓冲方式相同
void createIndexBuffer(){
VkDeviceSize bufferSize = sizeof(indices[0])*indices.size();
VkBuffer stagingBuffer ;//缓冲对象存放 CPU 加载的数据
VkDeviceMemory stagingBufferMemory ;//缓冲对象内存
createBuffer(bufferSize,VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer,stagingBufferMemory);
//将顶点数据复制到缓冲中
void* data;
vkMapMemory(device,stagingBufferMemory,0,bufferSize,0,&data);
memcpy(data,indices.data(),(size_t)bufferSize);
//结束内存映射
vkUnmapMemory(device,stagingBufferMemory);
createBuffer(bufferSize,VK_BUFFER_USAGE_TRANSFER_DST_BIT|
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
indexBuffer,indexBufferMemory);
copyBuffer(stagingBuffer , indexBuffer , bufferSize ) ;
vkDestroyBuffer(device , stagingBuffer , nullptr ) ;
vkFreeMemory(device , stagingBufferMemory , nullptr ) ;
}
//销毁索引缓冲
vkDestroyBuffer(device,indexBuffer,nullptr);
//释放索引缓冲缓冲的内存
vkFreeMemory(device,indexBufferMemory,nullptr);
之前说,我们应该申请一大块内存来分配给多个缓冲对象使用,实际上,可以更进一步,使用一个缓冲对象通过偏移值来存储多个不同的顶点缓冲和索引缓冲数据。这样做之后,由于数据之间非常紧凑,可以更好地被缓存。
对于没有同时进行的操作使用的内存块可以供多个对象复用,这种复用方式也被叫做混叠,许多Vulkan 函数包含有标记参数可以用于显式地指定混叠处理