Vulkan教程 - 16 MVP与统一缓冲对象

2024-08-21 19:58

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

        我们现在能为每个顶点传输任意属性到顶点着色器了,但是用全局变量怎么样呢?我们本章要转移到3D图形上,这就需要Model-View-Projection矩阵了,也就是MVP矩阵(模型-视口-投影矩阵)。我们可以将它包括进来作为顶点数据,但是这比较浪费内存,也要求我们在它的变换改变的时候更新顶点缓冲,而变换是很可能在每一帧都改变的。

        Vulkan中正确处理该问题的方法是使用资源描述符。描述符是着色器能自由访问缓冲和图像等资源的一种方式。我们要建立一个缓冲,它包含了变换矩阵及让顶点着色器通过描述符访问它们。描述符用法由以下三部分组成:

        管线创建阶段指定一个描述符布局;

        从描述符池指定一个描述符集合;

        在渲染阶段构建描述符。

        描述符布局指定了将要被管线访问的资源类型,就和渲染通道指定了将要访问的附件的类型一样。描述符集合指定了将要绑定到描述符的实际缓冲或者图像资源,就和帧缓冲指定了实际图像视图来绑定渲染通道附件一样。描述符集合为绘制命令绑定,就和顶点缓冲及帧缓冲一样。

        描述符有很多类型,但是本章我们就用统一缓冲对象。以后的章节再看其他类型的描述符,但是基本的处理都是一样的。假设我们有个C结构体如下,包含了我们想要顶点着色器拥有的数据:

struct UniformBufferObject {glm::mat4 model;glm::mat4 view;glm::mat4 proj;
};

        那么我们可以将数据拷贝到VkBuffer,然后通过一个统一缓冲对象描述符从顶点着色器访问如下:

layout(binding = 0) uniform UniformBufferObject {mat4 model;mat4 view;mat4 proj;
} ubo;void main() {gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);fragColor = inColor;
}

        我们将会每帧更新该MVP矩阵以让矩形在3D模式转动起来。

        修改顶点着色器以包括统一缓冲对象,我这里认为你对MVP矩阵比较熟悉,否则就看第一章提到的资源学习。

#version 450
#extension GL_ARB_separate_shader_objects : enablelayout(binding = 0) uniform UniformBufferObject {mat4 model;mat4 view;mat4 proj;
} ubo;layout(location = 0) in vec2 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, 0.0, 1.0);fragColor = inColor;
}

        uniform、in和out的声明顺序没有关系,对属性来说,binding指令和location指令类似。我们将会在描述符布局中引用该绑定。有gl_Position的行改成使用变换来计算最终在裁剪坐标系中的位置。不像是2D三角形,裁剪坐标最后的组件可能不是1,这将会导致转换到最后的屏幕上的归一化设备坐标的时候要进行相除。这在透视投影中用作透视除法,对制作近处对象比远处大的效果非常重要。

        下一步是在C++侧定义UBO,告诉Vulkan顶点着色器中的描述符信息:

struct UniformBufferObject {glm::mat4 model;glm::mat4 view;glm::mat4 proj;
};

        我们可以使用GLM中的数据类型严格匹配着色器中的定义。矩阵中的数据是和着色器所期望的那样兼容二进制的,所以我们之后可以memcpy UniformBufferObject到BkBuffer。

        我们要提供为创建管线在着色器中使用的每个描述符绑定的细节信息,就和我们要为每个顶点属性和它的location索引做的工作一样。我们将会设置一个新的函数来定义所有这些信息,该函数就是createDescriptorSetLayout。它应该在管线创建之前调用。

        每个绑定都要通过VkDescriptorSetLayoutBinding描述:

void createDescriptorSetLayout() {VkDescriptorSetLayoutBinding uboLayoutBinding = {};uboLayoutBinding.binding = 0;uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;uboLayoutBinding.descriptorCount = 1;
}

        最开始两个字段指定了着色器中使用的binding以及描述符类型,就是一个统一缓冲对象。着色器变量可以表示一组统一缓冲对象,descriptorCount指定了数组中的值的个数。比如,这个可以用于为骨骼动画中的骨骼的每个骨头指定一个变换。我们的MVP变换是一个单一的统一缓冲对象,所以我们使用descriptorCount为1。

uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

        我们也要指定引用哪个着色器阶段。stageFlags字段可以是VkShaderStageFlagBits或者VK_SHADER_STAGE_ALL_GRAPHICS的组合。我们这里仅仅引用来在顶点着色器的描述符。

uboLayoutBinding.pImmutableSamplers = nullptr;  // optional

        pImmutableSamplers字段只和图像采样有关的描述符有关,这里就留默认值即可。

        所有的描述符绑定都组合到单个VkDescriptorSetLayout对象,在pipelineLayout上定义一个类成员:

VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;

        我们可以用vkCreateDescriptorSetLayout创建了,该方法接收一个有一组绑定的VkDescriptorSetLayoutCreateInfo作为参数:

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor set layout!");
}

        我们需要在管线创建过程中指定描述符集合布局,以告诉Vulkan着色器将会使用哪个描述符。描述符集合布局在管线布局对象中指定。修改VkPipelineLayoutCreateInfo来引用布局对象:

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

        你可能好奇,为什么可以指定多个描述符集合布局,因为单个已经包含了所有的绑定。我们以后会再看,那时候我们还会介绍描述符池和描述符集合。

        在我们创建新的图形管线的时候,描述符布局应该就在旁边,直到程序结束。

vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

        这一行就放在清理交换链之后执行。

        下一章我们会为着色器指定含有统一缓冲对象的缓冲,但是我们要先创建缓冲。我们打算每一帧都先复制新的数据到统一缓冲。

        我们应该有多个缓冲,因为可能同时有很多帧都在准备中,而前一帧还在读取的时候,我们不想在下一帧准备的时候就更新缓冲。我们可以每一帧或者每个交换链图形都做一个统一缓冲。但是,由于我们需要从命令缓冲引用统一缓冲,我们选择每一个交换链图像都有一个统一缓冲的方式。

        为此,为uniformBuffers和uniformBuffersMemory添加新的类成员,

std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;

        类似的,创建一个新的方法createUniformBuffers,在createIndexBuffer之后调用来分配缓冲:

void createUniformBuffers() {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]);}
}

        我们要写一个单独的方法,每帧用一个新的变换更新统一缓冲,所以这里不会有vkMapMemory。统一数据会被所有绘制命令使用,所以包含它的缓冲应该在我们停止渲染的时候才进行销毁。由于它依赖于交换链图像个数,这个个数可能会在重建之后改变,所以我们在清理交换链部分结尾处清理它:

for (size_t i = 0; i < swapChainImages.size(); i++) {vkDestroyBuffer(device, uniformBuffers[i], nullptr);vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}

        这也意味着我们要在重建交换链的部分重建它:

createFramebuffers();
createUniformBuffers();
createCommandBuffers();

        创建一个新的方法updateUniformBuffer,然后从drawFrame中调用,就在知道我们获取的是哪个交换链图像之后:

updateUniformBuffer(imageIndex);VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

        该方法会每帧生成一个新的变换让几何体转动起来。我们要包含两个新的头文件:

#define GLM_FORCE_RADIANS#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>#include <chrono>

        头文件glm/gtc/matrix_transform.hpp暴露了可以用于生成模型变换的方法,如glm::rotate,视图变换如glm::lookAt以及投影变换如glm::perspective。GLM_FORCE_RADIANS对保证如glm::rotate之类的方法使用弧度作为参数是很有必要的,避免了可能的混淆问题。

        chrono标准库头文件暴露了做精准计时的方法。我们会使用该库保证几何体每秒旋转90度,不管它是什么帧率。

void updateUniformBuffer(uint32_t currentImage) {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();
}

        updateUniformBuffer方法开始的时候会计算从开始渲染起以秒为单位的时间。

        我们现在在同意缓冲对象中定义模型、视图和投影变换。模型旋转就是一个简单的绕着Z轴根据时间变量的旋转:

UniformBufferObject ubo = {};
ubo.model = glm::rotate(glm::mat4(1.0f), time*glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));

        glm::rotate方法接收一个已存在的变换,旋转角度以及旋转轴作为参数。glm::mat4(1.0f)构造器返回一个单位矩阵。使用time * glm::radians(90.0f)为旋转角度就满足了每秒旋转90度的目的。

ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));

        视图变换部分我决定从45度角高度看该几何体。glm::lookAt方法接收眼睛位置,中心点和向上的轴作为参数。

ubo.proj = glm::perspective(glm::radians(45.0f),swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 10.0f);

        我选择使用45度垂直视场角作为透视投影。其他参数为纵横比,远近视图平面。使用当前交换链程度计算综合比来考虑窗口调整大小后的宽高是有必要的。

ubo.proj[1][1] *= -1;

        GLM原本是为OpenGL设计的,裁剪坐标系中它的Y坐标是反向的。补偿方式中最简单的是翻转投影矩阵中Y轴的大小因子的符号。如果你不这么做,那么图像渲染后就是上下颠倒的。

        所有变换都定义了,所以我们可以从统一缓冲对象拷贝数据到当前统一缓冲中了。这就和我们对顶点缓冲所做的一样,除了没有临时缓冲:

void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);

        这种方式使用统一缓冲对象将频繁修改的值传输到着色器不是最高效的。更高效的一种方式将一小部分缓冲数据传到着色器,也就是push constants。

        后面我们会查看描述符集合,它会绑定VkBuffers到统一缓冲描述符,以便着色器能访问变换数据。

这篇关于Vulkan教程 - 16 MVP与统一缓冲对象的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

Java对象转换的实现方式汇总

《Java对象转换的实现方式汇总》:本文主要介绍Java对象转换的多种实现方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Java对象转换的多种实现方式1. 手动映射(Manual Mapping)2. Builder模式3. 工具类辅助映

如何为Yarn配置国内源的详细教程

《如何为Yarn配置国内源的详细教程》在使用Yarn进行项目开发时,由于网络原因,直接使用官方源可能会导致下载速度慢或连接失败,配置国内源可以显著提高包的下载速度和稳定性,本文将详细介绍如何为Yarn... 目录一、查询当前使用的镜像源二、设置国内源1. 设置为淘宝镜像源2. 设置为其他国内源三、还原为官方

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

Python中判断对象是否为空的方法

《Python中判断对象是否为空的方法》在Python开发中,判断对象是否为“空”是高频操作,但看似简单的需求却暗藏玄机,从None到空容器,从零值到自定义对象的“假值”状态,不同场景下的“空”需要精... 目录一、python中的“空”值体系二、精准判定方法对比三、常见误区解析四、进阶处理技巧五、性能优化

Python虚拟环境终极(含PyCharm的使用教程)

《Python虚拟环境终极(含PyCharm的使用教程)》:本文主要介绍Python虚拟环境终极(含PyCharm的使用教程),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录一、为什么需要虚拟环境?二、虚拟环境创建方式对比三、命令行创建虚拟环境(venv)3.1 基础命令3

使用Node.js制作图片上传服务的详细教程

《使用Node.js制作图片上传服务的详细教程》在现代Web应用开发中,图片上传是一项常见且重要的功能,借助Node.js强大的生态系统,我们可以轻松搭建高效的图片上传服务,本文将深入探讨如何使用No... 目录准备工作搭建 Express 服务器配置 multer 进行图片上传处理图片上传请求完整代码示例

python连接本地SQL server详细图文教程

《python连接本地SQLserver详细图文教程》在数据分析领域,经常需要从数据库中获取数据进行分析和处理,下面:本文主要介绍python连接本地SQLserver的相关资料,文中通过代码... 目录一.设置本地账号1.新建用户2.开启双重验证3,开启TCP/IP本地服务二js.python连接实例1.

Python 安装和配置flask, flask_cors的图文教程

《Python安装和配置flask,flask_cors的图文教程》:本文主要介绍Python安装和配置flask,flask_cors的图文教程,本文通过图文并茂的形式给大家介绍的非常详细,... 目录一.python安装:二,配置环境变量,三:检查Python安装和环境变量,四:安装flask和flas