渲染管线总览:从CPU到GPU的完整数据流
大家好,我是你们这趟渲染之旅的向导。今天咱们聊聊整个课程的地基——渲染管线。
说白了,渲染管线就是「数据怎么从CPU走到GPU,最后变成你屏幕上那幅画」的完整流程。我见过不少新手,一上来就扎进各种光照模型、PBR参数里,结果遇到性能瓶颈时完全不知道瓶颈在哪。嗯,这就是典型的「只见树木,不见森林」。
我个人习惯,在接触任何新引擎或新项目时,第一件事就是先把它的渲染管线摸清楚。这就像开车前先看仪表盘,心里有数。
一、CPU端:准备阶段
渲染的起点,永远在CPU。这里干的是「组织调度」的活。
- 视锥剔除(Frustum Culling):相机看不到的东西,直接扔掉。我在项目中遇到过,有一次场景里放了几千个静态网格体,帧率掉到20。一查,剔除没开,所有物体都送进了渲染队列。开了之后,直接回到60帧。
- 遮挡剔除(Occlusion Culling):就算在视锥内,被墙挡住的东西也不画。Unreal用的是硬件遮挡查询(HW Occlusion Queries),这个后面会细讲。
- 排序与合批:把同材质、同网格的物体排在一起,减少GPU的绘制调用(Draw Call)。
二、从CPU到GPU:数据搬运
CPU准备好数据后,通过图形API(DirectX 12 / Vulkan)把数据塞进GPU显存。这里有个关键概念——命令缓冲区(Command Buffer)。
CPU把绘制命令写进一个缓冲区,GPU异步地读取执行。这有点像流水线:CPU是备料员,GPU是加工员。备料员不能等加工员干完活再备下一批,否则效率极低。
我曾经踩过一个坑:在CPU端频繁更新顶点缓冲区,导致CPU和GPU之间出现同步等待。帧率直接腰斩。后来改成双缓冲(Double Buffering),问题解决。
三、GPU端:几何阶段
数据到了GPU,第一站是几何阶段。这里处理的是「形状」。
- 顶点着色器(Vertex Shader):每个顶点跑一遍。做坐标变换、顶点动画。比如草被风吹动,就是在这里算的。
- 曲面细分(Tessellation):把粗糙的模型变精细。Unreal里叫Tessellation,一般用在地形或角色皮肤上。
- 几何着色器(Geometry Shader):可以增删顶点。说实话,现在用得少了,性能开销大。Unreal里基本不用。
- 裁剪与屏幕映射:把3D坐标变成2D屏幕坐标。超出屏幕的部分直接裁掉。
四、GPU端:光栅化阶段
几何阶段输出的是三角形,光栅化阶段把它们变成「像素碎片」(Fragment)。
你想想看,一个三角形在屏幕上可能覆盖几百个像素。光栅化就是算出「哪些像素被这个三角形覆盖了」。每个被覆盖的像素,都会生成一个Fragment。
这里有个重要概念——像素着色器(Pixel Shader),也叫片段着色器。每个Fragment跑一遍。光照计算、纹理采样、颜色混合,全在这里。
五、GPU端:输出合并阶段
最后一步,把Fragment的颜色写入帧缓冲区(Frame Buffer)。
- 深度测试(Depth Test):离相机近的覆盖远的。不透明的物体用这个。
- 模板测试(Stencil Test):用来做特殊效果,比如镜子、轮廓线。
- 混合(Blending):半透明物体的颜色叠加。比如玻璃、烟雾。
Unreal里,最终输出的帧缓冲区其实不止一个。有颜色缓冲区、深度缓冲区、还有各种G-Buffer(用于延迟渲染)。这个后面会专门讲。
六、Unreal的渲染架构特点
Unreal的渲染架构,说白了就是「延迟渲染为主,前向渲染为辅」。
| 特性 | 延迟渲染(Deferred) | 前向渲染(Forward) |
|---|---|---|
| 光源数量 | 支持大量动态光源 | 光源数量受限 |
| 半透明物体 | 不支持(需回退到前向) | 原生支持 |
| 性能瓶颈 | 显存带宽(G-Buffer读写) | Draw Call数量 |
| Unreal默认 | 是(4.18+默认) | 移动端/VR常用 |
我个人习惯,做PC/主机项目时直接用延迟渲染。做移动端或VR时,会切到前向渲染,因为显存带宽是移动设备的硬伤。
七、一个完整的帧流程(伪代码)
// CPU端
void RenderFrame() {
// 1. 场景更新
UpdateScene(); // 移动物体、播放动画
// 2. 剔除
FrustumCulling(); // 视锥剔除
OcclusionCulling(); // 遮挡剔除
// 3. 排序与合批
SortByMaterial(); // 按材质排序
MergeDrawCalls(); // 合批
// 4. 提交命令
CommandBuffer cmd;
cmd.Draw(MeshA, MaterialX);
cmd.Draw(MeshB, MaterialX);
cmd.Draw(MeshC, MaterialY);
SubmitToGPU(cmd);
}
// GPU端(并行执行)
void GPUPipeline() {
// 几何阶段
VertexShader(); // 每个顶点
Tessellation(); // 可选
Clipping(); // 裁剪
// 光栅化
Rasterize(); // 三角形→Fragment
// 像素阶段
PixelShader(); // 每个Fragment
DepthTest(); // 深度测试
Blending(); // 混合输出
}
八、避坑指南
我曾经接手过一个项目,场景里放了几百个独立的静态网格体,每个都带自己的材质实例。结果Draw Call飙到3000+,帧率惨不忍睹。
后来怎么解决的?
- 把同材质的物体合并成一个静态网格体(Static Mesh Merging)。
- 用Hierarchical LOD(HLOD)把远处的多个物体合并成一个低模。
- 开启Instanced Static Mesh(ISM),让GPU一次绘制多个相同物体。
嗯,从那以后,我每次做场景搭建,都会先看一眼Draw Call数量。超过2000就开始警惕。
九、总结
渲染管线,说白了就是一条流水线。CPU负责「准备什么画」,GPU负责「怎么画」。两者配合得好,画面又美又流畅。配合不好,要么CPU在等GPU,要么GPU在等CPU。
我个人建议,刚开始学渲染的同学,先把这张「地图」刻在脑子里。以后遇到任何渲染问题,都能快速定位到是哪个环节出了岔子。
下一章,咱们会深入Unreal的渲染线程架构,看看CPU端到底是怎么并行工作的。到时候你会发现,Unreal的渲染线程、游戏线程、RHI线程这三者之间的关系,其实挺有意思的。