43.预渲染Shader变体与CommandBuffer

43.性能优化-CPU-脚本-预渲染Shader变体与CommandBuffer


43.1 知识点

知识回顾:Shader 关键字和变体

Shader Variant(变体) 指的是 Shader 在特定关键字组合下编译出的一个版本。不同关键字(Keywords)和设置组合会生成不同变体,并以二进制形式存储在构建文件中供运行时使用。

说得更直白:一个 Shader 会基于关键字组合编译出多个版本,运行时按配置选择匹配的变体。

举例: 一个 Shader 有两个关键字,每个关键字有“启用/禁用”两种状态,最终变体数量为 2^2 = 4
变体 1:无关键字(默认)
变体 2:启用关键字 1
变体 3:启用关键字 2
变体 4:同时启用关键字 1 和关键字 2

Unity 的一些内置功能会隐式生成变体,例如:光照模式、雾效、渲染管线等。

预热 Shader 变体

第一次用到某个 Shader 变体可能会卡顿,提前编译可避免首次使用时的卡顿。

预热方式: 使用 Shader Variant Collection(着色器变体集合)进行预编译:

  1. Project 窗口右键 → Create → Shader → Shader Variant Collection。
  2. Inspector 中手动添加需要预热的 Shader。
  3. 脚本中引用集合,调用 WarmUp() 进行预编译。

注意: 预热应放在合适时机进行,例如加载界面,避免关键帧卡顿。

创建变体集合:

添加需要预热的 Shader:

脚本里声明集合并进行预热:

public ShaderVariantCollection shaderVariantCollection;

void WarmUpShaderVariants()
{
    // 步骤 1:确保集合已绑定且尚未预热
    if (shaderVariantCollection == null || shaderVariantCollection.isWarmedUp)
        return;

    // 步骤 2:在合适时机进行预热(如加载界面)
    shaderVariantCollection.WarmUp();
}

利用 CommandBuffer 相关 API 进行预渲染

不创建隐藏相机时,可以使用 CommandBuffer 做一次“离屏哑渲染”,触发 CPU → GPU 数据上传与 Shader Pass 编译。

预渲染方式: 用 Renderer 获取材质,再用材质获取 Pass,借助 CommandBuffer 执行一次离屏绘制。

好处:

  1. 不需要创建相机,直接用 CommandBuffer 提交绘制命令。
  2. 可用很小的临时 RT(默认 64x64)减少开销。
  3. 不影响游戏画面。
  4. 可批量调用,对不同 Renderer / Pass 预热。
using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// 对指定 Renderer 使用 CommandBuffer 进行一次离屏渲染(哑渲染),
/// 以触发指定 Pass 的 Shader 编译、资源上传等预热操作。
/// </summary>
public static void WarmupRenderer(Renderer targetRenderer, string passName, int width = 64, int height = 64)
{
    // 步骤 1:获取共享材质(不会实例化材质)
    Material material = targetRenderer.sharedMaterial;

    // 步骤 2:查找指定 Pass
    int passIndex = material.FindPass(passName);
    if (passIndex < 0)
    {
        Debug.LogWarning($"Pass 没有找到: {passName}");
        return;
    }

    // 步骤 3:创建命令缓冲区并命名(便于调试定位)
    CommandBuffer commandBuffer = new CommandBuffer
    {
        name = $"Warmup Renderer:{targetRenderer.name}:{passName}"
    };

    // 步骤 4:申请临时 RenderTexture 作为离屏渲染目标
    int temporaryRenderTargetId = Shader.PropertyToID("_WarmupRT");
    commandBuffer.GetTemporaryRT(temporaryRenderTargetId, width, height, 0, FilterMode.Point, RenderTextureFormat.ARGB32);

    // 步骤 5:设置渲染目标并清屏
    commandBuffer.SetRenderTarget(temporaryRenderTargetId);
    commandBuffer.ClearRenderTarget(true, true, Color.clear);

    // 步骤 6:执行一次离屏绘制
    commandBuffer.DrawRenderer(targetRenderer, material, 0, passIndex);

    // 步骤 7:提交命令缓冲区(关键步骤)
    Graphics.ExecuteCommandBuffer(commandBuffer);

    // 步骤 8:释放临时 RT 和命令缓冲区,避免资源泄漏
    commandBuffer.ReleaseTemporaryRT(temporaryRenderTargetId);
    commandBuffer.Release();
}

43.2 知识点代码

Lesson43_性能优化_CPU_脚本_预渲染Shader变体与CommandBuffer.cs

using UnityEngine;
using UnityEngine.Rendering;

public class Lesson43_性能优化_CPU_脚本_预渲染Shader变体与CommandBuffer : MonoBehaviour
{
    public ShaderVariantCollection shaderVariantCollection;

    void Start()
    {
        #region 知识回顾 Shader关键字和变体

        //Shader Variant(变体)指的是一个Shader的特定配置
        //通过不同的关键字(Keywords)和 设置组合 来实现不同的效果
        //每一种关键字组合或设置都会生成一个独立的Shader变体
        //最终以二进制形式存储在构建文件中供运行时使用

        //说人话:
        //Shader变体就是基于一个Shader文件当中的代码,编译生成多个版本的Shader
        //它基于关键字来生成各种不同的版本

        //举例说明
        //一个Shader中有两个关键字
        //每个关键字有两种状态(启用或禁用)
        //最终生成的变体数量为2²=4
        //变体 1:没有启用任何关键字(默认)。
        //变体 2:启用了 关键字1。
        //变体 3:启用了 关键字2。
        //变体 4:同时启用了 关键字1 和 关键字2
        //运行时,Unity会根据具体设置选择与之匹配的变体来使用

        //Unity的一些内置功能会隐式的生成变体
        //比如
        //光照模式
        //雾效
        //渲染管线
        //等等

        #endregion

        #region 知识点一 预热Shader变体

        //第一次用到某个Shader的变体可能会卡顿
        //提前编译可以避免第一次使用时的卡顿

        //预热方式
        //利用Shader Variant Collection(着色器变体集合)进行着色器预编译
        //1.在 Project 窗口 右键 → Create → Shader → Shader Variant Collection(着色器变体集合)
        //2.在Inspector窗口中手动添加想要预热的Shader
        //3.在脚本中获取到Shader Variant Collection 利用其中的API WarmUp() 进行预编译

        if (shaderVariantCollection != null && !shaderVariantCollection.isWarmedUp)
        {
            //对Shader去进行预热(预编译) 可以避免在运行时使用时造成卡顿
            shaderVariantCollection.WarmUp();
        }

        //注意:
        //预热Shader还是需要在合适的时机进行,比如加载界面时

        #endregion

        #region 知识点二 利用CommandBuffer相关API进行预渲染

        //如果我们不希望创建隐藏摄像机
        //那么我们可以利用这种方式进行预渲染
        //可以直接以脚本命令触发一次CPU到GPU的通信

        //预渲染方式
        //利用Unity中的CommandBuffer(命令缓冲区)进行一次CPU到GPU的数据上传
        //通过Renderer(渲染器)获取到材质,再利用材质获取到Pass(渲染通道)
        //利用这些数据进行一次预热,可以有效避免首次渲染时造成的卡顿

        //这样做的好处是
        //1.不需要创建相机,直接用 CommandBuffer 提交绘制命令
        //2.用很小的 RT(默认 64×64)减少性能消耗
        //3.不会影响游戏画面
        //4.可批量调用,对不同 Renderer 或 Pass 预热

        #endregion
    }

    /// <summary>
    /// 对指定 Renderer 使用 CommandBuffer 进行一次离屏渲染(哑渲染),
    /// 以触发指定 Pass 的 Shader 编译、资源上传等预热操作。
    /// </summary>
    /// <param name="renderer">需要预热的 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 renderer, string passName, int width = 64, int height = 64)
    {
        // 获取 Renderer 使用的共享材质(不会实例化)
        Material material = renderer.sharedMaterial;

        // 查找该材质中指定 Pass 的索引
        int pass = material.FindPass(passName);
        if (pass < 0)
        {
            // 如果没找到该 Pass,打印警告并退出
            Debug.LogWarning($"Pass 没有找到: {passName}");
            return;
        }

        // 创建一个 CommandBuffer(命令缓冲区),用来批量提交 GPU 绘制指令
        // name 名字可以随意自定义,它的作用只是在调试工具中可以显示出来,方便你知道这个缓冲区的作用
        CommandBuffer commandBuffer = new CommandBuffer { name = $"Warmup Renderer:{renderer.name}:{passName}" };

        // 为临时 RenderTexture 申请一个 ID(Shader 属性 ID)
        // 把字符串转换成一个唯一的整数 ID,这个字符串也可以自定义
        int tempID = Shader.PropertyToID("_WarmupRT");

        // 申请一个临时 RenderTexture(指定宽高、无 MSAA、ARGB32 格式)
        commandBuffer.GetTemporaryRT(tempID, width, height, 0, FilterMode.Point, RenderTextureFormat.ARGB32);

        // 设置该临时 RenderTexture 作为渲染目标
        commandBuffer.SetRenderTarget(tempID);

        // 清空 RenderTexture(清颜色 & 深度)
        commandBuffer.ClearRenderTarget(true, true, Color.clear);

        // 使用该 Renderer 和材质,使用指定索引子网格,指定索引渲染通道 进行绘制
        commandBuffer.DrawRenderer(renderer, material, 0, pass);

        // 立即执行这个 CommandBuffer(提交给 GPU)
        // 关键步骤
        Graphics.ExecuteCommandBuffer(commandBuffer);

        // 释放临时 RenderTexture 资源(避免显存泄漏)
        commandBuffer.ReleaseTemporaryRT(tempID);

        // 释放命令缓冲区本身
        commandBuffer.Release();
    }
}

知识回顾 Shader关键字和变体
//Shader Variant(变体)指的是一个Shader的特定配置
//通过不同的关键字(Keywords)和 设置组合 来实现不同的效果
//每一种关键字组合或设置都会生成一个独立的Shader变体
//最终以二进制形式存储在构建文件中供运行时使用

    //说人话:
    //Shader变体就是基于一个Shader文件当中的代码,编译生成多个版本的Shader
    //它基于关键字来生成各种不同的版本

    //举例说明
    //一个Shader中有两个关键字
    //每个关键字有两种状态(启用或禁用)
    //最终生成的变体数量为2²=4
    //变体 1:没有启用任何关键字(默认)。
    //变体 2:启用了 关键字1。
    //变体 3:启用了 关键字2。
    //变体 4:同时启用了 关键字1 和 关键字2
    //运行时,Unity会根据具体设置选择与之匹配的变体来使用

    //Unity的一些内置功能会隐式的生成变体
    //比如
    //光照模式
    //雾效
    //渲染管线
    //等等

1.预热Shader变体
//第一次用到某个Shader的变体可能会卡顿
//提前编译可以避免第一次使用时的卡顿

    //预热方式
    //利用Shader Variant Collection(着色器变体集合)进行着色器预编译
    //1.在 Project 窗口 右键 → Create → Shader → Shader Variant Collection(着色器变体集合)
    //2.在Inspector窗口中手动添加想要预热的Shader
    //3.在脚本中获取到Shader Variant Collection 利用其中的API WarmUp() 进行预编译
        //注意:
    //预热Shader还是需要在合适的时机进行,比如加载界面时
创建Shader变体
    
可以添加需要预热的Shader
    
脚本里声明Shader变体集合变量并关联,进行预热
        public ShaderVariantCollection shaderVariantCollection;
            if (shaderVariantCollection != null && !shaderVariantCollection.isWarmedUp)
    {
        //对Shader去进行预热(预编译) 可以避免在运行时使用时造成卡顿
        shaderVariantCollection.WarmUp();
    }

    

2.利用CommandBuffer相关API进行预渲染
//如果我们不希望创建隐藏摄像机
//那么我们可以利用这种方式进行预渲染
//可以直接以脚本命令触发一次CPU到GPU的通信

    //预渲染方式
    //利用Unity中的CommandBuffer(命令缓冲区)进行一次CPU到GPU的数据上传
    //通过Renderer(渲染器)获取到材质,在利用材质获取到Pass(渲染通道)
    //利用这些数据进行一次预热,可以有效避免首次渲染时造成的卡顿

    //这样做的好处是
    //1.不需要创建相机,直接用 CommandBuffer 提交绘制命令
    //2.用很小的 RT(默认 64×64)减少性能消耗
    //3.不会影响游戏画面
    //4.可批量调用,对不同 Renderer 或 Pass 预热

    /// <summary>
/// 对指定 Renderer 使用 CommandBuffer 进行一次离屏渲染(哑渲染),
/// 以触发指定 Pass 的 Shader 编译、资源上传等预热操作。
/// </summary>
/// <param name="renderer">需要预热的 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 renderer, string passName, int width = 64, int height = 64)
{
    // 获取 Renderer 使用的共享材质(不会实例化)
    Material material = renderer.sharedMaterial;

    // 查找该材质中指定 Pass 的索引
    int pass = material.FindPass(passName);
    if (pass < 0)
    {
        // 如果没找到该 Pass,打印警告并退出
        Debug.LogWarning($"Pass 没有找到: {passName}");
        return;
    }

    // 创建一个 CommandBuffer(命令缓冲区),用来批量提交 GPU 绘制指令
    // name名字可以随意自定义,它的作用只是在调试工具中可以显示出来,方便你知道这个缓冲区的作用
    CommandBuffer commandBuffer = new CommandBuffer { name = $"Warmup Renderer:{renderer.name}:{passName}" };

    // 为临时 RenderTexture 申请一个 ID(Shader 属性 ID)
    // 把字符串转换成一个唯一的整数ID,这个字符串也可以自定义
    int tempID = Shader.PropertyToID("_WarmupRT");

    // 申请一个临时 RenderTexture(指定宽高、无 MSAA、ARGB32 格式)
    commandBuffer.GetTemporaryRT(tempID, width, height, 0, FilterMode.Point, RenderTextureFormat.ARGB32);

    // 设置该临时 RenderTexture 作为渲染目标
    commandBuffer.SetRenderTarget(tempID);

    // 清空 RenderTexture(清颜色 & 深度)
    commandBuffer.ClearRenderTarget(true, true, Color.clear);

    // 使用该 Renderer 和材质,使用指定索引子网格,指定索引渲染通道 进行绘制
    commandBuffer.DrawRenderer(renderer, material, 0, pass);

    // 立即执行这个 CommandBuffer(提交给 GPU)
    // 关键步骤
    Graphics.ExecuteCommandBuffer(commandBuffer);

    // 释放临时 RenderTexture 资源(避免显存泄漏)
    commandBuffer.ReleaseTemporaryRT(tempID);

    // 释放命令缓冲区本身
    commandBuffer.Release();
}

using UnityEngine;
using UnityEngine.Rendering;

public class Lesson43_性能优化_CPU_脚本_预渲染Shader变体与CommandBuffer : MonoBehaviour
{
public ShaderVariantCollection shaderVariantCollection;

void Start()
{
    #region 知识回顾 Shader关键字和变体

    //Shader Variant(变体)指的是一个Shader的特定配置
    //通过不同的关键字(Keywords)和 设置组合 来实现不同的效果
    //每一种关键字组合或设置都会生成一个独立的Shader变体
    //最终以二进制形式存储在构建文件中供运行时使用

    //说人话:
    //Shader变体就是基于一个Shader文件当中的代码,编译生成多个版本的Shader
    //它基于关键字来生成各种不同的版本

    //举例说明
    //一个Shader中有两个关键字
    //每个关键字有两种状态(启用或禁用)
    //最终生成的变体数量为2²=4
    //变体 1:没有启用任何关键字(默认)。
    //变体 2:启用了 关键字1。
    //变体 3:启用了 关键字2。
    //变体 4:同时启用了 关键字1 和 关键字2
    //运行时,Unity会根据具体设置选择与之匹配的变体来使用

    //Unity的一些内置功能会隐式的生成变体
    //比如
    //光照模式
    //雾效
    //渲染管线
    //等等

    #endregion

    #region 知识点一 预热Shader变体

    //第一次用到某个Shader的变体可能会卡顿
    //提前编译可以避免第一次使用时的卡顿

    //预热方式
    //利用Shader Variant Collection(着色器变体集合)进行着色器预编译
    //1.在 Project 窗口 右键 → Create → Shader → Shader Variant Collection(着色器变体集合)
    //2.在Inspector窗口中手动添加想要预热的Shader
    //3.在脚本中获取到Shader Variant Collection 利用其中的API WarmUp() 进行预编译
    
    if (shaderVariantCollection != null && !shaderVariantCollection.isWarmedUp)
    {
        //对Shader去进行预热(预编译) 可以避免在运行时使用时造成卡顿
        shaderVariantCollection.WarmUp();
    }

    //注意:
    //预热Shader还是需要在合适的时机进行,比如加载界面时

    #endregion

    #region 知识点二 利用CommandBuffer相关API进行预渲染

    //如果我们不希望创建隐藏摄像机
    //那么我们可以利用这种方式进行预渲染
    //可以直接以脚本命令触发一次CPU到GPU的通信

    //预渲染方式
    //利用Unity中的CommandBuffer(命令缓冲区)进行一次CPU到GPU的数据上传
    //通过Renderer(渲染器)获取到材质,在利用材质获取到Pass(渲染通道)
    //利用这些数据进行一次预热,可以有效避免首次渲染时造成的卡顿

    //这样做的好处是
    //1.不需要创建相机,直接用 CommandBuffer 提交绘制命令
    //2.用很小的 RT(默认 64×64)减少性能消耗
    //3.不会影响游戏画面
    //4.可批量调用,对不同 Renderer 或 Pass 预热

    #endregion
}

/// <summary>
/// 对指定 Renderer 使用 CommandBuffer 进行一次离屏渲染(哑渲染),
/// 以触发指定 Pass 的 Shader 编译、资源上传等预热操作。
/// </summary>
/// <param name="renderer">需要预热的 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 renderer, string passName, int width = 64, int height = 64)
{
    // 获取 Renderer 使用的共享材质(不会实例化)
    Material material = renderer.sharedMaterial;

    // 查找该材质中指定 Pass 的索引
    int pass = material.FindPass(passName);
    if (pass < 0)
    {
        // 如果没找到该 Pass,打印警告并退出
        Debug.LogWarning($"Pass 没有找到: {passName}");
        return;
    }

    // 创建一个 CommandBuffer(命令缓冲区),用来批量提交 GPU 绘制指令
    // name名字可以随意自定义,它的作用只是在调试工具中可以显示出来,方便你知道这个缓冲区的作用
    CommandBuffer commandBuffer = new CommandBuffer { name = $"Warmup Renderer:{renderer.name}:{passName}" };

    // 为临时 RenderTexture 申请一个 ID(Shader 属性 ID)
    // 把字符串转换成一个唯一的整数ID,这个字符串也可以自定义
    int tempID = Shader.PropertyToID("_WarmupRT");

    // 申请一个临时 RenderTexture(指定宽高、无 MSAA、ARGB32 格式)
    commandBuffer.GetTemporaryRT(tempID, width, height, 0, FilterMode.Point, RenderTextureFormat.ARGB32);

    // 设置该临时 RenderTexture 作为渲染目标
    commandBuffer.SetRenderTarget(tempID);

    // 清空 RenderTexture(清颜色 & 深度)
    commandBuffer.ClearRenderTarget(true, true, Color.clear);

    // 使用该 Renderer 和材质,使用指定索引子网格,指定索引渲染通道 进行绘制
    commandBuffer.DrawRenderer(renderer, material, 0, pass);

    // 立即执行这个 CommandBuffer(提交给 GPU)
    // 关键步骤
    Graphics.ExecuteCommandBuffer(commandBuffer);

    // 释放临时 RenderTexture 资源(避免显存泄漏)
    commandBuffer.ReleaseTemporaryRT(tempID);

    // 释放命令缓冲区本身
    commandBuffer.Release();
}

}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏