美文网首页
Unity Shader: 一个简单的(规则化)序列帧动画(性能

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

作者: Danny_Yan | 来源:发表于2022-01-11 22:36 被阅读0次

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

    image.png

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

    image.png

    此时DC就已经升高到1万+,帧率也很低.
    要解决此问题,需要用到GPUInstancing(官方文档为:https://docs.unity3d.com/Manual/GPUInstancing.html),使其满足同一Material具有不同表现的情况.
    修改后的shader如下:

    // 规则化序列帧播放,每帧大小应该一致
    // @author Danny_Yan
    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=9万个顶点, 刚好与原始设计(900个方阵,每个方阵内有25个对象,共22500个对象)的9万个顶点匹配.

    在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
    . 如果业务中用到角色动作数量是可预见的,则可以在一个Material中使用所有贴图

    GPUInstancing虽然可以大量减少DC,也不是随便滥用,因为需要在CPU侧计算当次合并中的顶点信息等,所以在移动设备上效率可能反而更低(帧率更低),这就需要根据项目实际业务场景进行反复测试,确定那些业务点中到底增加DC性价比更高还是增加单次合批信息性价比更高.

    转载请注明出处: https://www.jianshu.com/p/e633db24ba31

    相关文章

      网友评论

          本文标题:Unity Shader: 一个简单的(规则化)序列帧动画(性能

          本文链接:https://www.haomeiwen.com/subject/xlyicrtx.html