目录

Introduction

Vertex shader

Descriptor set layout

Uniform buffer

Updating uniform data


Introduction

我们现在可以将任意属性传递给每个顶点的顶点着色器,但是全局变量呢?从本章开始,我们将继续学习3D图形,这需要模型视图投影矩阵。我们可以将其包含为顶点数据,但这会浪费内存,而且每当变换发生变化时,都需要更新顶点缓冲区。变换可以很容易地改变每一帧。

在Vulkan中解决这一问题的正确方法是使用资源描述符。描述符是着色器自由访问缓冲区和图像等资源的一种方式。我们将设置一个包含变换矩阵的缓冲区,并让顶点着色器通过描述符访问它们。描述符的使用包括三个部分:

  • 在管道创建期间指定描述符布局
  • 从描述符池分配描述符集
  • 在渲染期间绑定描述符集

描述符布局指定管道将访问的资源类型,就像渲染过程指定将访问的附件类型一样。描述符集指定将绑定到描述符的实际缓冲区或图像资源,就像帧缓冲区指定要绑定到渲染过程附件的实际图像视图一样。然后像顶点缓冲区和帧缓冲区一样,为绘图命令绑定描述符集。

描述符有多种类型,但在本章中,我们将使用统一缓冲区对象(UBO)。我们将在以后的章节中讨论其他类型的描述符,但基本过程是相同的。假设我们有一个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;
}

我们将在每一帧更新模型、视图和投影矩阵,以使上一章中的矩形在3D中旋转。

Vertex shader

修改顶点着色器以包括上面指定的统一缓冲区对象。我假设您熟悉MVP转换。如果没有,请参阅第一章中提到的资源。

#version 450layout(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;
}

注意,统一、输入和输出声明的顺序无关紧要。绑定指令类似于属性的位置指令。我们将在描述符布局中引用此绑定。具有gl_Position的线被更改为使用变换来计算剪辑坐标中的最终位置。与2D三角形不同,剪辑坐标的最后一个分量可能不是1,当转换为屏幕上的最终规格化设备坐标时,这将导致分割。这在透视投影中用作透视分割,对于使较近的对象看起来比较远的对象更大至关重要。

Descriptor set layout

下一步是在C++端定义UBO,并告诉Vulkan顶点着色器中的这个描述符。

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

我们可以使用GLM中的数据类型精确匹配着色器中的定义。矩阵中的数据与着色器期望的方式是二进制兼容的,因此我们可以稍后将UniformBufferObject存储到VkBuffer中。

我们需要提供着色器中用于管道创建的每个描述符绑定的详细信息,就像我们必须为每个顶点属性及其位置索引所做的那样。我们将设置一个新函数来定义所有这些信息,称为createDescriptorSetLayout。应该在创建管道之前调用它,因为我们需要它。

void initVulkan() {...createDescriptorSetLayout();createGraphicsPipeline();...
}...void createDescriptorSetLayout() {}

每个绑定都需要通过VkDescriptorSetLayoutBinding结构来描述。

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

前两个字段指定着色器中使用的绑定和描述符的类型,这是一个统一的缓冲区对象。着色器变量可以表示统一缓冲区对象的数组,descriptorCount指定数组中的值数。例如,这可以用于为骨骼动画的骨骼中的每个骨骼指定变换。我们的MVP转换在一个统一的缓冲区对象中,因此我们使用的描述符计数为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;

您可能想知道为什么可以在这里指定多个描述符集布局,因为一个描述符集已经包含了所有绑定。我们将在下一章继续讨论这个问题,在那里我们将研究描述符池和描述符集。

描述符布局应该保持不变,同时我们可以创建新的图形管道,即直到程序结束:

void cleanup() {cleanupSwapChain();vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);...
}

Uniform buffer

在下一章中,我们将指定包含着色器的UBO数据的缓冲区,但我们需要先创建此缓冲区。我们将在每一帧将新数据复制到统一缓冲区,因此使用分段缓冲区没有任何意义。在这种情况下,它只会增加额外的开销,可能会降低性能而不是提高性能。

我们应该有多个缓冲区,因为多个帧可能同时在飞行中,我们不想在前一帧仍在读取缓冲区时更新缓冲区以准备下一帧!因此,我们需要有与运行中的帧一样多的统一缓冲区,并写入GPU当前未读取的统一缓冲。

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

VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
std::vector<void*> uniformBuffersMapped;

类似地,创建一个在createIndexBuffer之后调用的新函数createUniformBuffers,并分配缓冲区:

void initVulkan() {...createVertexBuffer();createIndexBuffer();createUniformBuffers();...
}...void createUniformBuffers() {VkDeviceSize bufferSize = sizeof(UniformBufferObject);uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; 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(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]);}
}

我们在创建后立即使用vkMapMemory映射缓冲区,以获得一个指针,稍后我们可以将数据写入该指针。在应用程序的整个生命周期中,缓冲区一直映射到该指针。这种技术被称为“持久映射”,适用于所有Vulkan实现。不必在每次需要更新时都映射缓冲区,这会提高性能,因为映射不会释放。

统一数据将用于所有绘制调用,因此只有当我们停止渲染时,包含它的缓冲区才会被破坏。

void cleanup() {...for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {vkDestroyBuffer(device, uniformBuffers[i], nullptr);vkFreeMemory(device, uniformBuffersMemory[i], nullptr);}vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);...}

Updating uniform data

创建一个新函数updateUniformBuffer,并在提交下一帧之前从drawFrame函数添加对它的调用:

void drawFrame() {...updateUniformBuffer(currentFrame);...VkSubmitInfo submitInfo{};submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;...
}...void updateUniformBuffer(uint32_t currentImage) {}

此函数将在每帧生成一个新的变换,以使几何体旋转。我们需要包含两个新的标头来实现此功能:

#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_RIANS定义对于确保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轴比例因子上的符号。如果不执行此操作,则图像将呈现为倒置。

现在定义了所有转换,因此我们可以将统一缓冲区对象中的数据复制到当前的统一缓冲区。这与我们对顶点缓冲区的处理方式完全相同,只是没有暂存缓冲区。如前所述,我们只映射一次统一缓冲区,因此我们可以直接写入它,而无需再次映射:

memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo));

这样使用UBO并不是将频繁更改的值传递给着色器的最有效方法。将一小部分数据缓冲区传递给着色器的一种更有效的方法是推送常量。我们可以在未来的章节中讨论这些问题。

在下一章中,我们将研究描述符集,它实际上将VkBuffers绑定到统一缓冲区描述符,以便着色器可以访问该转换数据。

Uniform_buffers/Descriptor layout and buffer相关推荐

  1. Linux Kernel TCP/IP Stack — L1 Layer — NIC Controller — Buffer descriptor table

    目录 文章目录 目录 DMA 与 Buffer descriptor DMA 与 Buffer descriptor NIC Controller 会在 RAM 中建立并为例两个环形队列,称为 BD ...

  2. 简单聊聊PostgreSQL buffer与OS cache

    0.概述 缓存可以说是数据库中相当重要的一部分,很多性能相关的问题都与之息息相关.那么我们今天就聊聊在PostgreSQL中的缓存. 1.为什么需要缓存? 在数据库中似乎我们最关心的是磁盘IO,经常会 ...

  3. DPDK vhost-user研究(九)

    消息机制 当使用vhost-user时,需要在系统中创建一个unix domain socket server,用来处理qemu发送给host的消息. 如果有新的socket连接,说明guest创建了 ...

  4. dpdk对虚拟化的支持调研

    目录: 虚拟化 dpdk的实现研究 virtio vhost SR-IOV 热迁移相关 研究拓展 本文记录近期对dpdk在虚拟化和云计算领域应用的研究成果,内容梳理如下. 虚拟化 虚拟化,抽象来说,就 ...

  5. Vulkan_Shader—高级光照_阴影_原理

    高级光照_阴影原理概述 阴影是光线被阻挡的结果:当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了.阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的 ...

  6. dpdk对虚拟化支持研究

    目录: 虚拟化 dpdk的实现研究 virtio vhost SR-IOV 热迁移相关 研究拓展 本文记录近期对dpdk在虚拟化和云计算领域应用的研究成果,内容梳理如下. 虚拟化 虚拟化,抽象来说,就 ...

  7. Vulkan Tutorial 6 统一缓冲区

    目录 20 layout and buffer 顶点着色器 描述符集布局 21 统一缓冲区 更新统一数据 22 Descriptor pool and sets 描述符池 描述符集 使用描述符集 对齐 ...

  8. Flutter Raw Image Provider

    Flutter 中的 Image Widget 内置支持 file.network.memory三种形式的文件. 但这几种都只支持常规的经过压缩后的图片文件或二进制数据,如jpg.png.webp文件 ...

  9. Loadrunner中socket协议中的三个关联函数

    这3个函数其实都可以动态获取运行中收到的数据包中的数据,只要跟在要获取的收取数据包脚本后面即可.其中:lrs_save_searched_string和lrs_save_param如果buf_desc ...

最新文章

  1. 【机器学习】ROC曲线和PR(Precision-Recall)曲线的联系
  2. linux传输文件到linux速度慢,linux中往nand(jffs2)中拷贝文件时速度慢的问题
  3. 毕业了,在Python中使用 OpenCV 和K-Means 聚类对毕业照进行图像分割
  4. 手写java数据库连接池,自定义实现数据库连接池,兼容springboot
  5. java 初始化二维数组_java二维数组的常见初始化
  6. uboot的常用命令详解
  7. 面向对象封装之无参无返,无参有返
  8. 艾伟:WinForm控件开发总结(三)------认识WinForm控件常用的Attribute
  9. php小偷程序实例代码
  10. Linux上安装RePlace
  11. 泰凌微ble mesh蓝牙模组天猫精灵学习之旅 ④ 初认识阿里天猫精灵官方Genie BT mesh Stack框架, windows平台搭建打印Hello World !
  12. 深耕智慧园区 看龙田科技与华为的“默契”
  13. 服务器搬迁方案_机房搬迁的一般步骤及实施方案
  14. Django笔记十一之外键查询优化select_related和prefetch_related
  15. Shader实现漫反射
  16. 薅羊毛php源码,基于AutoJs实现的薅羊毛App专业版源码大分享---更新啦
  17. 云杰恒指:9.3恒指期货早盘资讯
  18. vba里有日文粘贴出来是乱码
  19. FreeSurfer数据质量指标:欧拉数Euler Number
  20. Android仿ios微信左划条目删除、置顶的实现,代码简洁,更容易理解使用

热门文章

  1. Time.timeScale
  2. 【ROOT from CERN】——TH1绘制一维直方图
  3. 【附源码】Python计算机毕业设计汽车维修服务系统
  4. 微信小程序、小游戏盒子(市场)的制作
  5. HDU-6599 I Love Palindrome String 杭电第二次多校赛(Manacher+回文自动机)
  6. 用友 SAP 金蝶 季报
  7. deepfakes视频的网站_Dealing with Deepfake Videos 应对“深度伪造”视频
  8. 基于OPENCV的人脸识别学习笔记
  9. 第十章 STM32+ESP8266接入机智云 实现小型IOT智能家居项目
  10. VS正在加载符号导致程序启动变慢