当前位置: 首页 > news >正文

Vulkan 学习笔记14—模型加载(OBJ、glTF)

一、渲染层重构

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 场景中的一个可渲染网格对象。它包含以下核心功能:

  • 几何数据存储:使用 verticesindices 存储网格的几何形状
  • 纹理管理:通过 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{
http://www.lqws.cn/news/484849.html

相关文章:

  • Elasticsearch、Faiss、Milvus在向量索引实现上的核心差
  • 利用通义大模型构建个性化推荐系统——从数据预处理到实时API部署
  • 微处理器原理与应用篇---常见基础知识(7)
  • 【编程语言基础算法】前缀和
  • 【C++】C++枚举、const、static的用法
  • 73、单元测试-断言机制
  • 发送与接收
  • Spring Boot 项目初始化
  • EXPLAIN优化 SQL示例
  • MySQL之索引结构和分类深度详解
  • UML:类图
  • 电脑商城--购物车
  • Windows 后渗透中可能会遇到的加密字符串分析
  • 第16章 接口 笔记
  • 嵌入式C语言编程规范
  • 逻辑门电路Multisim电路仿真汇总——硬件工程师笔记
  • 等等等等等等
  • git安装使用详细教程
  • 每日算法刷题Day35 6.22:leetcode枚举技巧枚举中间2道题,用时1h
  • ruoyi-flowable-plus中satoken的配置使用
  • Kafka Streams架构深度解析:从并行处理到容错机制的全链路实践
  • TCP流量控制与拥塞控制:核心机制与区别
  • git 如何忽略某个文件夹文件
  • AI 辅助生成 Mermaid 流程图
  • Python 的内置函数 help
  • Matplotlib入门指南:从安装到绘制基本图形
  • 给docker 配置代理 IP机端口
  • Protobuf 与 JSON 的兼容性:技术选型的权衡与实践
  • Hadoop部署(HA)高可用集群
  • 编程语言分类大全:从系统到AI开发