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(&#34;CalculateEffectUIData&#34;,
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(&#34;UnityEditor.TextureUtil&#34;);
GetStorageMemorySize1 =
custom.GetMethod(&#34;GetStorageMemorySize&#34;, BindingFlags.Public | BindingFlags.Static);
}
//return (int)InvokeInternalAPI(&#34;UnityEditor.TextureUtil&#34;, &#34;GetStorageMemorySize&#34;, 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($&#34;<color=white> 粒子[{item.name}] 使用mesh顶点数<color=red>{item.mesh.vertexCount}</color> &#34;);
}
//冗余mesh问题
if (item.renderMode != ParticleSystemRenderMode.Mesh)
{
if (item.mesh)
{
sb.AppendLine($&#34;<color=white> 粒子[{item.name}] 使用非mesh渲染但是还引用了mesh资源,&#34;);
}
}
}
[*]overdraw和屏占比的计算稍微复杂些原理是通过相机每帧捕获一次特效的RT, 渲染的时候替换原shader,将g通道增加0.04, 然后shader混合改为oneone. 最后读取rt各个像素上g通道的值,除以0.04,就得到该像素点的绘制次数.将每帧的总像素绘制次数除以帧数得到平均overdraw, 将每帧绘制的像素总数除以rt像素总数得到屏占比 . 该算法参考了 如下. shader如下:
Shader &#34;ParticleProfiler/OverDraw&#34;
{
SubShader
{
Tags { &#34;RenderType&#34; = &#34;Transparent&#34; &#34;Queue&#34; = &#34;Transparent&#34; }
LOD 100
Fog { Mode Off }
ZWrite Off
ZTest Always
Blend One One
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include &#34;UnityCG.cginc&#34;
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(&#34;m_SavedProperties&#34;);
SerializedProperty texEnvs = emissionProperty.FindPropertyRelative(&#34;m_TexEnvs&#34;);
if (CleanMaterialSerializedProperty(texEnvs, material))
{
Debug.LogError(&#34;Find and clean useless texture propreties in &#34; + 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(&#34;first&#34;).stringValue;
if (!mat.HasProperty(propertyName))
{
if (propertyName.Equals(&#34;_MainTex&#34;))
{
//_MainTex是内建属性,是置空不删除,否则UITexture等控件在获取mat.maintexture的时候会报错
if (property.GetArrayElementAtIndex(j).FindPropertyRelative(&#34;second&#34;)
.FindPropertyRelative(&#34;m_Texture&#34;).objectReferenceValue != null)
{
property.GetArrayElementAtIndex(j).FindPropertyRelative(&#34;second&#34;)
.FindPropertyRelative(&#34;m_Texture&#34;).objectReferenceValue = null;
Debug.Log(&#34;Set _MainTex is null&#34;);
res = true;
}
}
else
{
property.DeleteArrayElementAtIndex(j);
Debug.Log(&#34;Delete property in serialized object : &#34; + propertyName);
res = true;
}
}
}
return res;
}使用方法
给预设挂上脚本ParticleProfile, 选好特效等级然后点击start即可看到性能报告
测试工程 链接: https://pan.baidu.com/s/1qkwugqcTSrmotW5H8mRS9A?pwd=rv0a 提取码: rv0a
页:
[1]