shader变体是什么_GitHub - 7732050/ShaderVariantCollector
一种Shader变体收集和打包编译优化的思路
介绍
什么是变体
引用Unity官方文档的解释: ShaderVariant
In Unity, many shaders internally have multiple "variants", to account for different light modes, lightmaps, shadows and so on. These variants are indentified by a shader pass type, and a set of shader keywords.
Unity的shader资源不仅含有GPU上执行的着色器代码,还包含了渲染状态,属性的定义,以及对应不同渲染管线不同渲染阶段用于的着色器代码,每一小段代码也可能会有不同的编译参数,以对应不同的渲染功能
在带有多个变体的shader代码片段中,最显著的特征就是拥有这些预编译开关如:
#pragma multi_compile_fwdbase // unity 内置前向管线编译设置集合,控制光照,阴影等很多相关功能
#pragma shader_feature _USE_FEATURE_A // 自定义功能开关
#pragma multi_compile _USE_FUNCTION_A _USE_FUNCTION_B // 自定义多编译选项
有了这些编译开关标记,我们就可以只写很少的shader代码,从而依附在这份骨架代码上,来实现含有细微差异功能的变种shader代码。当然功能越多,这些变体数量也成指数级增长,如何控制这些变体可能产生的数量,也需要较为丰富的经验和技巧。
为什么要收集变体
游戏初始化的时候一般需要提前把渲染要使用的Shader全部都加载进来,以降低游戏运行时及时加载和编译带来的卡顿,这时候我们可以调用Shader.WarmupAllShaders来把当前已经加载到内存的Shader全部编译一次,包含所有的变体。
随着项目渲染效果的丰富,Shader变体变得越来越多,粗暴的调用全加载接口,会导致游戏的启动时间变得更长,影响游戏体验。
后来Unity进入了变体集合ShaderVariantCollection来取代上面的粗暴全加载接口,达到按需加载,提高加载速度
官方解释中最为关键的内容如下:
This is used for shader preloading ("warmup"), so that a game can make sure "actually required" shader variants are loaded at startup (or level load time), to avoid shader compilation related hiccups later on in the game.
也就是说变体记录的是游戏实际上使用到的变体集合,那么对其进行按需加载能够大大提高游戏加载速度
其他一些理解
从官方文档上我们得知,变体集合是用于预加载Shader,但是并没有提及打包发布过程中的编译,以及如何筛选实际使用的编译进入AssetBundle。
在实际发布的游戏包里的Shader资源,如果缺失了需要的某个变体,那么可能得到的渲染结果就不正确了。(Unity在加载带有多个变体的时候,应该应该是使用了某种匹配方法,如果没有找到最佳匹配,它就会fallback到另外keyword匹配数量最多的变体,从而渲染出有部分差异或者甚至完全错误的结果)最可气的是,你在实际运行设备上是看不到变体缺失的精准信息的,一旦出错,可能你只能全盘再来。
Shader变体丢失通常都由与AssetBundle的分包打包导致。Unity内部搜集实际使用的变体是通过扫描使用Shader的材质,以及场景渲染器上的光照参数来综合获取(应该就是把内部渲染管线中,实际使用的Shader变体记录下来)。为了实现AssetBundle的更新,我们通常会把Shader作为单独的资源放在一个独立的AssetBundle中,而其他引用这些Shader的材质和场景,将作为AssetBundle的依赖加载。Shader一旦脱离了了使用他们的载体,Unity在打包的时候无法全盘考虑那些变体需要实际发布,从而随机性的出现恼人的变体丢失现象。
网上的一些解决变体丢失办法:
把Shader和使用他们的材质一同打到一个AssetBundle
在Editor通跑一遍整个项目场景,Unity会把搜集到的所有shader以及变体记录下来,然后把这个信息保存成变体集合,把它和shader一并打包
Unity在Project Settings面板最下面隐藏了这个最为重要的功能:
上面提到的方法从多数情况下都能正常工作,但是:
方法一中,光从材质上不能获得完整的变体使用记录,还和实际渲染器和所在场景的全局光照有关系
方法二中,人为搜集始终有漏网之鱼,而且Unity保存出来的Shader变体全在一起,如果不经过拆分,打包分包策略会有些许影响
我的解决办法
项目中的用于渲染的资源一般来说只有三大类
场景
动态加载的模型,角色,特效等
UI & UIEffect
其中,一般UI都是直接使用UGUI内置的着色器,变体都是通过multi_compile提供,这种编译开关可以保证无论此开关在材质中是否使用,变体都会得到编译并且经入发布真机包,并且用于UI的Shader不会太多,我们就简单处理了。
故最终我们只需要考虑Shader的两种使用情形:场景中动态加载和场景静态资源。
实现一个自动shader变体收集器,步骤如下:
把当前需要打包的资源路径搜集起来(按照工程发布设置,如多语言,渠道)
通过依赖关系,把动态加载的prefab这类资源依赖的材质路径搜集起来
打开一个新的空场景,创建一个游戏场景中的动态光源环境,如实时平行光
反射调用ShaderUtil.ClearCurrentShaderVariantCollection清空当前项目搜集到的变体,我们需要重新搜集一次
场景中创建一个用于渲染的相机
在场景中创建一堆sphere几何体,并排列整齐,然后把渲染相机对齐它们,并保证他们都可以看见
分批次把这些材质资源赋予这些sphere几何体,渲染一帧
渲染完毕之后,依次打开场景,并且设置好一个全景相机视角并渲染
这样基本上项目上的shader变体已经搜集完毕,反射调用ShaderUtil.SaveCurrentShaderVariantCollection保存到一个整体变体集合资源中去
自动搜集工具任务完成
有了这个变体集合,就OK了吗?
不,任务只完成了一半。有些自定义的shader,尤其是那些只通过UsePass被引用到的Shader,并没有出现在任何材质资源上,故不能被Unity搜集到它们的变体。(这一点我肯定。我们实际项目中,使用了一些多Pass的Shader,这些Pass都通过UsePass包含那些未开放给美术使用的内部Shader提供,美术直接使用的Shader是没有实际代码的。这样做的好处是由更大的灵活性来组合更多的多Pass组合Shader,而不增加代码量。)
举个例子:
有三个Shader:
ABC.shader
InternalA.shader
InternalB.shader
ABC使用了InternalA, InternalB的内部pass, ABC并没有实际的代码片段。这种情况下,Unity搜集到的变体集合,是属于ABC的,而没有分别区分InternalA, InternalB,如果你直接拿这份Unity的导出结果,很有可能导致变体丢失。
我们需要把Unity导出的变体集合挨个拆分成零散资源,一来可以创建那些被关联进来的shader变体集合,二来也可以方便打包粒度拆分。
继续
在继续之前,先做一些准备工作:
通过反射ShaderUtil.OpenShaderCombinations( shader, usedBySceneOnly = true )可以打开一个unity生成的Library/ParsedCombinations-xxx.shader文件,通过文本解析,可以得到所有有效的builtin, shader_features, multi_compiles这三大类keyword,以及代码snippets标记
通过反射方法读取ShaderVariantCollection中每一组shader的变体集
做好一些缓存工作,便于后期重复获取这些信息
为了让下面的逻辑表述更为清晰,用伪代码表示:
// 开始拆分总集,并为所有的shder创建独立变体集
ShaderVariantCollection unityVAC; // unity导出的总集
foreach ( curSVC in unityVAC ) {
// 集合中当前一个子集shader
var cur_shader = curSVC.shader;
// 当前shader的所有变体
var cur_shaderVariants = curSVC.variants;
// 为当前创建新的独立变体集
var va = new ShaderVariantCollection();
// 尝试把这些变体拷贝到新的变体集中去
foreach ( cur_v in cur_shaderVariants ) {
try {
var realSV = new ShaderVariantCollection.ShaderVariant( cur_v.shader, cur_v.passType, cur_v.keywords );
va.Add( realSV );
} catch ( ... ) {
// 说明此变体不属于当前创建的shader的指定pass类型
// 走到这里,一般都因unity搜集的变体属于依赖项
}
}
Save( va );
// 获取依赖,通过UsePass, Fallback进来的
// 依次为子shader创建或更新变体集
var child_shaders = GetDependencies( GetAssetPath( cur_shader ) );
foreach ( child_shader in child_shaders ) {
// 被依赖shader可以被多个不同shader多次依赖,这里要注意缓存
var child_va = TryGet_New_ShaderVariantCollection( child_shader );
// 把变体依次传入测试是否同时属于依赖child_shader
foreach ( cur_v in cur_shaderVariants ) {
var _keywords = copy( cur_v.keywords );
// 此变体中可能含有不属于child_shader的关键字
// 通过之前提供的解析ParsedCombinations文件,排除它们
RemoveInvalidKeyword( _keywords, child_shader );
try {
var realSV = new ShaderVariantCollection.ShaderVariant( child_shader, cur_v.passType, _keywords );
// 注意去重
if ( !child_va.Contains( realSV ) ) {
child_va.Add( realSV );
}
} catch ( ... ) {
// ...
}
}
}
// 保存所有的被依赖进来的shader的变体集合
// ...
// 这里有一些问题:
// 1. 由于变体的从属pass不能获取完整,
//(既没有passName,也没有passIndex)
// 所以不能精准地为依赖项创建变体,所以上面代码中只要变体合法,就当他使用了
// 2. 一个shader既可以直接被材质使用,被Unity搜集到变体,
// 也可能被其他Shader引用,那么被引用部分产生的变体能否被unity搜集到,
// 还需要进一步测试验证
}
这样经过上面一番折腾,希望能创建最为完整的变体使用记录,开始进行下一阶段折腾
编译时间优化
我发现有了变体集之后,打包shader时,依然对shader进行了长时间的编译,就算加上了multi_compile数量的预估,也超过变体集中生命数量太多。由此我从官方文档对变体集合的解释文字上推断,变体集它只能用于预加载,和指定shader变体的使用子集,至于编译,那是另外一个资源处理阶段,需要我们自行过滤排除。
unity2018.2引入了一个可编程的shader变体移除管道:IPreprocessShaders.OnProcessShader,有了这个接口,我们就可以在Unity编译Shader的时候收到回调通知,我们可以实现自己的Shader变体删除逻辑,进一步减少编译时间。
项目里面可以通过实现多个IPreprocessShaders接口对象,Unity在编译shader时,会自行创建这些处理器实例,并执行其中的回调接口,我们需要在这些回调中,对传入的参数进行排除
一个例子:
/// 一个简单排除Unity内建变体编译的处理器
class BuiltinShaderPreprocessor : IPreprocessShaders {
static ShaderKeyword[] s_uselessKeywords;
public int callbackOrder {
get { return 0; } // 可以指定多个处理器之间回调的顺序
}
static BuiltinShaderPreprocessor() {
s_uselessKeywords = new ShaderKeyword[] {
new ShaderKeyword( "DIRLIGHTMAP_COMBINED" ),
new ShaderKeyword( "LIGHTMAP_SHADOW_MIXING" ),
new ShaderKeyword( "SHADOWS_SCREEN" ),
};
}
public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IList data ) {
for ( int i = data.Count - 1; i >= 0; --i ) {
for ( int j = 0; j < s_uselessKeywords.Length; ++j ) {
if ( data[ i ].shaderKeywordSet.IsEnabled( s_uselessKeywords[ j ] ) ) {
data.RemoveAt( i );
break;
}
}
}
}
}
我们需要创建更为细致和精准的编译排除逻辑(代码片段不完整)
class ShaderPreprocessor : IPreprocessShaders {
public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IList data ) {
// 跳过处理系统shader,不处理
// return;
// 读取对应shader的变体集:
// 上一步我们为每一个使用到的shader都创建了独立的编译集合
// 获取指定shader的编译信息
var comb = ShaderUtils.ParseShaderCombinations( shader, true );
// 跳过一些完全Use其他shader,自己不含有代码的shader, 不处理
// return;
// 反向遍历,利于删除操作
for ( int i = data.Count - 1; i >= 0; --i ) {
// 当前编译单元中的变体关键字列表
var _keywords = data[ i ].shaderKeywordSet.GetShaderKeywords();
// 只剔除有关键字的情形,减少代码复杂度
// 实际上,无关键字的变体也可能被丢弃不用,简单舍弃这次剔除操作并不会增加太多编译负担
if ( _keywords.Length > 0 ) {
var keywordList = new HashSet();
for ( int j = 0; j < _keywords.Length; ++j ) {
var name = _keywords[ j ].GetKeywordName();
fullKeywords.Add( name );
if ( snippetCombinations.multi_compiles != null ) {
if ( Array.IndexOf( snippetCombinations.multi_compiles, name ) < 0 ) {
// 排除multi_compiles编译宏,这些是必须使用的,不能剔除
// 这里只添加不含multi_compile关键字
keywordList.Add( name );
}
}
}
if ( keywordList.Count > 0 ) {
// 说明这个变体的关键字时可以被剔除编译的
// 进一步判定:
// 由这一关键字序列构成的变体,是否在我们提前存储的变体集资源中出现
// 在遍历判定已经使用的变体集的时候,注意要把含有multi_compile项
// 的关键字去掉,在无序对比,如果能完全匹配,则说明当前次编译的
// shader变体可能会使用,否则就剔除
// ...
var matched = false;
// 遍历所有从项目中搜集到的变体
for ( int n = 0; n < rawVariants.Count; ++n ) {
var variant = rawVariants[ n ];
var matchCount = -1;
var mismatchCount = 0;
var skipCount = 0;
if ( variant.shader == shader && variant.passType == snippet.passType ) {
matchCount = 0;
// 需要说明一下:
// 查找匹配的变体时,需要排除multi_compiles关键字
// snippetCombinations数据从手工解析ParsedCombinations-XXX.shader而来
// 如果直接调用ShaderUtil.GetShaderVariantEntries,可能会因为全变体数量过大而内存爆掉
for ( var m = 0; m < variant.keywords.Length; ++m ) {
var keyword = variant.keywords[ m ];
if ( Array.IndexOf( snippetCombinations.multi_compiles, keyword ) < 0 ) {
if ( keywordList.Contains( keyword ) ) {
++matchCount;
} else {
++mismatchCount;
break;
}
} else {
++skipCount;
}
}
}
if ( matchCount >= 0 && mismatchCount == 0 && matchCount + skipCount == keywordList.Count ) {
matched = true;
break;
}
}
if ( !matched ) {
data.RemoveAt( i );
}
}
}
}
}
}
结束
经过上面一系列的操作,shader变体收集流程和编译时间都得到的优化。但在实现整个了流程的过程中,使用了不少unity并不常用的编辑器API,由于部分过程获取的信息不完整,导致最终的结果肯定还有一些难以察觉的错误,该方法也需要进一步研究和改进。
shader变体是什么_GitHub - 7732050/ShaderVariantCollector相关推荐
- shader变体是什么_[Unity/shaderlab]关于着色器变体
在Unity中可以通过#pragma multi_compile或者#pragma shader_feature指令来实现着色器多样化. 在运行时,相应的着色器变体是从材质的关键词中取得的(Mater ...
- Unity Shader variants (shader 变体)
官方地址 https://docs.unity3d.com/cn/2022.2/Manual/SL-MultipleProgramVariants.html 教程可以看这里 https://www.j ...
- Unity Shader 变体处理与预加载流程
一.什么是Shader变体,它是怎么出现的 当我们写完一个shader以后,unity需要加载和编译,这个过程由着色器的构建管线来完成,它的输入是着色器,而它的输出就是今天的主角---着色器变体:每一 ...
- Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板
写在之前 Shader变体.Shader属性定义技巧.自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用 ...
- shader变体是什么_Shader Variants 打包遇到的问题
遇到的问题 最常见的是打包到手机后效果与PC上不一致,具体情况比如: 光照贴图失效 雾失效 透明或者cutoff失效 以上首先需要检查的地方是Shader变体的编译设置 超级着色器编译成N个变体 如果 ...
- unity打包后运行出错_一种Shader变体收集和打包编译优化的思路
这是侑虎科技第646篇文章,感谢作者卢建供稿.欢迎转发分享,未经作者授权请勿转载.如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨.(QQ群:793972859) 作者主页:https://gi ...
- Shader山下(二十一)多重变体(Multiple Variants)
2019独角兽企业重金招聘Python工程师标准>>> Shader山下(二十)编译指令(Compilation Directives)介绍了如何使用编译指令,本文就专文介绍一下多重 ...
- Shader:优化破解变体的“影分身”之术
本期我们将剖析刚上新的Shader Analyzer中和Shader变体相关的规则:"Build后生成变体数过多的Shader"."项目中可能生成变体数过多的Shader ...
- Unity Shader - Built-in管线下优化 multi_compile_fwdbase、multi_compile_fog 变体
文章目录 变体过多的缺点 项目情况 #pragma multi_compile_fwdbase 和 multi_compile_fog 生存的变体(keyword) 生存的变体 变体的数量 查看编译生 ...
最新文章
- 我也说说Emacs吧(6) - Lisp速成
- 漫画:你会感觉容器使用起来很痛苦吗?
- spark结构化流保存mysql_Spark结构化流异常:不支持没有水印的附加输出模式
- python3精要(33)-字典解析与集合解析,if else 用于解析
- NLP word2vec 计算优化
- 春风十里不如春城一聚:华平解决方案巡展走进昆明
- IMP-00041: 警告: 创建的对象带有编译警告解决办法
- iOS 设计模式之工厂模式
- ADN中国团队參加微软的Kinect全国大赛获得三等奖
- mysql 1130本地连接_mysql ERROR 1130 问题解决方案
- 【机器人】关于驱动器与控制器的工作机制
- java w3c解析xml乱码_下载xml 中文乱码
- windows。forms.timer设置第一次不等待_混凝土密封固化剂个人简易施工方案(不打磨)...
- paip.XXListener is already configured监听器已经被配置的解决
- 监控系统中的几种服务器,监控系统各种服务器
- 超级无敌diao炸天的手写堆
- 上海数学高考 计算机,编程已进入高考?别被广告文洗脑,编程并未纳入上海高考科目!...
- Unity3D Shader 新手教程(2/6) —— 积雪Shader
- java 打印星号_JAVA打印星号
- 数据中台稳定性的“四高” | StartDT Tech Lab 18