2.8 预渲染

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")
相关推荐
老朱佩琪!2 小时前
Unity代理模式
unity·游戏引擎·代理模式
老朱佩琪!6 小时前
Unity命令模式
unity·游戏引擎·命令模式
世洋Blog7 小时前
Unity编辑器基础
unity·c#·编辑器·游戏引擎
老朱佩琪!7 小时前
Unity责任链模式
unity·设计模式·责任链模式
WarPigs7 小时前
Unity NetCode for GameObject笔记
笔记·unity·游戏引擎
qq_399407181 天前
2025年Unity国际版下载及安装
unity·游戏引擎
鹿野素材屋1 天前
Unity做出果冻胸部的效果
unity·游戏引擎
两水先木示1 天前
【Unity】坐标转换(屏幕坐标、世界坐标、UI坐标)
unity·游戏引擎·空间转换
老朱佩琪!1 天前
Unity模板方法模式
unity·游戏引擎·模板方法模式