上一篇:教程 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
| 特性 | OpenGL | Vulkan |
|---|---|---|
| 管线创建 | 运行时动态创建 | 初始化时预先创建 |
| 状态管理 | 全局状态机 | 独立的管线对象 |
| 性能 | 驱动开销大 | 接近零开销 |
| 验证 | 运行时验证 | 创建时验证 |
| 切换成本 | 较高 | 极低 |
╔════════════════════════════════════════════════════════╗
║ 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 MSAAVK_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) // 紫红色
| 混合因子 | 值 | 用途 |
|---|---|---|
ZERO | 0 | 忽略此颜色 |
ONE | 1 | 完全使用此颜色 |
SRC_ALPHA | 源Alpha | 标准透明度 |
ONE_MINUS_SRC_ALPHA | 1 - 源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:
create→use→destroy- 易于扩展更多着色器类型(天空盒、地形等)
🏗️ 架构图解
图形管线创建流程
管线状态依赖关系
┌────────────────────────────────────────────────────┐
│ 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() {
// 忘记销毁管线 → 内存泄漏
}
✅ 性能优化建议
- 减少管线数量 - 使用动态状态和推送常量替代新管线
- 管线派生 - 使用
basePipelineHandle加速创建 - 异步创建 - 在后台线程创建管线
- 缓存序列化 - 保存管线缓存到磁盘
❓ 常见问题
Q1: 为什么Vulkan管线如此复杂?答:这是Vulkan的设计哲学 - 显式优于隐式。
OpenGL的"简单"是假象:
- 驱动会在运行时做大量猜测和优化
- 导致不可预测的性能和行为差异
- 开发者无法精确控制GPU状态
Vulkan的"复杂"带来:
- 完全的控制权 - 你决定每个细节
- 可预测的性能 - 无隐藏开销
- 最佳优化机会 - 驱动只做你要求的事
权衡:前期投入更多,但长期收益巨大。
Q2: 什么时候需要创建新管线?需要新管线的情况:
- ✅ 不同的着色器组合(对象 vs 天空盒)
- ✅ 不同的混合模式(不透明 vs 透明)
- ✅ 不同的面剔除(双面 vs 单面)
- ✅ 不同的深度测试(深度写入 vs 不写入)
- ✅ 不同的拓扑(三角形 vs 线框)
可以复用管线的情况(使用动态状态或推送常量):
- ❌ 不同的MVP矩阵 → 推送常量
- ❌ 不同的材质参数 → 描述符集
- ❌ 不同的视口大小 → 动态状态
答:这是为了翻转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。
步骤:
-
启用验证层 - 参见教程10
// 验证层会提供详细的错误信息 -
检查返回值
VkResult result = vkCreateGraphicsPipelines(...); KERROR("Pipeline creation failed: %s", vulkan_result_string(result, true)); -
分阶段验证 - 单独测试每个结构体
// 先测试只有顶点着色器 // 再添加片段着色器 // 逐步添加状态配置 -
使用RenderDoc - 图形调试器可以查看完整的管线状态
答:是的!布局必须完全匹配。
着色器:
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;
}
📚 参考资料
🔗 官方文档
- Vulkan规范 - https://www.khronos.org/registry/vulkan/specs/1.3/html/
- VkGraphicsPipelineCreateInfo - https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkGraphicsPipelineCreateInfo.html
- 管线状态对象 - https://www.khronos.org/opengl/wiki/Pipeline_State_Object
📖 深入学习
- Vulkan Tutorial - Graphics Pipeline - https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics
- Sascha Willems Examples - https://github.com/SaschaWillems/Vulkan
- GPU Gems: Pipeline Optimization - https://developer.nvidia.com/gpugems/
🛠️ 工具
- RenderDoc - Vulkan图形调试器 https://renderdoc.org/
- NVIDIA Nsight - 性能分析 https://developer.nvidia.com/nsight-graphics
- AMD GPU Profiler - https://gpuopen.com/rgp/
📂 Kohi引擎资源
- 源代码 - https://github.com/travisvroman/kohi
- Commit 6af7037 - 查看本教程对应的提交
🎯 本章总结
✅ 你已经掌握了
┌─────────────────────────────────────────────────────────┐
│ ✓ Vulkan图形管线的完整概念和工作流程 │
│ ✓ 顶点输入和属性描述的配置方法 │
│ ✓ 固定功能阶段的各项配置(光栅化、深度、混合等) │
│ ✓ 动态状态的使用和优势 │
│ ✓ 管线布局的创建和作用 │
│ ✓ 完整的图形管线创建流程 │
│ ✓ 对象着色器系统的封装设计 │
└─────────────────────────────────────────────────────────┘
🎯 核心要点
- 管线是不可变的 - 创建后无法修改(除动态状态),需要切换管线
- 显式配置所有状态 - Vulkan不会做任何假设,必须明确指定
- 前期验证 - 所有错误在创建时发现,运行时零开销
- 性能关键 - 管线设计直接影响渲染性能
- 可缓存和复用 - 管线可以序列化并在下次启动时快速加载
🚀 下一步学习
| 教程 | 主题 | 依赖 |
|---|---|---|
| 教程 22 | 顶点缓冲区和索引缓冲区 | 本教程 |
| 教程 23 | 渲染第一个三角形 | 教程 22 |
| 教程 24 | 描述符集和UBO | 教程 23 |
💬 反馈与支持
🎉 恭喜你完成了Vulkan图形管线的学习!
现在你已经掌握了现代图形API最核心的概念之一。
下一步:
📖 关注公众号

关注我,领取章节视频教程
💖 支持作者
如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕
您的支持是我持续创作的动力!
感谢每一位支持者!🙏
📅 最后更新:2025-11-21
✍️ 作者:上手实验室
415

被折叠的 条评论
为什么被折叠?



