一、着色器代码更新及构建时自动编译着色器脚本
用内存中的顶点缓冲区替换顶点着色器中硬编码的顶点数据
之前的顶点着色器:
#version 450
layout(location = 0) out vec3 fragColor;
// 顶点数据硬编码
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
// 颜色数据硬编码
vec3 colors[3] = vec3[](
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
fragColor = colors[gl_VertexIndex];
}
现在的顶点着色器:
#version 450
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
每次改动着色器都手动输入glslc 命令把着色器编译为 spv 字节码有些效率低下,可在 CMake 构建阶段自动调用 glslc 命令编译着色器,修改 CMakeLists.txt :
# ------------后面追加修改的配置部分-----------
# 查找所有着色器源文件
file(GLOB SHADER_SRC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders/*.vert" "${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders/*.frag")
set(SHADER_SPV_FILES "")
foreach(SHADER ${SHADER_SRC_FILES})
message("compile shader file "${SHADER})
get_filename_component(FILE_NAME ${SHADER} NAME)
set(SPV "${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders/${FILE_NAME}.spv")
message("spv path: "${SPV})
add_custom_command(
OUTPUT ${SPV}
COMMAND glslc ${SHADER} -o ${SPV}
DEPENDS ${SHADER}
COMMENT "Compiling shader ${FILE_NAME}"
)
list(APPEND SHADER_SPV_FILES ${SPV})
endforeach()
add_custom_target(CompileShaders ALL DEPENDS ${SHADER_SPV_FILES})
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/assets
$<TARGET_FILE_DIR:${PROJECT_NAME}>/assets
COMMENT "Copying assets to build directory..."
)
二、顶点数据与绑定描述
创建一个VkTypes.h头文件,添加顶点数据结构体 Vertex,顶点/颜色都作为属性,这称为交错顶点属性。
#pragma once
#include <vulkan/vulkan.h>
#include <array>
#include <glm/glm.hpp>
namespace renderer { // 渲染器相关类和结构的命名空间
// 顶点数据结构,包含位置和颜色信息
struct Vertex {
glm::vec2 pos; // 二维坐标位置(x,y)
glm::vec3 color; // RGB颜色值
// 获取顶点输入绑定描述
// 描述顶点数据如何按字节偏移量组织在内存中
static VkVertexInputBindingDescription getBindingDescription() {
VkVertexInputBindingDescription bindingDescription{};
bindingDescription.binding = 0; // 绑定点索引
bindingDescription.stride = sizeof(Vertex); // 顶点数据大小(字节)
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; // 按顶点处理数据
return bindingDescription;
}
// 获取顶点属性描述数组
// 描述如何从顶点数据中提取各个属性
static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions{};
// 位置属性描述
attributeDescriptions[0].binding = 0; // 对应绑定点索引
attributeDescriptions[0].location = 0; // 在顶点着色器中对应的location
attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; // 32位浮点数x2 (x,y)
attributeDescriptions[0].offset = offsetof(Vertex, pos); // 位置属性的内存偏移
// 颜色属性描述
attributeDescriptions[1].binding = 0; // 对应绑定点索引
attributeDescriptions[1].location = 1; // 在顶点着色器中对应的location
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; // 32位浮点数x3 (R,G,B)
attributeDescriptions[1].offset = offsetof(Vertex, color); // 颜色属性的内存偏移
return attributeDescriptions;
}
};
} // namespace renderer
- Vertex 结构:包含顶点的位置 (pos) 和颜色 (color) 数据
- getBindingDescription:
- 告诉 Vulkan 顶点数据如何组织
- stride 设置为整个 Vertex 大小,表示每个顶点数据是连续排列的
- getAttributeDescriptions:
- 定义每个属性的存储格式和内存偏移
- pos 使用 2 个 32 位浮点数 (VK_FORMAT_R32G32_SFLOAT)
- color 使用 3 个 32 位浮点数 (VK_FORMAT_R32G32B32_SFLOAT)
- offsetof 宏:计算结构体成员相对于起始地址的字节偏移量
三、顶点缓冲
可见缓冲区并使用 memcpy 将顶点数据直接复制到其中的最简单方法开始,之后我们将了解如何使用暂存缓冲区将顶点数据复制到高性能内存中。
在 vkInit 中添加创建顶点缓冲代码:
// 创建命令池
{
...
}
// 创建顶点缓冲
{
// 1. 创建顶点缓冲区对象
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; // 结构体类型
bufferInfo.size = vkcontext->vertexBufferSize; // 缓冲区大小(字节)
bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; // 用作顶点缓冲区
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // 独占模式(单队列族访问)
// 创建缓冲区
if (vkCreateBuffer(vkcontext->device, &bufferInfo, nullptr, &vkcontext->vertexBuffer) != VK_SUCCESS) {
throw std::runtime_error("顶点缓冲创建失败!");
}
// 2. 查询缓冲区内存需求
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(vkcontext->device, vkcontext->vertexBuffer, &memRequirements);
// 3. 分配内存
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; // 结构体类型
allocInfo.allocationSize = memRequirements.size; // 分配大小与需求一致
// 查找合适的内存类型:主机可见(可映射CPU内存)且具有一致性
allocInfo.memoryTypeIndex = findMemoryType(
memRequirements.memoryTypeBits, // 可用内存类型位掩码
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, // 内存属性
vkcontext->physicalDevice // 物理设备
);
// 分配内存
if (vkAllocateMemory(vkcontext->device, &allocInfo, nullptr, &vkcontext->vertexBufferMemory) != VK_SUCCESS) {
throw std::runtime_error("为顶点缓冲分配内存失败!");
}
// 4. 将内存绑定到缓冲区
vkBindBufferMemory(vkcontext->device, vkcontext->vertexBuffer, vkcontext->vertexBufferMemory, 0);
// 5. 向顶点缓冲区写入数据
void* data;
// 映射内存到CPU可访问的地址空间
vkMapMemory(vkcontext->device, vkcontext->vertexBufferMemory, 0, bufferInfo.size, 0, &data);
// 使用memcpy复制顶点数据到映射的内存区域
memcpy(data, vkcontext->vertexData, (size_t) bufferInfo.size);
// 解除内存映射(数据已提交)
vkUnmapMemory(vkcontext->device, vkcontext->vertexBufferMemory);
// 注:由于使用了VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
// 无需额外的内存屏障来确保GPU可见性
}
// 创建命令缓冲
{
...
}
关键步骤解析:
创建缓冲区对象:
- 指定缓冲区大小和用途(顶点缓冲区)
- 分享模式设为独占,因为通常只由图形队列族访问
查询内存需求:
- Vulkan 要求先查询缓冲区的内存需求(大小、对齐和可用内存类型)
分配内存:
- 创建 CPU 使用 findMemoryType 函数查找符合条件的内存类型
- 选择主机可见内存以便 CPU 可以写入数据
- 选择一致性内存以避免手动刷新内存范围
绑定内存到缓冲区:
- 将分配的内存块与缓冲区对象关联
数据传输:
- 映射内存到 CPU 地址空间
- 使用
memcpy
复制顶点数据 - 解除映射,完成数据传输
四、绑定顶点缓冲
修改HelloTriangle类,添加顶点颜色数据:
--------------HelloTriangle.h----------
#pragma once
#include <vector>
#include "renderer/VkTypes.h"
using namespace renderer;
class HelloTriangle {
public:
HelloTriangle();
std::vector<Vertex> vertices;
};
-------------HelloTriangle.cpp-------------
#include "HelloTriangle.h"
HelloTriangle::HelloTriangle(): vertices({
{{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}},
{{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}
}) {}
在VkContext 中新增成员变量:
struct VkContext {
...
void* vertexData;
uint32_t vertexNum{0};
size_t vertexBufferSize{0};
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
};
在修改主函数逻辑:
int main() {
initWindow();
vkcontext.window = window;
HelloTriangle helloTriangleApp;
vkcontext.vertexData = helloTriangleApp.vertices.data();
vkcontext.vertexNum = helloTriangleApp.vertices.size();
vkcontext.vertexBufferSize = helloTriangleApp.vertices.size() * sizeof(Vertex);
if (!vkInit(&vkcontext)) {
throw std::runtime_error("Vulkan 初始化失败!");
}
try {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
vkRender(&vkcontext);
}
vkDeviceWaitIdle(vkcontext.device);
vkClean(&vkcontext);
glfwDestroyWindow(window);
glfwTerminate();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
修改命令录制部分,绑定顶点缓冲:
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = vkcontext->swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
// --------修改部分开始-----------------------------
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vkcontext->vertexBuffer, offsets);
vkCmdDraw(commandBuffer, vkcontext->vertexNum, 1, 0, 0);
//---------修改部分结束--------------------------------------
vkCmdEndRenderPass(commandBuffer);
现在修改下顶点颜色值,构建运行看看效果:
一切正常,Validation Layer 也没有输出错误日志!
当前代码分支为 06_vertexInputdescription_vertexbuffer