前言
这篇博文是对最近遇到的一次渲染材质异常的分析和梳理,打算以简练的形式记录下来以备后续查阅。PS. 个人深感像渲染引擎这样庞大的工程,要想驾驭得又好又快,还是得靠点滴积累的经验。
正文
原本的问题现象是这样的:某项目组在美术工程中正常表现的材质,将资源打包bundle导入到程序工程后出现了表现上的异常(变暗),同时原本支持的实例化能力也消失了。初步排查后发现,程序工程并没有读取到AssetBundle内正确的着色器资源变体(shader variant),项目所需的变体看来并没有成功打包到bundle内!
Unity在无法正确获取目标着色器变体时会按照某种“Fallback”规则定位到另一个存在的备胎,具体可以参考这篇博文中的“变体调用规则”部分。我们知道材质“变暗”是由于这种Fallback机制所致,不过我们并不关心Unity最终选择了哪个shader,真正关心的是为何项目所需的变体没有进入bundle。
第一步是去美术工程确认变体收集情况,可以在目标场景下通过 ProjectSettings->Graphics->Save to asset按钮激活收集逻辑,保存当前场景所有着色器变体信息到本地。参考如下图示:
这里面每一个shader可能对应复数个变体,每个变体是由一种不同与其他变体的KeyWords组合决定的,如果你在编写shader时使用“shader_feature”去控制/定义关键字,那么Unity在收集变体时会判断该材质实际可能使用到的关键词,自动组合,生成如上图这样的变体列表。
在美术工程中收集的变体列表中,我们发现了所需的变体,参考下面节选代码标注部分。
unity_collected.shadervariants
- first: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
second:
variants:
- keywords:
passType: 8
- keywords: INSTANCING_ON _ALPHATEST_ON
passType: 8
- keywords: _ALPHATEST_ON
passType: 8
- keywords:
passType: 13
//以下是问题变体以及构成该变体的关键词组合:
- keywords: DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON LIGHTMAP_ON _ADDITIONAL_LIGHT_SHADOWS
_MAIN_LIGHT_SHADOWS INSTANCING_ON _NORMALMAP _OCCLUSIONMAP _SHADOWS_SOFT
passType: 13
- keywords: _ADDITIONAL_LIGHT_SHADOWS _ALPHATEST_ON _MAIN_LIGHT_SHADOWS _METALLICSPECGLOSSMAP
_NORMALMAP _OCCLUSIONMAP _SHADOWS_SOFT
passType: 13
- keywords: _ADDITIONAL_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS _METALLICSPECGLOSSMAP
_NORMALMAP _OCCLUSIONMAP _SHADOWS_SOFT
passType: 13
- keywords: _ALPHATEST_ON
passType: 13
美术工程能够收集到目标信息,但是打包时没有打入AssetBundle,这种情况下只有可能是打包逻辑在作祟,于是决定利用Vistual Studio的挂载断点功能单步下具体打包逻辑。
首先是构造出打包的目标资源列表,通过官方提供的 “AssetBundlesBrowser”插件触发Unity自己的打包逻辑,具体不赘述。
接下来是要点,打包方法的入口逻辑在 URP自己的Editor工程目录下的 ShaderPreprocessor.cs文件内,具体而言是一个叫“OnProcessShader”的方法。我们可以通过实现 “IPreprocessShaders”这个接口中的“OnProcessShader”来重载shader variant打包处理逻辑,这是后话了。总之成功断点进入目标逻辑:
这个函数的入参很清晰,shader就是目前在处理的待打包的着色器本体,snipperData则是一系列Unity收集好的属性信息,最后一个List存放了所有在变体收集阶段获得到的变体信息。该方法内有嵌套的两次循环,外层循环负责遍历所有变体,内层循环负责校验参与构成当前变体的所有关键字是否“合法可用”,由“StripUnused”负责处理。
//URP-Editor::ShaderPreprocessor.cs
public void OnProcessShader(Shader shader, ShaderSnippetData snippetData, IList compilerDataList)
{
...
for (int i = 0; i < inputShaderVariantCount;)
{
bool removeInput = true;
foreach (var supportedFeatures in ShaderBuildPreprocessor.supportedFeaturesList)
{
if (!StripUnused(supportedFeatures, shader, snippetData, compilerDataList[i]))
{
removeInput = false;
break;
}
}
...
}
...
}
然后很快就在如下位置发现了蹊跷:打包代码阻止了关键词“Additional_Light_Shadows”进入变体,这会使得所有包含了该条关键词的着色器变体文件无法生成!
//URP-Editor::ShaderPreprocessor.cs
bool StripUnusedFeatures(ShaderFeatures features, Shader shader, ShaderSnippetData snippetData, ShaderCompilerData compilerData)
{
...
// No additional light shadows
if (IsFeatureEnabled(ShaderFeatures.ShadowsKeepOffVariants, features))
{
if (stripTool.StripMultiCompileKeepOffVariant(m_AdditionalLightShadows, ShaderFeatures.AdditionalLightShadows))
return true;
}
else
{
if (stripTool.StripMultiCompile(m_AdditionalLightShadows, ShaderFeatures.AdditionalLightShadows))
return true;
}
...
}
通过进一步追踪,在如下代码中发现,美术工程并没有开启对AdditionalLightShadows的支持!
//URP-Editor::ShaderPreprocessor.cs
private static ShaderFeatures GetSupportedShaderFeatures(UniversalRenderPipelineAsset pipelineAsset, int rendererIndex)
{
...
if (pipelineAsset.additionalLightsRenderingMode == LightRenderingMode.PerPixel || clusteredRendering)
{
if (pipelineAsset.supportsAdditionalLightShadows)
{
shaderFeatures |= ShaderFeatures.AdditionalLightShadows;
}
}
return shaderFeatures;
}
该项(supportAdditionalLightShadows)可以通过查看pipelineAsset文件获知,而这个Asset就是URP渲染管线的管线资源文件,可以在Grapics页签下的“Scriptable Render Pipeline Settings”中找到。
我们在Inspector页签中观察目标Asset文件,记得只有切换到Debug模式才能看到所有设置数据。
下图中红框标注的地方就是所谓的控制标识符“supportsAdditionalLightShadows”数值的地方,美术工程中并没有开启额外光源的阴影渲染。
事实上出于性能考量,项目组并不打算要开启主光源之外灯光的阴影渲染,那么为何我们在变体收集阶段还是能收集到这一项关键词呢?经过进一步诊断后发现:
Unity收集shader变体的工作是在运行时进行的,urp管线默认总是会尝试添加 AdditionalLightsShadowCastPass,如果在收集变体的美术场景中不小心放入了额外的光源,使得场景总光源数大于1,那么如下运行时代码的Setup操作就会返回true,这样如果你的shader中带有 shader_feature Additional_Light_Shadow 这样的变体关键字,那么就会被成功收录到变体序列中,与其他重要且不可或缺的KeyWords黏合到一个变体中去。
//URP-Runtime::UniversalRenderer.cs
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
...
bool additionalLightShadows = m_AdditionalLightsShadowCasterPass.Setup(ref renderingData);
...
if (additionalLightShadows)
EnqueuePass(m_AdditionalLightsShadowCasterPass);
}
最后我们在美术场景中找到多余的那一盏“点光源”,将之去除后再次收集+打包,成功输出了目标shader variant。
后记
我们最好能通过覆写 “IPreprocessShaders”的方式,给shader打包逻辑增加必要的告警,尽量避免或提早发现某些变体关键词被拒绝后导致的连坐其他重要关键词(比如 Instance_On)的情况。
以上!