Vulkan 中 Vertex Shader 与 Fragment Shader 的作用、关系与工程实践解析
一、引言:为什么必须理解 vert 与 frag
在 Vulkan 图形渲染中,Shader 是 GPU 可编程管线的核心。一个最基本的 Vulkan 图形管线通常至少包含两个 Shader 阶段:
-
Vertex Shader,通常写作
.vert -
Fragment Shader,通常写作
.frag
这两个阶段分别对应图形渲染中两个完全不同的问题:
Vertex Shader 主要回答:
“一个顶点应该出现在屏幕的什么位置,并携带哪些属性进入后续管线?”
Fragment Shader 主要回答:
“屏幕上某个片元最终应该是什么颜色,是否应该被丢弃,是否需要输出深度等信息?”
很多初学 Vulkan 的开发者会把 vert 和 frag 简单理解为“前者处理三角形,后者处理颜色”。这个说法并不完全错误,但过于粗略。更准确地说:
Vertex Shader 处理的是顶点级别的数据变换,Fragment Shader 处理的是片元级别的着色计算。二者之间由 Vulkan 图形管线连接,并通过显式声明的输入输出接口传递数据。
理解这两个阶段,不只是为了写出一个能显示三角形的程序,更是为了理解 Vulkan 的渲染架构、资源绑定方式、图形管线状态设计以及现代实时渲染中的数据流组织方式。
二、Vulkan 图形管线中的 vert 与 frag 位置
在 Vulkan 中,一次典型的三角形绘制流程可以概括为:
顶点缓冲区 Vertex Buffer
↓
输入装配 Input Assembly
↓
顶点着色器 Vertex Shader
↓
可选:曲面细分 Tessellation Shader
↓
可选:几何着色器 Geometry Shader
↓
裁剪、透视除法、视口变换
↓
光栅化 Rasterization
↓
片元着色器 Fragment Shader
↓
深度测试、模板测试、混合
↓
颜色附件 / 深度附件 / Framebuffer

其中,Vertex Shader 位于光栅化之前,属于 pre-rasterization 阶段;Fragment Shader 位于光栅化之后,属于 fragment shading 阶段。
这意味着二者的根本区别在于:
Vertex Shader 处理的是几何输入。
Fragment Shader 处理的是光栅化之后产生的片元。
也就是说,Vertex Shader 的工作对象是顶点,Fragment Shader 的工作对象是 fragment。这里的 fragment 不是最终像素,而是“可能成为像素的一次候选片元”。它可能会在后续的深度测试、模板测试、混合等阶段被保留、覆盖、丢弃或参与混合。
三、Vertex Shader:顶点着色器的核心作用
Vertex Shader 是图形管线中最基础的可编程阶段。只要使用传统图形管线绘制几何体,通常就必须提供 Vertex Shader。
它的主要职责包括以下几个方面。
四、Vertex Shader 的第一职责:坐标变换
顶点最初通常位于模型自身的局部坐标系中。例如,一个立方体模型的顶点坐标可能是:
(-1, -1, -1)
( 1, -1, -1)
( 1, 1, -1)
...
这些坐标只描述模型自身的形状,并不知道模型在世界中的位置,也不知道摄像机在哪里,更不知道屏幕在哪里。
Vertex Shader 要做的第一件事,就是将这些顶点从模型空间逐步变换到裁剪空间:
模型空间 Model Space
↓ Model Matrix
世界空间 World Space
↓ View Matrix
观察空间 View / Camera Space
↓ Projection Matrix
裁剪空间 Clip Space
典型代码如下:
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(set = 0, binding = 0) uniform CameraUBO {
mat4 model;
mat4 view;
mat4 projection;
} ubo;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
}
这里最关键的是:
gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPosition, 1.0);
gl_Position 是 Vertex Shader 最重要的内建输出变量。它表示当前顶点在裁剪空间中的位置。后续管线会基于它进行裁剪、透视除法、视口映射和光栅化。
如果 Vertex Shader 没有正确写入 gl_Position,后续管线就无法知道顶点应该被投影到屏幕的什么位置。
五、Vertex Shader 的第二职责:向后续阶段传递顶点属性
除了位置变换,Vertex Shader 还会把一些顶点属性传递给后续的 Fragment Shader,例如:
-
顶点颜色
-
法线方向
-
纹理坐标
-
切线空间数据
-
世界空间位置
-
阴影坐标
-
实例 ID 相关数据
例如:
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
fragColor = inColor;
}
这里的 fragColor 并不是直接写入屏幕颜色,而是输出给后续阶段。经过光栅化后,GPU 会对顶点输出的数据进行插值,然后将插值后的结果作为 Fragment Shader 的输入。
举一个简单例子:
一个三角形有三个顶点:
顶点 A:红色
顶点 B:绿色
顶点 C:蓝色
Vertex Shader 分别输出三个顶点的颜色。光栅化阶段会在三角形内部自动插值这些颜色。Fragment Shader 接收到的不是某个顶点的原始颜色,而是当前片元位置对应的插值颜色。
因此,一个三角形内部会自然形成平滑渐变效果。
六、Vertex Shader 的第三职责:参与实例化与顶点级别控制
在 Vulkan 中,Vertex Shader 不仅可以处理普通顶点,还可以结合实例化绘制处理大量重复对象。
常见内建变量包括:
gl_VertexIndex
gl_InstanceIndex
gl_VertexIndex 表示当前顶点索引,常用于不使用 vertex buffer 的简单绘制,或者从 buffer 中手动索引数据。
gl_InstanceIndex 表示当前实例索引,常用于实例化渲染。例如绘制一片森林、一组粒子、多个相同模型时,每个实例可以拥有不同的模型矩阵、颜色、材质编号等。
示例:
layout(set = 0, 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);
}
这种方式可以显著减少 CPU 提交 draw call 的次数,是 Vulkan 高性能渲染中非常常见的技术。
七、Vertex Shader 的特点总结
Vertex Shader 的核心特点可以概括为:
-
按顶点执行
每个输入顶点通常都会触发一次 Vertex Shader 执行。 -
主要负责几何变换
它将模型顶点从局部坐标变换到裁剪空间。 -
必须输出
gl_Position
这是后续光栅化阶段的基础。 -
可以输出自定义属性
例如颜色、法线、UV、世界坐标等。 -
不直接决定最终像素颜色
它可以传递颜色数据,但最终写入 framebuffer 的颜色通常由 Fragment Shader 决定。 -
适合做顶点级计算
例如骨骼蒙皮、实例化变换、顶点动画、风场扰动、形变等。
八、Fragment Shader:片元着色器的核心作用
Fragment Shader 通常写作 .frag,它位于光栅化之后。
当 GPU 根据顶点位置生成三角形区域后,会把三角形覆盖到屏幕空间。屏幕上被三角形覆盖的位置会产生 fragment。每个 fragment 都会携带经过插值后的属性,例如颜色、UV、法线、深度等。
Fragment Shader 的主要职责是计算这些 fragment 的输出结果。
最常见的输出是颜色:
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
这段代码表示每个片元都输出红色。
九、Fragment Shader 的第一职责:颜色计算
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);
}
这里 fragUV 来自 Vertex Shader 输出的纹理坐标,经过光栅化插值后传入 Fragment Shader。texture() 函数根据当前片元的 UV 坐标采样纹理,最终得到颜色。
十、Fragment Shader 的第二职责:光照计算
Fragment Shader 是传统 forward rendering 中最常见的光照计算位置。
例如一个简单 Lambert 漫反射模型:
layout(location = 0) in vec3 fragNormal;
layout(location = 1) in vec3 fragWorldPos;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 1) uniform LightUBO {
vec3 lightDir;
vec3 lightColor;
} light;
void main() {
vec3 N = normalize(fragNormal);
vec3 L = normalize(-light.lightDir);
float NdotL = max(dot(N, L), 0.0);
vec3 diffuse = NdotL * light.lightColor;
outColor = vec4(diffuse, 1.0);
}
这类计算通常放在 Fragment Shader 中,是因为光照结果需要对每个片元独立计算。这样可以得到比顶点级光照更细腻的结果。
如果把光照计算放在 Vertex Shader 中,那么光照只在顶点处计算,三角形内部通过插值得到结果,可能会出现明显的插值误差,尤其是在模型面数较低或高光较强时。
十一、Fragment Shader 的第三职责:片元丢弃与透明控制
Fragment Shader 可以使用 discard 丢弃当前片元。例如在 alpha cutout 材质中:
vec4 baseColor = texture(baseColorTexture, fragUV);
if (baseColor.a < 0.5) {
discard;
}
outColor = baseColor;
这在草地、树叶、铁丝网、镂空材质等场景中非常常见。
不过需要注意,discard 可能影响早期深度测试优化,过度使用可能降低性能。因此在 Vulkan 工程实践中,alpha test、透明排序、depth pre-pass 等问题需要结合具体渲染架构设计。
十二、Fragment Shader 的第四职责:输出多个渲染目标
Fragment Shader 不一定只输出一个颜色。现代渲染中经常会使用 MRT,也就是 Multiple Render Targets。
例如在 deferred rendering 的 G-buffer pass 中,一个 Fragment Shader 可能同时输出:
-
世界空间位置
-
法线
-
漫反射颜色
-
粗糙度
-
金属度
-
材质 ID
示例:
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, materialID);
}
这说明 Fragment Shader 不只是“输出颜色”,它也可以输出中间渲染数据,为后续 lighting pass、post-processing 或 compositing 阶段服务。
十三、Fragment Shader 的特点总结
Fragment Shader 的核心特点可以概括为:
-
按片元执行
它的执行次数通常远多于 Vertex Shader,因为一个三角形可能覆盖大量屏幕像素。 -
主要负责着色
包括颜色、纹理、光照、材质、透明度等计算。 -
接收插值后的输入
来自 Vertex Shader 的输出会在光栅化过程中被插值,然后作为 Fragment Shader 的输入。 -
可以输出颜色或多个渲染目标
它可以写入一个或多个 color attachment。 -
可以丢弃片元
通过discard可以阻止当前片元继续进入后续输出流程。 -
性能压力通常更大
由于 Fragment Shader 执行次数可能非常高,复杂光照、纹理采样、分支判断都可能显著影响性能。
十四、vert 与 frag 的关系:不是独立文件,而是管线阶段
在 Vulkan 中,.vert 和 .frag 不是两个孤立的程序文件,而是同一个 graphics pipeline 中的两个 shader stage。
它们之间的关系可以理解为:
Vertex Shader 输出顶点属性
↓
光栅化阶段对属性进行插值
↓
Fragment Shader 接收插值后的属性
↓
Fragment Shader 计算片元输出
一个典型的数据流如下:
// triangle.vert
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = vec4(inPosition, 1.0);
fragColor = inColor;
}
// triangle.frag
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
关键点在于:
layout(location = 0) out vec3 fragColor;
必须与:
layout(location = 0) in vec3 fragColor;
匹配。
Vulkan 中 Shader 阶段之间的数据传递不是靠变量名匹配,而是靠 location、component、类型等接口规则匹配。也就是说,变量名可以不同,但 location 和数据类型必须合理对应。
例如下面这样仍然可以工作:
// vert
layout(location = 0) out vec3 vertexColor;
// frag
layout(location = 0) in vec3 interpolatedColor;
因为它们都使用 location = 0,并且类型兼容。
这体现了 Vulkan 的一个重要设计思想:显式性。Vulkan 不倾向于让驱动猜测你的意图,而是要求开发者把接口、资源绑定、管线状态明确写出来。
十五、光栅化与插值:连接 vert 和 frag 的关键过程
很多人理解 Vertex Shader 和 Fragment Shader 时,会忽略中间的光栅化阶段。实际上,光栅化是二者之间最重要的桥梁。
假设有一个三角形:
A 顶点:颜色 red
B 顶点:颜色 green
C 顶点:颜色 blue
Vertex Shader 对三个顶点分别执行一次,输出三个颜色。
随后,光栅化阶段会确定这个三角形覆盖屏幕上的哪些片元,并根据片元在三角形内部的位置,对顶点属性进行插值。
因此 Fragment Shader 接收到的颜色可能是:
靠近 A:偏红
靠近 B:偏绿
靠近 C:偏蓝
三角形中心:红绿蓝混合
这就是为什么只在三个顶点上设置不同颜色,也能在整个三角形内部看到平滑渐变。
常见可插值数据包括:
-
颜色
-
UV 坐标
-
法线
-
世界坐标
-
切线
-
阴影坐标
如果不希望某个变量被平滑插值,可以使用 flat qualifier:
layout(location = 0) flat out int materialID;
Fragment Shader 中对应:
layout(location = 0) flat in int materialID;
这对于材质 ID、对象 ID、整数索引等数据非常重要。整数类型通常不能按浮点方式平滑插值,否则语义会出错。
十六、Vulkan 中 Shader 的资源绑定特点
在 OpenGL 中,很多资源绑定行为较为隐式;而 Vulkan 中,资源绑定非常显式。Shader 中经常会看到:
layout(set = 0, binding = 0) uniform CameraUBO {
mat4 view;
mat4 projection;
} camera;
这里的 set 和 binding 对应 Vulkan 中的 descriptor set layout。
可以简单理解为:
set :资源组编号
binding:资源在该组中的槽位编号
例如:
layout(set = 0, binding = 0) uniform CameraUBO { ... };
layout(set = 1, binding = 0) uniform sampler2D baseColorTexture;
layout(set = 2, binding = 0) readonly buffer InstanceData { ... };
一种常见的组织方式是:
set = 0:每帧资源,例如相机矩阵、光照参数
set = 1:每材质资源,例如纹理、材质参数
set = 2:每对象资源,例如模型矩阵、对象 ID
这样做的优点是资源更新粒度清晰。
例如相机数据每帧更新一次,材质数据可能多个物体共享,对象数据则每个 draw call 或每个 instance 不同。合理划分 descriptor set 可以减少资源绑定开销,提高管线组织效率。
十七、Vertex Shader 与 Fragment Shader 的性能差异
Vertex Shader 和 Fragment Shader 都运行在 GPU 上,但它们的性能瓶颈通常不同。
Vertex Shader 的执行次数主要与顶点数量有关。
如果一个模型有 100 万个顶点,那么 Vertex Shader 理论上会处理大量顶点数据。顶点阶段的典型瓶颈包括:
-
顶点数量过多
-
骨骼动画矩阵计算复杂
-
顶点属性带宽过大
-
实例数据读取不合理
-
顶点缓存利用率低
Fragment Shader 的执行次数主要与屏幕覆盖面积有关。
一个只有几百个顶点的大三角形,如果覆盖整个 4K 屏幕,就可能触发数百万次 Fragment Shader 执行。片元阶段的典型瓶颈包括:
-
分辨率过高
-
overdraw 严重
-
纹理采样次数过多
-
分支复杂
-
光照计算复杂
-
透明物体排序与混合成本高
因此,不能简单地说 Vertex Shader 更重要或 Fragment Shader 更重要。它们面向的是不同阶段的成本。
通常来说:
模型复杂度高,Vertex Shader 压力更明显。
屏幕覆盖面积大、材质复杂、光照复杂,Fragment Shader 压力更明显。
十八、常见误区一:Fragment Shader 处理的是像素吗?
严格来说,Fragment Shader 处理的是 fragment,而不是最终 pixel。
fragment 可以理解为一个“候选像素贡献”。它可能通过深度测试,也可能被深度测试拒绝;可能写入颜色附件,也可能被模板测试丢弃;可能直接覆盖目标颜色,也可能与已有颜色混合。
因此更准确的说法是:
Fragment Shader 计算片元输出,后续管线再决定它如何影响 framebuffer。
在很多入门语境中,把 Fragment Shader 简称为“像素着色器”可以帮助理解,但在 Vulkan 或现代 GPU 管线语境下,fragment 与 pixel 不是完全等价的概念。
十九、常见误区二:Vertex Shader 能不能决定颜色?
Vertex Shader 可以输出颜色数据,但它通常不直接决定最终颜色。
例如:
layout(location = 0) out vec3 fragColor;
fragColor = inColor;
这只是把颜色交给后续管线。最终颜色仍然由 Fragment Shader 输出:
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
当然,也可以在 Vertex Shader 中计算部分光照,然后让 Fragment Shader 只接收插值后的光照结果。但这种做法通常属于顶点级光照,精度较低,适合低成本风格化渲染或移动端低负载场景。
二十、常见误区三:Fragment Shader 能不能改变几何形状?
Fragment Shader 通常不能改变几何形状。
它无法生成新的三角形,也无法改变顶点位置。它能做的是决定当前片元的颜色、深度、是否丢弃等。
如果要改变几何形状,应考虑:
-
Vertex Shader:顶点形变、顶点动画、骨骼蒙皮
-
Tessellation Shader:曲面细分
-
Geometry Shader:几何扩展或修改
-
Mesh Shader:现代 GPU 上更灵活的几何处理方式
Fragment Shader 更接近于“表面着色”阶段,而不是“几何构造”阶段。
二十一、一个完整的 Vulkan 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;
}
Fragment Shader:
#version 450
layout(location = 0) in vec3 fragWorldPos;
layout(location = 1) in vec3 fragNormal;
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;
vec3 cameraPosition;
} 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 diffuseFactor = max(dot(N, L), 0.0);
vec3 diffuse = baseColor * light.lightColor * diffuseFactor;
vec3 ambient = baseColor * 0.05;
outColor = vec4(ambient + diffuse, 1.0);
}
这个例子中:
Vertex Shader 负责:
输入顶点位置、法线、UV
计算世界坐标
变换到裁剪空间
把世界坐标、法线、UV 传给 Fragment Shader
Fragment Shader 负责:
接收插值后的世界坐标、法线、UV
采样纹理
计算光照
输出最终颜色
这就是 vert 与 frag 在真实渲染中的典型分工。
二十二、Vulkan 中 Shader 编译流程
Vulkan 并不直接执行 GLSL 源码。通常流程是:
GLSL / HLSL / Slang
↓
编译为 SPIR-V
↓
创建 VkShaderModule
↓
通过 VkPipelineShaderStageCreateInfo 加入 Graphics Pipeline
↓
创建 VkPipeline
↓
绘制时绑定 Pipeline 并执行
例如:
glslangValidator -V shader.vert -o vert.spv
glslangValidator -V shader.frag -o frag.spv
在 Vulkan 程序中,这两个 SPIR-V 文件会分别创建为 shader module,并以不同的 stage 加入 pipeline:
VkPipelineShaderStageCreateInfo vertStageInfo{};
vertStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertStageInfo.module = vertShaderModule;
vertStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragStageInfo{};
fragStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragStageInfo.module = fragShaderModule;
fragStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[] = {
vertStageInfo,
fragStageInfo
};
这里的 VK_SHADER_STAGE_VERTEX_BIT 和 VK_SHADER_STAGE_FRAGMENT_BIT 明确告诉 Vulkan:这两个 shader module 分别属于顶点阶段和片元阶段。
这也是 Vulkan 与一些传统 API 使用体验不同的地方。Vulkan 不会自动推断 Shader 属于哪个阶段,开发者必须在 pipeline creation 阶段明确声明。
二十三、工程实践中的接口设计建议
在实际 Vulkan 项目中,建议把 Vertex Shader 和 Fragment Shader 的接口设计成稳定、清晰、可扩展的形式。
例如基础 PBR 材质可以设计如下:
Vertex Shader 输出:
layout(location = 0) out vec3 fragWorldPos;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec2 fragUV;
layout(location = 3) out vec4 fragTangent;
Fragment Shader 输入:
layout(location = 0) in vec3 fragWorldPos;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec2 fragUV;
layout(location = 3) in vec4 fragTangent;
这样 Fragment Shader 就可以基于这些数据进行:
-
base color 采样
-
normal mapping
-
roughness/metallic 采样
-
IBL 环境光照
-
shadow mapping
-
BRDF 计算
接口设计的核心原则是:
Vertex Shader 输出“几何相关数据”,Fragment Shader 使用这些数据进行“材质与光照计算”。
不要在 Vertex Shader 中塞入过多片元级逻辑,也不要期望 Fragment Shader 修改几何结构。职责边界越清晰,Shader 系统越容易维护。
二十四、vert 与 frag 的对比总结
| 对比项 | Vertex Shader .vert |
Fragment Shader .frag |
|---|---|---|
| 执行位置 | 光栅化之前 | 光栅化之后 |
| 执行对象 | 顶点 | 片元 |
| 核心任务 | 坐标变换、顶点属性输出 | 着色、纹理采样、光照计算 |
| 必要输出 | gl_Position |
颜色输出,如 outColor |
| 输入来源 | Vertex Buffer、Instance Buffer、Uniform、Storage Buffer 等 | 光栅化插值结果、纹理、Uniform、Storage Buffer 等 |
| 是否能改变几何形状 | 可以改变顶点位置 | 通常不能改变几何形状 |
| 是否能采样纹理 | 可以,但不常用于普通材质采样 | 可以,且非常常见 |
| 性能瓶颈来源 | 顶点数量、顶点属性带宽、骨骼/实例计算 | 分辨率、overdraw、纹理采样、光照复杂度 |
| 常见应用 | MVP 变换、骨骼动画、实例化、顶点动画 | PBR、纹理、阴影、透明、后处理前的材质计算 |
二十五、从渲染架构角度理解二者关系
如果从更高层的渲染架构来看,Vertex Shader 和 Fragment Shader 其实对应了实时渲染中的两个基本维度:
Vertex Shader 解决“空间问题”。
Fragment Shader 解决“表面问题”。
空间问题包括:
物体在哪里?
顶点如何变换?
模型如何投影到屏幕?
实例如何排列?
动画如何改变形状?
表面问题包括:
物体看起来是什么材质?
表面颜色是什么?
是否粗糙?
是否金属?
是否透明?
受光照影响如何?
是否处于阴影中?
因此,在一个现代 Vulkan 渲染器中,Vertex Shader 往往更接近 scene transform、geometry processing、instance system;Fragment Shader 则更接近 material system、lighting model、texture system。
这个视角有助于我们构建更清晰的引擎架构。
例如:
Scene / Transform System
↓
Vertex Shader
↓
Rasterization
↓
Material / Lighting System
↓
Fragment Shader
↓
Framebuffer / Render Target
这种分层思想在工业渲染器、游戏引擎和实时可视化系统中都非常重要。
二十六、结语
在 Vulkan 中,.vert 和 .frag 虽然只是两个 Shader 文件后缀,但它们代表的是图形管线中两个非常关键的可编程阶段。
Vertex Shader 面向顶点,负责几何变换和顶点属性输出。它决定几何体如何进入裁剪空间,是模型从三维世界走向屏幕空间的入口。
Fragment Shader 面向片元,负责颜色、纹理、光照、透明、材质等计算。它决定物体表面最终如何呈现,是视觉效果形成的核心阶段。
二者之间并不是简单的前后文件关系,而是 Vulkan Graphics Pipeline 中明确分工、通过接口连接的数据流关系:
Vertex Shader 产生几何与属性
光栅化阶段进行覆盖判断与插值
Fragment Shader 基于插值数据完成片元着色
真正掌握 Vulkan Shader 编程,不能只会写一个三角形,而要理解每个阶段处理什么数据、处在管线什么位置、受哪些 pipeline state 影响、如何与 descriptor set 和 render target 配合。
只有理解了这些底层关系,才能进一步进入更复杂的 Vulkan 渲染技术,例如 PBR、shadow mapping、deferred rendering、forward+、clustered shading、GPU instancing、mesh shader 和实时光线追踪等方向。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)