第七章的核心目标,是在第二章已经能够绘制三角形的基础上,继续完善 Vulkan 中最基础的数据传递流程。
第七章完成后,整个程序将从“顶点写死在着色器中的简单三角形”,逐步扩展为一个具备顶点、索引、实例化、Uniform、Push Constant 和贴图能力的基础 Vulkan 渲染框架。
一、Ch7-1 初识顶点缓冲区#
在第二章中,三角形的顶点位置通常直接写在顶点着色器中,并通过 gl_VertexIndex 进行读取。
真正的渲染程序中,顶点数据通常保存在 CPU 内存中,然后上传到 GPU 的顶点缓冲区中。
1CPU 顶点数组
2→ 创建 VkBuffer
3→ 分配 VkDeviceMemory
4→ 上传顶点数据
5→ 绑定顶点缓冲区
6→ 顶点着色器读取数据
1. 顶点结构#
1struct Vertex
2{
3 glm::vec2 position;
4 glm::vec3 color;
5 glm::vec2 texCoord;
6};
1顶点0:位置、颜色、UV
2顶点1:位置、颜色、UV
3顶点2:位置、颜色、UV
2. VkBuffer 与 VkDeviceMemory#
1创建 VkBuffer
2→ 查询内存需求
3→ 查找合适的内存类型
4→ 分配 VkDeviceMemory
5→ 将内存绑定到 Buffer
1VkBuffer = 资源对象
2VkDeviceMemory = 实际内存
1vkBindBufferMemory(...)
3. 顶点缓冲区用途#
1VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
如果使用 Staging Buffer,则最终的顶点缓冲区通常还需要:
1VK_BUFFER_USAGE_TRANSFER_DST_BIT
1VK_BUFFER_USAGE_TRANSFER_DST_BIT |
2VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
4. 顶点输入描述#
Graphics Pipeline 必须知道顶点缓冲区中的数据如何排列。
1VkVertexInputBindingDescription
2VkVertexInputAttributeDescription
Binding Description#
1binding
2stride
3inputRate
1binding = 0;
2stride = sizeof(Vertex);
3inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
Attribute Description#
1location
2binding
3format
4offset
1location = 0;
2binding = 0;
3format = VK_FORMAT_R32G32_SFLOAT;
4offset = offsetof(Vertex, position);
这里的 location 必须与顶点着色器中的输入位置一致:
1layout(location = 0) in vec2 inPosition;
2layout(location = 1) in vec3 inColor;
3layout(location = 2) in vec2 inTexCoord;
1Binding 描述一整条数据
2Attribute 描述数据中的某个成员
3Location 对应 Shader 中的输入位置
5. 绑定顶点缓冲区#
1vkCmdBindVertexBuffers(...)
1vkCmdDraw(...)
1创建顶点缓冲区
2→ 配置 Pipeline Vertex Input
3→ vkCmdBindVertexBuffers
4→ vkCmdDraw
6. Staging Buffer#
性能较高的顶点缓冲区通常放在 Device Local 内存中。
但是 Device Local 内存通常无法直接被 CPU 写入。
1CPU 数据
2→ Host Visible Staging Buffer
3→ vkCmdCopyBuffer
4→ Device Local Vertex Buffer
1VK_BUFFER_USAGE_TRANSFER_SRC_BIT
1VK_BUFFER_USAGE_TRANSFER_DST_BIT |
2VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
Staging Buffer 只负责临时上传数据,上传完成后即可销毁。
二、Ch7-2 初识索引缓冲区#
例如,一个矩形由两个三角形组成。如果不使用索引缓冲区,需要六个顶点:
1三角形1:0、1、2
2三角形2:3、4、5
其中部分顶点的位置实际上是重复的。使用索引缓冲区后,只需要四个顶点:
1Vertex vertices[] =
2{
3 左上,
4 右上,
5 左下,
6 右下
7};
1uint16_t indices[] =
2{
3 0, 1, 2,
4 1, 3, 2
5};
1. 索引缓冲区用途#
1VK_BUFFER_USAGE_INDEX_BUFFER_BIT
1VK_BUFFER_USAGE_TRANSFER_DST_BIT |
2VK_BUFFER_USAGE_INDEX_BUFFER_BIT
2. 绑定索引缓冲区#
1vkCmdBindIndexBuffer(...)
1VK_INDEX_TYPE_UINT16
1VK_INDEX_TYPE_UINT32
1std::vector<uint16_t>
1VK_INDEX_TYPE_UINT16
3. 索引绘制#
1vkCmdDrawIndexed(...)
1vkCmdDraw(...)
1Vertex Buffer 提供顶点数据
2Index Buffer 提供顶点编号
3→ vkCmdBindVertexBuffers
4→ vkCmdBindIndexBuffer
5→ vkCmdDrawIndexed
索引缓冲区中保存的并不是顶点数据,而是顶点在顶点缓冲区中的编号。
三、Ch7-3 实例化绘制#
实例化绘制用于一次 Draw Call 绘制多个相同结构的物体。
如果每个物体都单独调用一次 Draw,会产生大量 CPU 到 GPU 的绘制调用开销。
1vkCmdDrawIndexed(
2 commandBuffer,
3 indexCount,
4 instanceCount,
5 0,
6 0,
7 0);
1indexCount = 每个实例使用多少索引
2instanceCount = 绘制多少个实例
1. 顶点数据与实例数据#
1逐顶点数据
2逐实例数据
2. Input Rate#
1VK_VERTEX_INPUT_RATE_VERTEX
1VK_VERTEX_INPUT_RATE_INSTANCE
1VkVertexInputBindingDescription vertexBinding{};
2vertexBinding.binding = 0;
3vertexBinding.stride = sizeof(Vertex);
4vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1VkVertexInputBindingDescription instanceBinding{};
2instanceBinding.binding = 1;
3instanceBinding.stride = sizeof(InstanceData);
4instanceBinding.inputRate = VK_VERTEX_INPUT_RATE_INSTANCE;
1VERTEX:每处理一个顶点读取下一条数据
2INSTANCE:每开始一个新实例读取下一条数据
3. gl_InstanceIndex#
1gl_InstanceIndex
1vec2 offset = offsets[gl_InstanceIndex];
1一次 Draw Call
2→ 绘制多个相同网格
3→ 每个实例拥有不同数据
四、Ch7-4 初识 Push Constant#
Push Constant 用于从 CPU 向 Shader 传递少量数据。
只需要在 Pipeline Layout 中声明,然后在录制命令时写入。
1定义 Push Constant 数据
2→ 创建 Push Constant Range
3→ 加入 Pipeline Layout
4→ Shader 声明 Push Constant
5→ vkCmdPushConstants
1. Push Constant Range#
1VkPushConstantRange pushConstantRange{};
1stageFlags
2offset
3size
1pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
2pushConstantRange.offset = 0;
3pushConstantRange.size = sizeof(PushConstantData);
2. Shader 中声明#
1layout(push_constant) uniform PushConstantBlock
2{
3 vec2 offset;
4 vec4 color;
5} pushConstant;
1struct PushConstantData
2{
3 glm::vec2 offset;
4 glm::vec4 color;
5};
3. 写入 Push Constant#
1vkCmdPushConstants(...)
Push Constant 可以在 Draw Call 之间频繁修改。
1Push Constant A
2→ Draw Object A
3
4Push Constant B
5→ Draw Object B
4. Push Constant 特点#
1128 Bytes
1maxPushConstantsSize
五、Ch7-5 初识 Uniform Buffer#
Uniform Buffer 用于向 Shader 传递较大的只读数据。
Uniform Buffer 本质上仍然是一个普通的 VkBuffer,但是用途被指定为:
1VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
1. Uniform Buffer 完整流程#
1创建 Uniform Buffer
2→ 创建 Descriptor Set Layout
3→ 创建 Descriptor Pool
4→ 分配 Descriptor Set
5→ 更新 Descriptor Set
6→ 绑定 Descriptor Set
7→ Shader 读取 Uniform Buffer
2. Descriptor Set Layout#
Descriptor Set Layout 用于描述 Shader 将会使用什么资源。
1VkDescriptorSetLayoutBinding uboLayoutBinding{};
1binding
2descriptorType
3descriptorCount
4stageFlags
1uboLayoutBinding.binding = 0;
2uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
3uboLayoutBinding.descriptorCount = 1;
4uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
1layout(set = 0, binding = 0) uniform UniformBufferObject
2{
3 mat4 model;
4 mat4 view;
5 mat4 projection;
6} ubo;
3. Descriptor Pool#
Descriptor Pool 用于分配 Descriptor Set。
1VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
4. Descriptor Set#
Descriptor Set 是实际绑定 Buffer 和 Shader 的对象。
1VkDescriptorBufferInfo
1buffer
2offset
3range
1vkUpdateDescriptorSets(...)
将 Uniform Buffer 写入 Descriptor Set。
5. 绑定 Descriptor Set#
1vkCmdBindDescriptorSets(...)
之后 Shader 才能读取对应 Uniform Buffer。
1Shader Binding
2↕
3Descriptor Set Layout
4↕
5Descriptor Set
6↕
7VkBuffer
6. 每帧 Uniform Buffer#
如果程序存在多个 Frames In Flight,不建议所有帧共用同一个正在频繁修改的 Uniform Buffer。
1Frame 0 → Uniform Buffer 0
2Frame 1 → Uniform Buffer 1
这样可以避免 CPU 修改数据时,GPU 仍在读取同一块内存。
7. Dynamic Uniform Buffer#
Dynamic Uniform Buffer 可以让多个对象共用同一个大 Buffer。
1Object 0 Data
2Object 1 Data
3Object 2 Data
绑定 Descriptor Set 时,再通过动态偏移指定当前读取哪一段数据。
1minUniformBufferOffsetAlignment
1alignedSize = (dataSize + alignment - 1) & ~(alignment - 1);
8. Push Constant 与 Uniform Buffer 区别#
1Push Constant:
2少量数据
3更新频繁
4不需要 Descriptor
5适合每次 Draw 不同的数据
6
7Uniform Buffer:
8数据较大
9需要 Descriptor
10适合矩阵、相机、灯光等全局数据
1Push Constant = 小而频繁
2Uniform Buffer = 大而稳定
六、Ch7-6 拷贝图像到屏幕#
1Buffer:
2线性数据
3没有宽高
4适合顶点、索引、Uniform
5
6Image:
7二维或三维图像数据
8具有宽、高、格式、Mip Level
9适合贴图、渲染目标、深度缓冲
1CPU 图片数据
2→ Staging Buffer
3→ VkImage
4→ 图像布局转换
5→ Copy 或 Blit
6→ 交换链图像
1. 创建 VkImage#
1VK_IMAGE_USAGE_TRANSFER_SRC_BIT
2VK_IMAGE_USAGE_TRANSFER_DST_BIT
3VK_IMAGE_USAGE_SAMPLED_BIT
1创建 VkImage
2→ 查询内存需求
3→ 分配 VkDeviceMemory
4→ vkBindImageMemory
2. Image Layout#
Vulkan Image 在不同用途下需要处于不同 Layout。
1VK_IMAGE_LAYOUT_UNDEFINED
2VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
3VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
4VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
5VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
6VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
1准备接收复制
2→ TRANSFER_DST_OPTIMAL
3
4作为复制源
5→ TRANSFER_SRC_OPTIMAL
6
7被 Shader 读取
8→ SHADER_READ_ONLY_OPTIMAL
9
10用于屏幕显示
11→ PRESENT_SRC_KHR
3. Image Memory Barrier#
1VkImageMemoryBarrier
1vkCmdPipelineBarrier(...)
1UNDEFINED
2→ TRANSFER_DST_OPTIMAL
1TRANSFER_DST_OPTIMAL
2→ SHADER_READ_ONLY_OPTIMAL
4. Copy Buffer To Image#
1vkCmdCopyBufferToImage(...)
可以将 Staging Buffer 中的像素数据复制到 Image。
1VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
5. Copy Image 与 Blit Image#
6. 拷贝到交换链#
1源图像 → TRANSFER_SRC_OPTIMAL
2交换链图像 → TRANSFER_DST_OPTIMAL
1VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
1Swapchain Image
2PRESENT_SRC_KHR
3→ TRANSFER_DST_OPTIMAL
4→ 接收图像
5→ PRESENT_SRC_KHR
七、Ch7-7 使用贴图#
贴图本质上是一个可以被 Shader 读取的 Vulkan Image。
1读取图片文件
2→ Staging Buffer
3→ Texture Image
4→ 生成 Mipmap
5→ 创建 Image View
6→ 创建 Sampler
7→ 创建 Combined Image Sampler Descriptor
8→ Shader 采样
1. Texture Image#
1VK_IMAGE_USAGE_TRANSFER_DST_BIT
2VK_IMAGE_USAGE_SAMPLED_BIT
1VK_IMAGE_USAGE_TRANSFER_SRC_BIT
1VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
2VK_IMAGE_USAGE_TRANSFER_DST_BIT |
3VK_IMAGE_USAGE_SAMPLED_BIT
2. Image View#
1VkImageView
1VkImage = 实际图像资源
2VkImageView = 访问图像资源的视图
同一个 Image 可以创建多个不同的 Image View。
3. Sampler#
1magFilter
2minFilter
3mipmapMode
4addressModeU
5addressModeV
6addressModeW
7minLod
8maxLod
9maxAnisotropy
1Image = 贴图数据本身
2Image View = 如何访问贴图
3Sampler = 如何对贴图进行采样
4. Combined Image Sampler#
1VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
1VkDescriptorImageInfo
1sampler
2imageView
3imageLayout
1vkUpdateDescriptorSets(...)
5. Shader 采样#
1layout(set = 0, binding = 1) uniform sampler2D textureSampler;
1vec4 color = texture(textureSampler, inTexCoord);
6. 纹理坐标#
1U:0 到 1
2V:0 到 1
1glm::vec2 texCoord;
1layout(location = 2) in vec2 inTexCoord;
2layout(location = 0) out vec2 outTexCoord;
1layout(location = 0) in vec2 inTexCoord;
7. Mipmap#
11024 × 1024
2512 × 512
3256 × 256
4128 × 128
5...
61 × 1
1floor(log2(max(width, height))) + 1
1vkCmdBlitImage(...)
1Mip 0 → Mip 1
2Mip 1 → Mip 2
3Mip 2 → Mip 3
1VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
1VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
八、第七章整体关系#
1Ch7-1
2CPU 顶点数据
3→ Vertex Buffer
4→ Vertex Shader
5
6Ch7-2
7Index Buffer
8→ 复用顶点
9→ Indexed Draw
10
11Ch7-3
12Instance Data
13→ 一次绘制多个实例
14
15Ch7-4
16Push Constant
17→ 传递少量高频数据
18
19Ch7-5
20Uniform Buffer
21→ Descriptor Set
22→ 传递矩阵和全局数据
23
24Ch7-6
25Staging Buffer
26→ VkImage
27→ 图像复制和布局转换
28
29Ch7-7
30Texture Image
31→ Image View
32→ Sampler
33→ Descriptor Set
34→ Fragment Shader
九、各类数据的选择#
1. 顶点缓冲区#
1每个顶点读取一次
2. 索引缓冲区#
1保存顶点编号
3. 实例缓冲区#
1每个实例读取一次
4. Push Constant#
5. Uniform Buffer#
6. Texture Image#
十、资源生命周期#
1vkDestroyBuffer
2→ vkFreeMemory
1vkDestroyImageView
2→ vkDestroyImage
3→ vkFreeMemory
1vkDestroySampler
1vkDestroyDescriptorPool
2vkDestroyDescriptorSetLayout
1vkDestroyPipeline
2→ vkDestroyPipelineLayout
1使用某资源的对象先销毁
2被依赖的资源后销毁
1vkDeviceWaitIdle(...)
十一、总结#
从第二章中写死在 Shader 内部的三角形开始,逐步加入了:
1顶点缓冲区
2索引缓冲区
3实例化绘制
4Push Constant
5Uniform Buffer
6Descriptor Set
7图像复制
8纹理采样
9Mipmap
1CPU 数据
2→ Vulkan Buffer 或 Image
3→ Device Memory
4→ Descriptor 或 Vertex Input
5→ Graphics Pipeline
6→ Shader
7→ 屏幕
第七章解决了几何数据、实例数据、常量数据和图像数据如何从 CPU 传递到 GPU,并最终被 Vulkan Graphics Pipeline 和 Shader 使用的问题。