Deep Read

渲染流水线完全解析

diagram.svg

完整的渲染流水线是一个高度并行、高度优化的过程。

这里按 数据流动 的顺序拆解:CPU 应用准备 → 驱动与命令提交 → GPU 前端几何处理 → GPU 后端像素处理 → 显示。

 

应用准备(CPU 端)

 

  这一阶段完全在 CPU 上运行,是渲染的"备菜"阶段:把网格、纹理从慢速硬盘读进内存(RAM),解码成 GPU 能理解的原始数据结构(顶点数组、像素位图)。整个过程是串行的,受限于 I/O 和 CPU 解码速度。

  数据从磁盘到 RAM 要走一条完整链路:应用程序(Unity/UE)向操作系统发出文件读取请求(如 ReadFile API),内核的文件系统驱动定位硬盘上的文件(如 Tank.fbxTank_Diffuse.png),通过磁盘控制器读取;数据先被读入系统文件缓存(RAM),再复制到应用进程的内存空间。

进入内存后还要解析(Parsing)成引擎可用的结构:

  • 网格:CPU 上的解析库(如 Assimp 或引擎自定义代码)读取 .fbx 的二进制内容,解码成顶点数组(float3 位置、float2 UV、float3 法线……)和索引数组(uint32)。
  • 纹理:图像库(如 stb_image 或 WIC)读取 .png,执行解压(PNG 用 DEFLATE 算法),解码成 RAM 中一张巨大的未压缩位图——一张 4K RGBA 纹理就是 4096 × 2160 × 4 = 35,389,440 字节。若是 .dds 这种 GPU 原生压缩格式则无需解码,直接把压缩数据块读入内存即可。

驱动与命令提交(CPU → GPU)

  CPU 和 GPU 是两个独立处理器,各自使用独立的物理内存,RAM 中的数据对 GPU 不可见。必须通过图形 API(DX、GL 等)把数据从 RAM 经 PCIe 总线传到显存、创建 GPU 原生资源——这是图形驱动的核心工作,它是 CPU 与 GPU 之间的"桥梁"和"翻译官"。

资源创建与数据传输(RAM → VRAM)

应用通过 图形 API 发起资源创建,驱动接收命令后在 VRAM 中分配所需大小的专用内存块,随后协调 CPU 把 RAM 中的数据经 PCIe 总线,用 DMA(直接内存访问)异步传输到新分配的资源里。DMA 对 CPU 是非阻塞的,传输期间 CPU 可以继续干别的;压缩纹理(.dds)则直接传输压缩块,大幅节省 VRAM 和带宽。

传过去的数据被创建成三类 GPU 原生资源:

  • 顶点缓冲(VBO):顶点列表(位置、UV 等),存放在 VRAM 的高速缓存区。
  • 索引缓冲(EBO):索引列表。VBO/EBO/VAO 三者的职责与绑定关系见 VAO,VBO EBOVAO,VBO EBOVAO、VBO、EBO 是现代 OpenGL 组织几何数据的三个核心对象。要理解它们,关键是抓住一条主线:顶点数据要驻留在显存里、要能被复用、还要能让 GPU 知道怎么解析——这三件事分别对应 VBO、EBO 和 VAO。 三个对象各自的职责 VBO(Vertex Buffer Object,顶点缓冲对象)**:在 GPU 显存中开辟的一块内存,存储顶点本身的数据——坐标、法线、颜色、纹理坐标(UV)等。它是原始数据的容器。 EBO(Element Buffer Object,索引缓冲对象,也叫 IBO):同样是显存中的一块内存,但存的不是顶点数据,而是顶点索引**。绘制时 GPU 按 EBO 里的索引顺序去 VBO 取顶点、组装图元。 VAO(Vertex Array Object,顶点数组对象)**:一个"状态容器",记录 VBO 与顶点着色器输入属性之间的对应关系,以及 EBO 的绑定状态。它本身不存任何顶点或索引数据,只存"怎么用"这些数据的配置。 为什么需要 VBO 和 EBO VBO 解决的是 CPU→GPU 的数据传输瓶颈。 早期的立即模式(immediate m
  • 纹理对象:解码后的像素数据,创建成 GPU 可高效采样的结构。它并非像文件那样存进 VRAM 某个"文件夹",而是一种对程序员不透明的硬件原生数据结构,由驱动和操作系统内存管理器(Windows 上是 WDDM)共同管理。

每帧的渲染准备:剔除、排序、合批(CPU 端)

资源上传是一次性(或流式)的,而这一步每帧都要做。游戏逻辑更新完各物体的变换矩阵后,CPU 在录制命令前要先回答"这一帧画哪些物体、按什么顺序画";

  • 剔除(Culling):把看不见的物体从渲染列表里去掉; 注意这是对象级的过滤(整个物体不提交);后文 GPU 的裁剪(Clipping)是 三角形级 的精确切割。两者是同一个漏斗的两级,并不重复。
    • 视锥剔除用物体包围盒对视锥体六个平面做相交测试,
    • 遮挡剔除进一步去掉被挡住的物体。
  • 排序(Sorting):不透明物体从前往后排,让近处物体先写入深度,后面的片段能被 Early-Z 大量丢弃;透明物体从后往前排,保证混合结果正确。这两条策略为什么有效,在后文 Early-Z 和 Blending 两节能找到出处。
  • 合批(Batching):按材质/Shader/渲染状态给剔除后幸存的物体分组,状态相同的连续绘制甚至合并成一个 Draw Call,减少状态切换和提交开销。

这一步的产出是一份精简、有序的绘制列表,它直接决定接下来要录制多少条命令——也是 CPU 端渲染性能优化(减少 Draw Call)的主战场。

命令列表生成(CPU 端)

现代图形 API(D3D12/Vulkan)中,CPU 不会立即发"绘制!"命令,而是多线程地录制命令列表(Command List)——一个纯 CPU 侧的二进制指令缓冲。CPU 在其中填入 GPU 要执行的所有指令,例如:

  • Cmd_SetPipelineStateObject(...):绑定包含所有着色器(VS/PS)和渲染状态(如深度测试开启)的管线状态对象(PSO)。
  • Cmd_SetGraphicsRootSignature(...):绑定资源布局。
  • Cmd_SetRenderTargets(...):设置绘制目标。
  • Cmd_SetViewport(...):设置视口。
  • Cmd_IASetVertexBuffers(...) / Cmd_IASetIndexBuffer(...):绑定顶点/索引缓冲。
  • Cmd_SetDescriptorHeaps(...):绑定资源描述符堆(含纹理和常量缓冲的"指针")。
  • Cmd_SetGraphicsRootDescriptorTable(...):绑定该物体的 MVP 矩阵(常量缓冲)、漫反射纹理等。
  • Cmd_DrawIndexedInstanced(IndexCount=6000, InstanceCount=1, ...):最后发出绘制指令。

命令提交与 GPU 调动

CPU 录制完一批命令列表后,通过 ID3D12CommandQueue::ExecuteCommandLists 提交,驱动把它放入 GPU 硬件命令队列。GPU 前端有一个专门的命令处理器(Command Processor)硬件单元,不断从队列拉取、解析二进制命令,把具体工作分发给内部各执行单元。

这就是现代渲染的核心解耦:CPU 可以领先 GPU 1–2 帧录制命令,GPU 在后面异步消耗,两者通过 Fence(围栏)对象同步。

GPU 前端处理(几何)

一旦命令处理器读到 DrawIndexedInstanced,硬件流水线全速启动。这一阶段回答的问题是"什么东西在哪里":从读顶点开始,经坐标变换(模型空间 → 裁剪空间)、裁剪掉视锥外的几何,最终把顶点映射到 2D 屏幕空间,为光栅化做好准备。

输入装配器(Input Assembler, IA)[固定功能]

IA 读取 Draw 命令中的索引数量,访问绑定的 IBO 逐个读取索引值(如 0, 1, 2, 0, 2, 3……);再从当前绑定的 PSO 中读出"顶点布局"信息,据此解析 VBO 的原始字节流——例如它知道每个顶点前 12 字节是 POSITION (float3)、接下来 8 字节是 TEXCOORD0 (float2),从而精确抓取每个索引对应的顶点属性。最后把抓取的顶点组装成图元:对 Triangle List 拓扑,每读 3 个索引就组一个三角形,送给 VS。

顶点着色器(Vertex Shader, VS)[可编程]

GPU 有数千个着色器核心,为该 Draw Call 中每个唯一顶点(若启用顶点缓存)分配一个 VS 线程并行执行。输入是来自 IA 的单个顶点数据(局部位置、UV、法线),核心工作是矩阵乘法 ClipPos = P * V * M * LocalPos,把顶点从模型空间变换到 4D 齐次裁剪空间——MVP 各步的推导见 【空间变换】基本变换【空间变换】基本变换有了 [[【空间变换】齐次坐标]] 打底,就可以来看最基础的三种变换:平移、缩放、旋转。它们是模型变换(Model Transformation)的三块积木,把物体从局部空间摆放到世界空间里的正确位置、朝向和大小,靠的就是这三者的组合。 因为统一用了齐次坐标,三种变换都写成 $4\times4$ 矩阵,可以直接连乘合并,最后对每个顶点只做一次矩阵乘法。下面逐个推导,并附上在着色器里的写法。 平移变换 平移就是给每个坐标分量加上一个偏移量,借助齐次坐标的第四列实现: $$ \begin{bmatrix} 1 & 0 & 0 & x \\ 0 & 1 & 0 & y \\ 0 & 0 & 1 & z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = (P_x + x,\ P_y + y,\ P_z + z,\ 1) $$ 对应的着色器代码: float4x4 T = float4x4( 1, 0, 0, _TransForm.x, 0, 1, 0,【空间变换】观察变换【空间变换】观察变换经过 [[【空间变换】基本变换]] 的模型变换,物体已经摆放在世界空间里。接下来要换个视角看世界——把相机放到合适的位置和角度,这一步叫观察变换(View Transformation),也叫视图变换。 打个拍照的比方:先把要拍的东西摆放好,这是模型变换;再挑一个角度架好相机,这是观察变换;最后按下快门把三维场景拍成二维照片,那是后面的 [[【空间变换】投影变换]]。 观察空间 观察空间(View Space)就是以相机原点为坐标原点建立起来的坐标系。它的三个基向量在世界空间里的取值,描述的正是相机的朝向。 按照惯例,观察空间是右手坐标系,相机看向 $-Z$ 方向。也就是说,相机的视线方向对应的是观察空间的 $-Z$ 轴。如果记相机位置为 $ViewPos$、注视目标为 $ViewTarget$,那么视线方向(gaze)指向目标,而观察空间的 $+Z$ 轴与视线相反: $$Z_{view} = -(ViewTarget - ViewPos) = ViewPos - ViewTarget$$ 要唯一确定一个相机,需要三个量: 1. 相机的位置。 2. 相机看向的方向(gaz【空间变换】投影变换【空间变换】投影变换经过 [[【空间变换】观察变换]],顶点已经来到以相机为原点的观察空间。投影变换要做的,是把观察空间里相机能看到的那块区域,映射到一个规整的立方体里,方便后续裁剪。这一步的矩阵叫裁剪矩阵,也叫投影矩阵。本篇沿用 Games101 的思路推导。 先把结论列在前面,推完再回看会更清楚: - 把顶点变换到裁剪空间的目的,是方便做裁剪。 - 正交相机的裁剪空间,就是一个标准立方体(CVV)。 - 推导透视投影时,我们会乘上一个观察空间的 $z$ 值($z_{view}$),这个 $z$ 最终成为裁剪空间的 $w$ 分量。 - 透视相机的裁剪,是判断坐标 $(x, y, z)$ 是否落在 $(-w, w)$ 之内。 裁剪空间 顶点从观察空间变换到裁剪空间(Clip Space,也叫齐次裁剪空间),用的就是投影矩阵。之所以要这块空间,是为了方便对图元做裁剪: - 完全落在这块空间内部的图元,保留; - 完全落在外部的,剔除; - 与边界相交的,裁剪掉超出的部分。 这块空间由视锥体(View Frustum) 决定——它是空间中一块由六个裁剪平面(Clip Planes)围成的区域,界定。每个 VS 线程必须输出一个 SV_Position(裁剪空间位置),并把其他数据(UV、世界空间法线等)打包成 Varyings(可变插值量),传给下一阶段。

曲面细分(Tessellation)[可选]

  • 外壳着色器(Hull Shader):接收一组控制点(Patch),决定这个 Patch 要细分得多精细,输出 Tessellation Factors。
  • Tessellator [固定功能]:硬件单元,根据细分因子在 Patch 内部生成大量新点(及其重心坐标),并定义它们如何连成更小的三角形。
  • 域着色器(Domain Shader)[可编程]:为每个新顶点运行一次,用重心坐标对原始控制点插值(如贝塞尔插值),算出新顶点的最终裁剪空间位置和其他属性。输出格式与 VS 一致。

几何着色器(Geometry Shader, GS)[可编程,可选]

每个完整图元(如一个含 3 顶点的三角形)运行一次。它可以审视图元、决定保留或丢弃,或者(这是它的主要特点)凭空创造新图元——例如把一个点扩展成一个四边形用于粒子渲染。输出顶点同样在裁剪空间。

裁剪(Clipping)[固定功能]

在 4D 齐次裁剪空间中进行。硬件按视锥体定义(裁剪空间中通常是六个平面:-w ≤ x ≤ w-w ≤ y ≤ w0 ≤ z ≤ w)测试三角形:

  • 完全在外:丢弃。
  • 完全在内:保留通过。
  • 跨越裁剪平面:用 Sutherland-Hodgman(逐边裁剪,算法细节见 多边形裁剪算法多边形裁剪算法在光栅化之前,落在可视范围之外的几何体必须被裁掉,否则会浪费光栅化和着色的算力,甚至因为顶点跑到相机背后(w ≤ 0)而算出错误的屏幕坐标。裁剪就发生在这个环节:在裁剪空间里,用视锥体的各个平面去切割图元。这里以二维的窗口裁剪为例,讲解经典的 Sutherland-Hodgman 算法。 目标分析 对一个 Mesh 的一次 Draw 调用,其中的每个三角形相对可视范围会有三种情况,裁剪要分别处理: 1. 三角形完全在可视范围内:所有顶点都保留,原样通过。 2. 三角形与可视范围边界相交:去掉范围外的顶点,并在边界上产生新的交点顶点,重新组成一个(或多个)位于范围内的多边形。 3. 三角形完全在可视范围外:丢弃所有顶点,整个图元被剔除。   第二种情况是裁剪的难点:一个三角形被边界切开后,剩下的部分可能是四边形甚至更多边,需要生成新的顶点来"封住"切口。Sutherland-Hodgman 算法正是用来系统地处理这件事的。 Sutherland-Hodgman 算法 Sutherland-Hodgman 算法也叫逐边裁剪法,核心思路是"分割处理、逐边裁剪":不一次性用)或类似算法精确切割,生成新顶点和一个或多个更小的三角形。这一步必须在 4D 空间进行,才能正确处理透视(即跨越 w = 0 平面的情况)。

透视除法与视口变换 [固定功能]

所有通过裁剪的顶点做透视除法 (x/w, y/w, z/w)。这个除法是"近大远小"透视的数学关键,把视锥体压成立方体,结果是归一化设备坐标(NDC),X/Y/Z 范围在 [-1, 1](D3D 中 Z 为 [0, 1])。

随后硬件读取 Cmd_SetViewport 设置的参数做视口变换,把 NDC 映射到屏幕像素坐标:X(-1..1)映到 [0, 1920],Y(-1..1)映到 [0, 1080](Y 轴可能翻转),Z(0..1)缩放后存为深度缓冲格式。这两步的推导见 【空间变换】屏幕映射(Screen Mapping)【空间变换】屏幕映射(Screen Mapping)经过 [[【空间变换】投影变换]],顶点已经在裁剪空间里了。屏幕映射是这条管线的最后一站,负责把裁剪空间的顶点落到屏幕上具体的像素位置。它包含两步: - 透视除法(Perspective Division) - 视口变换(Viewport Transform) 透视除法 image.png 上图是顶点数据的完整变换流程。局部空间的顶点,依次经过模型矩阵 Model、观察矩阵 View、投影矩阵 Projection,来到裁剪空间,此时满足 $-w_{clip} \leq x, y, z \leq w_{clip}$。接着做透视除法——每个分量都除以 $w$ 分量——就得到了标准设备坐标(NDC),它所在的空间也叫标准视体(Canonical View Volume,CVV)。 为什么这一步能实现近大远小? 关键在 $w$。回顾 [[【空间变换】投影变换]] 的推导,投影矩阵刻意把观察空间的 $z$ 坐标塞进了裁剪空间的 $w$ 分量。于是透视除法(除以 $w$,等于除以 $z$)就让离相机越远、$z$ 越大的顶点,坐标被压缩得越狠——这正是透视效果的来源。这也回扣了 [[【空

GPU 后端处理(像素)

至此 GPU 已知道几何体在屏幕空间的确切 2D 位置,接下来要填充颜色,回答"每个像素是什么颜色"。这是流水线中计算量最大的部分。

光栅化(Rasterization)[固定功能]

硬件接收屏幕空间三角形(三个 2D 顶点),先算出描述三条边的线方程(Edge Equations)和斜率;然后遍历覆盖该三角形的像素网格(通常是并行的 Tile-Based 光栅化),对每个像素中心点用线方程测试它是否位于三条边内部。

对所有"内部"像素,计算其中心相对三个顶点的重心坐标(Barycentric Coordinates),再用它对 VS/DS/GS 输出的所有 Varyings(UV、世界法线等)做透视校正插值,最终为每个通过测试的像素生成一个片段(Fragment)——一个"准像素",携带像素位置、插值后的所有数据和屏幕 Z 深度。

Early-Z / 深度预测试 [固定功能]

关键优化。 在运行昂贵的片段着色器之前,光栅器生成的片段 Z 值先与深度缓冲(Z-Buffer)中该位置已有的 Z 比较(依据 PSO 设定的深度测试函数,如"小于等于")。若测试失败(新片段比已画的更远),立即丢弃,GPU 完全跳过为它运行片段着色器,节省大量计算和带宽。

注意:如果片段着色器会修改深度或使用 discard,Early-Z 通常会被硬件自动禁用。

片段着色器(Fragment / Pixel Shader, PS)[可编程]

GPU 为每个通过 Early-Z 的片段启动一个 PS 线程,输入是来自光栅器的插值数据(精确到像素中心的 UV、法线等)。绝大多数视觉效果在此发生,核心是两件事:

纹理采样:PS 执行 sample 指令,指示 GPU 的**纹理单元(TMU)**用插值 UV 去 VRAM 中绑定的纹理里查颜色。TMU 用绑定的采样器对象决定如何查找——

  • 寻址:UV 超出 [0,1] 时是平铺(Wrap)、镜像(Mirror)还是钳制(Clamp)?
  • 滤波:UV 落在 4 个纹素之间时取最近(Nearest)还是线性插值(Bilinear,推导见 双线性插值算法双线性插值算法双线性插值(Bilinear Interpolation)主要用来解决纹理采样中的图像失真。纹理被放大(Magnification)或缩小(Minification)贴到模型表面时,如果不做合适的插值,就会出现明显的像素化(马赛克)或锯齿(走样的成因见 [[锯齿和抗锯齿]])。 问题来源:UV 坐标与纹素的错位 渲染管线里,我们为模型每个顶点指定 UV 坐标(纹理坐标),它对应纹理图上的一个点。三角形被光栅化成屏幕像素(Fragment)时,每个像素的 UV 是由三角形顶点 UV 插值得到的,结果通常是浮点数,例如 uv(0.35, 0.72)。 而纹理图是由离散的纹素(Texel)组成的,纹素坐标是整数。于是就产生了矛盾:我们必须决定如何用一个浮点 UV 去整数网格的纹理上"取色"。双线性插值正是对这个问题的一种回答。 对照:最近邻采样 最直接的做法是把浮点 UV 四舍五入到最近的整数坐标,直接取那个纹素的颜色,即最近邻采样(Nearest-neighbor Sampling)。 优点**:极快,只需一次取整和一次内存读取。 缺点**:纹理放大时,多个屏幕像素会映射到同一)?
  • Mipmapping:物体很远(一像素覆盖多个纹素)时用高分辨率贴图(会闪烁)还是切到预计算的低分辨率 Mip 层(并可能在两层间做三线性 Trilinear 滤波)?是否用各向异性(Anisotropic)改善斜视角清晰度?

光照计算:用采样到的纹理色(Albedo)、插值法线、常量缓冲里的灯光数据和材质属性(粗糙度、金属度),执行 PBR 等光照模型(见 Physically Based RenderingPhysically Based RenderingPBR 的核心目标不是 100% 物理精确——那是离线渲染的事——而是在物理规律的约束下建立一套可预测的光照模型:美术用真实世界的材质属性(金属度、粗糙度)来创作,材质在任何光照环境下都能表现出一致的效果。整套 PBR 可以拆成三层来理解:先是对现实世界光与物质交互的三个物理观察,然后是把这些观察编码成数学的渲染方程与 BRDF,最后是为了实时跑起来所做的一系列工程近似(尤其是环境光照部分)。 三大物理原理 所有复杂的公式,都是在模拟下面三个现象。 微表面理论 (Microfacet Theory)   宏观上光滑的表面,在微观尺度下都是由无数朝向随机的微小镜面(Microfacets)组成的。粗糙度 (Roughness) 描述的就是这些微观平面朝向的混乱程度:Roughness ≈ 0 时微观平面朝向基本一致,整体像一面大镜子,高光集中而清晰;Roughness ≈ 1 时朝向混乱,光线被散射到四面八方,高光模糊且范围大。 数学上用法线分布函数 (Normal Distribution Function, NDF) 来描述这种混乱程度——它回答的问题是:有多大比例),算出该片段的最终颜色。

输出是一个(或多个,若用 MRT 多重渲染目标)RGBA 颜色。

输出合并(Output Merger / ROP)[固定功能]

流水线的最后关卡,由 ROP(光栅操作单元)硬件执行,接收 PS 输出的颜色和片段 Z:

  • Late-Z Test(若 Early-Z 被禁用):在此再做一次深度测试。
  • 模板测试(Stencil):若启用,对模板缓冲做读/写/比较,是实现阴影体积、高级遮罩、某些后处理的强大掩码技术。
  • 混合(Blending):若启用(通常用于透明物体),片段不直接覆盖后台缓冲颜色,而是按混合状态执行混合方程,例如 SrcBlend=SRC_ALPHA, DstBlend=ONE_MINUS_SRC_ALPHAFinalColor = PS.rgb * PS.a + Dst.rgb * (1 - PS.a)
  • 最终写入:若物体不透明(或混合禁用)且通过所有测试,片段的 RGBA 原子性地写入后台缓冲(Back Buffer)对应像素,同时 Z 值写入深度缓冲相同位置(覆盖旧深度)。

显示(Present)

GPU 对命令列表中的所有 Draw Call 重复"几何处理 → PS 着色输出",直到把所有物体(不透明的、透明的)都画进后台缓冲。当 GPU 完成命令队列中标记为"这一帧结束"的所有工作后,通过 Fence 通知 CPU,CPU 随即调用交换链的 Present(),把 VRAM 中的计算结果呈现到物理屏幕。

开启 V-Sync 时,Present() 会阻塞,直到显示器完成当前刷新周期、进入垂直空白期(V-Blank,即扫描线从右下移回左上的间隙);在 V-Blank 期间,操作系统(DWM 桌面窗口管理器)或驱动安全地把后台缓冲与前台缓冲(显示器正在读取的那个)的指针互换,几乎瞬时。关闭 V-Sync 则立即交换,可能发生在显示器扫描到一半时,导致画面撕裂(Tearing)。G-Sync/FreeSync 是更高级的同步:反过来让显示器等待 GPU,动态调整刷新率。

交换完成后,显示器的时序控制器(TCON)逐行读取新的前台缓冲,经 DisplayPort/HDMI 转成数字信号送到物理像素(LCD、OLED),最终显示成图像。

总结

流水线里流动的数据分两条线,旅程并不相同:

  • 顶点/模型数据:磁盘 .fbx → RAM 顶点/索引数组 → VRAM 的 VBO/EBO → 被 IA 读入后一路变换形态——裁剪空间顶点 → 屏幕空间三角形 → 片段 → 像素。它是流水线的主角,每个阶段都在把它变换成下一个阶段能直接消费的形态。
  • 纹理贴图数据:磁盘 .png/.dds → RAM 位图 → 上传成 VRAM 纹理对象后旅程就暂停了。它不经过几何阶段,而是驻留在显存里,直到片段着色器执行采样指令时才被 TMU 按 UV 读取。

两条线在片段着色器汇合:用插值出的 UV(来自顶点数据)去查纹理(贴图数据),算出最终颜色写入后台缓冲。

阶段数据形态变化执行者关键机制
应用准备磁盘文件 → RAM 数组/位图CPU(串行)文件 I/O、解码
驱动与命令提交RAM 数据 → VRAM 资源 + 命令列表CPU + 驱动DMA 异步传输、剔除/排序/合批、Fence 解耦
GPU 前端(几何)顶点 → 屏幕空间三角形几何单元 + 着色器核心MVP 变换、裁剪、透视除法
GPU 后端(像素)三角形 → 后台缓冲像素光栅器 + PS + ROP重心插值、Early-Z、混合
显示后台缓冲 → 屏幕图像交换链 + 显示器缓冲交换、V-Sync

贯穿所有阶段的设计思想只有一个:让每个环节都不等别人。CPU 用命令列表领先 GPU 1–2 帧录制,DMA 让传输不阻塞计算,数千个着色器核心并行处理顶点和片段,Early-Z 在昂贵计算之前提前丢弃无效工作,双缓冲让绘制和显示互不干扰。理解了这一点,性能优化的思路也就自然浮现——用 Profiler 找到这条链上真正在等待的那一环,而不是凭感觉猜。瓶颈判定和各环节的优化手段见 性能优化大纲性能优化大纲diagram (1).png 前提:先测量,再优化 没有数据支撑的优化都是猜。动手前先用 Profiler 定位瓶颈,CPU/GPU Profiler、Memory Profiler、Frame Debugger 分别覆盖不同维度。 下面列出的所有手段,都是针对已定位的具体问题的备选方案,而不是一份要逐条执行的清单。 判定性能瓶颈 一帧之内发生了什么 一帧由 CPU 和 GPU 分工完成。要判断瓶颈在哪一端,得先知道这一帧里两边各自在做什么、按什么顺序做。 image.png CPU 侧 CPU 侧在主线程上按固定顺序推进: - 物理 >> 脚本逻辑 >> 动画 >> UI 重建 >> 渲染数据准备 渲染准备就是 Profiler 里的 Camera.Render,它做三件事: 剔除**(视锥剔除、遮挡剔除,筛掉看不见的物体); 排序**(不透明物体从前往后、半透明从后往前); 合批** 最后为每个要画的物体生成渲染命令。 Unity 默认开启多线程渲染,主线程生成渲染指令,渲染线程逐条取出指令,调用对应的图形 API(DX/GL),由驱动将命令写入 Comma