渲染管线总览:从CPU到GPU的完整数据流

大家好,我是你们这趟渲染之旅的向导。今天咱们聊聊整个课程的地基——渲染管线。

说白了,渲染管线就是「数据怎么从CPU走到GPU,最后变成你屏幕上那幅画」的完整流程。我见过不少新手,一上来就扎进各种光照模型、PBR参数里,结果遇到性能瓶颈时完全不知道瓶颈在哪。嗯,这就是典型的「只见树木,不见森林」。

我个人习惯,在接触任何新引擎或新项目时,第一件事就是先把它的渲染管线摸清楚。这就像开车前先看仪表盘,心里有数。

一、CPU端:准备阶段

渲染的起点,永远在CPU。这里干的是「组织调度」的活。

  • 视锥剔除(Frustum Culling):相机看不到的东西,直接扔掉。我在项目中遇到过,有一次场景里放了几千个静态网格体,帧率掉到20。一查,剔除没开,所有物体都送进了渲染队列。开了之后,直接回到60帧。
  • 遮挡剔除(Occlusion Culling):就算在视锥内,被墙挡住的东西也不画。Unreal用的是硬件遮挡查询(HW Occlusion Queries),这个后面会细讲。
  • 排序与合批:把同材质、同网格的物体排在一起,减少GPU的绘制调用(Draw Call)。
核心要点:CPU阶段的目标是「少送」。送得越少,GPU压力越小。

二、从CPU到GPU:数据搬运

CPU准备好数据后,通过图形API(DirectX 12 / Vulkan)把数据塞进GPU显存。这里有个关键概念——命令缓冲区(Command Buffer)

CPU把绘制命令写进一个缓冲区,GPU异步地读取执行。这有点像流水线:CPU是备料员,GPU是加工员。备料员不能等加工员干完活再备下一批,否则效率极低。

我曾经踩过一个坑:在CPU端频繁更新顶点缓冲区,导致CPU和GPU之间出现同步等待。帧率直接腰斩。后来改成双缓冲(Double Buffering),问题解决。

三、GPU端:几何阶段

数据到了GPU,第一站是几何阶段。这里处理的是「形状」。

  1. 顶点着色器(Vertex Shader):每个顶点跑一遍。做坐标变换、顶点动画。比如草被风吹动,就是在这里算的。
  2. 曲面细分(Tessellation):把粗糙的模型变精细。Unreal里叫Tessellation,一般用在地形或角色皮肤上。
  3. 几何着色器(Geometry Shader):可以增删顶点。说实话,现在用得少了,性能开销大。Unreal里基本不用。
  4. 裁剪与屏幕映射:把3D坐标变成2D屏幕坐标。超出屏幕的部分直接裁掉。
小提示:顶点着色器是「每个顶点执行一次」,不是「每个像素」。所以顶点数越少,这里越快。但顶点太少,模型又会显得粗糙。这是个经典的权衡。

四、GPU端:光栅化阶段

几何阶段输出的是三角形,光栅化阶段把它们变成「像素碎片」(Fragment)。

你想想看,一个三角形在屏幕上可能覆盖几百个像素。光栅化就是算出「哪些像素被这个三角形覆盖了」。每个被覆盖的像素,都会生成一个Fragment。

这里有个重要概念——像素着色器(Pixel Shader),也叫片段着色器。每个Fragment跑一遍。光照计算、纹理采样、颜色混合,全在这里。

注意:Fragment的数量不等于屏幕分辨率。因为一个像素可能被多个三角形覆盖(比如半透明物体叠加),每个三角形都会生成一个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();             // 混合输出
}
记住:CPU和GPU是异步工作的。CPU提交完一帧的命令后,立刻开始准备下一帧。GPU则在后台慢慢渲染。这就是为什么帧率能跑到60甚至120。

八、避坑指南

我曾经接手过一个项目,场景里放了几百个独立的静态网格体,每个都带自己的材质实例。结果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线程这三者之间的关系,其实挺有意思的。