- 变体过多的缺点
- 项目情况
- #pragma multi_compile_fwdbase 和 multi_compile_fog 生存的变体(keyword)
- 生存的变体
- 变体的数量
- 查看编译生存的各个变体的代码,并搜索 Global Keywords - 源码级别
- 查看编译生存的各个变体 - keyword 级别
- 如何优化
- 将 #pragma multi_compile_fwdbase 和 multi_compile_fog 预生成项拆解为单个的手动定义项
- 优化之后
- 优化后的小问题、如何解决
- 编辑器 源码
- 操作界面
- 示例
- multi_compile 的一些 built-in 快件方式包含的 keyword
- ShaderFinderTool.cs
- 其他优化方式 shader_feature_local 加上 shader + ref material 打包的方式
- IPreprocessShaders - 在shader AB 构建处理时,删除对应的变体
- UBer Shader 拆分为 #define + 少量 #multi_compile 的多份 shader 优化实践结果
- 优化前 - Shader - 298.5 MB
- 优化后 - Shader - 20.2 MB
- 优化思路
- 优点
- 缺点
- 阴影兼容性缺点要慎重处理
- References
为了让大家了解为何要减少变体,这里列出变体过多的缺点
- 打包时编译变体的时间增加(如果你的变体使用 unity built-in standard shader,那么可能会有几千个变体,编译单单这个 shader 也许会需要 10 分钟)
- 增加 运行时 shaderlab 内存,游戏与运行时使用的变体多,那么意味着 shader 的实例很多,每个 shader 实例都是需要占用内存的
- 增加包体大小,因为 unity shader 变体多的话 编译出来 的样本就会很多,每个样本就是一个 shader 文件(如果我们自己写引擎的话,就知道这个变体是怎么回事)
由于项目没使用 SRP,还是 built-in 管线,那么要使用 built-in 的阴影就需要使用到 #pragma multi_compile_fwdbase
该内置的 pragma 会生存很多不需要的 multi_compile
的 keyword
,为了定制效果,让 shader 尽可能的小,那么我们可以这么整:将 #pragma multi_compile_fwdbase
和 multi_compile_fog
编译生存的 keywrod
都手动来定义需要的
如果我们只要
- fog linear
- lighting
- shadow
的功能
生存的变体如下图,会生存一堆不需要的 keyword
如下图:98 个
选中 shader 文件,点击 Inspector 视图中的按钮:Compile and show code
,如下图的: 生存的代码中搜索
Global Keywords
,就可以看到各个变体的代码
以上两种方式都可以查看变体情况
如何优化根据上面 搜索 Global Keywords 的方式,我们可以知道生存了很多不必要的变体代码
变体多的缺点 上面有提到
为了优化,可以这么做,上面也有提到,这里再次重新强调一下:#pragma multi_compile_fwdbase
和 multi_compile_fog
编译生存的 keywrod
都手动来定义需要的
如:
//#pragma multi_compile_fog
//#pragma multi_compile_fwdbase
#define FOG_LINEAR
#define DIRECTIONAL
#define SHADOWS_SCREEN
优化之后
变体的数量剧减,有原来的 98 个变成了 6 个(还有一些自定义的 multi_compile,不然只有3个,而这三个都是内置生成的 tier:1,2,3的,这个 tier: 1,2,3暂时不知道如何删除(注:后续发现可以再 IPreprocessShaders
中删除),不然的话,只有1个变体,那么这个 shader 文件就会小的可怜,打包速度、占用内存都会极小)
但是这样优化后的了另一个问题:Unity Editor 下的 Prefab、Material 都无法预览正确的效果
所以我们可以写个工具,在:打包前,批量修改 shader 的 fwdbase, fog 的变体,在打包后,在恢复过来,这样,Unity Editor 下既可以正常预览 Prefab、Material 的渲染效果,也可以在打包后变体减少
public void BuildPackage()
{
// 打包资源前,先处理一波 shader 的 fwdbase, fog 变体替换
BeforeAndAfterBuildShadersHandler.BeforeBuildHandle();
// 正常 building 逻辑在此
...
// 打包资源后,恢复 shader 文件
BeforeAndAfterBuildShadersHandler.AfterBuldHandle();
}
编辑器 源码
#define __DATAFORM_TXT__ // 使用 文本来存 shader 数据
// jave.lin 2021/08/30
// 打包构建前、后 shader 的处理工具
// 因为使用的是 built-in 管线,基本很多光影都需要 fwdbase, fog 等 built-in 的变体
// 而这个工具是不使用 built-in 变体,改用手动的方式来定义需要的变体
// 所以会导致 unity 编辑器是对 material 的预览效果出问题
// 因为在发布程序前,可以使用会 fwdbase, fog 等 built-in 变体
// 但是在发布程序时,必须使用自己手动的方式来定义变体(可以减少很多变体的数量)
// 如果使用 SRP 的话,可以变体的把控会更容易
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
// jave.lin shader 文件信息
public class ShaderFileInfo
{
public string path;
public UnityEngine.Shader obj;
public string lowPath;
public string lowShaderName;
}
// jave.lin shader 在 building 处理的工具类
public class BeforeAndAfterBuildShadersEditorWindow : EditorWindow
{
private string defaultShadersPath = "Assets/GameAssets/shaders";
private List shaderInfoList = new List();
private List shaderList = null;
// 根据 shader name 过滤
private bool shadeNameFilter = true;
private string shaderNameFilterContent = "";
// 根据 file name 过滤
private bool fileNameFilter = true;
private string fileNameFilterContent = "";
private Vector2 dragDropFileScrollViewPos;
[MenuItem("实用工具/资源工具/打包构建前、后 shader 的处理工具")]
public static void _Show()
{
var win = EditorWindow.GetWindow();
win.titleContent = new GUIContent("打包构建前、后 shader 的处理工具");
win.Show();
}
private void OnGUI()
{
#if __DATAFORM_TXT__
if (shaderList == null)
{
shaderList = new List();
BeforeAndAfterBuildShadersHandler.LoadDataFromTxt(BeforeAndAfterBuildShadersHandler.dataPath, shaderList);
}
#else
if (shaderList == null)
{
shaderList = new List();
var data = AssetDatabase.LoadAssetAtPath(BeforeAndAfterBuildShadersHandler.dataPath);
shaderList.AddRange(data.shaders);
}
#endif
var src_endabled = GUI.enabled;
if (BeforeAndAfterBuildShadersHandler.ShaderBackupCount() > 0) GUI.enabled = false;
if (GUILayout.Button("Building前 手动 处理"))
{
BeforeAndAfterBuildShadersHandler.BeforeBuildHandle();
}
GUI.enabled = src_endabled;
if (BeforeAndAfterBuildShadersHandler.ShaderBackupCount() == 0) GUI.enabled = false;
if (GUILayout.Button("Building后 手动 处理"))
{
BeforeAndAfterBuildShadersHandler.AfterBuldHandle();
}
GUI.enabled = src_endabled;
if (BeforeAndAfterBuildShadersHandler.IsMemoryBK() && GUILayout.Button("清空备份数据"))
{
BeforeAndAfterBuildShadersHandler.ClearBackupInfo();
}
if (GUILayout.Button("清理配置中不存在的shaders文件"))
{
BeforeAndAfterBuildShadersHandler.ClearNotExistsShaders();
}
DisplayCombineFileInfoList();
}
private void DisplayCombineFileInfoList()
{
EditorGUILayout.Space();
var srcCol = GUI.contentColor;
GUI.contentColor = Color.gray;
EditorGUILayout.LabelField("================================================ Data List ================================================");
GUI.contentColor = srcCol;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Clear"))
{
shaderInfoList.Clear();
}
if (GUILayout.Button("Reload"))
{
RefreshFileListInfo();
}
if (GUILayout.Button("Save"))
{
SaveDataAsset();
}
if (GUILayout.Button("Select"))
{
SelectAssetList();
}
if (GUILayout.Button("LoadShadersFolder"))
{
LoadShadersFolder();
}
EditorGUILayout.EndHorizontal();
GUI.contentColor = Color.green;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Shader Name Filter : ", GUILayout.Width(150));
shadeNameFilter = EditorGUILayout.Toggle(shadeNameFilter, GUILayout.Width(20));
shaderNameFilterContent = EditorGUILayout.TextField(shaderNameFilterContent);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"File Name Filter : ", GUILayout.Width(150));
fileNameFilter = EditorGUILayout.Toggle(fileNameFilter, GUILayout.Width(20));
fileNameFilterContent = EditorGUILayout.TextField(fileNameFilterContent);
EditorGUILayout.EndHorizontal();
GUI.contentColor = srcCol;
var lowShaderNameFilterContent = shaderNameFilterContent.ToLower();
var lowFileNameFilterContent = fileNameFilterContent.ToLower();
dragDropFileScrollViewPos = EditorGUILayout.BeginScrollView(dragDropFileScrollViewPos/*, GUILayout.Width(300), GUILayout.Width(300)*/);
for (int i = 0; i 1)
{
SortDataList();
}
}
private void SortDataList()
{
shaderInfoList.Sort((a, b) => { return string.Compare(a.path, b.path); });
}
private void SaveDataAsset()
{
if (shaderList != null)
{
shaderList.Clear();
foreach (var info in shaderInfoList)
{
if (info.obj == null)
{
// 中途被删除
continue;
}
shaderList.Add(info.obj);
}
BeforeAndAfterBuildShadersHandler.SaveAsset(shaderList);
}
}
private void SelectAssetList()
{
// jave.lin : method0, Selection.gameObjects Read Only
//var arr = new GameObject[combineFileInfoList.Count];
//for (int i = 0; i < arr.Length; i++)
//{
// arr[i] = combineFileInfoList[i].obj as GameObject;
//}
//Selection.gameObjects = arr; // read only
// jave.lin : method1 : reflection, 但是获取不了 add 方法
//var addFunc = typeof(Selection).GetMethod(
// "Add",
// new Type[]
// {
// typeof(UnityEngine.Object)
// },
// new ParameterModifier[] { new ParameterModifier(1) }
// );
//Debug.Log($"PrefabCombinerEditorWindow.SelectPrefabList addFunc : {addFunc}");
// jave.lin : method2 : Selection.objects, 后来发现此 API 可以 setter
var arr = new UnityEngine.Object[shaderInfoList.Count];
for (int i = 0; i shaderBackInfoList != null ? shaderBackInfoList.Count : 0;
// statics
public static string GetFullPath(string assetPath)
{
return $"{Application.dataPath.Replace("Assets", "")}/{assetPath}".Replace("//", "/");
}
public static bool IsMemoryBK()=> eFileRevertType == eFileRevertType.MEMORY;
public static void ClearBackupInfo()
{
shaderBackInfoList.Clear();
}
public static void LoadData(string assetPath, List ret)
{
#if __DATAFORM_TXT__
LoadDataFromTxt(assetPath, ret);
#else
LoadDataFromAsset(assetPath, ret);
#endif
}
public static void LoadDataFromAsset(string assetPath, List ret)
{
var data = AssetDatabase.LoadAssetAtPath(assetPath);
if (data == null) return;
ret.AddRange(data.shaders);
}
public static void LoadDataFromTxt(string txtPath, List ret)
{
string txtContent;
if (File.Exists(txtPath))
{
txtContent = File.ReadAllText(txtPath);
}
else
{
var fullPath = GetFullPath(txtPath);
if (!File.Exists(fullPath))
{
return;
}
txtContent = File.ReadAllText(fullPath);
}
if (string.IsNullOrEmpty(txtContent))
{
return;
}
var paths = txtContent.Split(new string[] { "\n" }, System.StringSplitOptions.RemoveEmptyEntries);
foreach (var path in paths)
{
var shader = AssetDatabase.LoadAssetAtPath(path);
if (shader == null) continue;
ret.Add(shader);
}
}
public static void SaveAsset(List shaders)
{
#if __DATAFORM_TXT__
var content = "";
foreach (var shader in shaders)
{
var shaderPath = AssetDatabase.GetAssetPath(shader);
content += $"{shaderPath}\n";
}
var fullPath = GetFullPath(dataPath);
File.WriteAllText(fullPath, content);
AssetDatabase.Refresh();
#else
var isNew = false;
var data = AssetDatabase.LoadAssetAtPath(dataPath);
if (data == null)
{
data = ScriptableObject.CreateInstance();
isNew = true;
}
var saveList = new List();
foreach (var shader in shaders)
{
if (shader == null) continue; // 中途被删除
saveList.Add(shader);
}
data.shaders.Clear();
data.shaders.AddRange(saveList);
if (!isNew) data = data.Clone();
AssetDatabase.CreateAsset(data, dataPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
#endif
}
// 读取 *.asset 配置的数据
public static void BeforeBuildHandle()
{
// jave.lin : 遍历里面的 *.shader 逐一处理
/*
- 如果是shader代码使用内存恢复先备份到内存
- 如果是shader代码使用文件恢复先备份到临时文件
- 如果是shader代码使用svn/git的版本控制,不需要我们手动备份,恢复时直接使用svn/git来恢复即可
- #pragma multi_compile_fog 替换为:
- #define FOG_LINEAR
- #pragma multi_compile_fwdbase 替换为:
- #define DIRECTIONAL
- #define SHADOWS_SCREEN
- 将对应的 *.shader 文件标记为:EditorUtility.SetDirty(shaderObj);
- 然后 AssetsDatabase.SaveAssets(); AssetsDatabase.Refresh();
*/
shaderBackInfoList.Clear();
shaderList.Clear();
LoadData(dataPath, shaderList);
if (shaderList.Count > 0)
{
var encoding = new System.Text.UTF8Encoding(readAndWriteWithBOM);
var endLineStr = endLineFlagWithCRLF ? "\r\n" : "\n"; // Environment.NewLine;
var fogReplacmentStr = $"{endLineStr}$1#define FOG_LINEAR";
var fwdbaseReplacmentStr = $"{endLineStr}$1#define DIRECTIONAL{endLineStr}$1#define SHADOWS_SCREEN";
foreach (var shader in shaderList)
{
if (shader == null) continue;
var path = AssetDatabase.GetAssetPath(shader);
var fullPath = GetFullPath(path);
if (!File.Exists(fullPath)) continue;
try
{
var txt = File.ReadAllText(fullPath, encoding);
shaderBackInfoList.Add(new ShaderBackupInfo { path = fullPath, source = txt });
// jave.lin : 普通替换,缩进很难看
//txt = txt.Replace("#pragma multi_compile_fog", "#define FOG_LINEAR");
//txt = txt.Replace("#pragma multi_compile_fwdbase", "#define DIRECTIONAL\n#define SHADOWS_SCREEN");
// jave.lin : 使用正则来替换,缩进就可以还原来的
txt = fogReplaceRegex.Replace(txt, fogReplacmentStr);
txt = fwdbaseReplaceRegex.Replace(txt, fwdbaseReplacmentStr);
File.WriteAllText(fullPath, txt);
}
catch(System.Exception er)
{
Debug.LogError(er);
}
}
}
}
public static void AfterBuldHandle()
{
// jave.lin : 遍历里面的 *.shader 逐一处理
/*
// method 0:
- 使用从内存中恢复shader代码(如果shader代码过多,可以考虑先backup 到 disk,然后再从 disk 恢复
// method 1:
- #define FOG_LINEAR 替换为:
- #pragma multi_compile_fog
- #define DIRECTIONAL、#define SHADOWS_SCREEN 替换为:
- #pragma multi_compile_fwdbase
- 将对应的 *.shader 文件标记为:EditorUtility.SetDirty(shaderObj);
- 然后 AssetsDatabase.SaveAssets(); AssetsDatabase.Refresh();
// method 2:(更方便的方式)
- 使用 svn/git 命令行来还原打包后的 shaders 文件
*/
if (shaderBackInfoList.Count == 0)
{
return;
}
if (eFileRevertType == eFileRevertType.MEMORY)
{
var encoding = new System.Text.UTF8Encoding(readAndWriteWithBOM);
foreach (var info in shaderBackInfoList)
{
try
{
File.WriteAllText(info.path, info.source, encoding);
}
catch (System.Exception er)
{
Debug.LogError(er);
}
}
}
else
{
var revertFiles = new List();
foreach (var info in shaderBackInfoList)
{
revertFiles.Add(info.path);
}
try
{
RevertFiles(revertFiles);
}
catch(System.Exception er)
{
Debug.LogError(er);
}
}
shaderBackInfoList.Clear();
}
public static void ClearNotExistsShaders()
{
shaderList.Clear();
LoadData(dataPath, shaderList);
if (shaderList.Count > 0)
{
for (int i = shaderList.Count - 1; i > -1; i--)
{
var shader = shaderList[i];
if (shader == null) continue;
var path = AssetDatabase.GetAssetPath(shader);
var fullPath = GetFullPath(path);
if (!File.Exists(fullPath))
{
shaderList.RemoveAt(i);
}
}
}
SaveAsset(shaderList);
}
private static void RevertFiles(List files)
{
// TortoiseProc.exe /command:log /path:"H:\WorkFiles\ProjectArt\Assets\Art\(Temporary).meta" /closeonend:1
var revision = eFileRevertType.ToString().ToLower();
System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo(revision);
var revertFiles = string.Join(" ", files);
if (eFileRevertType == eFileRevertType.SVN)
{
// 需要确保安装了 小乌龟 svn 命令行工具,可参考:https://blog.csdn.net/linjf520/article/details/119617076
// e.g.: svn revert "C:\\test1.txt" "C:\\test2.txt"
Debug.Log($"svn revert {revertFiles}");
info.Arguments = $" revert {revertFiles}";
}
else if (eFileRevertType == eFileRevertType.GIT)
{
// 需要确保安装了 git,使用 git checkout ...
关注
打赏
- 3D Assets (Textures & Model & Animations) & Game Design Ideas & DCC Tutorials & TA
- LearnGL - 学习笔记目录
- Unity - Timeline 知识汇总
- Unity Graphics - 知识点目录 - 停止翻译,因为发现官方有中文文档了
- Graphic资料
- Unity Lightmap&LightProbe局部动态加载(亲测2020以及以上版本官方修复了)
- Unity - 踩坑日志 - 低版本线性颜色空间渲染异常的 “BUG”
- Unity Shader - PBR 渲染 SP 导出的素材
- 什么是 3A 游戏?
- Photosohp - 实现 2D MetaBall、MetaFont