一、渲染层重构
VkRender.cpp 已经膨胀到1500行代码左右,网格渲染、纹理映射很多业务数据对象是直接挂在 VkContext 大对象上,不利于后期扩展。需要重构代码,封装基础网格对象,把顶点/颜色/纹理坐标/法线等统统打包到一个网格对象中(MeshInstance)。
1.VkTexture 修改
// 封装 Vulkan 纹理资源的结构体,管理 CPU 端图像数据和 GPU 端渲染资源
struct VkTexture {
// 纹理资源的唯一标识符(通常是文件路径),用于资源查找和管理
std::string assetId;
// 存储纹理的原始像素数据(例如从 obj/glTF/glb 文件内嵌的图像数据)
// 当纹理从外部文件加载时可能为空,仅在需要时保留(如动态生成纹理)
std::vector<uint8_t> pixels;
// 图像的尺寸信息:宽度、高度和通道数(如 RGB 为 3,RGBA 为 4)
int width = 0;
int height = 0;
int channels = 0;
// 以下是 Vulkan 纹理资源的核心句柄:
// VkImage:表示 GPU 端的图像资源,存储纹理数据
VkImage image = VK_NULL_HANDLE;
// VkDeviceMemory:与图像关联的设备内存,用于存储图像数据
VkDeviceMemory memory = VK_NULL_HANDLE;
// VkImageView:图像的视图,定义了如何访问图像数据(如格式、层、级别)
VkImageView view = VK_NULL_HANDLE;
// VkSampler:采样器对象,定义了纹理采样时的过滤和寻址方式
VkSampler sampler = VK_NULL_HANDLE;
// 默认构造函数,使用编译器生成的默认实现
VkTexture() = default;
};
2. Vertex 结构扩展
namespace renderer {
...
struct Vertex {
...
// 必须重载 operator== 用于键比较
bool operator==(const Vertex& other) const {
return pos == other.pos && color == other.color && texCoord == other.texCoord;
}
};
...
} // namespace renderer
// 特化哈希函数
namespace std {
template <>
struct hash<renderer::Vertex> {
size_t operator()(renderer::Vertex const& vertex) const {
// 使用 Boost 风格的哈希组合
size_t seed = 0;
auto hashCombine = [&seed](auto v) {
seed ^= std::hash<decltype(v)>()(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
};
hashCombine(vertex.pos);
hashCombine(vertex.color);
hashCombine(vertex.texCoord);
return seed;
}
};
}
3. MeshInstance 封装
#pragma once
#include <glm/glm.hpp>
#include <vector>
#include "renderer/VkTypes.h"
using namespace renderer;
namespace scene {
// 表示场景中的一个网格实例,包含几何数据和渲染状态
struct MeshInstance {
// 存储网格的顶点数据,每个顶点包含位置、法线、纹理坐标等信息
std::vector<Vertex> vertices;
// 存储顶点索引,用于索引绘制
std::vector<uint32_t> indices;
// 存储与网格关联的纹理,VkTexture 可能是自定义的纹理包装结构
std::vector<VkTexture> textures;
// 模型矩阵,用于将网格从局部空间变换到世界空间
glm::mat4 modelMatrix;
// 以下是 Vulkan 相关的句柄,用于 GPU 资源访问
// 顶点缓冲区句柄,存储顶点数据在 GPU 内存中的位置
VkBuffer vertexBuffer = VK_NULL_HANDLE;
// 索引缓冲区句柄,存储索引数据在 GPU 内存中的位置
VkBuffer indexBuffer = VK_NULL_HANDLE;
// 顶点缓冲区对应的设备内存句柄
VkDeviceMemory vertexMemory = VK_NULL_HANDLE;
// 索引缓冲区对应的设备内存句柄
VkDeviceMemory indexMemory = VK_NULL_HANDLE;
// 带参数的构造函数,使用提供的顶点、索引和模型矩阵初始化网格实例
MeshInstance(const std::vector<Vertex>& v,
const std::vector<uint32_t>& i,
const glm::mat4& matrix = glm::mat4(1.0f))
: vertices(v), indices(i), modelMatrix(matrix) {
}
// 默认构造函数,创建一个单位矩阵的空网格实例
MeshInstance():modelMatrix(glm::mat4(1.0f)){
}
};
} // namespace scene
代码功能概述
这段代码定义了一个名为 MeshInstance
的结构体,用于表示 3D 场景中的一个可渲染网格对象。它包含以下核心功能:
- 几何数据存储:使用
vertices
和indices
存储网格的几何形状 - 纹理管理:通过
textures
向量管理与网格关联的纹理 - 空间变换:使用
modelMatrix
控制网格在场景中的位置、旋转和缩放 - Vulkan 资源:包含用于渲染的 GPU 缓冲区和内存句柄
- 构造函数:提供灵活的初始化方式,支持自定义或默认参数
设计思路
- 数据与渲染分离:将 CPU 端的几何数据(vertices, indices)与 GPU 端的资源(vertexBuffer, indexBuffer)分开存储
- 灵活的实例化:通过 modelMatrix 支持同一个网格模型的多次实例化,节省内存
- 资源管理:使用 RAII 原则之外的方式管理 Vulkan 资源(需手动释放这些句柄)
潜在问题与注意事项
- 资源泄漏风险:Vulkan 句柄需要手动释放,建议在析构函数或单独的清理函数中添加释放代码
- 线程安全:在多线程环境中使用这些资源时需要额外同步
- 纹理处理:VkTexture 类型需要确保正确管理纹理加载和 Vulkan 图像资源
- 内存对齐:在创建 GPU 缓冲区时需要考虑 Vulkan 的内存对齐要求
4. 更新 VkContext 对象
namespace renderer {
...
struct VkContext {
GLFWwindow* window;
VkInstance instance;
VkDebugUtilsMessengerEXT debugMessenger;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkDevice device;
QueueFamilyIndices queueFamilyIndices;
VkQueue graphicsQueue;
VkQueue presentQueue;
uint32_t currentFrame = 0;
VkSurfaceKHR surface;
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
std::vector<VkImageView> swapChainImageViews;
VkRenderPass renderPass;
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
VkPipeline graphicsPipeline;
std::vector<VkFramebuffer> swapChainFramebuffers;
VkCommandPool commandPool;
std::vector<VkCommandBuffer> commandBuffers;
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
std::vector<void*> uniformBuffersMapped;
UBO* ubo;
VkDescriptorPool descriptorPool;
std::vector<std::vector<VkDescriptorSet>> descriptorSets; // 存放每个网格实例每个并行帧绑定的描述符集
VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;
std::vector<scene::MeshInstance*> meshInstances; // 所有要渲染的网格实例
scene::Camera* camera;
bool framebufferResized = false;
};
}; // namespace renderer
5. 修改 vkInit 纹理创建代码块
// 创建纹理: 每个MeshInstance可能有多个纹理
for (scene::MeshInstance* mesh : ctx->meshInstances) {
std::vector<VkTexture>& textures = mesh->textures;
for (VkTexture& texture : textures) {
vkCreateTexture(ctx, texture);
}
}
...
// 对于没有纹理的 Mesh,我会给它生成一个透明的默认纹理,这样着色器中就不用判断
// 当前渲染对象有没有纹理,统一标准易维护。
void vkCreateTexture(VkContext* ctx, VkTexture& texture) {
// 1. 加载纹理
int texWidth, texHeight, texChannels;
stbi_uc* pixels = nullptr;
if (texture.pixels.size()) {
pixels = reinterpret_cast<stbi_uc*>(texture.pixels.data());
texWidth = texture.width;
texHeight = texture.height;
texChannels = 4;
} else if (texture.assetId == "") {
// 透明纹理
static uint8_t opacityPixel[4] = {
255, 255, 255, 0};
pixels = opacityPixel;
texWidth = 1;
texHeight = 1;
texChannels = 4;
} else {
pixels = stbi_load(texture.assetId.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
}
if (!pixels) {
throw std::runtime_error("加载纹理图片失败!");
}
...
}
6. 修改 vkInit 顶点缓冲区初始化代码块
// 遍历场景中的所有网格实例,为每个网格创建顶点缓冲区
for (scene::MeshInstance* mesh : ctx->meshInstances) {
// 跳过没有顶点数据的网格
if (mesh->vertices.empty()) continue;
// 计算顶点缓冲区的大小(以字节为单位)
size_t vertexBufferSize = mesh->vertices.size() * sizeof(Vertex);
// 临时缓冲区(暂存区),用于在 CPU 和 GPU 之间传输数据
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
// 第一步:创建暂存缓冲区
// 这个缓冲区在 CPU 可见的内存中,我们可以向其中写入数据
createBuffer(ctx->physicalDevice,
ctx->device,
vertexBufferSize,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT, // 用作传输源
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer,
stagingBufferMemory);
// 把顶点数据从 CPU 内存复制到暂存缓冲区
void* data;
vkMapMemory(ctx->device, stagingBufferMemory, 0, vertexBufferSize, 0, &data);
memcpy(data, mesh->vertices.data(), vertexBufferSize);
vkUnmapMemory(ctx->device, stagingBufferMemory);
// 第二步:创建设备本地顶点缓冲区
// 这个缓冲区位于 GPU 内存中,适合高性能访问
createBuffer(ctx->physicalDevice,
ctx->device,
vertexBufferSize,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, // 设备本地内存(GPU 专用)
mesh->vertexBuffer,
mesh->vertexMemory);
// 第三步:把数据从暂存缓冲区复制到设备本地缓冲区
copyBuffer(stagingBuffer, mesh->vertexBuffer, vertexBufferSize, ctx);
// 第四步:清理暂存资源
// 数据传输完成后,暂存缓冲区就不再需要了
vkDestroyBuffer(ctx->device, stagingBuffer, nullptr);
vkFreeMemory(ctx->device, stagingBufferMemory, nullptr);
}
7. 修改 vkInit 索引缓冲区初始化代码块
// 遍历场景中的所有网格实例,为每个网格创建索引缓冲区
for (scene::MeshInstance* mesh : ctx->meshInstances) {
// 跳过没有索引数据的网格
if (mesh->indices.empty()) continue;
// 计算索引缓冲区的大小(以字节为单位)
size_t indexBufferSize = mesh->indices.size() * sizeof(mesh->indices[0]);
// 临时缓冲区(暂存区),用于在 CPU 和 GPU 之间传输数据
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
// 第一步:创建暂存缓冲区
// 这个缓冲区在 CPU 可见的内存中,我们可以向其中写入数据
createBuffer(ctx->physicalDevice,
ctx->device,
indexBufferSize,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT, // 用作传输源
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer,
stagingBufferMemory);
// 把索引数据从 CPU 内存复制到暂存缓冲区
void* data;
vkMapMemory(ctx->device, stagingBufferMemory, 0, indexBufferSize, 0, &data);
memcpy(data, mesh->indices.data(), indexBufferSize);
vkUnmapMemory(ctx->device, stagingBufferMemory);
// 第二步:创建设备本地索引缓冲区
// 这个缓冲区位于 GPU 内存中,适合高性能访问
createBuffer(ctx->physicalDevice,
ctx->device,
indexBufferSize,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, // 设备本地内存(GPU 专用)
mesh->indexBuffer,
mesh->indexMemory);
// 第三步:把数据从暂存缓冲区复制到设备本地缓冲区
copyBuffer(stagingBuffer, mesh->indexBuffer, indexBufferSize, ctx);
// 第四步:清理暂存资源
// 数据传输完成后,暂存缓冲区就不再需要了
vkDestroyBuffer(ctx->device, stagingBuffer, nullptr);
vkFreeMemory(ctx->device, stagingBufferMemory, nullptr);
}
8. 修改 vkInit 描述符池容量初始化代码块
// 创建描述符池
{
// 定义描述符池支持的描述符类型和数量
std::array<VkDescriptorPoolSize, 2> poolSizes{
};
// 获取场景中网格实例的数量
size_t meshCount = ctx->meshInstances.size();
// 第一种类型:统一缓冲区描述符(用于 MVP 矩阵等数据)
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
// 每个网格在每一帧都需要一个统一缓冲区描述符
poolSizes[0].descriptorCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES * meshCount);
// 第二种类型:组合图像采样器描述符(用于纹理)
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
// 每个网格在每一帧都需要一个图像采样器描述符
poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES * meshCount);
// 描述符池创建信息
VkDescriptorPoolCreateInfo poolInfo{