教程 21 - Vulkan 图形管线

上一篇:教程 20 - 文件系统与着色器模块 | 下一篇:待定 | 返回目录


📚 快速导航

目录 (点击展开/折叠)

🎯 本章目标

通过本教程,你将学会:

🎯 目标📝 描述✅ 成果
管线概念理解Vulkan图形管线的工作原理掌握可编程和固定功能阶段
顶点输入配置顶点数据的布局和属性定义vertex_3d结构
光栅化设置配置光栅化、深度测试和混合实现完整管线状态
管线创建创建VkPipeline对象完成vulkan_pipeline.h/c
动态状态使用动态视口和裁剪支持窗口调整大小
着色器系统封装对象着色器完成vulkan_object_shader

📖 教程概述

🔍 什么是图形管线?

图形管线(Graphics Pipeline)是GPU渲染的核心,它定义了顶点数据如何变成屏幕上的像素

┌─────────────────────────────────────────────────────────┐
│             Vulkan 图形管线完整流程                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  [顶点数据]                                              │
│      ↓                                                  │
│  ┌──────────────────────┐                              │
│  │ 1. 顶点输入 (VB/IB)  │ ← 固定功能                    │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 2. 顶点着色器        │ ← 可编程 🎨                   │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 3. 曲面细分 (可选)   │ ← 可编程                      │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 4. 几何着色器 (可选) │ ← 可编程                      │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 5. 光栅化            │ ← 固定功能                    │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 6. 片段着色器        │ ← 可编程 🎨                   │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 7. 深度/模板测试     │ ← 固定功能                    │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  ┌──────────────────────┐                              │
│  │ 8. 颜色混合          │ ← 固定功能                    │
│  └──────────┬───────────┘                              │
│             ↓                                           │
│  [帧缓冲输出]                                            │
│                                                         │
└─────────────────────────────────────────────────────────┘

❓ 为什么管线如此重要?

Vulkan vs OpenGL
特性OpenGLVulkan
管线创建运行时动态创建初始化时预先创建
状态管理全局状态机独立的管线对象
性能驱动开销大接近零开销
验证运行时验证创建时验证
切换成本较高极低
╔════════════════════════════════════════════════════════╗
║  Vulkan 图形管线设计理念                                ║
╠════════════════════════════════════════════════════════╣
║  ✓ 显式 - 所有状态必须明确设置                          ║
║  ✓ 不可变 - 创建后无法修改(除动态状态)                 ║
║  ✓ 预编译 - 初始化时完成所有验证和优化                   ║
║  ✓ 高效 - 运行时切换管线成本极低                        ║
║  ✓ 可缓存 - 可序列化并复用                              ║
╚════════════════════════════════════════════════════════╝

🛠️ 实现路线图

定义顶点结构
配置顶点输入
设置输入装配
配置视口/裁剪
设置光栅化
配置多重采样
深度/模板测试
颜色混合
动态状态
创建管线布局
创建图形管线
封装着色器系统

🧩 图形管线阶段详解

可编程阶段 vs 固定功能阶段

阶段类型作用本教程涉及
顶点着色器🎨 可编程处理顶点(变换、光照等)
曲面细分🎨 可编程细分几何体❌ 未来
几何着色器🎨 可编程生成新几何体❌ 未来
片段着色器🎨 可编程计算像素颜色
顶点输入⚙️ 固定从缓冲区读取顶点
输入装配⚙️ 固定组装图元(三角形等)
光栅化⚙️ 固定转换为片段
深度测试⚙️ 固定深度缓冲比较
颜色混合⚙️ 固定混合颜色(透明度)

管线状态对象

Vulkan使用**PSO(Pipeline State Object)**概念,将所有状态封装在一个对象中:

typedef struct vulkan_pipeline {
    VkPipeline handle;              // 管线句柄
    VkPipelineLayout pipeline_layout;  // 管线布局(描述符、推送常量)
} vulkan_pipeline;

💻 核心实现

步骤1:定义顶点结构

首先定义我们的顶点数据格式。在 engine/src/math/math_types.h 中:

/**
 * @brief 3D顶点结构
 *
 * 定义了顶点的属性布局,必须与着色器输入匹配
 */
typedef struct vertex_3d {
    /// 顶点位置(模型空间)
    vec3 position;

    // 未来扩展:
    // vec3 normal;      // 法线
    // vec2 texcoord;    // 纹理坐标
    // vec4 color;       // 顶点颜色
} vertex_3d;

💡 设计说明

  • 当前仅包含位置,保持简单
  • 结构体内存布局必须与着色器 layout(location = X) 对应
  • 未来会扩展法线、纹理坐标等

步骤2:顶点输入状态

创建 engine/src/renderer/vulkan/vulkan_pipeline.h

#pragma once

#include "vulkan_types.inl"

/**
 * @brief 创建Vulkan图形管线
 *
 * @param context Vulkan上下文
 * @param renderpass 渲染通道
 * @param attribute_count 顶点属性数量
 * @param attributes 顶点属性描述数组
 * @param descriptor_set_layout_count 描述符集布局数量
 * @param descriptor_set_layouts 描述符集布局数组
 * @param stage_count 着色器阶段数量
 * @param stages 着色器阶段数组
 * @param viewport 视口
 * @param scissor 裁剪矩形
 * @param is_wireframe 是否线框模式
 * @param out_pipeline 输出管线对象
 * @return 成功返回true
 */
b8 vulkan_graphics_pipeline_create(
    vulkan_context* context,
    vulkan_renderpass* renderpass,
    u32 attribute_count,
    VkVertexInputAttributeDescription* attributes,
    u32 descriptor_set_layout_count,
    VkDescriptorSetLayout* descriptor_set_layouts,
    u32 stage_count,
    VkPipelineShaderStageCreateInfo* stages,
    VkViewport viewport,
    VkRect2D scissor,
    b8 is_wireframe,
    vulkan_pipeline* out_pipeline
);

/**
 * @brief 销毁Vulkan管线
 */
void vulkan_pipeline_destroy(vulkan_context* context, vulkan_pipeline* pipeline);

/**
 * @brief 绑定管线到命令缓冲
 */
void vulkan_pipeline_bind(
    vulkan_command_buffer* command_buffer,
    VkPipelineBindPoint bind_point,
    vulkan_pipeline* pipeline
);

创建 engine/src/renderer/vulkan/vulkan_pipeline.c,开始实现:

#include "vulkan_pipeline.h"
#include "vulkan_utils.h"

#include "core/kmemory.h"
#include "core/logger.h"
#include "math/math_types.h"

b8 vulkan_graphics_pipeline_create(
    vulkan_context* context,
    vulkan_renderpass* renderpass,
    u32 attribute_count,
    VkVertexInputAttributeDescription* attributes,
    u32 descriptor_set_layout_count,
    VkDescriptorSetLayout* descriptor_set_layouts,
    u32 stage_count,
    VkPipelineShaderStageCreateInfo* stages,
    VkViewport viewport,
    VkRect2D scissor,
    b8 is_wireframe,
    vulkan_pipeline* out_pipeline) {

    // ========================================
    // 1. 顶点输入绑定描述
    // ========================================
    VkVertexInputBindingDescription binding_description;
    binding_description.binding = 0;  // 绑定点索引
    binding_description.stride = sizeof(vertex_3d);  // 顶点大小
    binding_description.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;  // 每顶点数据

    // ========================================
    // 2. 顶点输入状态
    // ========================================
    VkPipelineVertexInputStateCreateInfo vertex_input_info = {
        VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
    };
    vertex_input_info.vertexBindingDescriptionCount = 1;
    vertex_input_info.pVertexBindingDescriptions = &binding_description;
    vertex_input_info.vertexAttributeDescriptionCount = attribute_count;
    vertex_input_info.pVertexAttributeDescriptions = attributes;

🔍 顶点输入解析

  • 绑定描述 - 告诉Vulkan如何从缓冲区读取数据
  • stride - 每个顶点的字节数
  • inputRate - VERTEX表示每顶点,INSTANCE表示每实例(实例化渲染)
  • 属性描述 - 描述每个属性的格式和偏移量(稍后设置)

步骤3:输入装配状态

继续在 vulkan_pipeline.c 中:

    // ========================================
    // 3. 输入装配状态 - 如何组装顶点为图元
    // ========================================
    VkPipelineInputAssemblyStateCreateInfo input_assembly = {
        VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
    };
    input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;  // 三角形列表
    input_assembly.primitiveRestartEnable = VK_FALSE;  // 不使用图元重启
图元拓扑类型
拓扑类型说明用途
POINT_LIST点列表粒子系统
LINE_LIST线段列表线框、调试
LINE_STRIP连续线段路径、轨迹
TRIANGLE_LIST三角形列表最常用,3D模型
TRIANGLE_STRIP三角形带优化顶点数量
TRIANGLE_FAN三角形扇圆形、扇形
TRIANGLE_LIST (本教程使用):
  V0----V1    V3----V4
   \    /      \    /
    \  /        \  /
     V2          V5

每3个顶点 = 1个三角形

步骤4:视口与裁剪

    // ========================================
    // 4. 视口状态 - 定义渲染区域
    // ========================================
    VkPipelineViewportStateCreateInfo viewport_state = {
        VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO
    };
    viewport_state.viewportCount = 1;
    viewport_state.pViewports = &viewport;  // 从参数传入
    viewport_state.scissorCount = 1;
    viewport_state.pScissors = &scissor;    // 从参数传入
视口 vs 裁剪矩形
┌─────────────────────────────────────────────┐
│  窗口 (1280x720)                             │
│                                             │
│  ┌───────────────────────────────┐         │
│  │ 视口 (Viewport)                │         │
│  │ - 定义NDC到屏幕坐标的映射       │         │
│  │ - 可以翻转Y轴                  │         │
│  │                                │         │
│  │   ┌─────────────────┐          │         │
│  │   │ 裁剪 (Scissor)  │          │         │
│  │   │ - 定义实际渲染区域│         │         │
│  │   │ - 矩形裁剪       │          │         │
│  │   └─────────────────┘          │         │
│  └───────────────────────────────┘         │
└─────────────────────────────────────────────┘

💡 Y轴翻转

viewport.y = (f32)context->framebuffer_height;  // 从底部开始
viewport.height = -(f32)context->framebuffer_height;  // 负高度翻转

这使得OpenGL风格的坐标系(左下角原点)转换为Vulkan风格(左上角原点)。


步骤5:光栅化状态

    // ========================================
    // 5. 光栅化状态 - 控制三角形如何转换为片段
    // ========================================
    VkPipelineRasterizationStateCreateInfo rasterizer_create_info = {
        VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO
    };
    rasterizer_create_info.depthClampEnable = VK_FALSE;  // 不裁剪深度(超出near/far的片段)
    rasterizer_create_info.rasterizerDiscardEnable = VK_FALSE;  // 不丢弃光栅化(如果true,不产生片段)

    // 多边形模式
    rasterizer_create_info.polygonMode = is_wireframe
        ? VK_POLYGON_MODE_LINE   // 线框模式
        : VK_POLYGON_MODE_FILL;  // 填充模式

    rasterizer_create_info.lineWidth = 1.0f;  // 线宽(像素)

    // 面剔除
    rasterizer_create_info.cullMode = VK_CULL_MODE_BACK_BIT;  // 背面剔除
    rasterizer_create_info.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;  // 逆时针为正面

    // 深度偏移(阴影映射常用)
    rasterizer_create_info.depthBiasEnable = VK_FALSE;
    rasterizer_create_info.depthBiasConstantFactor = 0.0f;
    rasterizer_create_info.depthBiasClamp = 0.0f;
    rasterizer_create_info.depthBiasSlopeFactor = 0.0f;
面剔除原理
       正面(逆时针)              背面(顺时针)
           V0                         V0
          ╱ ╲                        ╱ ╲
         ╱   ╲                      ╱   ╲
        V1───→V2                   V2←───V1

    ✅ 渲染(朝向相机)          ❌ 剔除(背向相机)
剔除模式说明用途
NONE不剔除双面材质(布料、叶子)
FRONT_BIT剔除正面阴影体内部渲染
BACK_BIT剔除背面标准3D模型(节省50%片段)
FRONT_AND_BACK全部剔除深度预渲染

步骤6:多重采样

    // ========================================
    // 6. 多重采样(抗锯齿)
    // ========================================
    VkPipelineMultisampleStateCreateInfo multisampling_create_info = {
        VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
    };
    multisampling_create_info.sampleShadingEnable = VK_FALSE;  // 关闭采样着色
    multisampling_create_info.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;  // 无MSAA
    multisampling_create_info.minSampleShading = 1.0f;
    multisampling_create_info.pSampleMask = 0;
    multisampling_create_info.alphaToCoverageEnable = VK_FALSE;
    multisampling_create_info.alphaToOneEnable = VK_FALSE;

📝 MSAA等级

  • VK_SAMPLE_COUNT_1_BIT - 无抗锯齿(本教程)
  • VK_SAMPLE_COUNT_2_BIT - 2x MSAA
  • VK_SAMPLE_COUNT_4_BIT - 4x MSAA(常用)
  • VK_SAMPLE_COUNT_8_BIT - 8x MSAA(高质量)

步骤7:深度与模板测试

    // ========================================
    // 7. 深度与模板测试
    // ========================================
    VkPipelineDepthStencilStateCreateInfo depth_stencil = {
        VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
    };
    depth_stencil.depthTestEnable = VK_TRUE;   // 启用深度测试
    depth_stencil.depthWriteEnable = VK_TRUE;  // 写入深度缓冲
    depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS;  // 深度小于则通过
    depth_stencil.depthBoundsTestEnable = VK_FALSE;  // 不使用深度范围测试
    depth_stencil.stencilTestEnable = VK_FALSE;  // 不使用模板测试
深度比较操作
比较操作通过条件用途
NEVER永不通过禁用深度测试
LESS新深度 < 旧深度标准深度测试
EQUAL新深度 == 旧深度深度相等检测
LESS_OR_EQUAL新深度 <= 旧深度允许共面
GREATER新深度 > 旧深度反向深度缓冲
ALWAYS总是通过覆盖深度
深度测试原理:
  相机          近物体          远物体
   👁️  ────→   🔷(0.2)  ────→  🔶(0.8)

深度缓冲初始值: 1.0 (最远)
1. 渲染远物体 → 深度=0.8 < 1.0 → ✅通过,写入0.8
2. 渲染近物体 → 深度=0.2 < 0.8 → ✅通过,写入0.2
   最终看到近物体(正确遮挡)

步骤8:颜色混合

    // ========================================
    // 8. 颜色混合附件 - 单个附件的混合配置
    // ========================================
    VkPipelineColorBlendAttachmentState color_blend_attachment_state;
    kzero_memory(&color_blend_attachment_state, sizeof(VkPipelineColorBlendAttachmentState));

    color_blend_attachment_state.blendEnable = VK_TRUE;  // 启用混合

    // 颜色混合:最终颜色 = (源颜色 * 源因子) OP (目标颜色 * 目标因子)
    color_blend_attachment_state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;  // 源Alpha
    color_blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;  // 1 - 源Alpha
    color_blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD;  // 加法混合

    // Alpha混合(同样配置)
    color_blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
    color_blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
    color_blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD;

    // 颜色写入掩码(RGBA全部写入)
    color_blend_attachment_state.colorWriteMask =
        VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
        VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

    // ========================================
    // 9. 颜色混合全局状态
    // ========================================
    VkPipelineColorBlendStateCreateInfo color_blend_state_create_info = {
        VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
    };
    color_blend_state_create_info.logicOpEnable = VK_FALSE;  // 不使用逻辑操作
    color_blend_state_create_info.logicOp = VK_LOGIC_OP_COPY;
    color_blend_state_create_info.attachmentCount = 1;
    color_blend_state_create_info.pAttachments = &color_blend_attachment_state;
Alpha混合公式
标准Alpha混合(透明度):
  最终颜色 = 源颜色 × 源Alpha + 目标颜色 × (1 - 源Alpha)

示例:
  源颜色 = (1.0, 0.0, 0.0, 0.6)  // 60%透明的红色
  目标颜色 = (0.0, 0.0, 1.0, 1.0)  // 不透明的蓝色

  最终 = (1, 0, 0) × 0.6 + (0, 0, 1) × 0.4
       = (0.6, 0, 0) + (0, 0, 0.4)
       = (0.6, 0, 0.4)  // 紫红色
混合因子用途
ZERO0忽略此颜色
ONE1完全使用此颜色
SRC_ALPHA源Alpha标准透明度
ONE_MINUS_SRC_ALPHA1 - 源Alpha标准透明度
DST_COLOR目标颜色调制混合

步骤9:动态状态

    // ========================================
    // 10. 动态状态 - 可在运行时修改的状态
    // ========================================
    const u32 dynamic_state_count = 3;
    VkDynamicState dynamic_states[dynamic_state_count] = {
        VK_DYNAMIC_STATE_VIEWPORT,    // 视口可动态修改(窗口调整)
        VK_DYNAMIC_STATE_SCISSOR,     // 裁剪矩形可动态修改
        VK_DYNAMIC_STATE_LINE_WIDTH   // 线宽可动态修改
    };

    VkPipelineDynamicStateCreateInfo dynamic_state_create_info = {
        VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO
    };
    dynamic_state_create_info.dynamicStateCount = dynamic_state_count;
    dynamic_state_create_info.pDynamicStates = dynamic_states;

💡 动态状态的优势

  • 避免为每个窗口大小创建新管线
  • 使用 vkCmdSetViewport() / vkCmdSetScissor() 在命令缓冲中修改
  • 其他状态仍然是不可变的(需要新管线)

步骤10:管线布局

    // ========================================
    // 11. 管线布局 - 着色器资源绑定
    // ========================================
    VkPipelineLayoutCreateInfo pipeline_layout_create_info = {
        VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
    };

    // 描述符集布局(UBO、纹理采样器等)
    pipeline_layout_create_info.setLayoutCount = descriptor_set_layout_count;
    pipeline_layout_create_info.pSetLayouts = descriptor_set_layouts;

    // TODO: 推送常量(未来教程)
    pipeline_layout_create_info.pushConstantRangeCount = 0;
    pipeline_layout_create_info.pPushConstantRanges = 0;

    // 创建管线布局
    VK_CHECK(vkCreatePipelineLayout(
        context->device.logical_device,
        &pipeline_layout_create_info,
        context->allocator,
        &out_pipeline->pipeline_layout
    ));

📝 管线布局

  • 定义着色器可以访问的资源(UBO、SSBO、纹理等)
  • 必须在创建管线前创建
  • 多个管线可以共享同一个布局

步骤11:创建图形管线

    // ========================================
    // 12. 图形管线创建信息 - 汇总所有配置
    // ========================================
    VkGraphicsPipelineCreateInfo pipeline_create_info = {
        VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
    };

    // 着色器阶段
    pipeline_create_info.stageCount = stage_count;
    pipeline_create_info.pStages = stages;

    // 固定功能阶段
    pipeline_create_info.pVertexInputState = &vertex_input_info;
    pipeline_create_info.pInputAssemblyState = &input_assembly;
    pipeline_create_info.pViewportState = &viewport_state;
    pipeline_create_info.pRasterizationState = &rasterizer_create_info;
    pipeline_create_info.pMultisampleState = &multisampling_create_info;
    pipeline_create_info.pDepthStencilState = &depth_stencil;
    pipeline_create_info.pColorBlendState = &color_blend_state_create_info;
    pipeline_create_info.pDynamicState = &dynamic_state_create_info;
    pipeline_create_info.pTessellationState = 0;  // 无曲面细分

    // 管线布局
    pipeline_create_info.layout = out_pipeline->pipeline_layout;

    // 渲染通道
    pipeline_create_info.renderPass = renderpass->handle;
    pipeline_create_info.subpass = 0;  // 子通道索引

    // 管线派生(优化创建)
    pipeline_create_info.basePipelineHandle = VK_NULL_HANDLE;
    pipeline_create_info.basePipelineIndex = -1;

    // ========================================
    // 13. 创建图形管线!
    // ========================================
    VkResult result = vkCreateGraphicsPipelines(
        context->device.logical_device,
        VK_NULL_HANDLE,  // 管线缓存(未来优化)
        1,  // 创建1个管线
        &pipeline_create_info,
        context->allocator,
        &out_pipeline->handle
    );

    if (vulkan_result_is_success(result)) {
        KDEBUG("Graphics pipeline created!");
        return true;
    }

    KERROR("vkCreateGraphicsPipelines failed with %s.",
           vulkan_result_string(result, true));
    return false;
}

销毁和绑定函数

/**
 * @brief 销毁图形管线
 */
void vulkan_pipeline_destroy(vulkan_context* context, vulkan_pipeline* pipeline) {
    if (pipeline) {
        // 销毁管线对象
        if (pipeline->handle) {
            vkDestroyPipeline(context->device.logical_device, pipeline->handle, context->allocator);
            pipeline->handle = 0;
        }

        // 销毁管线布局
        if (pipeline->pipeline_layout) {
            vkDestroyPipelineLayout(context->device.logical_device,
                                   pipeline->pipeline_layout,
                                   context->allocator);
            pipeline->pipeline_layout = 0;
        }
    }
}

/**
 * @brief 绑定管线到命令缓冲
 */
void vulkan_pipeline_bind(vulkan_command_buffer* command_buffer,
                         VkPipelineBindPoint bind_point,
                         vulkan_pipeline* pipeline) {
    vkCmdBindPipeline(command_buffer->handle, bind_point, pipeline->handle);
}

🎨 对象着色器系统

着色器抽象设计

我们将创建一个对象着色器系统,封装管线创建的复杂性。

engine/src/renderer/vulkan/vulkan_types.inl 中添加:

#define OBJECT_SHADER_STAGE_COUNT 2  // 顶点 + 片段

typedef struct vulkan_object_shader {
    vulkan_shader_stage stages[OBJECT_SHADER_STAGE_COUNT];
    vulkan_pipeline pipeline;
} vulkan_object_shader;

创建对象着色器

创建 engine/src/renderer/vulkan/shaders/vulkan_object_shader.h

#pragma once

#include "../vulkan_types.inl"

/**
 * @brief 创建对象着色器
 */
b8 vulkan_object_shader_create(vulkan_context* context, vulkan_object_shader* out_shader);

/**
 * @brief 销毁对象着色器
 */
void vulkan_object_shader_destroy(vulkan_context* context, vulkan_object_shader* shader);

/**
 * @brief 使用对象着色器(绑定管线)
 */
void vulkan_object_shader_use(vulkan_context* context, vulkan_object_shader* shader);

创建 engine/src/renderer/vulkan/shaders/vulkan_object_shader.c

#include "vulkan_object_shader.h"

#include "core/logger.h"
#include "core/kmemory.h"
#include "math/math_types.h"

#include "renderer/vulkan/vulkan_shader_utils.h"
#include "renderer/vulkan/vulkan_pipeline.h"

#define BUILTIN_SHADER_NAME_OBJECT "Builtin.ObjectShader"

b8 vulkan_object_shader_create(vulkan_context* context, vulkan_object_shader* out_shader) {

    // ========================================
    // 1. 加载着色器模块
    // ========================================
    char stage_type_strs[OBJECT_SHADER_STAGE_COUNT][5] = {"vert", "frag"};
    VkShaderStageFlagBits stage_types[OBJECT_SHADER_STAGE_COUNT] = {
        VK_SHADER_STAGE_VERTEX_BIT,
        VK_SHADER_STAGE_FRAGMENT_BIT
    };

    for (u32 i = 0; i < OBJECT_SHADER_STAGE_COUNT; ++i) {
        if (!create_shader_module(
                context,
                BUILTIN_SHADER_NAME_OBJECT,
                stage_type_strs[i],
                stage_types[i],
                i,
                out_shader->stages)) {
            KERROR("Unable to create %s shader module for '%s'.",
                   stage_type_strs[i],
                   BUILTIN_SHADER_NAME_OBJECT);
            return false;
        }
    }

    // ========================================
    // 2. 配置视口
    // ========================================
    VkViewport viewport;
    viewport.x = 0.0f;
    viewport.y = (f32)context->framebuffer_height;  // 从底部开始
    viewport.width = (f32)context->framebuffer_width;
    viewport.height = -(f32)context->framebuffer_height;  // 负高度翻转Y轴
    viewport.minDepth = 0.0f;
    viewport.maxDepth = 1.0f;

    // ========================================
    // 3. 配置裁剪矩形
    // ========================================
    VkRect2D scissor;
    scissor.offset.x = scissor.offset.y = 0;
    scissor.extent.width = context->framebuffer_width;
    scissor.extent.height = context->framebuffer_height;

    // ========================================
    // 4. 定义顶点属性
    // ========================================
    const i32 attribute_count = 1;
    VkVertexInputAttributeDescription attribute_descriptions[attribute_count];

    // 属性格式和大小
    VkFormat formats[attribute_count] = {
        VK_FORMAT_R32G32B32_SFLOAT  // vec3 位置
    };
    u64 sizes[attribute_count] = {
        sizeof(vec3)
    };

    // 配置每个属性
    u32 offset = 0;
    for (u32 i = 0; i < attribute_count; ++i) {
        attribute_descriptions[i].binding = 0;   // 绑定点索引
        attribute_descriptions[i].location = i;  // 对应 layout(location = i)
        attribute_descriptions[i].format = formats[i];
        attribute_descriptions[i].offset = offset;
        offset += sizes[i];
    }

    // ========================================
    // 5. 准备着色器阶段信息
    // ========================================
    VkPipelineShaderStageCreateInfo stage_create_infos[OBJECT_SHADER_STAGE_COUNT];
    kzero_memory(stage_create_infos, sizeof(stage_create_infos));
    for (u32 i = 0; i < OBJECT_SHADER_STAGE_COUNT; ++i) {
        stage_create_infos[i] = out_shader->stages[i].shader_stage_create_info;
    }

    // ========================================
    // 6. 创建图形管线
    // ========================================
    if (!vulkan_graphics_pipeline_create(
            context,
            &context->main_renderpass,
            attribute_count,
            attribute_descriptions,
            0,  // 暂无描述符集布局
            0,
            OBJECT_SHADER_STAGE_COUNT,
            stage_create_infos,
            viewport,
            scissor,
            false,  // 非线框模式
            &out_shader->pipeline)) {
        KERROR("Failed to load graphics pipeline for object shader.");
        return false;
    }

    return true;
}

void vulkan_object_shader_destroy(vulkan_context* context, vulkan_object_shader* shader) {
    // 销毁管线
    vulkan_pipeline_destroy(context, &shader->pipeline);

    // 销毁着色器模块
    for (u32 i = 0; i < OBJECT_SHADER_STAGE_COUNT; ++i) {
        vkDestroyShaderModule(
            context->device.logical_device,
            shader->stages[i].handle,
            context->allocator
        );
        shader->stages[i].handle = 0;
    }
}

void vulkan_object_shader_use(vulkan_context* context, vulkan_object_shader* shader) {
    // TODO: 在未来的教程中,这里会绑定描述符集等
}

💡 设计亮点

  • 封装了着色器加载和管线创建的所有细节
  • 简单的API:createusedestroy
  • 易于扩展更多着色器类型(天空盒、地形等)

🏗️ 架构图解

图形管线创建流程

开始
定义顶点结构
配置顶点输入
设置输入装配
配置视口/裁剪
光栅化设置
多重采样
深度/模板测试
颜色混合
动态状态
创建管线布局
加载着色器模块
创建VkPipeline
完成

管线状态依赖关系

┌────────────────────────────────────────────────────┐
│           VkGraphicsPipeline                       │
├────────────────────────────────────────────────────┤
│                                                    │
│  ┌──────────────────┐   ┌──────────────────┐     │
│  │  着色器阶段       │   │  管线布局         │     │
│  │  - 顶点着色器     │   │  - 描述符集       │     │
│  │  - 片段着色器     │   │  - 推送常量       │     │
│  └──────────────────┘   └──────────────────┘     │
│                                                    │
│  ┌──────────────────────────────────────────┐     │
│  │  固定功能状态                             │     │
│  │  ├─ 顶点输入                              │     │
│  │  ├─ 输入装配                              │     │
│  │  ├─ 视口/裁剪                             │     │
│  │  ├─ 光栅化                                │     │
│  │  ├─ 多重采样                              │     │
│  │  ├─ 深度/模板                             │     │
│  │  └─ 颜色混合                              │     │
│  └──────────────────────────────────────────┘     │
│                                                    │
│  ┌──────────────────┐                             │
│  │  渲染通道         │                             │
│  │  - 附件格式       │                             │
│  │  - 子通道索引     │                             │
│  └──────────────────┘                             │
│                                                    │
└────────────────────────────────────────────────────┘

💡 最佳实践

✅ 管线创建最佳实践

// ✅ 好的做法:在初始化时创建所有管线
void renderer_init() {
    create_object_pipeline();
    create_skybox_pipeline();
    create_terrain_pipeline();
    // 启动时完成验证和编译
}

// ✅ 好的做法:复用管线布局
VkPipelineLayout shared_layout = create_common_layout();
create_pipeline_a(shared_layout);
create_pipeline_b(shared_layout);

// ✅ 好的做法:使用管线缓存
VkPipelineCache cache = load_pipeline_cache();
vkCreateGraphicsPipelines(device, cache, ...);
save_pipeline_cache(cache);  // 下次启动更快
// ❌ 坏的做法:运行时动态创建管线
void render() {
    if (wireframe_changed) {
        destroy_pipeline();
        create_pipeline();  // 卡顿!
    }
}

// ❌ 坏的做法:忘记销毁
void shutdown() {
    // 忘记销毁管线 → 内存泄漏
}

✅ 性能优化建议

  1. 减少管线数量 - 使用动态状态和推送常量替代新管线
  2. 管线派生 - 使用 basePipelineHandle 加速创建
  3. 异步创建 - 在后台线程创建管线
  4. 缓存序列化 - 保存管线缓存到磁盘

❓ 常见问题

Q1: 为什么Vulkan管线如此复杂?

:这是Vulkan的设计哲学 - 显式优于隐式

OpenGL的"简单"是假象:

  • 驱动会在运行时做大量猜测和优化
  • 导致不可预测的性能和行为差异
  • 开发者无法精确控制GPU状态

Vulkan的"复杂"带来:

  • 完全的控制权 - 你决定每个细节
  • 可预测的性能 - 无隐藏开销
  • 最佳优化机会 - 驱动只做你要求的事

权衡:前期投入更多,但长期收益巨大。

Q2: 什么时候需要创建新管线?

需要新管线的情况

  • ✅ 不同的着色器组合(对象 vs 天空盒)
  • ✅ 不同的混合模式(不透明 vs 透明)
  • ✅ 不同的面剔除(双面 vs 单面)
  • ✅ 不同的深度测试(深度写入 vs 不写入)
  • ✅ 不同的拓扑(三角形 vs 线框)

可以复用管线的情况(使用动态状态或推送常量):

  • ❌ 不同的MVP矩阵 → 推送常量
  • ❌ 不同的材质参数 → 描述符集
  • ❌ 不同的视口大小 → 动态状态
Q3: 为什么视口高度是负数?

:这是为了翻转Y轴坐标系统。

// Vulkan默认:左上角 (0, 0)
//     0 ────→ X
//     │
//     ↓ Y

// OpenGL/大多数引擎:左下角 (0, 0)
//     ↑ Y
//     │
//     0 ────→ X

// 负高度翻转Y轴,使NDC空间与OpenGL一致
viewport.y = framebuffer_height;  // 从底部开始
viewport.height = -framebuffer_height;  // 向上延伸

这避免了在着色器中手动翻转 gl_Position.y

Q4: 如何调试管线创建失败?

步骤

  1. 启用验证层 - 参见教程10

    // 验证层会提供详细的错误信息
    
  2. 检查返回值

    VkResult result = vkCreateGraphicsPipelines(...);
    KERROR("Pipeline creation failed: %s", vulkan_result_string(result, true));
    
  3. 分阶段验证 - 单独测试每个结构体

    // 先测试只有顶点着色器
    // 再添加片段着色器
    // 逐步添加状态配置
    
  4. 使用RenderDoc - 图形调试器可以查看完整的管线状态

Q5: 顶点属性描述必须精确匹配着色器吗?

:是的!布局必须完全匹配。

着色器

layout(location = 0) in vec3 in_position;   // offset 0, size 12
layout(location = 1) in vec3 in_normal;     // offset 12, size 12
layout(location = 2) in vec2 in_texcoord;   // offset 24, size 8

C代码

typedef struct vertex {
    vec3 position;   // offset 0
    vec3 normal;     // offset 12
    vec2 texcoord;   // offset 24
} vertex;  // 总大小 32字节

attribute_descriptions[0].location = 0;
attribute_descriptions[0].offset = offsetof(vertex, position);
attribute_descriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
// ... 其他属性

不匹配会导致:

  • 验证层错误
  • 渲染结果错误(错位的数据)
  • 潜在的崩溃

🐛 故障排查

问题现象可能原因解决方法
管线创建失败着色器模块无效检查SPIR-V文件是否存在和有效
验证层错误:顶点属性不匹配C结构体与着色器不匹配使用 offsetof() 确保偏移量正确
渲染黑屏面剔除设置错误尝试 cullMode = NONE 测试
深度测试失败深度缓冲未清除确保渲染通道清除深度
透明度不工作混合未启用或配置错误检查 blendEnable 和混合因子
视口倒置Y轴翻转错误检查 viewport.height 是否为负
性能下降过多的管线切换减少管线数量,批量渲染

📝 练习题

基础练习

练习1:线框模式切换

修改代码,添加运行时切换线框模式的功能:

// 提示:需要创建两个管线(填充 + 线框)
vulkan_pipeline solid_pipeline;
vulkan_pipeline wireframe_pipeline;

void toggle_wireframe() {
    current_pipeline = use_wireframe ? &wireframe_pipeline : &solid_pipeline;
}
💡 参考答案
// 在对象着色器中添加两个管线
typedef struct vulkan_object_shader {
    vulkan_shader_stage stages[OBJECT_SHADER_STAGE_COUNT];
    vulkan_pipeline solid_pipeline;
    vulkan_pipeline wireframe_pipeline;
    b8 use_wireframe;
} vulkan_object_shader;

// 创建时初始化两个管线
b8 vulkan_object_shader_create(vulkan_context* context, vulkan_object_shader* out_shader) {
    // ... 加载着色器模块 ...

    // 创建填充模式管线
    vulkan_graphics_pipeline_create(..., false, &out_shader->solid_pipeline);

    // 创建线框模式管线
    vulkan_graphics_pipeline_create(..., true, &out_shader->wireframe_pipeline);

    out_shader->use_wireframe = false;
    return true;
}

// 使用时选择管线
void vulkan_object_shader_use(vulkan_context* context, vulkan_object_shader* shader) {
    vulkan_pipeline* active = shader->use_wireframe
        ? &shader->wireframe_pipeline
        : &shader->solid_pipeline;
    vulkan_pipeline_bind(&context->graphics_command_buffers[...],
                        VK_PIPELINE_BIND_POINT_GRAPHICS,
                        active);
}

练习2:添加顶点颜色属性

扩展 vertex_3d 结构,添加颜色属性并修改着色器:

💡 参考答案

C代码

typedef struct vertex_3d {
    vec3 position;
    vec4 color;  // 新增
} vertex_3d;

// 更新属性描述
const i32 attribute_count = 2;
VkFormat formats[attribute_count] = {
    VK_FORMAT_R32G32B32_SFLOAT,    // position
    VK_FORMAT_R32G32B32A32_SFLOAT  // color
};
u64 sizes[attribute_count] = {
    sizeof(vec3),
    sizeof(vec4)
};

顶点着色器

#version 450

layout(location = 0) in vec3 in_position;
layout(location = 1) in vec4 in_color;

layout(location = 0) out vec4 frag_color;

void main() {
    gl_Position = vec4(in_position, 1.0);
    frag_color = in_color;  // 传递给片段着色器
}

片段着色器

#version 450

layout(location = 0) in vec4 frag_color;
layout(location = 0) out vec4 out_colour;

void main() {
    out_colour = frag_color;  // 使用顶点颜色
}

进阶挑战

挑战1:管线缓存系统

实现管线缓存的序列化和反序列化:

b8 save_pipeline_cache(const char* path, VkPipelineCache cache);
b8 load_pipeline_cache(const char* path, VkPipelineCache* out_cache);
💡 参考答案
#include "platform/filesystem.h"

b8 save_pipeline_cache(const char* path, VkDevice device, VkPipelineCache cache) {
    // 获取缓存大小
    size_t cache_size;
    vkGetPipelineCacheData(device, cache, &cache_size, NULL);

    // 读取缓存数据
    void* cache_data = kallocate(cache_size, MEMORY_TAG_RENDERER);
    vkGetPipelineCacheData(device, cache, &cache_size, cache_data);

    // 写入文件
    file_handle handle;
    if (!filesystem_open(path, FILE_MODE_WRITE, true, &handle)) {
        kfree(cache_data, cache_size, MEMORY_TAG_RENDERER);
        return false;
    }

    u64 bytes_written;
    filesystem_write(&handle, cache_size, cache_data, &bytes_written);
    filesystem_close(&handle);

    kfree(cache_data, cache_size, MEMORY_TAG_RENDERER);
    return bytes_written == cache_size;
}

b8 load_pipeline_cache(const char* path, VkDevice device, VkPipelineCache* out_cache) {
    file_handle handle;
    if (!filesystem_open(path, FILE_MODE_READ, true, &handle)) {
        // 文件不存在,创建空缓存
        VkPipelineCacheCreateInfo cache_info = {VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
        VK_CHECK(vkCreatePipelineCache(device, &cache_info, NULL, out_cache));
        return true;
    }

    // 读取缓存数据
    u8* cache_data;
    u64 cache_size;
    filesystem_read_all_bytes(&handle, &cache_data, &cache_size);
    filesystem_close(&handle);

    // 创建缓存
    VkPipelineCacheCreateInfo cache_info = {VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
    cache_info.initialDataSize = cache_size;
    cache_info.pInitialData = cache_data;

    VkResult result = vkCreatePipelineCache(device, &cache_info, NULL, out_cache);

    kfree(cache_data, cache_size, MEMORY_TAG_STRING);
    return result == VK_SUCCESS;
}

使用

// 加载缓存
VkPipelineCache cache;
load_pipeline_cache("pipeline.cache", device, &cache);

// 创建管线时使用缓存
vkCreateGraphicsPipelines(device, cache, 1, &create_info, NULL, &pipeline);

// 关闭时保存缓存
save_pipeline_cache("pipeline.cache", device, cache);
vkDestroyPipelineCache(device, cache, NULL);

挑战2:多种混合模式

实现多种混合模式的管线:

  • 不透明
  • Alpha混合
  • 加法混合
  • 乘法混合
💡 参考答案
typedef enum blend_mode {
    BLEND_MODE_OPAQUE,
    BLEND_MODE_ALPHA,
    BLEND_MODE_ADDITIVE,
    BLEND_MODE_MULTIPLICATIVE
} blend_mode;

VkPipelineColorBlendAttachmentState get_blend_state(blend_mode mode) {
    VkPipelineColorBlendAttachmentState state = {0};
    state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
                          VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

    switch (mode) {
        case BLEND_MODE_OPAQUE:
            state.blendEnable = VK_FALSE;
            break;

        case BLEND_MODE_ALPHA:
            // 标准Alpha混合:Src * SrcAlpha + Dst * (1 - SrcAlpha)
            state.blendEnable = VK_TRUE;
            state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
            state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
            state.colorBlendOp = VK_BLEND_OP_ADD;
            state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
            state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
            state.alphaBlendOp = VK_BLEND_OP_ADD;
            break;

        case BLEND_MODE_ADDITIVE:
            // 加法混合:Src + Dst(光效、火焰)
            state.blendEnable = VK_TRUE;
            state.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
            state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
            state.colorBlendOp = VK_BLEND_OP_ADD;
            state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
            state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
            state.alphaBlendOp = VK_BLEND_OP_ADD;
            break;

        case BLEND_MODE_MULTIPLICATIVE:
            // 乘法混合:Src * Dst(阴影、遮罩)
            state.blendEnable = VK_TRUE;
            state.srcColorBlendFactor = VK_BLEND_FACTOR_DST_COLOR;
            state.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
            state.colorBlendOp = VK_BLEND_OP_ADD;
            state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
            state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
            state.alphaBlendOp = VK_BLEND_OP_ADD;
            break;
    }

    return state;
}

📚 参考资料

🔗 官方文档

📖 深入学习

🛠️ 工具

📂 Kohi引擎资源


🎯 本章总结

✅ 你已经掌握了

┌─────────────────────────────────────────────────────────┐
│  ✓ Vulkan图形管线的完整概念和工作流程                     │
│  ✓ 顶点输入和属性描述的配置方法                          │
│  ✓ 固定功能阶段的各项配置(光栅化、深度、混合等)          │
│  ✓ 动态状态的使用和优势                                  │
│  ✓ 管线布局的创建和作用                                  │
│  ✓ 完整的图形管线创建流程                                │
│  ✓ 对象着色器系统的封装设计                              │
└─────────────────────────────────────────────────────────┘

🎯 核心要点

  1. 管线是不可变的 - 创建后无法修改(除动态状态),需要切换管线
  2. 显式配置所有状态 - Vulkan不会做任何假设,必须明确指定
  3. 前期验证 - 所有错误在创建时发现,运行时零开销
  4. 性能关键 - 管线设计直接影响渲染性能
  5. 可缓存和复用 - 管线可以序列化并在下次启动时快速加载

🚀 下一步学习

教程主题依赖
教程 22顶点缓冲区和索引缓冲区本教程
教程 23渲染第一个三角形教程 22
教程 24描述符集和UBO教程 23

💬 反馈与支持


🎉 恭喜你完成了Vulkan图形管线的学习!

现在你已经掌握了现代图形API最核心的概念之一。

下一步


📖 关注公众号

在这里插入图片描述

关注我,领取章节视频教程


💖 支持作者

如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕

您的支持是我持续创作的动力!

感谢每一位支持者!🙏


📅 最后更新:2025-11-21
✍️ 作者:上手实验室

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值