CPU-GPU 交互流程
关注点:CPU 如何告诉 GPU “要画什么”?这决定了代码架构和内存管理方式。
立刻渲染模式(Immediate Mode)
每一帧都在 Update 或 OnGUI 里写 DrawButton(),简单直观,但如果物体多,CPU 喊口令会累死。
工作原理
- 核心在于“每帧重新绘制”,无需保存图形对象的复杂状态
- 它具有简单直接、易于动态更新的优点,但因缺乏对象层级的记录,CPU负载通常较高
- 每次调用绘制命令(如 glDrawArrays、DrawPrimitive),GPU 就立即执行渲染,结果直接写入帧缓冲区。
执行流程
1 | 绘制物体A → 立即渲染A → 绘制物体B → 立即渲染B → 绘制物体C → 立即渲染C |
特点
- 无状态管理 - 每次绘制独立,不保存场景信息
- 实时响应 - 调用即渲染,适合简单场景
- 内存占用小 - 不需要额外的场景数据结构
保留模式(Retained Mode)
创建一个 GameObject 或 DOM 节点,之后除非它变了,否则你不用管它,内存占用高(要存菜单),但 CPU 负担轻,库会帮你自动优化。
工作原理
- 与立刻模式相对的渲染模式,其核心逻辑是“由图形库管理场景”
- 在这种模式下,应用程序通过构建一个场景模型(如对象树或场景图)来告诉库“要画什么”,然后库会负责具体的绘制细节、状态维护和按需重绘
- 例如在Unity中,场景模型就是对象树,对象树中的每个对象都包含着对象模型(如Mesh)、材质(如Shader)、变换矩阵等信息
执行流程
1 | 构建场景图 → 提交给引擎 → 引擎优化(剔除、排序、批处理)→ 统一渲染 |
特点
- 层次化管理 - 通过场景图管理物体关系
- 自动优化 - 视锥剔除、遮挡剔除、状态排序
- 延迟执行 - 可以批量处理多个绘制调用,适合大型场景的优化
渲染路径
GPU 收到命令后,“具体怎么算光影”?这决定了显卡的计算逻辑和画面表现。
前向渲染(Forward Rendering)
Unity URP 默认使用的是前向渲染
拿一个零件(物体),直接打磨、上色、抛光(算所有光照),彻底做完再拿下一个零件。
但如果零件受 10 盏灯照,每个零件都要算 10 遍,灯多了会慢。
工作原理
- 图形学中最传统、最基础的渲染路径。
- 它的核心逻辑是“遍历物体,直接着色”:针对场景中的每一个几何物体,依次进行顶点变换、光栅化,并直接在像素着色器中计算所有相关的光照效果,最后输出到屏幕缓冲区。
单 Pass 方式
- 一次绘制计算所有光照
- 光源数量有限(通常 4-8 个)
- 性能随光源线性下降
多 Pass 方式
1 | Pass 1: 渲染物体 + 主光源 |
特点
- 直接计算 - 几何和光照同时处理,适合光源较少的场景
- 透明物体友好 - 天然支持透明度排序
- MSAA 原生支持 - 多重采样抗锯齿直接可用
延迟渲染(Deferred Rendering / Deferred Shading)
Unity HDRP 默认使用延迟渲染
第一批人先给零件分类(记录位置、颜色到 G-Buffer)。第二批人最后统一刷漆(对着 G-Buffer 统一算光照)。
不怕灯多,适合 PC 大作;但费内存(G-Buffer 很大),且处理不了半透明物体。
工作原理
它的核心理念是“先记录信息,最后统一算光照”,流程分为以下两个阶段:
- 几何处理阶段 (Geometry Pass):将场景中的每个物体进行顶点变换、光栅化,并记录所有相关的几何信息(如位置、法线、漫反射颜色、粗糙度等),将物体的几何属性存储在一组被称为 G-Buffer (Geometry Buffer) 的高带宽纹理中
- 光照处理阶段 (Lighting Pass):将G-Buffer里的记录的光照信息进行计算,针对屏幕上的每个像素统一计算最终的光照效果并输出到屏幕缓冲区。
几何 Pass
1 | 对于每个物体: |
光照 Pass
1 | 对于每个像素: |
G-Buffer 结构
1 | RT0: RGB = Albedo颜色 A = Metallic |
每个渲染目标(RT)有 4 个通道:R、G、B、A
RT0: 基础材质属性
1 | ┌─────────────────────────────────────────────┐ |
RT1: 表面细节属性
1 | ┌─────────────────────────────────────────────┐ |
RT2: 发光与环境光遮蔽
1 | ┌─────────────────────────────────────────────┐ |
RT3: 深度/位置信息
1 | ┌─────────────────────────────────────────────┐ |
特点
- 解耦几何与光照 - 几何阶段只处理位置、法线等
- 光照与物体数无关 - 光照计算只针对可见像素
- 需要大量显存 - G-Buffer 占用显著
延迟渲染为什么处理不了半透明物体
G-Buffer 只能存储“最前面”的信息
延迟渲染的第一步是把物体的几何信息(颜色、法线、深度等)存入 G-Buffer。
硬件限制:对于屏幕上的每一个像素,G-Buffer 通常只能存储一个物体的属性。
信息丢失:当你画一个半透明物体(如玻璃)时,G-Buffer 会记录玻璃的信息。但半透明物体的本质是让你看到它后面的东西。延迟渲染无法在一个像素位置同时存下“玻璃”和“玻璃后的墙”这两层信息。混合(Blending)需要“即时颜色”
在前向渲染中,当你画半透明物体时,背景已经画好了。你可以直接把玻璃的颜色和背景颜色按比例混合(Alpha Blending)。
在延迟渲染中,第一步根本不计算颜色,只存原始数据。到了第二步统一算光照时,由于 G-Buffer 里只有最前面的玻璃数据,后台已经没有“被挡住的物体”的数据了,因此无法进行物理上正确的混合计算。排序难题
半透明物体必须遵循从后往前的顺序绘制,才能得到正确的视觉叠加效果。
延迟渲染的初衷是“不关心顺序”,它通过深度测试只保留最前面的像素来优化性能。这与半透明物体需要的“多层有序叠加”逻辑完全冲突。
行业通用的解决方案:混合模式 (Hybrid Rendering)
为了解决这个问题,现代引擎(如 Unity 和 Unreal)通常采用混合方案:
- 先用延迟渲染:画完场景中所有的不透明物体。
- 再用前向渲染:在已经算好光照的底图上,再用前向渲染的方式按照从后往前的顺序,把半透明物体“贴”上去。
帧缓冲内存与瓦片渲染(Tile-Based Rendering)
TBR 是一种 GPU 渲染架构 / 帧缓冲处理方式,通过把屏幕划分为小瓦片来组织光栅化和帧缓冲访问。
- TBR 关注的是:GPU 如何在底层处理像素、深度、缓存和内存带宽。
- Retained mode 关注的是:应用如何描述场景,谁负责保存和管理可渲染对象。
- TBR 可以支持 retained mode 的渲染引擎,也可以支持 immediate mode 的渲染流。
IMR:Immediate Mode Rendering
- 定义:每次 API 提交一个三角形,就马上开始光栅化并写入颜色/深度缓冲。
- 特点:渲染顺序与三角形提交顺序一致。
- 问题:
- 读写帧缓冲和深度缓冲时,内存访问非常分散、不可预测。
- 重叠像素(overdraw)会被重复计算、重复写回。
- 即使有缓存,缓存命中率也会因为几何提交顺序和屏幕空间不一致而低。
内存带宽消耗问题
- 传统 IMR 中,三角形按提交顺序处理,导致帧缓冲访问顺序混乱。
- 如果缓存行是线性排列,渲染过程会频繁加载/写回不同位置的数据,导致大量内存带宽浪费。
- 帧缓冲是按屏幕坐标组织的二维像素网格。未压缩情况下,内存里通常按行优先存储:
- 一行像素连续
- 不同行之间的像素不连续 -> 屏幕上的垂直相邻像素在内存中不连续
- 如果缓存行也是“线性排列”,它就只覆盖一段连续内存,而不是一个二维小块。
Tiled Memory(瓦片化内存)
- 核心思想:把帧缓冲按“瓦片”切成多个小二维区域。
- 缓存行不再是线性像素序列,而是覆盖一个小矩形区域。
- 好处:
- 空间局部性更好:同一瓦片内的像素更可能连续访问。
- 缓存命中率增加。
- 可在片上缓存内完成更多渲染操作,减少外部内存访问。
这一步是 TBR 的基础:把大帧缓冲拆成可控的小块,减少外存读写。
Rasterizing within tiles(瓦片内光栅化)
- 进一步优化:不再按照三角形整体顺序光栅化,而是先完成一个瓦片内三角形所有像素,再处理下一个瓦片。
- 一个大三角形跨多个瓦片时,会被分成多段,每段在对应瓦片处理。这样可以避免大三角形跨多瓦片时频繁换入换出缓存。
- 同一个三角形的几何信息被重用多次,但每次只处理与当前瓦片相交的部分。
这就是“先处理瓦片,再处理三角形”的思路。
Binning(几何分箱)
- 在 TBR 中,提交几何体时并不马上光栅化。
- 先进行“分箱”:将三角形归类到它可能覆盖的瓦片中。
- 这一过程需要顶点处理来确定三角形屏幕位置,但不涉及片段着色。这样后续光栅化阶段只需按瓦片读取对应几何列表。
Binning 是把“几何”和“像素”分离,先用几何决定哪些瓦片受影响,再按瓦片单独渲染。
Tile-based rasterization(基于瓦片的光栅化)
- 处理方式:逐瓦片渲染,每个瓦片在片上缓存中独立完成。
- 帧缓冲和深度缓冲只在瓦片处理完后写回一次。
- 优势:
- 外部内存访问减少到“每个瓦片一次写回”。
- 深度缓存可直接在片上使用,甚至不必写回主存。
- 清除操作也可在瓦片内完成。
这正是移动 GPU 的核心高效方案:把对外内存的数据流降到最低。
深度缓冲(Z-buffer)
- 每个像素位置有一个深度值
- 这个深度值表示该像素处距相机最近的物体的深度
- 光栅化三角形时,逐像素进行深度测试:
- 读取该像素在深度缓冲中的当前深度值
- 与新三角形在该像素的深度比较,如果新三角形更近,更新该像素的深度值和颜色
- 一个像素可能被多个三角形覆盖(overdraw),但深度缓冲只保留最近的那个三角形的深度值。
MultiSampling AA(多重采样反锯齿)
- 在瓦片渲染中,多重采样可以在片上缓存里完成采样和 resolve。
- 传统 IMR:多采样需要读写高分辨率帧缓冲(每个像素多个样本),带宽消耗巨大
- 多采样在瓦片缓存内完成(片上内存),只在最后 resolve(合并样本)时写回外部内存。外部内存只存储最终低分辨率结果,带宽大幅降低。
- 瓦片缓存(片上内存)允许在本地处理高频读写操作,只在瓦片完成时与外部内存交互一次,从而把昂贵的带宽开销控制在可控范围内
Deferred shading 与 TBR
- 传统延迟渲染需要先写入多个 G-buffer,再重新读取进行光照,带宽消耗大。
- 如果在瓦片内完成延迟渲染,则:
- G-buffer 数据可保留在片上缓存,光照计算直接在瓦片缓存上进行
- 最终只写出渲染结果,减少外部内存读写
- 带宽成本与普通渲染相当
这说明“TBDR”(Tile-Based Deferred Rendering)特别适合移动端延迟渲染场景。
G-buffer 和 瓦片缓存
- 光照数据(光源参数,如位置、颜色、强度)是 CPU 在应用阶段通过 uniform 变量或缓冲区传给 GPU 的
- GPU 渲染几何到 G-buffer
- 顶点着色器负责把模型空间变换到屏幕空间(通过 MVP 矩阵)
- 片段着色器计算每个像素的几何信息(如位置、法线、颜色),并写入 G-buffer
延迟渲染流程:
- 几何阶段:光栅化 + 片段着色 → 写入 G-buffer(存储几何信息,如位置、法线)。
- 光照阶段:使用 G-buffer 数据进行光照计算 → 输出最终颜色。
TBDR:
- 第一阶段:几何分箱 → 按瓦片光栅化 → 片段着色 → G-buffer 数据留在瓦片缓存(不写回外部内存)。
- 第二阶段:瓦片缓存直接使用这些 G-buffer 数据进行光照计算(无需读取外部 G-buffer)。
TBR 的优势
- 大幅降低帧缓冲带宽。
- 降低功耗、提高渲染速度。
- 移动端共享内存、带宽贵,TBR 是最优方案。
- 对纹理访问也有好处:一次处理一个瓦片,纹理访问局部性增强。
- 相比通用帧缓冲缓存,TBR 需要更少片上缓存空间。
TBR 的限制
- 引入渲染延迟:必须先 binning 再 rasterize。
- 需要双缓冲以避免流水线停顿。
- 对需要全局帧缓冲读取的功能不友好,如屏幕空间光线追踪、某些后处理。
- 几何分箱本身也有成本,顶点着色器瓶颈场景可能受限。
- 切换渲染目标、频繁读写 framebuffer 会迫使瓦片数据 flush。
总结
- 传统 IMR 带宽和功耗太高;瓦片渲染通过“先分箱、后按瓦片渲染”把大帧缓冲操作压缩到片上缓存
- 这对移动 GPU 极为关键,且现代 API(如 Vulkan Subpasses、OpenGL ES Pixel Local Storage)都开始支持这种模式
- 对于多采样和延迟渲染,瓦片化处理能把原本非常昂贵的带宽开销降到可控范围
- 然而瓦片化也有规则和限制,开发者要避免频繁 framebuffer 读写、避免不适合的渲染模式
Credits
https://blog.csdn.net/weixin_70073176/article/details/141165074