4、基础渲染管线:理解渲染管线、顶点着色器与片段着色器,用Shader Graph实现简单立体效果

渲染管线,说白了就是GPU把3D数据变成屏幕上像素的那套流水线。我刚入行时总觉得这东西太底层,写Shader直接抄公式就行。直到有一次做AR项目,模型在手机上死活显示不对——嗯,从那以后我老老实实把管线啃了一遍。

今天咱们就拆开这条流水线,看看顶点着色器和片段着色器到底在干什么。最后用Shader Graph搭个立体效果,你马上就能感受到“控制像素”的快感。

核心认知:渲染管线不是黑盒。理解它,你才能精准控制GPU的每一分算力。

4.1 渲染管线:从顶点到像素的流水线

渲染管线可以分成三个阶段:应用阶段(CPU准备数据)、几何阶段(GPU处理顶点)、光栅化阶段(GPU生成像素)。我们写Shader主要管后两个阶段。

下面这张图是我自己项目里常用的管线示意,你一看就明白:

渲染管线核心流程 应用阶段 CPU准备顶点数据 几何阶段 顶点着色器 → 裁剪 → 屏幕映射 光栅化阶段 三角形设置 → 遍历 → 片段着色器 帧缓冲 各阶段核心任务 顶点着色器: • 接收模型空间的顶点位置、法线、UV • 执行 MVP 矩阵变换,将顶点转换到裁剪空间 • 可以修改顶点位置(比如做顶点动画) 片段着色器: • 接收光栅化后的插值数据(位置、法线、UV) • 计算每个像素的最终颜色 • 可以采样纹理、计算光照、做后处理
我的习惯:调试渲染问题时,先确认数据卡在哪个阶段。顶点位置不对查顶点着色器,颜色不对查片段着色器,效率低查光栅化设置。按这个思路排查,80%的问题十分钟内能定位。

4.2 顶点着色器:控制每个顶点的命运

顶点着色器是GPU处理每个顶点的入口。它接收一个顶点,输出一个变换后的顶点。我见过很多新手以为顶点着色器能“创造”顶点——其实不行,它只能修改已有的顶点。

顶点着色器最核心的工作就是做坐标变换。模型坐标 → 世界坐标 → 观察坐标 → 裁剪坐标,这一套MVP矩阵乘法就在这儿完成。

坐标空间 变换矩阵 作用
模型空间 → 世界空间 模型矩阵 (M) 将顶点从局部坐标系移到世界坐标系
世界空间 → 观察空间 观察矩阵 (V) 以摄像机为原点重新定位所有顶点
观察空间 → 裁剪空间 投影矩阵 (P) 将视锥体压缩成标准立方体,准备裁剪

举个例子,你想让一个模型沿着Y轴上下浮动。在顶点着色器里加一行代码就行:

// 伪代码:顶点浮动效果
float offsetY = sin(_Time.y * _Speed + v.vertex.x * _Frequency) * _Amplitude;
v.vertex.y += offsetY;
// 然后正常做MVP变换
o.vertex = UnityObjectToClipPos(v.vertex);
我曾经踩过的坑:在顶点着色器里修改顶点位置后,法线没有同步更新。结果模型动起来了,光照却完全不对——因为法线还是原来的方向。记住:顶点位置变了,法线必须用变换矩阵的逆转置矩阵重新计算。

4.3 片段着色器:决定每个像素的颜色

顶点着色器处理完顶点,GPU会做光栅化——把三角形拆成一个个像素。每个像素都会调用一次片段着色器。你想想看,一个1080p的画面有200多万个像素,片段着色器每秒要跑几十亿次。所以它的效率至关重要。

片段着色器能拿到什么数据?光栅化阶段会把顶点着色器的输出做插值。比如三角形三个顶点的颜色分别是红、绿、蓝,中间的像素就会得到渐变混合的颜色。这个插值过程是硬件自动完成的,我们只管用。

最简单的片段着色器就是输出一个固定颜色:

// 伪代码:纯色输出
fixed4 frag (v2f i) : SV_Target
{
    return fixed4(1.0, 0.5, 0.2, 1.0); // 橙色
}

但实际项目中,我们通常要采样纹理、计算光照、做各种效果。我个人习惯把片段着色器拆成几个函数:颜色采样、光照计算、后处理叠加。这样调试起来特别方便。

4.4 用Shader Graph实现立体效果

理论说完了,咱们动手搭一个。Shader Graph是Unity的可视化Shader编辑器,你不用写代码,拖拽节点就能生成Shader。我建议新手先用它建立直觉,再去看代码实现。

下面这个例子,我们做一个边缘发光+顶点波动的立体效果:

  1. 创建Shader Graph:在Project窗口右键 → Create → Shader → URP/Lit Graph,命名为“GlowWave”。
  2. 顶点波动:在Graph中右键 → Create Node → 搜索“Position”节点,连到“Add”节点。再创建一个“Sine”节点,用Time驱动,加到Position的Y分量上。
  3. 边缘发光:创建一个“Fresnel Effect”节点,它根据视角与法线的夹角输出0~1的值。边缘处值接近1,正面接近0。
  4. 混合颜色:用“Lerp”节点把基础颜色和发光颜色混合,用Fresnel的输出作为混合因子。
  5. 连接输出:把Lerp的结果连到Fragment的Base Color和Emission端口。

你看,整个过程不需要写一行代码。但Shader Graph背后生成的代码,其实就是我们前面讲的顶点着色器和片段着色器。

关键点:Shader Graph的“Vertex”阶段对应顶点着色器,“Fragment”阶段对应片段着色器。你在Vertex阶段修改Position,就是在改顶点位置;在Fragment阶段改Base Color,就是在改像素颜色。

4.5 避坑指南与性能建议

做渲染管线优化时,我总结了几条经验:

  • 顶点着色器别做复杂计算:顶点数通常比像素数少得多,但每个顶点的计算会影响到后续所有像素。把光照计算放到片段着色器里,除非你明确知道自己在做什么。
  • 片段着色器里少用分支:GPU是并行处理器,分支会导致线程束分化,性能骤降。能用lerp就用lerp,别写if-else。
  • 注意精度:移动端用half精度,PC端用float。我见过一个项目在手机上用float算UV,结果性能直接腰斩。
  • 调试用可视化:把法线、UV、深度值直接输出成颜色,一眼就能看出数据对不对。这个技巧我用了十年,屡试不爽。
优化项 推荐做法 不推荐做法
顶点数量 用LOD控制不同距离的顶点数 所有距离都用高模
纹理采样 用纹理图集减少采样次数 每个材质单独一张纹理
光照计算 用预计算或烘焙 实时计算所有光源
Shader变体 用关键字控制功能开关 为每个效果写独立Shader
我的调试习惯:在Shader Graph里加一个“Debug”模式,用一个开关控制是否输出法线/UV/深度。这样在编辑器里切换一下就能看到数据流,比看代码快多了。

渲染管线就像一条高速公路。顶点着色器是入口收费站,片段着色器是每个路段的监控摄像头。你理解了它们各自能做什么、不能做什么,就能精准地控制GPU的每一分算力。下次遇到渲染问题,别急着搜代码——先想想数据流卡在哪个阶段,然后对症下药。