Vulkan教程 - 15 索引缓冲

2024-08-21 19:58
文章标签 15 教程 索引 缓冲 vulkan

本文主要是介绍Vulkan教程 - 15 索引缓冲,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        顶点缓冲已经能正常工作了,但是让我们能够从CPU访问的内存类型可能对显卡本身读取来说不是最优的。最好的内存会有VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT标记,且通常在专用显卡上不可以用CPU访问。本章我们创建两个顶点缓冲,一个位于CPU可访问内存中的临时缓冲来上传来自顶点数组的数据,一个设备本地内存中的最终的顶点缓冲。我们使用缓冲复制命令来移动数据,从临时缓冲移动到实际顶点缓冲中。

        缓冲复制命令要求队列族支持转移操作,用VK_QUEUE_TRANSFER_BIT标记。一个好消息是,任意队列族,有VK_QUEUE_GRAPHICS_BIT或者VK_QUEUE_COMPUTE_BIT能力的话,其实已经隐式支持VK_QUEUE_TRANSFER_BIT操作了。这些情况下,实现并不要显式罗列到queueFlags中。

        如果你喜欢挑战自己,那么你仍然可以尝试使用一个不同的专门用于转移操作的队列族。它会要求你做以下修改:

        修改QueueFamilyIndices和findQueueFamilies以显式查找有VK_QUEUE_TRANSFER位的队列族,但不是VK_QUEUE_GRAPHICS_BIT;

        修改createLogicalDevice来获取转移队列句柄;

        为已经提交到转移队列族的命令缓冲创建一个次命令池;

        修改资源的sharingMode为VK_SHARING_MODE_CONCURRENT,并同时指定图形和转移队列族;

        提交任何转移命令如vkCmdCopyBuffer(本章我们也是用这个)到转移队列而不是图形队列。

        是有一些工作量,但是它会教你很多东西,就是关于资源如何在不同队列族间共享的内容。

        因为我们要创建多重缓冲,将缓冲创建移动到助手方法中是个不错的想法。创建一个新的方法createBuffer,移动createVertexBuffer中的代码(除了映射外)到它里面:

void createBuffer(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;if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {throw std::runtime_error("failed to create buffer!");}VkMemoryRequirements memRequirements;vkGetBufferMemoryRequirements(device, buffer, &memRequirements);VkMemoryAllocateInfo allocInfo = {};allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;allocInfo.allocationSize = memRequirements.size;allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {throw std::runtime_error("failed to allocate buffer memory!");}vkBindBufferMemory(device, buffer, bufferMemory, 0);
}

        确保添加了缓冲大小,内存属性和使用方法等参数以便我们用该方法创建多个不同类型的缓冲。最后两个参数是输出变量,以便向其写入句柄。

        现在可以从createVertexBuffer中移除缓冲创建和内存分配的代码,然后调用createBuffer:

void createVertexBuffer() {VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,vertexBuffer, vertexBufferMemory);void* data;vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data);memcpy(data, vertices.data(), (size_t) bufferSize);vkUnmapMemory(device, vertexBufferMemory);
}

        运行下程序,确保顶点缓冲没有问题。

        我们现在打算修改createVertexBuffer,以仅仅使用一个可见缓冲作为临时缓冲,并使用设备本地的一个作为实际顶点缓冲。

void createVertexBuffer() {VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();VkBuffer stagingBuffer;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, vertices.data(), (size_t)bufferSize);vkUnmapMemory(device, stagingBufferMemory);createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,vertexBuffer, vertexBufferMemory);
}

        我们现在使用一个新的带stagingBufferMemory的stagingBuffer用于映射和拷贝顶点数据。本章中我们将会使用两个新的缓冲用法标记:

        VK_BUFFER_USAGE_TRANSFER_SRC_BIT:在内存转移操作中,缓冲可以用作源地址;

        VK_BUFFER_USAGE_TRANSFER_DST_BIT:在内存转移操作中,缓冲可以用作目的地。

        vertexBuffer现在从设备本地类型的内存中分配,一般表示我们无法使用vkMapMemory了。但是,我们可以从stagingBuffer中拷贝数据到vertexBuffer。我们必须通过指定stagingBuffer的转移源标记,vertexBuffer的转移目的地标记,以及顶点缓冲用法标记,来表示我们想要那么做。

        我们现在打算写一个方法来从一个缓冲拷贝内容到另一个:

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {}

        内存转移操作通过命令缓冲执行,就和绘制命令一样。因此我们必须首先分配一个临时命令缓冲。你可能希望能为这些短暂存在的缓冲创建一个单独的命令池,因为实现可能会应用于内存分配优化。在这种情况下,你应该在命令池生成过程中使用VK_COMMAND_POOL_CREATE_TRANSIENT_BIT标记。

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {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(device, &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);

        我们为绘制命令缓冲使用过的VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT标记这里并不是必需的,因为我们只是打算使用一次命令缓冲,然后从方法中用返回来等待,直到复制操作已经完成。告诉驱动我们使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT的意图是一个比较好的做法。

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

        缓冲的内容使用vkCmdCopyBuffer命令进行转移。它接收源和目的缓冲作为参数,以及一个要拷贝的区域数组。区域在VkBufferCopy结构体中定义,由一个源缓冲偏置,目的缓冲偏置和大小组成。不像是vkMapMemory命令,这里不能指定VK_WHOLE_SIZE。

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);

        不像是绘制命令,我们不用等待事件。我们就是想要立即完成缓冲上的转移。还是有两种方式来等待该缓冲完成。我们可以通过vkWaitForFences使用一个栅栏,或者简单地用vkQueueWaitIdle等待转移队列变空闲。栅栏会允许你同时计划多个转移,并等待所有都完成,而不是一次只能执行一个。这样也给驱动更多机会优化。

vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

        别忘记清理用于转移操作的命令缓冲。现在我们可以从createVertexBuffer中调用copyBuffer来将顶点数据移动到设备本地缓冲中:

createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT |VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,vertexBuffer, vertexBufferMemory);copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

        从临时缓冲拷贝数据到设备缓冲后,我们应该将其清理掉:

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);

        运行程序确保能看到原来熟悉的三角形。现在可能还看不到我们的改进,但是现在顶点数据是从高性能内存中加载的。当我们渲染更复杂几何对象的时候会有影响。

        要注意的是,真实的程序中不应该对每个缓冲调用vkAllocateMemory。内存分配数量最大值由物理设备maxMemoryAllocationCount限制,可能在高端显卡如1080上也仅有4096而已。对大量对象分配内存的正确方法是创建一个自定义的分配器,将多个不同物体的一个分配操作使用offset参数进行切分。

        你要渲染在真实程序中的3D网格常常会在多个三角形中共享顶点。就是很简单的东西如绘制一个矩形就会发生这种事情:

        绘制一个矩形需要两个三角形,意味着我们需要一个有6个顶点的顶点缓冲。问题是,两个顶点的数据需要重复,导致50%的冗余。对于更复杂的网格表现会更糟,解决办法就是使用索引缓冲。

        索引缓冲实际上是一组指向顶点缓冲的指针。它允许你记录顶点数据,对多个顶点重用已有的数据。上面的插图表明了矩形的索引缓冲看起来会是什么样子,如果我们有一个顶点缓冲包含了所有四个不同顶点的话。第一组三个顶点定义了右上三角形,后面三个顶点定义了左下的三角形。

        本章我们要修改顶点数据,添加索引数据来绘制矩形。修改顶点数据来表示四个角:

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}}
};

        左上角是红色,右上角是绿色,右下角是蓝色,左下角是白色。我们添加一个新的数组indices来表示索引缓冲的内容。它应该和插图中绘制右上和左下三角形的索引匹配:

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

        索引缓冲可以使用uint16_t或者uint32_t,这取决于vertices中记录的个数。我们还是用uint16_t,因为我们使用的互不相同的顶点少于65535。

        就和顶点数据一样,索引需要加载到VkBuffer以便GPU能访问。定义两个新的类成员来存储索引缓冲资源:

VkCommandPool commandPool;
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

        我们将要添加的createIndexBuffer方法就和createVertexBuffer基本一样:

void createIndexBuffer() {VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();VkBuffer stagingBuffer;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);
}

        该方法在initVulkan的createVertexBuffer后调用。

        只有两处不同。一处是bufferSize现在等于索引个数乘以索引类型大小,大小就是uint16_t或者uint32_t。indexBuffer用法应该是VK_BUFFER_USAGE_INDEX_BUFFER_BIT而不是VK_BUFFER_USAGE_VERTEX_BUFFER_BIT了。除此之外,处理都是一样的。我们创建一个临时缓冲以便向其拷贝索引内容,然后将它拷贝到最终设备本地索引缓冲中。

        索引缓冲应该在程序结尾清理掉,就和顶点缓冲一样:

cleanupSwapChain();vkDestroyBuffer(device, indexBuffer, nullptr);
vkFreeMemory(device, indexBufferMemory, nullptr);vkDestroyBuffer(device, vertexBuffer, nullptr);
vkFreeMemory(device, vertexBufferMemory, nullptr);

        绘制的时候使用索引缓冲涉及到对createCommandBuffers的两处修改。我们首先需要绑定索引缓冲,就和我们之前对顶点缓冲做的工作一样。不同之处是你只能有一个索引缓冲。很不幸,不能为每个顶点属性使用不同索引,所以我们还是完全复制顶点数据,即使它就有一个属性不同。

vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

        索引缓冲用vkCmdBindIndexBuffer绑定,参数有索引缓冲,字节偏移量,索引数据类型。

        只是绑定索引缓冲并不会改变什么,我们还要修改绘制命令,告诉Vulkan使用索引缓冲。删除vkCmdDraw,替换为vkCmdDrawIndexed:

vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

        该方法的调用和vkCmdDraw类似。头两个参数指定了索引个数和实例个数。我们不用实例,所以就是1。索引个数表示将要传递到顶点缓冲上的顶点的个数。下一个参数指定索引缓冲偏置,使用1会导致显卡开始从第二个索引读取。倒数第二个参数指定了在索引缓冲中添加索引的时候的偏移量。最后的参数指定了实例的偏置,这里我们不用。

        现在运行程序看到如下的矩形:

        你现在知道如何通过顶点缓冲重用顶点来节省内存了,这在将来加载复杂3D模型的时候尤其重要。

        之前的章节已经提到,你应该分配多个资源,如同来自单个内存分配的缓冲那样,但实际上还要多进一步。驱动开发者建议你也要存储多个缓冲到单个VkBuffer并在类似vkCmdBindVertexBuffers的命令中使用偏置,就和顶点和索引缓冲一样。其优势是你的数据会更方便缓存,因为它们更接近在一起。甚至可以对多个资源重用相同块的内存,如果它们不是相同的渲染操作中使用,当然也要保证它们的数据是刷新过的。这就是混叠,一些Vulkan方法有明确的标记来让你指定想要这么做。

这篇关于Vulkan教程 - 15 索引缓冲的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1094137

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程

目录 1.  RISC-V简介 2.  CPU架构现状 3.  MounRiver Studio软件下载 4.  MounRiver Studio软件安装 5.  MounRiver Studio软件介绍 6.  创建工程 7.  编译代码 1.  RISC-V简介         RISC就是精简指令集计算机(Reduced Instruction SetCom

前端技术(七)——less 教程

一、less简介 1. less是什么? less是一种动态样式语言,属于css预处理器的范畴,它扩展了CSS语言,增加了变量、Mixin、函数等特性,使CSS 更易维护和扩展LESS 既可以在 客户端 上运行 ,也可以借助Node.js在服务端运行。 less的中文官网:https://lesscss.cn/ 2. less编译工具 koala 官网 http://koala-app.

【Shiro】Shiro 的学习教程(三)之 SpringBoot 集成 Shiro

目录 1、环境准备2、引入 Shiro3、实现认证、退出3.1、使用死数据实现3.2、引入数据库,添加注册功能后端代码前端代码 3.3、MD5、Salt 的认证流程 4.、实现授权4.1、基于角色授权4.2、基于资源授权 5、引入缓存5.1、EhCache 实现缓存5.2、集成 Redis 实现 Shiro 缓存 1、环境准备 新建一个 SpringBoot 工程,引入依赖:

Windows环境利用VS2022编译 libvpx 源码教程

libvpx libvpx 是一个开源的视频编码库,由 WebM 项目开发和维护,专门用于 VP8 和 VP9 视频编码格式的编解码处理。它支持高质量的视频压缩,广泛应用于视频会议、在线教育、视频直播服务等多种场景中。libvpx 的特点包括跨平台兼容性、硬件加速支持以及灵活的接口设计,使其可以轻松集成到各种应用程序中。 libvpx 的安装和配置过程相对简单,用户可以从官方网站下载源代码