一、引言:Shader 不只是"画图代码"
在 Vulkan 渲染系统中,Shader 是 GPU 可编程能力的核心入口。我们经常看到如下几类文件后缀:
shader.vert
shader.frag
shader.mesh
shader.comp
它们分别代表:
.vert :Vertex Shader,顶点着色器
.frag :Fragment Shader,片元着色器
.mesh :Mesh Shader,网格着色器
.comp :Compute Shader,计算着色器
这几个 Shader 并不是简单的"不同文件类型",而是 Vulkan 中不同 pipeline stage 的程序入口。它们处在不同的管线位置,处理不同粒度的数据,并承担完全不同的渲染或计算任务。
如果从现代实时渲染架构来看,可以这样理解:
vert 负责传统几何入口:处理顶点
mesh 负责现代几何生成:处理 meshlet / primitive group
frag 负责表面着色:处理片元
comp 负责通用计算:处理 workgroup
其中,vert、mesh、frag 属于图形渲染链路中的不同阶段;comp 则通常属于独立的计算管线。它们之间可以直接连接,也可以通过 buffer、image、descriptor、pipeline barrier 等机制间接协作。
本文将从 Vulkan 图形管线、计算管线、Shader 数据流、工程实践和现代渲染架构几个层面,系统介绍 .vert、.frag、.mesh、.comp 的作用、关系、特点与使用场景。

二、先理解 Vulkan 中 Shader 的基本地位
在 Vulkan 中,Shader 通常不是直接以 GLSL/HLSL/Slang 源码形式交给驱动执行,而是先编译为 SPIR-V 中间表示。
典型流程如下:
GLSL / HLSL / Slang
↓
编译为 SPIR-V
↓
创建 VkShaderModule
↓
指定 VkPipelineShaderStageCreateInfo
↓
创建 Graphics Pipeline 或 Compute Pipeline
↓
在 Command Buffer 中绑定并执行
例如:
glslangValidator -V shader.vert -o vert.spv
glslangValidator -V shader.frag -o frag.spv
glslangValidator -V shader.comp -o comp.spv
在 Vulkan 代码中,不同 Shader 阶段需要通过不同的 stage flag 指定:
VK_SHADER_STAGE_VERTEX_BIT
VK_SHADER_STAGE_FRAGMENT_BIT
VK_SHADER_STAGE_MESH_BIT_EXT
VK_SHADER_STAGE_COMPUTE_BIT
也就是说,Vulkan 不会因为文件名叫 .vert 就自动把它当成 Vertex Shader。真正决定它属于哪个阶段的,是 SPIR-V 的执行模型以及 Vulkan pipeline 创建时指定的 shader stage。
文件后缀只是工程约定,管线阶段才是本质。
三、四类 Shader 的总体关系
从高层看,四类 Shader 可以分为两条路线。
第一条是传统图形管线:
Vertex Buffer / Index Buffer
↓
Input Assembly
↓
Vertex Shader (.vert)
↓
可选:Tessellation Shader
↓
可选:Geometry Shader
↓
Rasterization
↓
Fragment Shader (.frag)
↓
Color / Depth / Stencil Output
第二条是 Mesh Shader 图形管线:
可选:Task Shader
↓
Mesh Shader (.mesh)
↓
Rasterization
↓
Fragment Shader (.frag)
↓
Color / Depth / Stencil Output
第三条是计算管线:
Compute Shader (.comp)
↓
Buffer / Image / Storage Resource
↓
可供 Graphics Pipeline 或另一个 Compute Pipeline 使用
因此,.vert 和 .mesh 在某种意义上都属于"光栅化之前的几何阶段";.frag 属于"光栅化之后的着色阶段";.comp 则不依赖固定图形管线,可以独立完成通用 GPU 计算任务。
第一部分:Vertex Shader .vert
四、Vertex Shader 的核心作用
Vertex Shader 是传统图形管线中最基础、最常见的可编程阶段。它的执行对象是"顶点"。
每个输入顶点通常都会触发一次 Vertex Shader 执行。
Vertex Shader 的核心职责包括:
1. 读取顶点输入
2. 执行坐标变换
3. 输出 gl_Position
4. 向后续阶段传递顶点属性
5. 支持实例化、骨骼动画、顶点动画等顶点级计算
在 Vulkan 中,Vertex Shader 通常接收来自 Vertex Buffer 的数据,例如:
位置 position
法线 normal
切线 tangent
纹理坐标 uv
顶点颜色 color
骨骼索引 bone indices
骨骼权重 bone weights
这些数据通过 layout(location = n) 与 Vulkan 中的 vertex input attribute description 对应。
五、Vertex Shader 解决的是"几何在哪里"的问题
一个模型的顶点最初通常处于模型空间:
Model Space
它只知道自己相对于模型原点的位置,并不知道模型在世界中的位置,也不知道摄像机在哪里。
Vertex Shader 通常负责将顶点从模型空间变换到裁剪空间:
Model Space
↓ Model Matrix
World Space
↓ View Matrix
View Space
↓ Projection Matrix
Clip Space
典型 GLSL Vertex Shader:
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV;
layout(set = 0, binding = 0) uniform CameraUBO {
mat4 model;
mat4 view;
mat4 projection;
} camera;
layout(location = 0) out vec3 fragWorldPos;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec2 fragUV;
void main() {
vec4 worldPos = camera.model * vec4(inPosition, 1.0);
fragWorldPos = worldPos.xyz;
fragNormal = mat3(camera.model) * inNormal;
fragUV = inUV;
gl_Position = camera.projection * camera.view * worldPos;
}
其中最关键的是:
gl_Position = camera.projection * camera.view * worldPos;
gl_Position 是 Vertex Shader 最重要的内建输出。它告诉 Vulkan 后续管线:这个顶点位于裁剪空间中的什么位置。
如果不正确写入 gl_Position,后续的裁剪、透视除法、视口变换和光栅化都无法正确进行。
六、Vertex Shader 的输入与输出特点
Vertex Shader 的输入主要来自:
Vertex Buffer
Index Buffer
Instance Buffer
Uniform Buffer
Storage Buffer
Push Constant
Descriptor Set
内建变量,例如 gl_VertexIndex、gl_InstanceIndex
Vertex Shader 的输出主要包括:
gl_Position
自定义 varying 变量,例如颜色、UV、法线、世界坐标
可选:gl_PointSize
可选:Clip Distance / Cull Distance
可选:Layer / ViewportIndex
Vertex Shader 的输出会传递到后续管线。如果后面直接进入 Fragment Shader,那么这些输出会在光栅化阶段被插值,然后作为 Fragment Shader 的输入。
例如:
layout(location = 0) out vec3 fragColor;
对应 Fragment Shader 中:
layout(location = 0) in vec3 fragColor;
注意,Vulkan Shader 阶段之间的接口匹配主要依赖 location、component、类型和插值规则,而不是变量名。
七、Vertex Shader 的典型使用场景
Vertex Shader 常用于:
MVP 矩阵变换
骨骼动画 Skinning
实例化渲染 Instancing
顶点形变 Morph Target
顶点风场动画
顶点颜色传递
法线与切线空间构建
粒子 Billboard 的顶点展开
例如实例化渲染中,可以通过 gl_InstanceIndex 读取不同实例的模型矩阵:
layout(set = 1, binding = 0) readonly buffer InstanceBuffer {
mat4 modelMatrices[];
} instances;
void main() {
mat4 model = instances.modelMatrices[gl_InstanceIndex];
gl_Position = projection * view * model * vec4(inPosition, 1.0);
}
这种方式可以让一次 draw call 绘制大量相似物体,例如草、树、石头、建筑单元、粒子等。
八、Vertex Shader 的性能特点
Vertex Shader 的性能主要受以下因素影响:
顶点数量
顶点属性带宽
矩阵变换复杂度
骨骼数量
实例数据读取方式
顶点缓存命中率
Vertex Shader 通常不是按屏幕像素执行,而是按顶点执行。因此,一个低面数但覆盖全屏的大三角形,对 Vertex Shader 压力不大,但可能对 Fragment Shader 压力很大。
相反,一个高精度工业模型或扫描模型,可能顶点数量巨大,此时 Vertex Shader 和顶点数据带宽就会成为瓶颈。
第二部分:Fragment Shader .frag
九、Fragment Shader 的核心作用
Fragment Shader 位于光栅化之后,它的执行对象是 fragment,也就是"片元"。
片元不是最终像素,而是一个候选的像素贡献。它可能被深度测试拒绝,也可能通过混合参与最终 framebuffer 的颜色生成。
Fragment Shader 的核心职责包括:
1. 接收光栅化插值后的属性
2. 进行纹理采样
3. 进行材质计算
4. 进行光照计算
5. 输出颜色、深度或多个渲染目标
6. 可选择丢弃当前片元
如果说 Vertex Shader 解决的是"几何在哪里",那么 Fragment Shader 解决的是"表面看起来是什么"。
十、Fragment Shader 接收的是插值后的数据
假设 Vertex Shader 对三角形三个顶点分别输出颜色:
A 顶点:红色
B 顶点:绿色
C 顶点:蓝色
光栅化阶段会根据片元在三角形内部的位置,对这些顶点属性进行插值。
Fragment Shader 接收到的不是某个顶点的原始颜色,而是当前片元位置对应的插值颜色。
典型 Fragment Shader:
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
这就是为什么只给三个顶点设置不同颜色,三角形内部也会出现平滑渐变。
十一、Fragment Shader 的纹理与材质计算
Fragment Shader 最常见的任务是纹理采样:
#version 450
layout(location = 0) in vec2 fragUV;
layout(set = 1, binding = 0) uniform sampler2D baseColorTexture;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(baseColorTexture, fragUV);
}
在真实渲染中,Fragment Shader 通常不仅采样一张颜色纹理,还会处理:
Base Color
Normal Map
Roughness
Metallic
Ambient Occlusion
Emissive
Opacity
Height Map
Shadow Map
Environment Map
例如 PBR 材质中,Fragment Shader 可能会根据粗糙度、金属度、法线方向、视线方向、光源方向和环境贴图计算最终颜色。
十二、Fragment Shader 的光照计算
在 Forward Rendering 中,Fragment Shader 通常直接计算光照:
layout(location = 0) in vec3 fragNormal;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 2) in vec2 fragUV;
layout(set = 1, binding = 0) uniform sampler2D baseColorTexture;
layout(set = 0, binding = 1) uniform LightUBO {
vec3 lightDirection;
vec3 lightColor;
} light;
layout(location = 0) out vec4 outColor;
void main() {
vec3 baseColor = texture(baseColorTexture, fragUV).rgb;
vec3 N = normalize(fragNormal);
vec3 L = normalize(-light.lightDirection);
float NdotL = max(dot(N, L), 0.0);
vec3 diffuse = baseColor * light.lightColor * NdotL;
vec3 ambient = baseColor * 0.05;
outColor = vec4(ambient + diffuse, 1.0);
}
因为 Fragment Shader 是按片元执行,所以它可以得到比顶点级光照更精细的着色结果。
顶点级光照只在顶点计算光照,然后对光照结果插值,精度较低。片元级光照则在每个片元处计算法线、光照和材质响应,更适合现代真实感渲染。
十三、Fragment Shader 的输出能力
Fragment Shader 可以输出:
单个颜色目标
多个颜色目标 MRT
自定义深度
片元覆盖信息
透明度相关结果
G-buffer 数据
Deferred Rendering 中的 G-buffer pass 常见写法:
layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outMaterial;
void main() {
outAlbedo = vec4(baseColor, 1.0);
outNormal = vec4(normalize(worldNormal) * 0.5 + 0.5, 1.0);
outMaterial = vec4(roughness, metallic, ao, 1.0);
}
此时 Fragment Shader 输出的不只是最终颜色,而是后续 lighting pass 所需要的中间材质数据。
十四、Fragment Shader 的性能特点
Fragment Shader 的性能通常受以下因素影响:
屏幕分辨率
三角形覆盖面积
Overdraw
纹理采样次数
分支复杂度
光源数量
阴影采样
透明混合
PBR 计算复杂度
一个三角形即使只有三个顶点,如果它覆盖整个 4K 屏幕,也可能产生数百万个片元。因此 Fragment Shader 经常是实时渲染中最容易成为瓶颈的阶段之一。
优化 Fragment Shader 时,常见方向包括:
减少纹理采样次数
降低 overdraw
使用 depth pre-pass
合理使用 LOD
避免复杂动态分支
使用 tiled / clustered lighting
使用 variable rate shading
将部分计算移到 compute pass
第三部分:Mesh Shader .mesh
十五、Mesh Shader 的出现背景
传统图形管线以 Vertex Buffer 和 Input Assembly 为几何入口:
Vertex Buffer
↓
Input Assembly
↓
Vertex Shader
↓
Rasterization
这种方式非常成熟,但在现代大规模几何渲染中存在一些限制:
CPU 需要组织大量 draw call
顶点输入与图元装配固定功能较强
LOD、裁剪、meshlet 组织不够灵活
小三角形、大规模模型、GPU driven rendering 场景下效率受限
Mesh Shader 的目标是让几何处理更加 GPU-driven。它允许开发者用更接近 compute shader 的 workgroup 模型,在 GPU 上生成一组顶点和图元,然后直接送入光栅化。
Mesh Shader 可以理解为:
用可编程 workgroup 生成几何图元的现代几何阶段
它常用于 meshlet 渲染、GPU culling、大规模场景、clustered geometry、LOD selection 等现代渲染技术。
十六、Mesh Shader 在管线中的位置
Mesh Shader 位于光栅化之前,属于 pre-rasterization 阶段。
传统管线:
Vertex Input
↓
Vertex Shader
↓
Tessellation / Geometry 可选
↓
Rasterization
↓
Fragment Shader
Mesh Shader 管线:
Task Shader 可选
↓
Mesh Shader
↓
Rasterization
↓
Fragment Shader
Mesh Shader 可以替代传统的:
Vertex Input
Vertex Shader
Tessellation Shader
Geometry Shader
在使用 Mesh Shader 的图形管线中,几何不再必须由固定功能 Input Assembly 从 vertex buffer 中装配出来,而是由 Mesh Shader 自己在 GPU 上生成顶点和 primitive。
十七、Mesh Shader 的核心作用
Mesh Shader 的主要职责包括:
1. 以 workgroup 为单位执行
2. 读取 meshlet、cluster 或 procedural geometry 数据
3. 执行视锥裁剪、背面裁剪、LOD 选择等几何级逻辑
4. 生成一组输出顶点
5. 生成一组输出 primitive
6. 将生成的 primitive 送入光栅化阶段
传统 Vertex Shader 是"一个 invocation 处理一个顶点"。
Mesh Shader 则更像是"一个 workgroup 处理一小组几何"。
这种执行模型带来的最大变化是:几何处理不再以单个顶点为中心,而是以 meshlet 或 primitive cluster 为中心。
十八、Mesh Shader 与 meshlet
Mesh Shader 经常与 meshlet 结合使用。
Meshlet 可以理解为一个小型网格簇,通常包含有限数量的顶点和三角形,例如:
一个 meshlet:
最多 64 个顶点
最多 126 个三角形
一个 bounding sphere
一个 cone culling 信息
一组局部索引
实际数值取决于引擎设计和硬件限制。
Mesh Shader 可以针对每个 meshlet 做裁剪:
如果 meshlet 不在视锥内:不输出任何 primitive
如果 meshlet 太远:选择低 LOD
如果 meshlet 可见:生成顶点和三角形
这样可以在 GPU 上完成大量原本由 CPU 或传统 draw call 系统负责的几何筛选工作。
十九、Mesh Shader 的示意代码
下面是一个概念化的 Mesh Shader 示例。不同 GLSL 编译器和扩展语法细节可能略有差异,实际工程中需要根据 VK_EXT_mesh_shader 与编译工具链配置调整。
#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 32) in;
layout(triangles, max_vertices = 64, max_primitives = 126) out;
layout(location = 0) out PerVertexData {
vec3 color;
} vOut[];
void main() {
uint tid = gl_LocalInvocationID.x;
SetMeshOutputsEXT(3, 1);
if (tid == 0) {
gl_MeshVerticesEXT[0].gl_Position = vec4(-0.5, -0.5, 0.0, 1.0);
gl_MeshVerticesEXT[1].gl_Position = vec4( 0.5, -0.5, 0.0, 1.0);
gl_MeshVerticesEXT[2].gl_Position = vec4( 0.0, 0.5, 0.0, 1.0);
vOut[0].color = vec3(1.0, 0.0, 0.0);
vOut[1].color = vec3(0.0, 1.0, 0.0);
vOut[2].color = vec3(0.0, 0.0, 1.0);
gl_PrimitiveTriangleIndicesEXT[0] = uvec3(0, 1, 2);
}
}
这个例子中,Mesh Shader 没有从传统 Vertex Buffer 输入顶点,而是在 shader 内部直接生成了三个顶点和一个三角形。
随后这些 primitive 会进入光栅化,再进入 Fragment Shader。
二十、Mesh Shader 与 Fragment Shader 的关系
Mesh Shader 虽然替代了传统 Vertex Shader 的几何入口,但它仍然会连接到 Fragment Shader。
关系如下:
Mesh Shader 输出顶点与 primitive
↓
Rasterization
↓
Fragment Shader 接收插值后的属性
↓
输出颜色或 G-buffer
也就是说,Mesh Shader 负责生成几何,Fragment Shader 仍然负责表面着色。
Mesh Shader 输出的 per-vertex attribute 可以像 Vertex Shader 输出一样,被光栅化阶段插值,然后传给 Fragment Shader。
例如 Mesh Shader 输出:
layout(location = 0) out PerVertexData {
vec3 color;
} vOut[];
Fragment Shader 输入:
layout(location = 0) in vec3 fragColor;
从数据流角度看,Mesh Shader 是新的几何生产者;Fragment Shader 仍然是片元消费者。
二十一、Mesh Shader 的特点总结
Mesh Shader 的核心特点:
1. 属于图形管线,不是独立计算管线
2. 位于光栅化之前
3. 以 workgroup 为执行单位
4. 可以主动生成顶点和 primitive
5. 可以替代传统 vertex input / vertex shader / geometry shader 的部分职责
6. 适合 GPU driven rendering
7. 适合 meshlet、cluster、LOD、culling 等现代几何处理
8. 需要硬件与驱动支持
Mesh Shader 的优势:
几何处理更灵活
更适合大规模场景
更适合 GPU 端裁剪
减少 CPU draw call 压力
提高 meshlet 级别的局部性
可与 task shader 配合做层级调度
Mesh Shader 的代价:
工程复杂度更高
需要重构模型数据格式
需要考虑硬件兼容性
调试难度高于传统 vertex shader
不同 GPU 上性能差异可能明显
因此,Mesh Shader 并不是简单地"替代所有 Vertex Shader"。对于普通模型、简单场景和兼容性要求较高的应用,传统 Vertex Shader 仍然是最通用、最稳定的方案。
第四部分:Compute Shader .comp
二十二、Compute Shader 的核心定位
Compute Shader 与 .vert、.frag、.mesh 最大的区别是:
Compute Shader 不属于传统图形管线。
它属于 Compute Pipeline,通过 vkCmdDispatch 执行。
它不直接依赖:
Vertex Input
Input Assembly
Rasterization
Fragment Output
Framebuffer
Render Pass
Compute Shader 的核心目标是通用 GPU 计算,也就是 GPGPU。
它处理的不是顶点、片元或 primitive,而是 workgroup 和 invocation。
二十三、Compute Shader 的执行模型
Compute Shader 通过 workgroup 执行。
一个典型 Compute Shader:
#version 450
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
layout(set = 0, binding = 0, rgba8) uniform writeonly image2D outputImage;
void main() {
ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
vec2 uv = vec2(pixel) / vec2(imageSize(outputImage));
vec4 color = vec4(uv, 0.0, 1.0);
imageStore(outputImage, pixel, color);
}
CPU 侧通过:
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, ...);
vkCmdDispatch(commandBuffer, groupCountX, groupCountY, groupCountZ);
执行。
如果 local size 是:
16 × 16 × 1
而 dispatch 是:
vkCmdDispatch(cmd, 80, 45, 1)
那么理论上会覆盖:
80 × 16 = 1280
45 × 16 = 720
也就是一个 1280×720 的二维任务网格。
二十四、Compute Shader 的输入输出
Compute Shader 常用资源包括:
Storage Buffer
Storage Image
Uniform Buffer
Sampled Image
Acceleration Structure
Push Constant
Shared Memory
它的输出通常不是 framebuffer,而是:
Buffer
Image
Indirect Draw Command Buffer
Particle Buffer
Visibility Buffer
Culling Result Buffer
Prefix Sum Result
Histogram Result
Lighting Tile List
例如 GPU culling 中,Compute Shader 可以读取物体 bounding box,判断是否可见,然后写入 indirect draw command buffer。后续图形管线再使用 vkCmdDrawIndirect 或 vkCmdDrawIndexedIndirect 绘制可见对象。
数据流可能是:
Compute Shader
↓ 写入 visible draw commands
Indirect Draw Buffer
↓
Graphics Pipeline
↓
Vertex / Mesh Shader
↓
Fragment Shader
这就是现代 GPU driven rendering 的常见架构。
二十五、Compute Shader 与 Graphics Shader 的关系
Compute Shader 不直接连接到 Vertex Shader、Mesh Shader 或 Fragment Shader。它们之间通常通过资源间接协作。
典型关系如下:
Compute Shader 写入 Buffer / Image
↓
Pipeline Barrier / Memory Barrier
↓
Graphics Pipeline 读取 Buffer / Image
↓
Vertex / Mesh / Fragment Shader 使用数据
例如:
.comp 计算粒子位置
.vert 读取粒子位置并生成 billboard
.frag 计算粒子颜色
或者:
.comp 做光照 tile classification
.frag 读取每个 tile 的 light list 做 forward+ lighting
再或者:
.comp 做 GPU culling
.mesh 读取可见 meshlet 并生成 primitive
.frag 负责最终材质着色
因此,Compute Shader 更像是图形管线之外的"GPU 数据加工阶段"。
二十六、Compute Shader 的同步问题
Compute Shader 的一个重要特点是:它经常写入后续管线要读取的资源。
这就涉及 Vulkan 中非常关键的同步问题。
例如:
Compute Shader 写入 storage buffer
Graphics Pipeline 读取同一个 buffer
中间必须插入合适的 pipeline barrier,确保:
写入已经完成
缓存可见性正确
后续阶段读取到最新数据
概念上类似:
VkBufferMemoryBarrier barrier{};
barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(
cmd,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
0,
0, nullptr,
1, &barrier,
0, nullptr
);
如果后续是 Fragment Shader 读取,则目标阶段可能是:
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
如果后续是 indirect draw,则目标阶段可能涉及:
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT
Vulkan 的同步模型非常显式。Compute Shader 本身不难,真正容易出错的是资源状态、访问掩码、pipeline stage 和内存可见性。
二十七、Compute Shader 的典型使用场景
Compute Shader 常见用途包括:
粒子模拟
布料模拟
后处理
图像滤波
Bloom / Blur
SSAO
SSR
GPU Culling
LOD Selection
Prefix Sum
Histogram
Clustered / Tiled Lighting
Skinning Prepass
Ray Query 辅助计算
AI / ML 推理的一部分
体素化
Marching Cubes
GPU 生成 indirect draw command
在现代渲染器中,Compute Shader 经常作为图形管线的辅助系统,甚至成为整个 renderer 的调度核心。
例如:
Compute Pass 1:视锥裁剪
Compute Pass 2:遮挡裁剪
Compute Pass 3:生成 indirect draw commands
Graphics Pass:绘制可见物体
Compute Pass 4:后处理
这种设计被称为 GPU-driven rendering。
第五部分:四类 Shader 的关系总结
二十八、传统图形渲染路径
最经典的组合是:
.vert + .frag
数据流:
Vertex Buffer
↓
Vertex Shader
↓
Rasterization
↓
Fragment Shader
↓
Framebuffer
适合:
普通三角形渲染
基础模型显示
PBR 材质
传统 Forward Rendering
Deferred Rendering
简单实例化
大多数常规工程
这是最稳定、最通用、兼容性最好的组合。
二十九、现代 Mesh Shader 渲染路径
现代几何管线可以使用:
.mesh + .frag
或者:
.task + .mesh + .frag
数据流:
Meshlet / Cluster Buffer
↓
Task Shader 可选
↓
Mesh Shader
↓
Rasterization
↓
Fragment Shader
↓
Framebuffer
适合:
大规模模型
GPU driven rendering
meshlet 渲染
GPU 端裁剪
高复杂度场景
工业模型浏览器
扫描模型渲染
Nanite 类思想的自定义实现
这里 .mesh 替代传统 .vert 的几何入口,但 .frag 仍然保留,用于处理片元着色。
三十、Compute 辅助图形路径
现代引擎中常见组合是:
.comp + .vert + .frag
或:
.comp + .mesh + .frag
数据流:
Compute Shader 生成 / 筛选 / 预处理数据
↓
Buffer / Image / Indirect Command
↓
Graphics Pipeline 绘制
↓
Fragment Shader 着色
例如:
.comp:GPU culling
.mesh:生成可见 meshlet
.frag:PBR 着色
或者:
.comp:粒子模拟
.vert:粒子顶点展开
.frag:粒子透明着色
Compute Shader 不直接参与光栅化,但它可以深度影响后续图形管线绘制什么、怎么绘制、使用什么数据。
三十一、四类 Shader 对比表
| Shader 类型 | 文件后缀 | 所属管线 | 执行对象 | 主要职责 | 是否直接进入光栅化 | 典型用途 |
|---|---|---|---|---|---|---|
| Vertex Shader | .vert |
Graphics Pipeline | 顶点 | 坐标变换、属性输出 | 是 | MVP、骨骼动画、实例化 |
| Fragment Shader | .frag |
Graphics Pipeline | 片元 | 颜色、纹理、光照、材质 | 否,位于光栅化之后 | PBR、纹理、G-buffer、透明 |
| Mesh Shader | .mesh |
Graphics Pipeline | Workgroup / Meshlet | 生成顶点和 primitive | 是 | Meshlet、GPU culling、LOD |
| Compute Shader | .comp |
Compute Pipeline | Workgroup | 通用 GPU 计算 | 否 | 粒子、后处理、GPU culling、前缀和 |
三十二、按"解决什么问题"理解四类 Shader
更高级的理解方式是,不要只按文件后缀记忆,而要看它们解决的问题。
Vertex Shader:
解决单个顶点如何变换的问题。
Fragment Shader:
解决片元如何着色的问题。
Mesh Shader:
解决一组几何如何在 GPU 上生成、裁剪和组织的问题。
Compute Shader:
解决任意 GPU 并行计算和数据生成的问题。
从渲染架构角度看:
.vert :传统几何入口
.mesh :现代几何入口
.frag :表面着色出口
.comp :通用计算工具
三十三、四类 Shader 的工程选型建议
如果你只是绘制普通模型:
使用 .vert + .frag
这是最标准的路径。
如果你要做 PBR、纹理、阴影、G-buffer:
重点写好 .frag
Fragment Shader 是材质系统的核心。
如果你要做大量实例、骨骼动画、普通 mesh 渲染:
重点优化 .vert
Vertex Shader 仍然是传统渲染系统的基础。
如果你要做大规模场景、meshlet、GPU culling、LOD:
考虑 .mesh + .frag
Mesh Shader 适合更现代的 GPU-driven geometry pipeline。
如果你要做模拟、预处理、后处理、GPU 数据生成:
使用 .comp
Compute Shader 是现代 Vulkan 渲染器中非常重要的辅助甚至核心模块。
三十四、一个现代 Vulkan Renderer 的典型组合
一个较现代的 Vulkan 渲染器可能是这样组织的:
Compute Pass 1:
.comp 执行 GPU culling,生成可见 meshlet 列表
Compute Pass 2:
.comp 构建 light cluster / tile light list
Depth Prepass:
.mesh 或 .vert 生成几何
只写深度
G-buffer Pass:
.mesh 或 .vert 处理几何
.frag 输出 albedo、normal、roughness、metallic
Lighting Pass:
.comp 或 fullscreen .frag 计算光照
Post-processing:
.comp 执行 bloom、tone mapping、blur
或 .vert + .frag 绘制 fullscreen triangle
UI Pass:
.vert + .frag 绘制界面
在这个架构中,四类 Shader 各自承担不同角色:
.comp 负责准备数据
.mesh / .vert 负责生成几何
.frag 负责表面着色
.comp / .frag 负责后处理
这也是现代 Vulkan 渲染系统相比传统 OpenGL 式渲染流程更复杂、但也更强大的地方。
三十五、常见误区一:mesh shader 是 compute shader 吗?
Mesh Shader 的执行模型确实很像 Compute Shader,因为它也是以 workgroup 为单位执行,并且可以使用局部并行计算思维组织代码。
但 Mesh Shader 不是 Compute Shader。
区别在于:
Mesh Shader 属于 Graphics Pipeline
Compute Shader 属于 Compute Pipeline
Mesh Shader 的输出会进入光栅化阶段,它生成的是顶点和 primitive。
Compute Shader 不会自动进入光栅化阶段,它输出的是 buffer 或 image 数据。
简单说:
.mesh 是图形管线中的可编程几何生成阶段
.comp 是独立计算管线中的通用计算阶段
三十六、常见误区二:mesh shader 一定比 vertex shader 快吗?
不一定。
Mesh Shader 的优势在于更强的几何组织能力,尤其适合:
meshlet
GPU culling
LOD
大规模几何
GPU-driven rendering
但如果只是绘制简单模型,传统 .vert + .frag 可能更简单、更稳定,甚至性能更好。
Mesh Shader 的性能收益依赖:
数据布局是否适合 meshlet
裁剪是否有效
workgroup 大小是否合理
primitive 输出是否高效
硬件对 mesh shader 的支持质量
如果只是把传统 vertex shader 机械改写成 mesh shader,而没有重新设计几何数据结构,通常不会自然获得性能提升。
三十七、常见误区三:fragment shader 处理的是像素吗?
严格来说,Fragment Shader 处理的是 fragment,而不是最终 pixel。
fragment 是可能影响 framebuffer 的候选片元。它可能:
被深度测试丢弃
被模板测试丢弃
被 discard 丢弃
与已有颜色混合
写入多个 color attachment
只写深度不写颜色
所以更准确的说法是:
Fragment Shader 计算片元输出,后续管线决定它如何影响最终 framebuffer。
三十八、常见误区四:compute shader 可以替代所有图形管线吗?
理论上,Compute Shader 可以实现软件光栅化、图像生成、路径追踪、体渲染等任务。
但这不意味着它适合替代所有图形管线。
硬件固定功能光栅化管线仍然在三角形 rasterization、深度测试、插值、MSAA、ROP、混合等方面具有成熟优势。
Compute Shader 更适合:
图形管线不擅长的通用计算
图形管线之前的数据准备
图形管线之后的图像处理
特殊渲染算法
因此,现代 Vulkan 渲染器的趋势不是"用 compute 取代 graphics",而是:
graphics pipeline + compute pipeline 协同工作
三十九、Shader 资源绑定的共同基础
无论 .vert、.frag、.mesh 还是 .comp,它们都可以通过 descriptor set 访问资源。
常见资源绑定方式:
layout(set = 0, binding = 0) uniform CameraUBO {
mat4 view;
mat4 projection;
} camera;
layout(set = 1, binding = 0) uniform sampler2D baseColorTexture;
layout(set = 2, binding = 0) readonly buffer InstanceBuffer {
mat4 modelMatrices[];
} instances;
常见 descriptor set 组织方式:
set = 0:Frame 级资源,例如相机、时间、光照
set = 1:Material 级资源,例如纹理、材质参数
set = 2:Object / Instance 级资源,例如模型矩阵、实例数据
set = 3:Scene 级资源,例如全局 buffer、光源列表、meshlet 数据
这种显式资源模型是 Vulkan 的核心特征之一。
Shader 阶段不同,但它们访问资源的基本机制是统一的。
四十、最终总结
Vulkan 中 .vert、.frag、.mesh、.comp 分别代表四类非常重要的 Shader 阶段。
.vert 是传统图形管线的几何入口。它按顶点执行,负责坐标变换、顶点属性传递、实例化和顶点级动画。
.frag 是光栅化之后的表面着色阶段。它按片元执行,负责颜色、纹理、材质、光照、透明、G-buffer 等计算。
.mesh 是现代图形管线中的可编程几何生成阶段。它以 workgroup 为单位执行,可以生成顶点和 primitive,适合 meshlet、GPU culling、LOD 和 GPU-driven rendering。
.comp 是独立计算管线中的通用 GPU 计算阶段。它不直接进入光栅化,而是通过 buffer、image、indirect command 等资源与图形管线协作。
四者的关系可以浓缩为:
传统渲染:
.vert → Rasterization → .frag
现代 Mesh Shader 渲染:
.mesh → Rasterization → .frag
GPU-driven 渲染:
.comp → Buffer / Indirect Command → .mesh 或 .vert → .frag
后处理 / 通用计算:
.comp → Image / Buffer
如果从现代渲染架构角度看,它们分别承担:
.vert :传统顶点变换
.mesh :现代几何生成
.frag :片元材质着色
.comp :GPU 通用计算与数据调度
真正掌握 Vulkan Shader 编程,不是分别背诵这些文件后缀,而是理解它们在 pipeline 中的位置、执行粒度、输入输出接口、资源绑定方式以及与同步系统之间的关系。
当你能够把 .vert、.frag、.mesh、.comp 放进完整 renderer 架构中理解时,才算真正进入 Vulkan 现代渲染开发的核心区域。