一、引言:为什么必须理解 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 的核心特点可以概括为:

  1. 按顶点执行
    每个输入顶点通常都会触发一次 Vertex Shader 执行。

  2. 主要负责几何变换
    它将模型顶点从局部坐标变换到裁剪空间。

  3. 必须输出 gl_Position
    这是后续光栅化阶段的基础。

  4. 可以输出自定义属性
    例如颜色、法线、UV、世界坐标等。

  5. 不直接决定最终像素颜色
    它可以传递颜色数据,但最终写入 framebuffer 的颜色通常由 Fragment Shader 决定。

  6. 适合做顶点级计算
    例如骨骼蒙皮、实例化变换、顶点动画、风场扰动、形变等。


八、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 的核心特点可以概括为:

  1. 按片元执行
    它的执行次数通常远多于 Vertex Shader,因为一个三角形可能覆盖大量屏幕像素。

  2. 主要负责着色
    包括颜色、纹理、光照、材质、透明度等计算。

  3. 接收插值后的输入
    来自 Vertex Shader 的输出会在光栅化过程中被插值,然后作为 Fragment Shader 的输入。

  4. 可以输出颜色或多个渲染目标
    它可以写入一个或多个 color attachment。

  5. 可以丢弃片元
    通过 discard 可以阻止当前片元继续进入后续输出流程。

  6. 性能压力通常更大
    由于 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;

这里的 setbinding 对应 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_BITVK_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 和实时光线追踪等方向。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐