当项目打包后,Shader.Find寻找Shader的方式可能会失效,原因是当前项目可能没有引用此Shader。
一般解决方式,是Edit/Project Settings/Graphics,将Shader加入到Always Includes Shaders中,我不喜欢这种方法,因为Always Includes Shaders一般是加载那些Hidden Shader的。所以我是新建一个材质球引用Shader,并把材质球拖到需要用到的位置。
值得注意的是,URP中的Shader也能在打包后构建材质,我找到Shader的引用位置:
[Serializable, ReloadGroup]
public sealed class ShaderResources
{
[Reload("Shaders/PostProcessing/StopNaN.shader")]
public Shader stopNanPS;
[Reload("Shaders/PostProcessing/SubpixelMorphologicalAntialiasing.shader")]
public Shader subpixelMorphologicalAntialiasingPS;
[Reload("Shaders/PostProcessing/GaussianDepthOfField.shader")]
public Shader gaussianDepthOfFieldPS;
//....
用Reload标注的地址,这个路径基于URP Package地址。
点进去能看到ReloadAttribute,就是把路径存储起来。
ShaderResources类被PostProcessData引用:
public class PostProcessData : ScriptableObject
{
//....
public ShaderResources shaders;
//...
这是一个ScriptableObject,对象可以被存储为asset文件。
存储、加载过程是在ForawdRendererData中,ForwardRendererData本身也是一个ScriptableObject,它包含了一个PostProcessData对象,当Create时,它会先从文件中读取自己的属性,然后再从文件读取PostProcessData对象的属性:
//ForwardRendererData
protected override ScriptableRenderer Create()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
ResourceReloader.TryReloadAllNullIn(this, UniversalRenderPipelineAsset.packagePath);
ResourceReloader.TryReloadAllNullIn(postProcessData, UniversalRenderPipelineAsset.packagePath);
}
#endif
return new ForwardRenderer(this);
}
ResourceReloader.TryReloadAllNullIn间接调用ReloadAllNullIn:
public static bool ReloadAllNullIn(System.Object container, string basePath)
{
if (IsNull(container))
return false;
var changed = false;
foreach (var fieldInfo in container.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
//Recurse on sub-containers
// 如果标注了ReloadGroup属性
if (IsReloadGroup(fieldInfo))
{
changed |= FixGroupIfNeeded(container, fieldInfo);
changed |= ReloadAllNullIn(fieldInfo.GetValue(container), basePath);
}
//Find null field and reload them
//得到ReloadAttribute
var attribute = GetReloadAttribute(fieldInfo);
if (attribute != null)
{
if (attribute.paths.Length == 1)
{
//如果container.field是null,则从文件中读取并赋值给field
changed |= SetAndLoadIfNull(container, fieldInfo, GetFullPath(basePath, attribute),
attribute.package == ReloadAttribute.Package.Builtin);
}
//...
当ForwardRendererData调用ResourceReloader.TryReloadAllNullIn(this……
时,先对ForwardRendererData对象中的每个有Reload的成员赋值、寻找路径,其中postProcessData成员是这样的:
[Reload("Runtime/Data/PostProcessData.asset")]
public PostProcessData postProcessData = null;
PostProcessData.asset中存储了后处理需要的Shader和纹理,直接点看不见,是因为PostProcessDataEditor.cs中进行了开发模式判断,注释掉就好:
[CustomEditor(typeof(PostProcessData), true)]
public class PostProcessDataEditor : Editor
{
SerializedProperty m_Shaders;
SerializedProperty m_Textures;
private void OnEnable()
{
m_Shaders = serializedObject.FindProperty("shaders");
m_Textures = serializedObject.FindProperty("textures");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Add a "Reload All" button in inspector when we are in developer's mode
// 这里注释掉
//if (EditorPrefs.GetBool("DeveloperMode"))
//{
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_Shaders, true);
EditorGUILayout.PropertyField(m_Textures, true);
if (GUILayout.Button("Reload All"))
{
var resources = target as PostProcessData;
resources.shaders = null;
resources.textures = null;
ResourceReloader.ReloadAllNullIn(target, UniversalRenderPipelineAsset.packagePath);
}
//}
serializedObject.ApplyModifiedProperties();
}
}
能看到:
如果头一次加载,这个对象中的成员都是没有的,所以ForwardRendererData又调用了
ResourceReloader.TryReloadAllNullIn(postProcessData……
,让postProcessData根据成员Reload标注的路径,将null成员赋值。当我们改代码时,就会重新调用Reload,重新加载一遍这些ScriptableObject,而打包时,因为存在引用这些资源的对象,这些资源也会被打包进去。
不过有一点要注意,加载这些资源的方法SetAndLoadIfNull,顾名思义,只有资源是Null时才会load,所以我们单纯更改Reload的地址,重新调用这一系列流程时,Unity会判断变量不为Null,就不会重新寻找资源。我们可以通过改写代码的方式,或手动设置为Null的方式来解决这个问题。