当前位置: 首页 > news >正文

图形学渲染管线全解:从顶点到像素

第一遍:骨架(30 秒版本)

渲染管线= 把3D 顶点数据变成屏幕上像素颜色的一条流水线。

[顶点数据 + Shader + 纹理] ↓ ┌──────────────────┐ │ 1. Vertex Shader │ 处理每个顶点(坐标变换) └──────────────────┘ ↓ ┌──────────────────┐ │ 2. 图元装配 │ 把顶点组装成三角形 └──────────────────┘ ↓ ┌──────────────────┐ │ 3. 裁剪 + 剔除 │ 丢掉屏幕外/背面的三角形 └──────────────────┘ ↓ ┌──────────────────┐ │ 4. 光栅化 │ 三角形 → 一堆像素(fragment) └──────────────────┘ ↓ ┌──────────────────┐ │ 5. Fragment Shader│ 每个像素算颜色 └──────────────────┘ ↓ ┌──────────────────┐ │ 6. 逐片元操作 │ 深度测试、混合、写入 FB └──────────────────┘ ↓ [屏幕像素]

核心洞察:管线是"漏斗形"的——数据量越往后越大(1 个三角形能生成几万个 fragment),所以越靠后的阶段越贵,优化空间越大。


第二遍:每个阶段的详细扒开

阶段 0:输入 —— 我们有什么原料?

在管线开始前,GPU 已经通过 binding point 拿到了这些"原料":

📦 VBO(顶点数据): [顶点1: pos, uv, normal, color] [顶点2: pos, uv, normal, color] [顶点3: pos, uv, normal, color] ... 📦 EBO(索引数据): [0, 1, 2, 2, 3, 0, ...] ← 每 3 个索引组成一个三角形 🖼️ 纹理(在纹理工位上) 📜 Shader Program(在 shader 工位上) 📋 Uniform 数据(matrices、光源、参数...)

阶段 1:Vertex Shader(顶点着色器)

输入:一个顶点(位置 + 各种属性)
输出:变换后的顶点(裁剪空间位置 + 传给下一阶段的属性)

主要做什么?

核心工作:把顶点从模型空间变换到裁剪空间

一个顶点的坐标变换旅程,是图形学最经典的"三次变换":

[模型空间] → M(Model 矩阵) → [世界空间] [世界空间] → V(View 矩阵) → [观察空间] [观察空间] → P(Projection 矩阵) → [裁剪空间] 一步到位: gl_Position = P * V * M * vec4(vertexPos, 1.0);

用现实类比

把这个过程想象成给一个模型拍照:

  1. 模型空间:模型自己内部的坐标系(比如一个人的模型,原点在脚下)
  2. 世界空间:把模型摆到场景里的某个位置(比如人站在房子门口)
  3. 观察空间:相机的视角(以相机为原点)
  4. 裁剪空间:把 3D 投影到 2D 的准备阶段(近大远小的透视)

关键点:并行执行

每个顶点独立跑一遍 Vertex Shader,可以完全并行。GPU 上几千个核心同时算不同的顶点。

Vertex Shader 里能做什么?

  • 坐标变换(主要工作)
  • 顶点动画(骨骼蒙皮 skinning、顶点位移)
  • 计算传给 fragment shader 的插值属性(法线、切线、世界坐标等)
  • 顶点光照(便宜但粗糙的光照方案)

阶段 2:图元装配(Primitive Assembly)

输入:一堆变换后的顶点
输出:一堆三角形(或点、线,取决于 draw 模式)

做什么?

按索引把顶点组装成三角形

举例:

顶点:[v0, v1, v2, v3] 索引:[0, 1, 2, 0, 2, 3] 组装出两个三角形: 三角形A:(v0, v1, v2) 三角形B:(v0, v2, v3)

这个阶段还有个功能:处理图元的连接方式(TRIANGLES / TRIANGLE_STRIP / TRIANGLE_FAN)。

这个阶段基本上是硬件自动完成的,你没什么控制权(几何着色器/曲面细分是例外)。


阶段 3:裁剪 + 剔除(Clipping + Culling)

输入:一堆三角形
输出:留下"值得画"的三角形

3.1 视锥裁剪(Frustum Clipping)

问题:相机看不到的三角形,我们别浪费性能去光栅化了。

裁剪空间里,可见区域是一个[-1, 1]³的立方体(NDC 之前)。三角形分三种情况:

情况 A:完全在视锥内 → 保留 情况 B:完全在视锥外 → 丢弃 情况 C:部分在视锥内 → 切开!生成新的顶点

情况 C 是最麻烦的——三角形被视锥边界切开时,GPU 会生成新的顶点,可能把 1 个三角形变成 2 个甚至 3 个。

3.2 背面剔除(Back-face Culling)

问题:一个封闭模型(比如一个球),背面(远离相机的一半)看不见,不用画。

判断方法:根据三角形顶点的顺时针/逆时针方向判断朝向。

if(三角形是背对相机的)→ 丢弃

Unity 里Cull Back / Cull Front / Cull Off就是控制这个。

3.3 透视除法(Perspective Divide)

裁剪空间 → NDC(Normalized Device Coordinates):

NDC = ClipSpace.xyz / ClipSpace.w

这一步把"近大远小"的效果落实到坐标上。

3.4 视口变换(Viewport Transform)

NDC(范围 [-1, 1])映射到屏幕坐标(比如 [0, 1920] × [0, 1080])。

至此,每个三角形的顶点已经是"屏幕上的位置"了。


阶段 4:光栅化(Rasterization)—— 管线的"炸弹阶段"

输入:屏幕空间的三角形
输出:一大堆 fragment(片元 = 潜在的像素)

什么是 fragment?

Fragment ≠ Pixel

  • Pixel:屏幕上的物理像素(最终显示的东西)
  • Fragment:三角形覆盖某个像素后产生的候选颜色,一个 pixel 可能被多个 fragment 竞争

光栅化做什么?

判断"三角形覆盖了屏幕上的哪些像素",并为每个覆盖的像素生成一个 fragment。

三角形: v0 ▲ / \ / \ / \ /_______\ v1 v2 光栅化后生成: [fragment @ (100, 200)] [fragment @ (101, 200)] [fragment @ (100, 201)] ... 可能几千个 fragment

关键步骤

4.1 覆盖测试(Coverage Test)

对屏幕上每个像素,判断"我在不在这个三角形内?"

开了 MSAA 4x:判断像素里的 4 个采样点分别在不在 → 得到 4-bit 的coverage mask(还记得吗?)

4.2 属性插值(Attribute Interpolation)

这一步至关重要

Vertex Shader 输出的属性(uv、法线、颜色等),只有 3 个顶点上有值。但三角形内部的 fragment 需要各自的属性。

做法:根据 fragment 在三角形里的位置,用重心坐标插值:

v0 (uv = 0,0) ▲ /|\ / | \ / × \ ← × 处的 uv 是 v0/v1/v2 的加权平均 /___|___\ v1 (uv=1,0) v2 (uv=0.5,1)

所有 vertex shader 输出的varying/TEXCOORD,都在这一步被插值

4.3 透视校正插值

一个常被忽略但很重要的细节:插值不能是简单的线性插值,必须考虑透视效果(近大远小对纹理坐标的影响)。GPU 硬件自动处理,但底层实现涉及除以 w。


阶段 5:Fragment Shader(片元着色器)

输入:一个 fragment(带着插值好的属性)
输出:一个颜色(可能还有深度)

主要做什么?

为每个 fragment 计算最终颜色。这是整个管线里最贵的阶段,因为:

  1. Fragment 数量巨大(一个屏幕百万级)
  2. 光照、纹理采样、复杂计算都在这里

常见工作

half4 frag(v2f i) : SV_Target { // 1. 采样纹理 half4 albedo = tex2D(_MainTex, i.uv); // 2. 采样法线贴图 half3 normal = UnpackNormal(tex2D(_BumpMap, i.uv)); // 3. 光照计算(diffuse + specular) half3 lighting = ComputeLighting(normal, i.worldPos); // 4. 混合、后处理 half4 finalColor = albedo * lighting; return finalColor; }

为什么 Fragment Shader 这么贵?

规模的问题:

  • 一个 1080p 屏幕 = 200 万像素
  • 一个物体覆盖屏幕一半 = 100 万个 fragment 要跑 shader
  • 场景有几十个物体 + 重叠 = 几百万到上亿次 shader 执行 / 帧

这就是"移动端最怕 Overdraw"、"Fragment Shader 优化收益最大"的原因

Early-Z(早期深度测试)

优化:如果 GPU 能提前知道"这个 fragment 反正会被深度测试丢弃",就不用跑 fragment shader 了。

触发条件:

  • Shader 里不discard
  • Shader 里不修改深度(SV_Depth)
  • 不开启 alpha blend

这也是为什么discard(Alpha Test)、写深度都会破坏 Early-Z 优化


阶段 6:逐片元操作(Per-Fragment Operations)

输入:Fragment Shader 输出的颜色
输出:最终写入 framebuffer 的像素

这是一系列测试和混合操作,按顺序执行:

6.1 剪裁测试(Scissor Test)

只在指定的矩形区域内渲染(比如小地图、UI 面板范围)。

6.2 模板测试(Stencil Test)

有一个"模板缓冲区"(stencil buffer),可以基于它做各种"遮罩"效果:

  • 镜面反射
  • 描边
  • 阴影体积
  • UI 遮罩
if(stencil_value_at_pixel 满足某条件)通过;else丢弃 fragment;

6.3 深度测试(Depth Test)

核心的可见性判断

if(fragment.depth<depthBuffer[pixel])// 更近通过,可能更新深度缓冲else丢弃 fragment

Unity 里ZTest Less / Greater / LEqual / Always就是控制比较方式,ZWrite On/Off控制是否更新深度缓冲。

这是 Alpha Blend 物体必须关 ZWrite 的原因(否则透明物体会遮挡后面的透明物体)。

6.4 混合(Blending)

如果 fragment 没被丢弃,和 framebuffer 里已有的颜色混合:

finalColor=srcFactor*shaderColor+dstFactor*frameBufferColor;

这就是 Alpha Blend 的实现:

  • Blend SrcAlpha OneMinusSrcAlpha= 半透明混合
  • Blend One One= 加法混合(粒子、发光)
  • Blend DstColor Zero= 乘法混合(阴影)

6.5 写入 Framebuffer

最终颜色写入 framebuffer 对应的像素。这才是"像素被画出来"的时刻


阶段 7:(可选)MSAA Resolve

如果开了 MSAA,framebuffer 存的是每个像素的多个采样点颜色。最后需要"resolve"成最终显示的图:

每个像素的 4 个 sample 颜色 → 平均 → 最终像素颜色

在 TBDR 手机 GPU 上,这一步发生在片上缓存内,只把 resolve 后的结果写到系统内存。这就是"手机 MSAA 免费"的核心。


一张完整流程图

[顶点数据] [索引] [纹理] [Uniforms] │ │ │ │ ▼ │ │ │ ┌────────────────────────────────┐ │ 1. Vertex Shader(每顶点1次) │ ← 可编程 │ 坐标变换 + 属性计算 │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 2. 图元装配 │ ← 硬件固定 │ 按索引组装三角形 │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 3. 裁剪 + 剔除 │ ← 硬件固定 │ 视锥裁剪、背面剔除 │ │ 透视除法、视口变换 │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 4. 光栅化(生成大量 fragment) │ ← 硬件固定 │ 覆盖测试(coverage mask) │ │ 属性插值 │ └────────────────────────────────┘ │ ┌─────┴─────┐ ▼ │ ┌────────────────┐ │ │ Early-Z 测试 │────┘(可能提前丢弃) │(优化路径) │ └────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 5. Fragment Shader(每像素1次) │ ← 可编程,最贵 │ 采样纹理、光照、颜色计算 │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 6. 逐片元操作 │ ← 硬件固定 │ Scissor → Stencil → Depth │ │ → Blend → Framebuffer │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ 7. MSAA Resolve(可选) │ │ 多采样平均 → 最终像素 │ └────────────────────────────────┘ │ ▼ [屏幕像素]

关键"元规律"—— 从管线推导性能

现在你有了完整管线图,可以推导出图形学所有的性能规律:

规律 1:“越靠后越贵”

  • Vertex Shader:每顶点 1 次,通常几万次/帧
  • Fragment Shader:每像素 1 次,可能几千万次/帧

结论:优化的 80% 收益来自减少 Fragment Shader 的执行次数。

规律 2:"减少 Fragment Shader 执行"的方法

  • 视锥剔除(阶段 3):越早剔除越好
  • 遮挡剔除(游戏引擎实现):不画被遮挡的物体
  • Early-Z(阶段 5 前):不该跑的 fragment 提前丢
  • LOD:远处用低模,顶点少 → 光栅化产生的 fragment 也少
  • 不 discard、不写深度:保证 Early-Z 生效
  • 合理排序:不透明物体从前到后画,增加 Early-Z 剔除率

规律 3:“减少状态切换”

  • 阶段 0 的 binding point 切换 = SetPass Call
  • 合批= 减少 binding 切换 = 减少 draw 之间的开销

规律 4:“移动端的额外规律”(TBDR)

  • 阶段 6 写入 framebuffer 时,数据先写到片上 Tile 缓存,再一次性写到系统内存
  • MSAA 在片上缓存内做,几乎免费
  • 切换 RenderTarget 极贵(触发 Tile Store/Load)
  • discard 破坏 HSR(手机版 Early-Z)

规律 5:"Alpha 相关的所有雷区"都来自管线

  • Alpha Test:阶段 5 里 discard → 破坏 Early-Z / HSR
  • Alpha Blend:阶段 6 里需要读 framebuffer 混合 → 不能 Early-Z、需要排序
  • A2C:利用阶段 4 的 coverage mask → 绕过上面两个问题

一个完整的例子:画一个带纹理的立方体

来看看一个立方体的一个顶点、一个像素具体经历了什么:

顶点的旅程

顶点数据: position = (1.0, 1.0, 1.0) (立方体的一个角,模型空间) uv = (1.0, 1.0) normal = (0.577, 0.577, 0.577) ↓ 阶段 1:Vertex Shader pos_clip = P * V * M * (1, 1, 1, 1) = (0.8, 0.6, 0.5, 2.0) (裁剪空间) ↓ 阶段 3.3:透视除法 pos_ndc = pos_clip.xyz / 2.0 = (0.4, 0.3, 0.25) ↓ 阶段 3.4:视口变换 pos_screen = (0.4, 0.3) → (1152, 216) (1080p 屏幕坐标) 这个顶点在屏幕上的位置是 (1152, 216)

一个 fragment 的旅程

光栅化(阶段 4): 三角形覆盖了像素 (500, 400) 在这个像素处,重心坐标插值得到: uv = (0.6, 0.4) normal = (0.7, 0.5, 0.5) worldPos = (2.3, 1.5, 4.0) ↓ 阶段 5:Fragment Shader albedo = tex2D(_MainTex, (0.6, 0.4)) = (0.8, 0.3, 0.3) (红色) lighting = dot(normal, lightDir) = 0.75 finalColor = albedo * lighting = (0.6, 0.22, 0.22) ↓ 阶段 6:深度测试 fragment.depth = 0.6 depthBuffer[(500, 400)] = 0.7 0.6 < 0.7 → 通过,更新 depthBuffer[(500, 400)] = 0.6 ↓ 阶段 6:写入 framebuffer framebuffer[(500, 400)] = (0.6, 0.22, 0.22) 至此,像素 (500, 400) 变成暗红色

一个思考题(如果你能答上,说明彻底理解了)

问题:
一个场景有 1000 个不透明立方体,每个 12 个三角形,平均每个立方体在屏幕上占 100×100 像素。

  1. Vertex Shader 大约执行多少次?
  2. 光栅化生成多少 fragment?
  3. Fragment Shader 大约执行多少次?
  4. 如果你想优化性能,应该优先减少哪个数字?怎么减?
参考答案
  1. VS:1000 × 8 顶点 =8,000 次(立方体共享顶点,实际约 8 个/立方体)
  2. 光栅化产生:1000 × 100×100 =10,000,000 个 fragment
  3. FS 理论上执行 10M 次,但实际因为Overdraw(立方体前后重叠)和 Early-Z(前面挡住后面),真实执行次数可能在 2M ~ 30M 之间
  4. 优先优化 FS 执行次数。方法:
    • 从前到后排序渲染(增加 Early-Z 命中)
    • 遮挡剔除(不画完全被挡的立方体)
    • LOD(远处用小 quad 代替立方体)
    • 简化 Fragment Shader

总结:这张管线图是你理解一切的地图

从今天起,当你看任何图形学/渲染相关的文章、教程、优化技巧、Unity 特性,都可以在这张管线图上定位:

  • “Batching” = 减少管线开始前的状态切换
  • “MSAA” = 阶段 4 的 coverage 多采样
  • “Early-Z” = 阶段 5 前的智能剔除
  • “Alpha Blend” = 阶段 6 的 blending
  • “Post-Processing” = 阶段 6 之后,把 framebuffer 当纹理再走一遍完整管线
  • “Compute Shader” = 完全绕开管线,直接编程 GPU
  • “Deferred Rendering” = 分两次管线执行,第一次只算 G-Buffer

http://www.jsqmd.com/news/1386519/

相关文章:

  • 2026组装线生产厂家选购避坑,家电装配线定制核查安装调试能力 - 寻茫精选
  • 法人委托公证授权委托书怎么办?手把手教你少跑腿、不踩坑! - 指上通
  • PhotoGIMP完整指南:免费把GIMP变成Photoshop界面的配置补丁,一个文件夹搞定
  • 2026企业标讯平台哪里有?正规靠谱服务商盘点、深度解析、选型攻略+避坑FAQ,附**平台服务能力详解 - 商业大观
  • 2026年新疆旅行团怎么选?从行业视角看懂纯玩团的真相 - 新闻快传
  • 杭州设计师一对一专属服务公司推荐 悟空脉爆可放心托付 - 装企精灵GEO
  • 一文搞懂lamp-boot的网关统一鉴权:从原理到实践的完整教程
  • 怎样高效处理PDF文档:PDF补丁丁专业实战方案
  • 自动化装配线哪家做的比较靠谱,2026长三角非标厂家实力横向对比 - 寻茫精选
  • 5分钟快速上手:Hermes WebUI让你的AI助手拥有完美浏览器界面
  • 海牙认证本科学历怎么办?明细流程表,一次办清不返工! - 指上通
  • 探索Giffy Dialog核心组件:从GiffyBottomSheet到EntryAnimation全解析
  • TransUNet医学图像分割:从零开始掌握10个关键技巧
  • AWQ_GPTQ 量化踩坑:为什么量化之后还 OOM?90% 人忽略的几个关键点
  • 小白程序员必看:普通对话、RAG与大模型的本质区别,轻松入门大模型学习
  • 2026年招标平台免费版vs付费版全维度盘点:核心差异解析、靠谱服务商甄选指南+避坑FAQ - 产业观察报
  • 2026 成都岩棉板批发市场优选参考:本地靠谱保温供应商推荐评测 - 深度智识库
  • Montserrat 字体完整使用指南:一款免费开源字体家族的从入门到进阶全攻略
  • 饲料粗纤维测定仪主流品牌汇总:恒美智造代表国内头部厂家 - 专业仪器测评品牌推荐
  • 找装配流水线源头工厂要留心,2026行业热点解析非标产线选购逻辑 - 寻茫精选
  • Jupyter Notebook输出显示优化:高效实践与深度解析
  • MobaXterm中文版终极指南:如何在Windows上快速搭建专业远程开发环境
  • 如何用AutoRemesher实现智能四边形重网格化:免费高效的3D模型优化指南
  • 2026气膜体育馆厂家哪家实力更强?避坑和优选厂家汇总 - 商业新知
  • 从资源焦虑到观影自由:ZyPlayer如何用开源技术重塑你的视频体验
  • 100Days100Projects挑战全解析:如何通过百天项目提升编程技能
  • Snipe-IT 容器化部署实战:IT资产管理系统从两次翻车到十分钟上线的完整记录
  • 2026年标讯服务机构怎么选?靠谱平台盘点、实力解析+签约避坑全指南 - 行业观察网
  • 2026年8月保山漏水维修解答!雨季残留受潮、风雨渗水、墙体返碱发霉怎么修? - 宅安选房屋修缮
  • 5分钟上手react-native-autolink:从安装到实现文本智能链接