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{
   
<think>好的,用户想了解如何在OpenGL或Vulkan项目中使用Assimp加载和处理3D模型数据。首先,我需要回忆一下Assimp的基本用法和它在图形编程中的作用。Assimp是一个开源库,用于导入各种3D模型格式,如FBX、OBJ、3DS等,并将其转换为统一的内部数据结构,方便在程序中使用。 接下来,用户可能需要知道如何将Assimp集成到项目中。对于OpenGL和Vulkan这两个不同的图形API,集成步骤可能有所不同,但核心的模型加载和处理部分是相似的。需要分步骤讲解,比如安装Assimp库、加载模型数据、解析网格、纹理处理,最后渲染模型。 考虑到用户可能不熟悉具体的代码实现,应该提供一些代码示例。例如,在OpenGL中,如何使用Assimp读取模型文件,提取顶点、法线、纹理坐标,然后创建VBO和VAO。对于Vulkan,可能需要说明如何将数据转换为缓冲区,并处理内存分配的问题。 同时,需要注意不同API之间的差异。比如,Vulkan需要更详细的内存管理和管线设置,而OpenGL在这方面较为简化。在回答时,应该分别说明两者的关键步骤,避免混淆。 用户可能还会遇到常见的错误,比如模型路径不正确、纹理加载失败,或者内存泄漏。需要提醒一些注意事项,比如确保Assimp版本与项目兼容,正确链接库文件,处理多网格模型的情况,以及纹理的加载方式。 此外,引用相关的参考资料,比如站内提到的NanoVG和OpenGL扩展的内容,可能与模型渲染有关联,但需要确认是否直接相关。例如,NanoVG主要用于2D矢量绘图,而Assimp处理的是3D模型,所以可能关联不大。但可以提到在渲染时可能需要结合其他库进行优化或扩展功能,比如使用计算着色器处理模型数据,引用相关的扩展管理库如glad或glew。 最后,生成相关问题,帮助用户深入理解,比如性能优化、多格式支持、动画处理等。确保回答结构清晰,步骤明确,代码示例正确,并且覆盖常见问题。</think>在OpenGL/Vulkan项目中集成Assimp加载3D模型主要分为五个核心步骤: ### 一、环境配置 1. 下载Assimp库(建议5.0+版本) ```bash git clone https://github.com/assimp/assimp.git ``` 2. 配置项目依赖: - OpenGL需要GLFW/GLEW(引用[4]) - Vulkan需要Vulkan SDK - 添加Assimp头文件路径和链接库 ### 二、模型加载流程 ```cpp // OpenGL示例 Assimp::Importer importer; const aiScene* scene = importer.ReadFile("model.obj", aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs); ``` 关键处理标志: - `aiProcess_CalcTangentSpace`:生成切线空间 - `aiProcess_OptimizeMeshes`:网格优化 - `aiProcess_JoinIdenticalVertices`:顶点去重 ### 三、数据结构解析 ```cpp for(unsigned i=0; i<scene->mNumMeshes; i++){ aiMesh* mesh = scene->mMeshes[i]; // 提取顶点数据 vector<Vertex> vertices; for(unsigned v=0; v<mesh->mNumVertices; v++){ vertices.push_back({ {mesh->mVertices[v].x, ...}, // position {mesh->mNormals[v].x, ...}, // normal {mesh->mTextureCoords[0][v].x, ...} // UV }); } // 创建OpenGL VAO/VBO glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); // ... 类似Vulkan需要创建VkBuffer } ``` Vulkan特别注意: - 需要显式内存分配 - 使用`VkCommandBuffer`传输数据 - 对齐要求更严格(使用`vkCmdCopyBuffer`) ### 四、材质与纹理处理 ```cpp aiMaterial* mat = scene->mMaterials[mesh->mMaterialIndex]; aiString texPath; if(mat->GetTexture(aiTextureType_DIFFUSE, 0, &texPath) == AI_SUCCESS){ // 使用stb_image加载纹理 int width, height, channels; stbi_load(texPath.C_Str(), &width, ...); // OpenGL创建纹理对象 glGenTextures(1, &textureID); // Vulkan需创建VkImage/VkImageView } ``` ### 五、渲染循环 OpenGL示例: ```cpp glUseProgram(shader); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, indices.size(), ...); ``` Vulkan需要: - 描述符集绑定 - 管线状态管理 - 次级命令缓冲优化(对复杂模型) §§常见问题解决§§ 1. **模型不显示**:检查矩阵坐标系(Assimp使用右手系) 2. **纹理错乱**:验证UV坐标解析逻辑 3. **性能问题**:使用`aiProcess_OptimizeGraph`优化场景图
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值