深夜的后厨里,备菜师傅把一块牛排切成方形,端端正正摆在盘子的正中央;掌勺师傅则拎起酱汁壶,手腕一抖,浓稠的酱汁均匀地浇在整块牛排上,每一粒焦化的表面都泛着光泽。一盘菜的灵魂来自两者的配合:备菜师傅决定“东西放哪、长什么样”(几何),掌勺师傅决定“每一口吃到什么味”(颜色)。在 GPU 渲染世界里,这两个角色就是顶点着色器(Vertex Shader)与片元着色器(Fragment Shader)。前者负责把三维模型的每个顶点挪到正确位置,后者负责给屏幕上被三角形覆盖的每个“像素格子”涂上最终颜色。两者在一条流水线上接力,缺一不可。读完这篇文章,你会像熟悉自家后厨一样,熟悉这两个着色器的分工、频率、数据传递和优化要点。
对 AI 从业者而言,渲染管线是可视化、数字孪生、仿真引擎的正确率放大器——搞懂这两个着色器,你才看得懂 GPU 里那百万线程到底在忙什么,也能真正掌控从模型到屏幕的全链路。
目录
可视化演示
动态演示顶点着色器和片元着色器有什么的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
顶点着色器和片元着色器有什么:渲染管线 渲染管线中的两个核心阶段 执行频率的根本差异 核心机制可视化 顶点着色器和片元着色器有什么整体架构与关键组件
职责分工:备菜师傅与掌勺师傅
你走进一家中餐厅的后厨:备菜师傅站在案板前,把一块三文鱼切成统一的方块,认认真真地把它们码在盘子里。掌勺师傅接过盘子,在灶台上淋上酱汁、撒上芝麻,再点缀两片薄荷叶。两个师傅一个管“形状和位置”,一个管“颜色和味道”,配合默契。
渲染管线也遵循同样的分工。顶点着色器是备菜师傅,它对每一个顶点(模型的骨架点)执行一次:读取顶点位置、法线等属性,计算它在屏幕上的正确位置,然后输出给下一个环节。片元着色器则是掌勺师傅,它工作在光栅化之后,对每一个片元(覆盖在屏幕上的像素格子)执行一次:计算光照、采样纹理、决定这个格子最终呈现的颜色。两者之间隔着光栅化——像是传菜口,把几何图形“切碎”成海量的小格子。
一盘菜的成败,从来不是某一个师傅单独决定的:备菜师傅切得再好,掌勺师傅乱浇酱汁也白搭;反过来,备菜位置摆歪了,掌勺师傅再用心顾客也不满意。顶点和片元两个着色器,正是这样的“双人舞”。
- 顶点输入模型由一个个顶点组成,每个顶点自带位置、法线、颜色等属性,这是整个渲染的原材料。
- 顶点着色器处理对每个顶点做坐标变换和属性计算,输出裁剪空间坐标和带传递的 varying 变量。
- 图元生成顶点按索引拼成三角形等图元,交给固定管线做后续处理。
- 光栅化切分把三角形离散成覆盖屏幕的片元网格,并生成重心坐标供插值使用。
- 片元着色器上色对每个片元执行光照和纹理采样,输出该片元的颜色值。
执行频率:几千个配菜 vs 百万粒米饭
你点了一份蛋炒饭。备菜师傅只处理了三种配菜:虾仁、鸡蛋、葱花,总共不过几十块。可掌勺师傅要做的,是把每一粒米饭都炒到裹上蛋液、染上酱色——一盘饭有两万粒米,掌勺师傅的工作量是备菜师傅的上千倍。
GPU 里的情况几乎一模一样。一个三角形只有三个顶点,顶点着色器只需要跑三次;可这个三角形在屏幕上可能覆盖几十万个像素,片元着色器就要跑几十万次。在全屏渲染的场景里,片元数量通常是百万量级,而顶点数量常常只有几千到几十万。执行频率的巨大差异,决定了片元着色器是渲染性能的第一关注点。
这种差异来自 GPU 的并行结构:顶点着色器和片元着色器都由大量线程并行执行,但片元着色器的线程数量远超顶点着色器。以 1080p 屏幕为例,一帧画面约有 200 多万个像素,即使只有一半被物体覆盖,片元着色器也要执行 100 万次以上;而一个中等复杂度的模型,顶点数往往只有几千到几万。这也是为什么同样的代码放在片元着色器里,成本可能比放在顶点着色器里高几个数量级——每多一行运算,都要乘以百万级的执行次数。
所以下次做性能优化时,先问自己一句:这条计算需要在每个粒米上做,还是在每块配菜上做?如果能在备菜阶段完成,就绝不要拖到掌勺阶段。
数据传递:varying 传菜员
备菜师傅切好三块三文鱼,递给掌勺师傅时不能只递鱼肉——他还要喊一句:这块是刺身、那块要炙烤、最右边那块要剁碎。这些信息,就是后厨里的“传菜单”。
着色器之间也有类似的传菜员,叫 varying 变量。顶点着色器为每个顶点写入 varying 值(比如颜色、uv、法线),光栅化阶段会把这些值自动插值后交给片元着色器。片元并不需要知道每个顶点的原始计算过程,它只收到一个“已经依据自身位置插值好的结果”。此外还有两种数据:attribute(顶点属性,每个顶点自带的输入)和 uniform(全局常量,整帧渲染中不变,像是餐厅统一规定的营业时间)。其中,attribute 只进顶点着色器,uniform 两个着色器都能读,varying 则担当两个着色器之间的桥梁。
1. Cv0/Cv1/Cv2:三个顶点各自携带的 varying 值,比如颜色、纹理坐标或法线,它们是插值的原料。
2. λ0/λ1/λ2:片元在三角形内的重心坐标,由光栅化阶段计算,表示片元离三个顶点分别有多近。
3. 加权求和:将三个顶点的 varying 值按重心坐标加权相加,得到片元专属的插值结果。
传菜员越是称职,掌勺师傅就越省心。同一个三角形内部,varying 让颜色、纹理坐标平滑过渡,而不是一块块生硬地跳变——这正是物体表面看起来连续自然的关键。你不需要在片元着色器里手动插值,GPU 的固定硬件已经在传菜路上帮你做完了。
坐标变换:备菜师的四步摆盘
备菜师傅并非直接把菜放上桌。他的工序是:从冰箱拿出食材(模型本地坐标),在案板上摆好(世界坐标),再端到顾客面前的正中央(视图坐标),最后由服务员拍照发朋友圈的视角里呈现(裁剪坐标)。这四步在顶点着色器里有一套标准的矩阵组合。
顶点着色器的核心职责,就是完成本地空间到裁剪空间的坐标变换。模型矩阵负责把物体放到世界中合适的位置和朝向;视图矩阵把世界坐标变换到以摄像机为中心的观察空间;投影矩阵再把观察空间的坐标压缩到裁剪空间里,为后续裁剪和屏幕映射做准备。三者连乘,就得到了熟悉的 MVP 矩阵。
1. p_local:顶点在模型本地空间中的坐标,是建模软件里导出的原始位置。
2. M 模型矩阵:把本地坐标平移到世界空间,决定物体在世界中的位置、旋转和缩放。
3. V 视图矩阵:把世界坐标变换到以摄像机为原点的观察空间,相当于把镜头对准物体。
4. P 投影矩阵:把观察空间压缩到裁剪空间,模拟透视近大远小或正交投影。
每个顶点进入顶点着色器,都要走完这一套“冰箱→案板→餐桌→相机”的流程。这也是顶点着色器和片元着色器最关键的区别之一:坐标变换只发生在顶点阶段,片元着色器拿到的已经是光栅化后的屏幕对应位置,主要在颜色上做文章。
颜色计算:掌勺师傅的火候与酱汁
掌勺师傅站在灶台前,面前摆着一块纹理分明的新鲜牛肉。他要做的不是把牛肉炒熟就完事,而是要让牛排在灯光下呈现诱人的焦褐感——先看火候(光照方向),再看食材本身的纹理(贴图),最后决定这一口该是什么味道。
片元着色器的核心工作,就是逐片元计算颜色。它接收插值后的法线、uv 等 varying 数据,结合光照方向和观察方向计算漫反射、高光,再采样纹理得到物体本身的底色,最终混合输出。以简单的 Blinn-Phong 光照模型为例,片元颜色是多次乘加的结果。
1. N 与 L:片元法线与光源方向,点积越大说明表面越正对光源,漫反射越亮。
2. k_d 漫反射系数:控制材质对光的散射强度,颜色由物体固有色决定。
3. H 半程向量:光源方向与视线方向的中间向量,高光的强弱由它与法线的接近程度决定。
4. C_tex 纹理颜色:从贴图中采样得到的底色,相乘后让光照颜色与物体花纹叠加。
如果同样一套光照计算放在顶点着色器里做,叫逐顶点光照(Gouraud),颜色只在顶点处算好再插值,快但会丢失细节;放在片元着色器里逐片元计算,则能呈现更细腻的高光变化,代价是海量计算。掌勺师傅愿意多颠几次勺,就能让每一粒米都均匀入味——这就是片元着色器的价值,也是它的昂贵之处。
性能优化:高峰期先看哪条队伍在堵
晚上七点半,餐厅迎来高峰。备菜台前三个厨师手起刀落,出菜口却越积越多,掌勺师傅的灶前已经排起了长队。你跑去观察,发现配菜堆积如山——瓶颈根本不在备菜,而在掌勺。优化后厨,先定位堵点。
渲染优化的第一原则同样是定位瓶颈阶段。如果帧率低是因为顶点太多、CPU 提交慢,那就是顶点瓶颈,此时减面、合并 Draw Call、做 LOD 更有效;如果顶点很少但全屏片元计算量巨大,那就是片元瓶颈,此时就要降低片元着色器里的光照循环、缩减纹理采样次数、减少动态分支。
结合前文提到的执行频率差异,多数高分辨率场景都容易撞上片元瓶颈。常见的优化手段包括:把部分光照计算上移到顶点着色器,用近似公式减少片元中的复杂函数,尽量不用 if-else 或拆成等价的数学运算,以及使用 mipmap 降低纹理采样缓存压力。与此同时,你可以用 GPU Profiler 一类工具查看每个阶段的耗时占比,而不是靠猜。
回到后厨的比喻:优化不是一句笼统的“多雇人”,而是看哪条流水线堵住了——是备菜台(顶点)还是灶台(片元)。选错了方向,增再多人也只会让堵住的地方更拥挤。
调试实践:试菜要诀
新菜研发时,掌勺师傅每调一次酱汁,都要舀一勺尝一尝:颜色对不对?咸淡合不合适?如果太咸,是酱油多了还是没搅匀?他必须先看到中间状态,才知道下一步该改什么。
着色器调试也是一样。你的最终画面是多项计算叠加后的结果,直接看最终图往往很难定位问题。常用的调试技巧是把中间结果可视化:在顶点着色器里输出纯色位置,把位置信息直接当颜色写出来;或者把 varying 的 uv、法线直接映射到颜色通道上,让“看不见的插值”变成肉眼可见的渐变。工具链方面,RenderDoc、Shader 热重载、GPU Debugger 都能帮你在某一帧里检查每个顶点的输入输出、每个片元的插值结果。
常见错误也常有规律:顶点着色器输出的位置不对,画面会出现三角形撕裂或跑飞;varying 变量忘记声明,片元着色器会收到默认零值;坐标系混淆(比如行向量与列向量混用)会让模型整体错乱;精度不足则会出现闪烁色带。做到“把中间量画出来”,大多数问题在第一步就能暴露。
试菜时先看样,再调味。调试着色器时先可视化,再改代码——这条顺序能省下你无数个“为什么画面全黑”的深夜。
AI 应用场景
游戏引擎植被渲染
阶段:图形渲染管线开发
解决的问题:传统植被动画依赖 CPU 每帧更新顶点位置,模型面数一高就卡顿。利用顶点着色器在 GPU 端模拟风吹摆动,配合片元着色器按视角做透明渐变,能大幅降低 CPU 负载并提升细节。
具体案例:在 Unity URP 中为森林场景编写植被 Shader:顶点着色器按正弦函数带动所有树叶顶点起伏,片元着色器根据透明度贴图和风场强度输出叶片半透明颜色。
数字孪生地形热力图
阶段:数据可视化渲染
解决的问题:城市级 BIM 地形数据动辄上百万网格,逐网格生成颜色会拖慢帧率。顶点着色器负责高程拉伸和 LOD 位移,片元着色器按高程值或温度值映射颜色,实时输出热力图。
具体案例:某智慧园区数字孪生平台加载倾斜摄影模型,利用顶点着色器压缩顶点数据,片元着色器根据芯片温度采样值渲染红蓝渐变热力区。
科学粒子系统上色
阶段:GPU 并行计算与渲染
解决的问题:流场模拟中数百万粒子逐个由 CPU 更新位置极其低效。将粒子位置交给顶点着色器从缓冲区读取并绘制,片元着色器再根据粒子速度、温度等标量场映射颜色。
具体案例:流体 CFD 后处理中,顶点着色器在 GPU 上合并粒子位置与速度场,片元着色器按速度大小输出从蓝到红的渐变,完成一帧千万粒子的云图渲染。
Python 演示
import numpy as np def barycentric(px, py, v0, v1, v2): denom = (v1[1] - v2[1]) * (v0[0] - v2[0]) + (v2[0] - v1[0]) * (v0[1] - v2[1]) l0 = ((v1[1] - v2[1]) * (px - v2[0]) + (v2[0] - v1[0]) * (py - v2[1])) / denom l1 = ((v2[1] - v0[1]) * (px - v2[0]) + (v0[0] - v2[0]) * (py - v2[1])) / denom l2 = 1.0 - l0 - l1 return l0, l1, l2 def run(): verts = [ np.array([-0.8, -0.6]), np.array([0.8, -0.6]), np.array([0.0, 0.8]) ] colors = [ np.array([1.0, 0.2, 0.3]), np.array([0.1, 0.8, 0.4]), np.array([0.4, 0.4, 1.0]) ] width, height = 160, 160 framebuffer = np.zeros((height, width, 3)) for y in range(height): for x in range(width): px = (x + 0.5) / width * 2.0 - 1.0 py = 1.0 - (y + 0.5) / height * 2.0 l0, l1, l2 = barycentric(px, py, verts[0], verts[1], verts[2]) if l0 >= -0.01 and l1 >= -0.01 and l2 >= -0.01: color = l0 * colors[0] + l1 * colors[1] + l2 * colors[2] framebuffer[y, x] = np.clip(color, 0.0, 1.0) with open("output.ppm", "wb") as f: f.write("P6\n%d %d\n255\n" % (width, height)) f.write((framebuffer * 255).astype("uint8").tobytes()) print("saved output.ppm") if __name__ == "__main__": run()
def barycentric(px, py, v0, v1, v2):计算片元在三角形内的重心坐标,这是光栅化阶段的核心公式。denom = (v1[1] - v2[1]) * (v0[0] - v2[0]) + ...分母与两个顶点坐标有关,相当于三角形面积的两倍。l2 = 1.0 - l0 - l1第三个重心坐标由前两个自动补全,保证三者之和为 1。for y in range(height):遍历每个屏幕像素,模拟“海量片元”的逐片元执行。px = (x + 0.5) / width * 2.0 - 1.0把像素索引映射到 [-1, 1] 的归一化设备坐标。if l0 >= -0.01 and ...判断片元是否落在三角形内部,这里略作容差。color = l0 * colors[0] + ...这就是 varying 重心坐标插值:三个顶点颜色按权重混合。framebuffer[y, x] = np.clip(color, 0.0, 1.0)片元着色器输出颜色并写入帧缓冲。f.write("P6\n%d %d\n255\n" % (width, height))把帧缓冲写成简单的 PPM 图像文件。
常见误区
误区渲染时每个像素颜色都在变化,那顶点着色器是不是也像片元着色器一样在每个像素上跑一次?
正解顶点着色器只按顶点执行,一个三角形只有三次机会。屏幕上的颜色由光栅化生成片元,再交给片元着色器逐个上色。可以理解成备菜师傅只处理几十块食材,掌勺师傅才对每一粒米饭负责。
为何常见:因为从最终画面看所有像素都有颜色,初学者很容易把两个着色器的执行粒度混为一谈。
误区片元着色器返回什么颜色,屏幕上就一定会显示什么颜色。
正解片元着色器输出后还有深度测试、模板测试和混合等固定阶段。被遮挡的片元即使算好了颜色也会被丢弃;半透明物体还要与背景色混合。输出只是“候选颜色”,不是最终像素。
为何常见:简单 demo 往往只展示片元着色器的返回值,省略了后续固定管线的存在。
误区片元着色器里能用 varying,说明它把顶点着色器的逻辑重新跑了一遍。
正解varying 不是重算,而是光栅化阶段在三个顶点之间按重心坐标插值的结果。GPU 只做一次线性加权,性能远低于重新执行顶点逻辑。
为何常见:看到“片元也拥有 varying 值”就误以为系统为每个片元重新调用了顶点计算流程。
误区建模师常说面数太高卡顿,于是认为渲染性能只取决于顶点数量。
正解渲染耗时由顶点数和片元数共同决定,且很多场景片元数量远多于顶点。高分辨率下全屏约两百万片元,片元着色器开销往往更大。正确做法是先 profile,看瓶颈到底在顶点阶段还是片元阶段。
为何常见:顶点数的概念直观且建模优化常用减面,容易让人把性能问题都归结到顶点上。
误区既然 GPU 是并行计算,那每个线程独立跑 if 分支,应该和 CPU 一样便宜。
正解GPU 采用 SIMD 架构,一个执行组里的线程共享控制流。当同一组内线程走到不同分支时,GPU 会分别执行所有分支再选择结果,造成“线程发散”。分支越混乱,额外计算越多。
为何常见:CPU 编程里 if 很便宜,把这种惯性思维直接搬到了 GPU 着色器上。
面试题
Q1. 顶点着色器的基本输入单元是什么?
- A.每个像素格子
- B.每个顶点数据← 正确答案
- C.每个纹理贴图
- D.每个场景光源
答案:B。顶点着色器以顶点为最小输入单元,每个顶点执行一次。像素格子是片元着色器的处理对象,纹理和光源都不是独立输入单元。就像备菜师傅一次处理一块食材,而不是处理一粒米。
Q2. 片元着色器的主要输出是什么?
- A.顶点世界位置
- B.片元最终颜色← 正确答案
- C.顶点法线方向
- D.索引缓冲区
答案:B。片元着色器输出该片元的颜色(以及可选深度),供后续深度测试和混合使用。顶点位置和法线是顶点着色器关心的内容,索引缓冲属于网格数据,与片元输出无关。
Q3. varying 变量在光栅化阶段做的主要操作是什么?
- A.直接丢弃一半数据
- B.按重心坐标插值← 正确答案
- C.原样复制给所有片元
- D.做一次加密压缩
答案:B。varying 从顶点着色器输出后,光栅化阶段会按每个片元在三角形内的重心坐标对三个顶点值加权平均,得到连续过渡的插值结果。它既不是丢弃,也不是原样复制。
Q4. 顶点在裁剪空间的坐标由什么组合计算得到?
- A.仅使用模型变换矩阵
- B.仅使用投影变换矩阵
- C.模型-视图-投影组合← 正确答案
- D.法线矩阵替代投影
答案:C。裁剪坐标 = 投影 × 视图 × 模型 × 本地坐标。模型矩阵摆进世界,视图矩阵对准相机,投影矩阵压缩视锥。法线矩阵只用于法线变换,不参与顶点位置输出。
Q5. 关于两个着色器的执行次数,下列说法正确的有哪些?
- A.顶点着色器按顶点数量执行← 正确答案
- B.片元着器按覆盖片元数执行← 正确答案
- C.片元数量通常远多于顶点← 正确答案
- D.两者执行次数总是相等
答案:ABC。顶点着色器每个顶点跑一次,片元着色器每个覆盖片元跑一次。一个三角形只有三个顶点,却可能覆盖数百上千个片元,所以片元数远多于顶点。D 的说法与事实完全相反。
Q6. 下列哪种数据不能由顶点着色器直接读取?
- A.attribute 顶点属性
- B.uniform 全局常量
- C.varying 插值变量← 正确答案
- D.sampler 纹理采样
答案:C。attribute 和 uniform 是顶点着色器的输入,sampler 也可以在顶点阶段采样纹理。varying 在顶点着色器里是输出端数据,不是读取来源;它要到片元着色器阶段才作为输入被消费。
Q7. 下列属于片元着色器性能瓶颈常见原因的有?
- A.全屏片元数量巨大← 正确答案
- B.每个片元复杂光照循环← 正确答案
- C.每个片元多次纹理采样← 正确答案
- D.uniform 上传每帧太频繁
答案:ABC。片元着色器的执行次数等于覆盖片元数,全屏场景动辄百万次;复杂光照和多次纹理采样都会乘以这个百万基数。uniform 上传属于 CPU 到 GPU 的带宽问题,并不是片元着色器自身的执行瓶颈。
Q8. 三角形三顶点颜色分别为红(1,0,0)、绿(0,1,0)、蓝(0,0,1),某片元重心坐标为 λ0=0.2,λ1=0.5,λ2=0.3,插值后的颜色是?
- A.(0.2, 0.5, 0.3)← 正确答案
- B.(0.5, 0.2, 0.3)
- C.(0.3, 0.5, 0.2)
- D.(0.2, 0.3, 0.5)
答案:A。按分量加权:R=0.2×1+0.5×0+0.3×0=0.2,G=0.2×0+0.5×1+0.3×0=0.5,B=0.2×0+0.5×0+0.3×1=0.3,所以插值颜色是 (0.2, 0.5, 0.3)。这正是 varying 重心坐标插值的标准计算方式。
Q9. 降低片元着色器开销的常用手段有哪些?
- A.光照计算迁到顶点着色器← 正确答案
- B.为贴图使用 mipmap 层级← 正确答案
- C.减少着色器动态分支← 正确答案
- D.成倍增加场景光源数
答案:ABC。逐顶点光照用少量顶点计算换取近似效果;mipmap 减少纹理缓存未命中;减少动态分支能规避 GPU 线程发散。增加光源会让每个片元做更多计算,属于反向操作。
Q10. 在实时渲染中,哪个阶段位于片元着色器之后并影响最终像素颜色?
- A.几何光栅化阶段
- B.顶点变换与投影
- C.深度测试与混合← 正确答案
- D.图元装配与裁剪
答案:C。片元着色器输出后,GPU 还要做深度测试剔除被遮挡片元,再做颜色混合处理透明度,最终写入帧缓冲。光栅化、顶点变换、图元装配都发生在片元着色器之前,无法影响“输出之后”的像素合成。