FeastSC 发表于 2022-11-24 09:58

Unity粒子特效自检工具

在做这个粒子自检工具之前呢, 技术部门给过美术组各种规范,但是几乎没有遵守到位.都是直到项目中出现各种问题之后才去缝缝补补,此时再次修改费时费力.还会遇到各个部门的弹皮球和效果上的激烈针锋. 为此我做了这个自检工具可以让美术同学实时了解自己特效的问题. 同时还基于此做了jenkies自动化工具,每天定时将整个项目的资源跑一遍,找出问题资源. 将错误问题及早发现并消灭.
影响粒子特效性能的部分

特效场景所用的表现都是由粒子特效, 拖尾Trial, 通过animator控制的meshrenderer和skinrenderer构成.
粒子特效中发射器,粒子数,预热, 网格和贴图, overdraw, 屏占比 是几个重要的指标参数.使用的这些度会影响整体性能, 过多的发射器直接增大内存(一个接近50k),序列化时间增加drawcall, 粒子数和粒子预热会直接影响CPU. 网格和贴图会直接增加IO加载,内存和GPU带宽.overdraw重绘对GPU影响显著 .



.屏占比过大也会影响PGPU不过很多时候是特效使用了较大的粒子,但是实际Game窗口很小



同时还需要注意冗余资源的清除, 这是unity为了方便使用会记录历史资源,比如一个材质球使用了一个shader并给他赋值了一个贴图随后有替换了shader并赋值了新的参数,这时候会保留对之前的贴图的引用关系.同样keyword也会残留,这个会打断合批. 还有粒子系统一开始使用mesh渲染随后替换成了广告板模式.
获取运行指标数据


[*]发射器数量,这个很好处理直接获取
var m_ParticleSystems = GetComponentsInChildren<ParticleSystem>();

[*]运行时的粒子数量,这个需要使用Unity未暴露的API
//通过反射获取粒子数的方法
MethodInfo m_CalculateEffectUIDataMethod = typeof(ParticleSystem).GetMethod("CalculateEffectUIData",
            BindingFlags.Instance | BindingFlags.NonPublic);
//当前系统的粒子数
int m_ParticleCount = 0;
var m_ParticleSystems = GetComponentsInChildren<ParticleSystem>();
foreach (var ps in m_ParticleSystems)
{
    int count = 0;
    object[] invokeArgs = {count, 0.0f, Mathf.Infinity};
    m_CalculateEffectUIDataMethod.Invoke(ps, invokeArgs);
    count = (int) invokeArgs;
    m_ParticleCount += count;
}

[*]预热开启参数
var particleSystemlist = go.GetComponentsInChildren<ParticleSystem>(true);
foreach (ParticleSystem item in particleSystemlist)
{
    if (item.main.prewarm)
    {
      // to do;
    }
}

[*]网格和贴图的统计.   贴图可以通过获取使用的材质球,然后使用material.GetTexturePropertyNameIDs() 获取所有的贴图资源,如下可以获得贴图的数量,长宽,总内存大小
var textures = new List<Texture>();
//贴图数量
textureCount = 0;
//占用内存大小
int sumSize = 0;

var meshRendererlist = go.GetComponentsInChildren<ParticleSystemRenderer>(true);
foreach (ParticleSystemRenderer item in meshRendererlist)
{
    if (item.sharedMaterials != null)
    {
      foreach (var mat in item.sharedMaterials)
      {
            int[] textureIds = mat.GetTexturePropertyNameIDs();
            foreach (var id in textureIds)
            {
                Texture texture = mat.GetTexture(id);
                if (texture && !textures.Contains(texture))
                {
                  textures.Add(texture);
                  textureCount++;
                  sumSize = sumSize + GetStorageMemorySize(texture);
                }
            }
      }

    }
}
//获取贴图占用内存大小
MethodInfo GetStorageMemorySize1 = null;
private static int GetStorageMemorySize(Texture texture)
{
    if (GetStorageMemorySize1 == null)
    {
      var assembly = typeof(AssetDatabase).Assembly;
      var custom = assembly.GetType("UnityEditor.TextureUtil");
      GetStorageMemorySize1 =
            custom.GetMethod("GetStorageMemorySize", BindingFlags.Public | BindingFlags.Static);
    }

    //return (int)InvokeInternalAPI("UnityEditor.TextureUtil", "GetStorageMemorySize", texture);
    return GetStorageMemorySize1 != null ? (int) GetStorageMemorySize1.Invoke(null, new object[] {texture}) : 0;
}粒子系统网格资源可以通过发射器获取
var meshRendererlist = go.GetComponentsInChildren<ParticleSystemRenderer>(true);
foreach (ParticleSystemRenderer item in meshRendererlist)
{
    //mesh顶点过多
    if (item.mesh && item.mesh.vertexCount > max)
    {
      sb.AppendLine($"<color=white> 粒子[{item.name}] 使用mesh顶点数<color=red>{item.mesh.vertexCount}</color> ");
    }
    //冗余mesh问题
    if (item.renderMode != ParticleSystemRenderMode.Mesh)
    {
      if (item.mesh)
      {
            sb.AppendLine($"<color=white> 粒子[{item.name}] 使用非mesh渲染但是还引用了mesh资源,");
      }
    }
}

[*]overdraw和屏占比的计算稍微复杂些原理是通过相机每帧捕获一次特效的RT, 渲染的时候替换原shader,将g通道增加0.04, 然后shader混合改为oneone. 最后读取rt各个像素上g通道的值,除以0.04,就得到该像素点的绘制次数.将每帧的总像素绘制次数除以帧数得到平均overdraw, 将每帧绘制的像素总数除以rt像素总数得到屏占比 . 该算法参考了 如下.   shader如下:
Shader "ParticleProfiler/OverDraw"
{
    SubShader
    {
      Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
      LOD 100
      Fog { Mode Off }
      ZWrite Off
      ZTest Always
      Blend One One

      Pass
      {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return fixed4(0.1, 0.04, 0.02, 0);
            }
            ENDCG
      }
    }
}
//记录每帧像素渲染数据
      public void RecordFramePixelData(EffectEvlaData effectEvlaData)
      {
            int pixTotal = 0;
            int pixActualDraw = 0;

            GetCameraOverDrawData(out pixTotal, out pixActualDraw);

            // 历史数据+1
            effectEvlaData.UpdateOneData(pixTotal, pixActualDraw);
      }

      public void GetCameraOverDrawData(out int pixTotal, out int pixActualDraw)
      {
            if (!_camera)
            {
                pixTotal = 0;
                pixActualDraw = 0;
                return;
            }

            //记录当前激活的渲染纹理
            RenderTexture activeTextrue = RenderTexture.active;

            //渲染指定范围的rt,并记录范围内所有rgb像素值
            _camera.targetTexture = rt;
            _camera.Render();
            RenderTexture.active = rt;
            Texture2D texture = new Texture2D(rt.width, rt.height, TextureFormat.ARGB32, false);
            texture.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
            GetOverDrawData(texture, out pixTotal, out pixActualDraw);

            //恢复之前激活的渲染纹理
            RenderTexture.active = activeTextrue;
            Texture2D.DestroyImmediate(texture);
            rt.Release();
            _camera.targetTexture = null;
      }

      public void GetOverDrawData(Texture2D texture, out int pixTotal, out int pixActualDraw)
      {
            var texw = texture.width;
            var texh = texture.height;

            var pixels = texture.GetPixels();

            int index = 0;

            pixTotal = 0;
            pixActualDraw = 0;

            for (var y = 0; y < texh; y++)
            {
                for (var x = 0; x < texw; x++)
                {
                  float r = pixels.r;
                  float g = pixels.g;
                  float b = pixels.b;

                  bool isEmptyPix = IsEmptyPix(r, g, b);
                  if (!isEmptyPix)
                  {
                        pixTotal++;

                        int drawThisPixTimes = DrawPixTimes(r, g, b);
                        pixActualDraw += drawThisPixTimes;
                  }

                  index++;
                }
            }
      }

      //计算单像素的绘制次数,为什么是0.04,请看OverDraw.shader文件
      public int DrawPixTimes(float r, float g, float b)
      {
            return Mathf.CeilToInt(g / 0.039f);
      }

      public bool IsEmptyPix(float r, float g, float b)
      {
            return r == 0 && g == 0 && b == 0;
      }

[*]清除冗余资源, 这里主要就是材质球的无用贴图和keyword还有发射器的不使用的mesh
//清除无用mesh
ParticleSystemRenderer[] renders =
                            gameObject.GetComponentsInChildren<ParticleSystemRenderer>(true);
foreach (var renderer in renders)
{
if (renderer.renderMode != ParticleSystemRenderMode.Mesh)
{
    renderer.mesh = null;
}
}
//不能使用预热
ParticleSystem[] pss = gameObject.GetComponentsInChildren<ParticleSystem>(true);
foreach (var ps in pss)
{
ParticleSystem.MainModule main = ps.main;
main.prewarm = false;
}
private static void ClearMatProperteies(Material material)
      {
            if (material)
            {
                //remove unuse keyword
                List<string> materialKeywordsLst = new List<string>(material.shaderKeywords);
                List<string> notExistKeywords = CheckMaterialShaderKeywords(material);
                foreach (var each in notExistKeywords)
                {
                  materialKeywordsLst.Remove(each);
                }

                material.shaderKeywords = materialKeywordsLst.ToArray();
                //remove unuse texture
                SerializedObject psSource = new SerializedObject(material);
                SerializedProperty emissionProperty = psSource.FindProperty("m_SavedProperties");
                SerializedProperty texEnvs = emissionProperty.FindPropertyRelative("m_TexEnvs");

                if (CleanMaterialSerializedProperty(texEnvs, material))
                {
                  Debug.LogError("Find and clean useless texture propreties in " + material.name);
                }

                psSource.ApplyModifiedProperties();
                EditorUtility.SetDirty(material);
            }
      }

      //true: has useless propeties
      private static bool CleanMaterialSerializedProperty(SerializedProperty property, Material mat)
      {
            bool res = false;
            for (int j = property.arraySize - 1; j >= 0; j--)
            {
                string propertyName = property.GetArrayElementAtIndex(j)?.FindPropertyRelative("first").stringValue;

                if (!mat.HasProperty(propertyName))
                {
                  if (propertyName.Equals("_MainTex"))
                  {
                        //_MainTex是内建属性,是置空不删除,否则UITexture等控件在获取mat.maintexture的时候会报错
                        if (property.GetArrayElementAtIndex(j).FindPropertyRelative("second")
                              .FindPropertyRelative("m_Texture").objectReferenceValue != null)
                        {
                            property.GetArrayElementAtIndex(j).FindPropertyRelative("second")
                              .FindPropertyRelative("m_Texture").objectReferenceValue = null;
                            Debug.Log("Set _MainTex is null");
                            res = true;
                        }
                  }
                  else
                  {
                        property.DeleteArrayElementAtIndex(j);
                        Debug.Log("Delete property in serialized object : " + propertyName);
                        res = true;
                  }
                }
            }

            return res;
      }使用方法

给预设挂上脚本ParticleProfile, 选好特效等级然后点击start即可看到性能报告


测试工程 链接: https://pan.baidu.com/s/1qkwugqcTSrmotW5H8mRS9A?pwd=rv0a 提取码: rv0a
页: [1]
查看完整版本: Unity粒子特效自检工具