xiaozongpeng 发表于 2022-1-14 07:20

Unity Shader: 一个简单的(规则化)序列帧动画(2)

接前文: Unity Shader: 一个简单的(规则化)序列帧动画(1)
序列帧有时候会应用在数量特别庞大的场景,如下图所示:

image.png

创建了900个方阵,每个方阵内有25个对象,共22500个对象,每个对象使用统一的action,因为有自动合并批次,所以效率看起来似乎还可以,但实际应用中,我们不可能所有的方阵都整齐划一,不同的方阵在不同的时机有不同的action,所以通过交叉action的方式来模拟处理:

image.png

此时DC就已经升高到1万+,帧率也很低.
要解决此问题,需要用到GPUInstancing(官方文档为:https://docs.unity3d.com/Manual/GPUInstancing.html),使其满足同一Material具有不同表现的情况.
修改后的shader如下:
Shader "Test/SimpleMovieClip"{    Properties    {      _MainTex("Image Sequence", 2D) = "white" { }// 序列帧图片      _RowCount("总行数", Float) = 1 // 行数      _ColumnCount("总列数", Float) = 1 // 列数      _FrameRate("帧率", Range(1, 100)) = 30 // speed      _ActionRowIndex("ActionRowIndex", Range(0, 100)) = 0      _ActionFrames("当前action帧数", Range(0, 100)) = 0      _Color("Color", Color) = (1,1,1,1)    }      SubShader      {            //一般序列帧动画的纹理会带有Alpha通道,因此要按透明效果渲染,需要设置标签,关闭深度写入,使用并设置混合            Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"}            ZWrite Off            Blend SrcAlpha OneMinusSrcAlpha            Pass      {                Tags { "LightMode" = "ForwardBase" }                CGPROGRAM                #pragma vertex vert                #pragma fragment frag                //#pragma target 3.0                // make fog work                //#pragma multi_compile_fog                #pragma multi_compile_instancing                #include "UnityCG.cginc"                struct appdata                {                  float4 vertex : POSITION;                  float2 uv : TEXCOORD0;                  float4 uv2 : TEXCOORD1;                  UNITY_VERTEX_INPUT_INSTANCE_ID                };                struct v2f                {                  float2 uv : TEXCOORD0;                  //UNITY_FOG_COORDS(1)                  float4 vertex : SV_POSITION;                  UNITY_VERTEX_INPUT_INSTANCE_ID // necessary only if you want to access instanced properties in fragment Shader.                };                sampler2D _MainTex;                float4 _MainTex_ST;                float _RowCount;                float _ColumnCount;                // float _FrameRate;                // float _ActionRowIndex;                // float _ActionFrames;                UNITY_INSTANCING_BUFFER_START(Props)                  UNITY_DEFINE_INSTANCED_PROP(float, _FrameRate)                  UNITY_DEFINE_INSTANCED_PROP(float, _ActionRowIndex)                  UNITY_DEFINE_INSTANCED_PROP(float, _ActionFrames)                UNITY_INSTANCING_BUFFER_END(Props)                fixed4 _Color;                v2f vert(appdata v)                {                  v2f o;                  UNITY_SETUP_INSTANCE_ID(v);                  UNITY_TRANSFER_INSTANCE_ID(v, o); // necessary only if you want to access instanced properties in the fragment Shader.                  o.vertex = UnityObjectToClipPos(v.vertex);                  o.uv = TRANSFORM_TEX(v.uv, _MainTex);                  // 是用原始uv,不进行平铺和偏移                  // o.uv.xy = v.uv.xy;// * _MainTex_ST.xy + _MainTex_ST.zw;                  //UNITY_TRANSFER_FOG(o,o.vertex);                  return o;                }                fixed4 frag(v2f i) : SV_Target                {                  UNITY_SETUP_INSTANCE_ID(i); // necessary only if any instanced properties are going to be accessed in the fragment Shader.                  // 将时间取整(变成以秒为单位)相当于1秒1帧,放大到_FrameRate后,相当于得到帧index,通过index去计算行列索引.                  // 必须将纹理的wrap mode设置为Repeat(或类似的设定),因为当time>_ColumnCount*2时,row会大于_RowCount                  // uvoff中计算的y值会大于1,需要通过纹理的Repeat机制来重复显示.                  // 或者在外部维护一个index变量,并传进来,这样可以在外层将这个index进行重置为0                  float index = floor(_Time.y * UNITY_ACCESS_INSTANCED_PROP(Props, _FrameRate));                  // 取整得到行索引(播放顺序设计为从左到右,先行后列)                  float rowIndex = UNITY_ACCESS_INSTANCED_PROP(Props, _ActionRowIndex); // _ActionRowIndex;//floor(index / _ColumnCount);                  // 余数为列索引                     //float columnIndex = fmod(index, _ActionFrames); // index - rowIndex * _ColumnCount;                  float columnIndex = fmod(index, UNITY_ACCESS_INSTANCED_PROP(Props, _ActionFrames)); // index - rowIndex * _ColumnCount;                  half2 iuv = i.uv.xy; // /_MainTex_ST.xy;                  // 使用中的行列值作为分割计算的元值(总比值). 相当于一个窗口,通过该窗口的上下左右定位得到每帧图片的uv                  half2 rawSplit = half2(_ColumnCount, _RowCount);                  // 当前uv通过rawSplit分割后,得到当前uv在总uv中的占比. 相当于(窗口的)固定大小                  iuv /= rawSplit;                  // 通过当前计算出的行列值与总比值的比例,得到uv的起始偏移量. 相当于(窗口的)起始位置, row是从上到下,取反后转换为uv的从下到上                  half2 uvoff = half2(columnIndex, -rowIndex) / rawSplit;                  iuv += uvoff;                  // iuv*=-1;                  fixed4 col = tex2D(_MainTex, iuv) * _Color;                  // apply fog                  //UNITY_APPLY_FOG(i.fogCoord, col);                  return col;                }                ENDCG                }      }            FallBack "Transparent/VertexLit"}
将需要修改的(特性)变量由直接声明变更为了instanced模式:
UNITY_DEFINE_INSTANCED_PROP(float, _FrameRate)UNITY_DEFINE_INSTANCED_PROP(float, _ActionRowIndex)UNITY_DEFINE_INSTANCED_PROP(float, _ActionFrames)
同时勾选Enable GPU Instancing:


image.png

测试代码(C#)中,使用MaterialPropertyBlock进行属性修改:

// ...var propBlock = new MaterialPropertyBlock();propBlock.SetFloat("_ActionRowIndex", 0);propBlock.SetFloat("_ActionFrames", 5);propBlock.SetFloat("_FrameRate", 2);// ...// 进行设置:go.GetComponent<MeshRenderer>().SetPropertyBlock(propBlock);//...
(测试代码比较简单,请自行构建)
修改后效果图如下:

image.png

如上图所示DC大幅下降,帧率也有所提高. 此处额外查看下Instance的处理情况.
Instance的额外测试


通过Frame Debug观察这52个DC的具体情况:


图1.png


图2.png

默认1个,测试环境中的天空占6个,其它都是来自45个均来自Draw Mesh(instanced),图1处的红框部分是说明在instance机制下,每次合批中,有511个实例进行了合并(含有了2044个顶点). 预览图中看到的是2个三角形的片,是因为我的测试中是用生成的2个三角形来为显示对象的sharedMesh赋值的.
// ...var mesh = new Mesh();var size = 0.1f;var newVertices = new Vector3[]{      new Vector3(0, 0, 0), new Vector3(0, 0, size), new Vector3(size, 0, size), new Vector3(size, 0, 0),};var newUV = new Vector2[]{      new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1),new Vector2(1, 0),};var newTriangles = new int[]{      0, 1, 2, 2, 3, 0,};mesh.vertices = newVertices;mesh.uv = newUV;mesh.triangles = newTriangles;// ...go.GetComponent<MeshFilter>().sharedMesh = mesh;// ...
图2中,最后一个Draw Mesh(instanced)是64个顶点. 相当于24*2044+64=90000个顶点, 刚好与原始设计(900个方阵,每个方阵内有25个对象,共22500个对象)的90000个顶点匹配.

在Unity Dynamic Batching中,一般要求单个模型的顶点信息数据不超过900个,但通过Instancing就可以超过此限制.
将2个三角形的模型替换为一个2000个顶点的模型来观察:


image.png

测试结果如下:


image.png


image.png

DC不变,合并的实例数量也不变(511个),但单次合并的顶点数变为了100万. 此举虽然能加大mesh合并但加重的是CPU的负担(要在CPU侧进行合并计算),要达到最优效率,需要找到平衡点.
具体原理参考: https://github.com/vanCopper/Unity-GPU-Instancing
回到主题


虽然在Instancing加持下,多个action性能消耗有所降低,但到此还没结束,实际情况中我们往往会有多个角色多个action的情况,哪怕一个角色的所有action合并为了一张贴图,但总会有多个角色存在于场景中的情况,现在测试2张Material交替显示的情况(第二张material设置了一个不同的颜色以示区分),模拟2个角色各自表现2个action,结果如下图所示:

image.png

性能又下降了,说明在一个节点之下放置不同Material时,也会导致Instancing不生效.
解决方式1: 在使用不同Material的对象上挂载一个SortingGroup,即将不同的Material进行分层处理:
// ...if (ismat2)    go.AddComponent<UnityEngine.Rendering.SortingGroup>().sortingOrder = 1;else    go.AddComponent<UnityEngine.Rendering.SortingGroup>().sortingOrder = 2;// ...


image.png

解决方式2: 按角色进行分层处理(类似canvas中的分层优化),将同类Material归类到一个节点下,然后在该节点添加SortingGroup,但当一个SortingGroup下节点数过多会收到一个错误:


image.png

将测试(C#)代码修改为同类Material归类为一个大组节点,其下再按4096为小组节点,小组节点下再挂载显示对象:


image.png

测试结果如下:

image.png

Frame Debug中也能看到全部都是instanced后的结果:

image.png

此方式是在每个显示对象的层级交错时有问题,但也基本满足需求.

除上述(动态合并)方式外,还可以使用Graphics.DrawMeshInstanced()进行直接绘制(没有显示节点对象,直接自行管理绘制数据)
具体可以参考: https://gist.github.com/Cyanilux/e7afdc5c65094bfd0827467f8e4c3c54

转载请注明出处: https://www.jianshu.com/p/e633db24ba31
页: [1]
查看完整版本: Unity Shader: 一个简单的(规则化)序列帧动画(2)