Skip to content

Flurry-L/VkRenderer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vulkan PBR 渲染引擎

基于 vkguide.dev 教程构建的 Vulkan 1.3 实时渲染引擎,实现了从底层初始化到 GLTF 场景加载的完整渲染管线。支持 PBR Metallic-Roughness 材质模型、层次化场景树、计算着色器背景、ImGui 调试界面等特性。

技术栈

类别 技术
图形 API Vulkan 1.3(Dynamic Rendering, Synchronization2, Buffer Device Address)
窗口系统 SDL2
内存分配 Vulkan Memory Allocator (VMA)
场景格式 glTF 2.0(通过 fastgltf 解析)
数学库 GLM
图像解码 stb_image
调试 UI Dear ImGui (Vulkan + SDL2 backend)
构建系统 CMake + glslangValidator (SPIR-V 编译)
C++ 标准 C++20

项目结构

├── CMakeLists.txt              # 根构建脚本,含 GLSL→SPIR-V 编译规则
├── src/
│   ├── vk_engine.h/cpp         # 引擎核心:初始化、渲染循环、资源管理
│   ├── vk_types.h              # 基础类型:AllocatedImage/Buffer、Vertex、材质、场景节点
│   ├── vk_loader.h/cpp         # GLTF 场景加载器(Visitor 模式解析)
│   ├── vk_descriptors.h/cpp    # 描述符集布局构建、可增长池分配器、DescriptorWriter
│   ├── vk_pipelines.h/cpp      # 图形管线 Builder 模式封装、着色器模块加载
│   ├── vk_images.h/cpp         # 图像布局转换、Blit 复制工具
│   ├── vk_initializers.h/cpp   # Vulkan 结构体创建辅助函数集
│   ├── camera.h/cpp            # FPS 风格相机(键鼠控制、View 矩阵生成)
│   └── main.cpp                # 入口点
├── shaders/
│   ├── input_structures.glsl   # 共享 Uniform/Sampler 布局声明
│   ├── mesh.vert               # PBR 网格顶点着色器(Buffer Reference + Push Constants)
│   ├── mesh.frag               # PBR 网格片段着色器(Blinn-Phong 光照)
│   ├── gradient_color.comp     # 渐变背景计算着色器
│   └── sky.comp                # 天空背景计算着色器
├── assets/                     # GLTF 模型资源(structure.glb, basicmesh.glb)
└── third_party/                # SDL2, GLM, VMA, fastgltf, fmt, imgui, vkbootstrap

架构设计

1. 引擎初始化管线

init()
 ├─ init_vulkan()           创建 Instance → Validation Layer → Surface → 物理/逻辑设备 → Queue → VMA
 ├─ init_swapchain()        创建交换链 + 高精度 DrawImage (R16G16B16A16_SFLOAT) + DepthImage
 ├─ init_commands()         每帧 CommandPool/Buffer + Immediate Submit 专用 Pool
 ├─ init_sync_structures()  每帧 Fence + Swapchain/Render Semaphore
 ├─ init_descriptors()      全局/每帧 Descriptor Pool + 三种 SetLayout
 ├─ init_pipelines()        计算管线(背景)+ PBR 图形管线(Opaque/Transparent)
 ├─ init_imgui()            ImGui Vulkan Backend(Dynamic Rendering)
 └─ init_default_data()     默认纹理/采样器 + GLTF 加载 + 默认材质实例

2. Material — Mesh — RenderObject 三层架构

本引擎将渲染数据解耦为三个层次:

MaterialPipeline (管线层)           MaterialInstance (材质实例层)
┌──────────────────────┐          ┌────────────────────────────┐
│ VkPipeline pipeline  │◄─────────│ MaterialPipeline* pipeline │
│ VkPipelineLayout     │          │ VkDescriptorSet materialSet│
└──────────────────────┘          │ MaterialPass passType      │
 多个 MaterialInstance 可           └────────────────────────────┘
 共享同一条 Pipeline                         ▲
                                            │ 引用
MeshAsset (网格资产层)              RenderObject (渲染对象层)
┌─────────────────────────┐      ┌────────────────────────────────┐
│ string name             │      │ uint32_t indexCount, firstIndex│
│ vector<GeoSurface>      │─────►│ VkBuffer indexBuffer           │
│   └ startIndex, count   │      │ MaterialInstance* material     │
│   └ shared_ptr<Material>│      │ mat4 transform                 │
│ GPUMeshBuffers          │─────►│ VkDeviceAddress vertexBuffer   │
│   └ indexBuffer         │      └────────────────────────────────┘
│   └ vertexBuffer        │       每帧由 MeshNode::Draw() 生成
│   └ vertexBufferAddress │
└─────────────────────────┘

设计要点:

  • MaterialPipeline 仅存储 VkPipelineVkPipelineLayout,生命周期由 GLTFMetallic_Roughness 统一管理,不由单个材质负责销毁——因为材质实例数量可能很多,在每个实例上做引用计数开销不合理。
  • MaterialInstance 持有指向 Pipeline 的裸指针和自身的 VkDescriptorSet(包含 Uniform Buffer、颜色贴图、金属度-粗糙度贴图)。按 MaterialPass(Opaque / Transparent)区分渲染通道。
  • RenderObject 是每帧临时生成的绘制命令数据包,将几何数据(index/vertex)、材质、变换矩阵打包在一起,集中存入 DrawContext 以便批量绘制。

3. 层次化场景树

IRenderable (纯虚基类)
 └── Node (场景节点)
      ├── weak_ptr<Node> parent        // 弱引用避免循环依赖
      ├── vector<shared_ptr<Node>> children
      ├── localTransform / worldTransform
      ├── refreshTransform(parentMatrix)  // 自顶向下递归传播变换
      └── virtual Draw(topMatrix, ctx)    // 递归遍历所有子节点
           └── MeshNode : Node
                ├── shared_ptr<MeshAsset> mesh
                └── Draw() 重写:
                     1. nodeMatrix = topMatrix × worldTransform
                     2. 将 mesh 的每个 surface 封装为 RenderObject 加入 DrawContext
                     3. 调用 Node::Draw() 继续递归子节点

设计要点:

  • 利用 IRenderable 多态接口实现统一渲染遍历——不管节点是否含有网格,都通过 Draw() 虚函数递归。
  • Node::parent 使用 weak_ptr 避免 parent↔children 的循环强引用导致内存泄漏。
  • refreshTransform() 在场景加载后调用一次,从根节点向下递归计算 worldTransform = parentMatrix × localTransform
  • 实际渲染时 Draw() 传入 topMatrix 参数,与 worldTransform 相乘得到最终的模型矩阵,不会修改场景树本身的数据——确保遍历的无副作用性。

4. GLTF 加载与 Visitor 模式

loadGltf() 函数完成从文件解析到 GPU 资源创建的全流程:

loadGltf(engine, filePath)
 ├─ fastgltf::Parser 解析 .gltf / .glb
 ├─ 加载纹理 ─── std::visit(fastgltf::visitor{...}, image.data)
 │   ├─ URI 路径  → stbi_load() → engine->create_image()
 │   ├─ 内嵌 Vector → stbi_load_from_memory() → engine->create_image()
 │   └─ BufferView  → 二次 visit 取出数据 → engine->create_image()
 ├─ 加载材质 ─── 读取 PBR 参数 → write_material() 生成 MaterialInstance
 ├─ 加载网格 ─── iterateAccessor 读取 index/position/normal/uv/color → uploadMesh()
 ├─ 构建节点 ─── std::visit(fastgltf::visitor{...}, node.transform)
 │   ├─ TransformMatrix → 直接 memcpy
 │   └─ TRS → glm::translate × glm::toMat4(quat) × glm::scale
 ├─ 建立父子关系 → children.push_back / parent 赋值
 └─ 找到顶层节点 → refreshTransform(I)

Visitor 模式的应用:fastgltf 使用 std::variant 表示数据源(URI / 内嵌向量 / BufferView)和变换类型(矩阵 / TRS)。通过 std::visit + fastgltf::visitor(本质是 overloaded lambda 集合),针对每种变体类型提供不同的处理逻辑,在编译期进行类型安全的分派,避免了运行时的 dynamic_cast 或类型标签判断。

5. Push Constants 与 VkDeviceAddress 数据传递

GPU 端着色器(mesh.vert):
┌─────────────────────────────────────────────┐
│ layout(push_constant) uniform constants {   │
│   mat4 render_matrix;      ← 模型矩阵       │
│   VertexBuffer vertexBuffer; ← 64位GPU地址   │
│ }                                           │
│                                             │
│ layout(buffer_reference) readonly buffer     │
│   VertexBuffer { Vertex vertices[]; };      │
│                                             │
│ Vertex v = vertexBuffer.vertices[gl_VertexIndex]; │
└─────────────────────────────────────────────┘

CPU 端每个 DrawCall:
 vkCmdPushConstants(cmd, layout, VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push);
   └── push.worldMatrix = nodeMatrix      (glm::mat4, 64 bytes)
       push.vertexBuffer = bufferAddress  (VkDeviceAddress, 8 bytes)

设计要点:

  • 顶点数据不通过传统的 Vertex Input Binding 传递,而是以 Storage Buffer 形式上传到 GPU,通过 VkDeviceAddress(Buffer Device Address 特性)获取其 64 位 GPU 虚拟地址,以 Push Constants 的形式传给着色器。
  • 着色器中使用 GL_EXT_buffer_reference 扩展将该地址解引用为结构化的 VertexBuffer,直接用 gl_VertexIndex 索引获取顶点——无需 VkVertexInputBindingDescription,管线配置更简洁。
  • Push Constants 仅 72 字节(mat4 + uint64),远低于 Vulkan 规范保证的最小 128 字节上限,每次 vkCmdDraw 前更新,延迟极低。

6. 描述符管理系统

DescriptorLayoutBuilder         DescriptorAllocatorGrowable        DescriptorWriter
┌─────────────────────┐        ┌──────────────────────────┐       ┌─────────────────┐
│ add_binding(bindIdx, │        │ readyPools: 可用池列表   │       │ write_image()   │
│   descriptorType)   │        │ fullPools:  满载池列表   │       │ write_buffer()  │
│ build(device,stage) │        │ setsPerPool (×1.5 增长)  │       │ update_set()    │
└─────────────────────┘        │                          │       └─────────────────┘
                               │ allocate(device, layout): │
                               │  1. get_pool (ready→取出) │
                               │  2. 分配失败→移入fullPools│
                               │  3. 创建新pool重试       │
                               │  4. 成功→放回readyPools  │
                               └──────────────────────────┘

三种 DescriptorSetLayout:

Layout Binding 类型 用途
drawImageDescriptorLayout 0: STORAGE_IMAGE Compute 背景计算着色器写入 drawImage
gpuSceneDataDescriptorLayout 0: UNIFORM_BUFFER Vert+Frag 场景全局数据 (VP矩阵、光照)
materialLayout 0: UNIFORM_BUFFER, 1: IMAGE_SAMPLER, 2: IMAGE_SAMPLER Vert+Frag 材质常量 + 颜色贴图 + 金属度粗糙度贴图

全局资源使用全局 DescriptorAllocatorGrowable;每帧临时资源(如场景 Uniform Buffer)使用 FrameData._frameDescriptors,每帧 clear_pools() 重置后重新分配。

7. 渲染循环

draw() 每帧执行:
 ├─ update_scene()
 │   ├─ 更新相机 → 计算 View/Projection 矩阵
 │   ├─ 清空 DrawContext
 │   └─ 对场景树调用 Draw() → 填充 OpaqueSurfaces / TransparentSurfaces
 │
 ├─ vkWaitForFences (等待本帧 GPU 完成)
 ├─ 帧级 DeletionQueue.flush() + frameDescriptors.clear_pools()
 ├─ vkResetFences
 ├─ vkAcquireNextImageKHR (获取交换链图像索引, swapchainSemaphore 同步)
 │
 ├─ 录制 Command Buffer:
 │   ├─ transition drawImage → GENERAL
 │   ├─ draw_background():Compute Dispatch (天空/渐变)
 │   ├─ transition drawImage → COLOR_ATTACHMENT, depthImage → DEPTH_ATTACHMENT
 │   ├─ draw_geometry():
 │   │   ├─ 创建帧级 Uniform Buffer → 写入 GPUSceneData → 绑定描述符集
 │   │   ├─ vkCmdBeginRendering (Dynamic Rendering, 无 RenderPass)
 │   │   ├─ 遍历 OpaqueSurfaces → 绑定管线/描述符/IndexBuffer → PushConstants → DrawIndexed
 │   │   ├─ 遍历 TransparentSurfaces → 同上
 │   │   └─ vkCmdEndRendering
 │   ├─ transition drawImage → TRANSFER_SRC, swapchainImage → TRANSFER_DST
 │   ├─ vkCmdBlitImage2 (drawImage 拉伸复制到 swapchainImage)
 │   ├─ transition swapchainImage → COLOR_ATTACHMENT
 │   ├─ draw_imgui()
 │   └─ transition swapchainImage → PRESENT_SRC
 │
 ├─ vkQueueSubmit2 (renderFence 同步, swapchainSemaphore 等待, renderSemaphore 信号)
 └─ vkQueuePresentKHR (renderSemaphore 等待)

同步策略:

同步原语 类型 作用
renderFence CPU↔GPU CPU 等待上一帧绘制完成后才重用该帧的 CommandBuffer 和临时资源
swapchainSemaphore GPU↔GPU 确保交换链图像获取完毕后才开始渲染
renderSemaphore GPU↔GPU 确保渲染完毕后才提交到显示
VkImageMemoryBarrier2 GPU 内部 图像布局转换时的管线阶段与内存访问同步

8. RAII 与 DeletionQueue 生命周期管理

struct DeletionQueue {
    std::deque<std::function<void()>> deletors;
    void push_function(std::function<void()>&& func);
    void flush();  // 逆序执行所有销毁函数,保证依赖正确性
};

引擎使用两级 DeletionQueue:

  • _mainDeletionQueue(全局级):注册 VMA Allocator、Descriptor Pool/Layout、Pipeline、默认纹理/采样器、Immediate Submit Pool 等生命周期与引擎等长的资源。在 cleanup() 中 flush。
  • FrameData._deletionQueue(帧级):注册每帧临时创建的 Uniform Buffer 等资源。在下一次使用该帧槽位时 flush。

加上 LoadedGLTF 析构函数构成第三级——GLTF 场景拥有自己的描述符池、材质缓冲区、网格缓冲区和纹理,在 ~LoadedGLTF() 中通过 clearAll() 统一释放。由于场景存储为 shared_ptr<LoadedGLTF>,当从 loadedScenes 中移除时自动触发析构。

为什么使用逆序 flush:Vulkan 资源之间存在依赖关系(例如 ImageView 依赖 Image,DescriptorSet 依赖 DescriptorPool),后创建的资源通常依赖先创建的资源,因此必须先销毁后创建的(逆序)才能避免使用已释放资源的 Validation Error。

9. 多帧并行 (Frame Overlap)

CPU 帧 N:    [录制 CmdBuf N] ──submit──> [GPU 执行帧 N]
CPU 帧 N+1:  [录制 CmdBuf N+1] ─submit─> [GPU 执行帧 N+1]
              ↑                              ↑
          使用 _frames[1]              使用 _frames[0](fence 保护)

设置 FRAME_OVERLAP = 2,CPU 端维护 2 套 FrameData(CommandPool/Buffer、Fence、Semaphore、帧级 DeletionQueue/DescriptorPool),实现经典的乒乓缓冲。CPU 在录制帧 N+1 时 GPU 可以并行执行帧 N,通过 Fence 保证不会覆写正在执行的帧资源。

10. Vulkan 1.3 特性使用

  • Dynamic Rendering (VK_KHR_dynamic_rendering):不再创建 VkRenderPassVkFramebuffer,改用 vkCmdBeginRendering / vkCmdEndRendering,通过 VkRenderingInfo 直接指定颜色/深度附件,管线通过 VkPipelineRenderingCreateInfo 链接到 pNext。减少了大量模板对象管理。
  • Synchronization2 (VK_KHR_synchronization2):使用 vkQueueSubmit2VkImageMemoryBarrier2vkCmdPipelineBarrier2 等新 API,提供更精细的管线阶段 (VK_PIPELINE_STAGE_2_*) 和内存访问 (VK_ACCESS_2_*) 控制。
  • Buffer Device Address (VK_KHR_buffer_device_address):为顶点缓冲区启用 GPU 虚拟地址访问(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT),配合着色器 buffer_reference 扩展实现无绑定 (Bindless) 风格的顶点数据访问。

构建与运行

# 前提:安装 Vulkan SDK(含 glslangValidator)
cmake -B build -S .
cmake --build build --config Release

# SPIR-V 着色器编译
cmake --build build --target Shaders

# 运行(确保 assets/ 中有 structure.glb)
./bin/engine

控制

  • WASD — 相机移动
  • 鼠标右键拖动 — 相机旋转
  • ImGui 面板 — 调节渲染缩放、切换背景效果、修改着色器参数

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors