2、应用阶段:CPU与GPU的分工、Draw Call的概念、场景数据准备

好,咱们正式开始聊渲染管线的第一站——应用阶段

很多人一上来就盯着着色器、盯着GPU怎么画三角形。但我得说一句:应用阶段才是整个管线的起点,也是CPU和GPU第一次握手的地方。你想想看,如果这一阶段的数据没准备好,后面GPU再强也是白搭。

2.1 CPU与GPU的分工:谁该干什么?

先问一个问题:渲染这件事,CPU和GPU到底谁说了算?

答案是:CPU负责“准备”,GPU负责“执行”

CPU干的是管理、调度、数据准备这些活儿。比如:

  • 加载模型文件,解析顶点数据
  • 处理物理碰撞、动画更新
  • 判断哪些物体在视锥体内(视锥剔除)
  • 把最终要渲染的物体打包成“命令”,发给GPU

GPU呢?它只管一件事:拿到命令,拼命画

说白了,CPU是项目经理,GPU是施工队。项目经理把图纸、材料、施工顺序都安排好,施工队只管闷头干。

核心原则:CPU不要替GPU干活,GPU也不要替CPU做决策。各司其职,才能跑得快。

我在项目中遇到过一种典型问题:CPU把大量时间花在了逐顶点计算上。比如在CPU端做蒙皮动画的顶点变换,结果每帧几万个顶点算下来,CPU直接跑满,GPU反而闲着。嗯,这就是分工没做好。

2.2 Draw Call:CPU和GPU之间的“快递单”

接下来聊一个绕不开的概念——Draw Call

什么叫Draw Call?我习惯这么理解:CPU给GPU下的一道“绘制命令”

每次CPU说“嘿,GPU,把这个网格画出来”,这就是一次Draw Call。命令里包含了:

  • 要画哪个网格(顶点缓冲、索引缓冲)
  • 用什么材质(纹理、着色器)
  • 变换矩阵(位置、旋转、缩放)
  • 渲染状态(是否开启深度测试、混合模式等)

你可能会问:一次Draw Call能画多个物体吗?

理论上不行。每个物体通常对应一次Draw Call。但这里有个坑——Draw Call本身是有开销的

我曾经踩过的坑:一个场景里放了5000个小石块,每个石块单独一个网格、单独一次Draw Call。结果CPU每帧光发命令就花了十几毫秒,帧率直接掉到30以下。后来合并成50个批次,帧率瞬间回到60。

所以,减少Draw Call是优化CPU性能的关键。常用的手段有:

  • 静态合批:把多个不动的物体合并成一个网格
  • 动态合批:运行时把共享材质的小物体合并
  • 实例化(Instancing):一次Draw Call画多个相同网格,只传不同的变换矩阵
小技巧:在Unity或Unreal里,打开渲染统计面板,看看每帧的Draw Call数量。移动端建议控制在200以内,PC端可以到1000-2000,但再多就要小心了。

2.3 场景数据准备:顶点、索引、纹理

好,现在CPU决定要画一个物体了。它得准备哪些数据?

2.3.1 顶点数据

顶点是渲染的最小单元。一个顶点不只是“一个点”,它包含:

  • 位置(float3):在模型空间或世界空间中的坐标
  • 法线(float3):用于光照计算
  • 切线(float3):用于法线贴图
  • 纹理坐标(float2):也叫UV,决定纹理怎么贴上去
  • 颜色(float4):顶点颜色,有时用得到

举个例子,一个简单的三角形,三个顶点:

// 三个顶点,每个顶点包含位置和UV
Vertex vertices[] = {
    { {-0.5f, -0.5f, 0.0f}, {0.0f, 0.0f} },  // 左下
    { { 0.5f, -0.5f, 0.0f}, {1.0f, 0.0f} },  // 右下
    { { 0.0f,  0.5f, 0.0f}, {0.5f, 1.0f} }   // 顶部
};

这些数据会被上传到GPU的显存里,存在顶点缓冲(Vertex Buffer)中。

2.3.2 索引数据

你想想看,如果每个三角形都存三个独立顶点,那相邻三角形会重复存很多顶点。比如一个正方形需要两个三角形,共6个顶点,但实际只有4个不同的顶点。

索引缓冲(Index Buffer)就是来解决这个问题的。它存的是顶点的“编号”:

// 4个顶点
Vertex vertices[] = {
    { {-0.5f, -0.5f, 0.0f}, ... },  // 索引0
    { { 0.5f, -0.5f, 0.0f}, ... },  // 索引1
    { { 0.5f,  0.5f, 0.0f}, ... },  // 索引2
    { {-0.5f,  0.5f, 0.0f}, ... }   // 索引3
};

// 两个三角形,用索引表示
uint16_t indices[] = {
    0, 1, 2,  // 第一个三角形
    0, 2, 3   // 第二个三角形
};

这样,GPU只需要处理4个顶点,而不是6个。节省了显存,也节省了带宽。

记住:顶点缓冲存“数据”,索引缓冲存“连接关系”。两者配合,才能画出完整的网格。

2.3.3 纹理数据

纹理就是贴在模型表面的图片。但GPU不直接认jpg或png,它需要把图片解码成像素数据(texels),然后上传到显存中的纹理对象

纹理的常见参数:

  • 分辨率:如1024x1024、2048x2048
  • 格式:RGBA8、BC7、ETC2等压缩格式
  • Mipmap:预先生成的一系列缩小版本,用于远处物体
  • 采样方式:线性过滤、最近点采样、各向异性过滤
注意:纹理加载是CPU的活儿,但纹理采样是GPU的活儿。我曾经见过一个项目,每帧都在CPU端重新解码纹理,结果内存带宽爆炸。正确的做法是:在加载阶段一次性上传到GPU,之后CPU就不再碰纹理数据了

2.4 知识体系总览

下面这张图,我把应用阶段的核心逻辑画出来了。你可以看到数据从CPU流向GPU的完整路径:

CPU 应用阶段 场景数据准备 顶点 / 索引 / 纹理 视锥剔除 只保留可见物体 合批处理 减少 Draw Call 发出 Draw Call 命令 + 数据引用 GPU 渲染管线 顶点着色器 变换顶点位置 光栅化 生成片元 片元着色器 计算颜色 输出合并 写入帧缓冲 GPU 显存 顶点缓冲 位置/法线/UV 索引缓冲 三角形连接 纹理对象 像素数据 帧缓冲 最终输出图像

从这张图可以看得很清楚:CPU负责准备和调度,GPU负责执行和渲染,显存是两者之间的数据仓库。整个流程环环相扣,哪一环慢了,都会拖累整体帧率。

2.5 小结

应用阶段虽然看起来“不涉及渲染”,但它是整个管线的基石。我个人习惯在项目初期就花精力把这一块理清楚:

  • 明确CPU和GPU的分工边界
  • 控制Draw Call数量,避免CPU成为瓶颈
  • 顶点、索引、纹理数据要提前准备好,上传到GPU后尽量不动

把这些基础打牢了,后面的渲染阶段才能跑得顺畅。嗯,今天就先聊到这儿。


公众号:蓝海资料掘金营,微信deep3321