1.预渲染简介
2.隐藏相机 + RenderTexture 进行预渲染
3.Unity API进行预渲染
1.预渲染简介
csharp
复制代码
在游戏运行前或加载时, 提前将需要渲染的资源(如模型、纹理、着色器等)上传到GPU, 从而避免在游戏运行时首次使用该资
源时出现卡顿
1).问题来源:
a.当游戏运行时, 一个模型(包括网格、纹理、材质等)第一次被渲染时, CPU需要准备数据并上传到GPU; 这个过程包括:
- 将网格数据(顶点、索引等)上传到GPU的顶点缓冲区和索引缓冲区
- 将纹理数据上传到GPU的纹理资源
- 编译着色器(如果之前没有编译过)并创建GPU上的着色器资源
- 设置常量缓冲区等
b.虽然现代图形API(如DirectX12、Vulkan)支持多线程和异步上传, 但首次上传仍然可能引起主线程的等待, 尤其是当数据
量大时, 会导致帧率下降, 也就是我们常说的"首帧卡顿"
2).解决方案
预加载: 在游戏开始前(如加载界面)或者提前在场景中不可见的地方, 主动触发这些资源的首次上传. 让GPU提前拥有这些
资源; 这样当游戏运行时需要渲染这些资源时, 就不再需要上传, 从而避免卡顿
2.隐藏相机 + RenderTexture 进行预渲染
csharp
复制代码
一般在合适的时机, 比如在过场景显示加载界面时(读条界面), 我们可以利用"隐藏相机 + RenderTexture"的形式偷偷的在
后台预渲染目标对象, 这样做的目的是不仅可以预渲染目标对象, 还可以避免影响玩家体验
a.创建一个新的摄像机(最好只渲染一个专门用于预渲染的层)
b.设置它的targetTexture为一个RenderTexture
c.预渲染时, 激活摄像机, 让它渲染你想要预渲染的对象实例化到对应的位置
- 预加载(Resources、AB包、Addressables、UnityWebRequest等等)
- 预实例化(把想要预渲染的对象实例化到场景中, 最好设置到一个专门用于预渲染的层级)
d.预渲染结束后, 失活摄像机, 避免额外开销
3.Unity API进行预渲染
csharp
复制代码
如果我们不希望创建隐藏相机, 可以通过Unity API "CommandBuffer"(命令缓冲区)触发一次CPU到GPU的数据上传; 通过
Renderer(渲染器)获取材质, 利用材质获取Pass(渲染通道), 将渲染所需的数据提前上传到GPU, 可以有效避免首次渲染时
造成的卡顿
a.该方法的优点:
- 不需要创建隐藏相机, 直接使用CommandBuffer提交绘制命令
- 用很小的RT(默认64 × 64)减少性能消耗
b.并不是所有pass都需要预热, 通常只需要预热那些在运行时可能首次渲染会导致卡顿的pass, 比如:
- 阴影相关pass(ShadowCaster)
- 复杂的光照pass(ForwardAdd)
- 自定义的后期处理pass
csharp
复制代码
/// <summary>
/// 对指定 Renderer 使用 CommandBuffer 进行一次离屏渲染(哑渲染),
/// 以触发指定 Pass 的 Shader 编译、资源上传等预热操作。
/// </summary>
/// <param name="r">需要预热的 Renderer(其材质会被用来渲染)</param>
/// <param name="passName">Shader Pass 名(如 "ForwardBase", "ShadowCaster" 等)</param>
/// <param name="width">临时 RenderTexture 宽度(默认 64),越小越节约性能</param>
/// <param name="height">临时 RenderTexture 高度(默认 64),越小越节约性能</param>
public static void WarmupRenderer(Renderer r, string passName, int width = 64, int height = 64)
{
// 获取 Renderer 使用的共享材质(不会实例化)
Material mat = r.sharedMaterial;
// 查找该材质中指定 Pass 的索引
int pass = mat.FindPass(passName);
if (pass < 0)
{
// 如果没找到该 Pass,打印警告并退出
Debug.LogWarning($"Pass 没有找到: {passName}");
return;
}
// 1. 新建一个"GPU任务清单"(CommandBuffer)
// 就像你写一张购物清单,上面列好要GPU做的所有事,批量执行更高效
// 创建一个 CommandBuffer(命令缓冲区),用来批量提交 GPU 绘制指令
// name名字可以随意自定义,它的作用只是在调试工具中可以显示出来,方便你知道这个缓冲区的作用
CommandBuffer cb = new CommandBuffer { name = $"Warmup Renderer:{r.name}:{passName}" };
// 2.给"临时画布"编一个唯一编号(tempID)
// 相当于给一张临时画纸分配一个唯一的工号,方便GPU快速找到它,不用靠字符串"喊名字"(效率更高)
// 为临时 RenderTexture 申请一个 ID(Shader 属性 ID)
// 把字符串转换成一个唯一的整数ID,这个字符串也可以自定义
int tempID = Shader.PropertyToID("_WarmupRT");
// 3. 申请一张"临时画布"(临时RenderTexture)
// 向Unity借一张指定大小、格式的空白画纸,用完后Unity会自动回收,不用你手动清理
// 申请一个临时 RenderTexture(指定宽高、无 MSAA、ARGB32 格式)
cb.GetTemporaryRT(tempID, width, height, 0, FilterMode.Point, RenderTextureFormat.ARGB32);
// 4. 告诉GPU:接下来的绘制,都画在这张临时画布上
// 设置该临时 RenderTexture 作为渲染目标
cb.SetRenderTarget(tempID);
// 5. 先把画布擦干净(清空颜色和深度信息)
// 就像画画前先把画纸擦白,避免有残留的污渍影响效果
// 清空 RenderTexture(清颜色 & 深度)
cb.ClearRenderTarget(true, true, Color.clear);
// 6. 告诉GPU:把指定的模型(Renderer),用指定的材质,画在这张画布上
// 相当于让GPU"空画一遍"模型,重点不是画出好看的图,而是让GPU熟悉这个绘制流程
// 7. 立刻把这份"任务清单"交给GPU,让GPU马上执行
// 这一步是关键,相当于你把购物清单交给店员,店员开始按清单备货
// 使用该 Renderer 和材质,使用指定索引子网格,指定索引渲染通道 进行绘制
cb.DrawRenderer(r, mat, 0, pass);
// 立即执行这个 CommandBuffer(提交给 GPU)
// 关键步骤
Graphics.ExecuteCommandBuffer(cb);
// 释放临时 RenderTexture 资源(避免显存泄漏)
cb.ReleaseTemporaryRT(tempID);
// 释放命令缓冲区本身
cb.Release();
}
csharp
复制代码
注意事项:
预加载会增加内存占用(GPU内存)和加载时间, 所以需要权衡
不是所有资源都需要预加载, 通常只预加载那些在游戏过程中肯定会用到的、且较大的资源("加载耗时大于3ms")