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(着色器变体集合)进行预编译:
- Project 窗口右键 → Create → Shader → Shader Variant Collection。
- Inspector 中手动添加需要预热的 Shader。
- 脚本中引用集合,调用
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 执行一次离屏绘制。
好处:
- 不需要创建相机,直接用 CommandBuffer 提交绘制命令。
- 可用很小的临时 RT(默认 64x64)减少开销。
- 不影响游戏画面。
- 可批量调用,对不同 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