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

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

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=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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容