live2d_Live2D 性能优化

9f33f311578e7437c1eec429010c1f86.png

随着Live2D在项目研发中被广泛使用,其性能优化的需求已经不容忽视。笔者通过模型资源、Mesh、RenderTexture、Material和CPU耗时这5个方面来阐述优化的过程,并且最终实现了低端机上6个模型30帧的效果,值得大家参考。

优化结果

测试机型:低端机——红米4X
测试样例:同样的6个模型(游戏中同屏最多6个模型)
版本:3.2.05

8b58d2ed74342498de09c616e02a3d73.png

9d704c5549c2b1ac9ec0ad635bd1e159.png

CPU优化主要在CubismModel.Update、CubismModel.OnRenderObject
内存优化主要在Texture2D、Mesh、RenderTexture、Material以及Mono内存分配

接下来介绍从模型资源、Mesh、RenderTexture、Material和CPU耗时5个方面来优化性能。


1、模型资源

项目前期没有制定Live2D美术规范,美术导出文件的时候,直接使用自动布局顶点的方式,而Live2D自动生成Mesh的时候对于每个部件的内外边界各生成一份顶点,发现如下问题:
1、模型Mesh数量太多
2、模型的总顶点数达到6K左右

后续跟美术沟通之后,对Live2D模型(后续的模型都指Live2D模型)的导出规定如下:
1、手动布局顶点
2、ArtMesh数量控制在100以内(之前的模型由于改动的工作量太大,暂时不做修改)
3、模型顶点数控制在2k面以内(游戏最多有6个模型)
4、Edit Texture Altas中贴图大小1024*1024,自动布局且Margin为5px
5、为了避免多贴图导致DrawCall升高,一个模型只使用一张贴图

2小时后,通过Live2D Cubism Editor->File->Model Statistics查看优化结果如下:

30884802edf61015550ca0e21f2d7f59.png

9956a86f6ab8570903bb2561f8f20a18.png

性能数据测试结果如下:

80303bb2080ff4582158fedf075c6513.png

2、Mesh

测试发现游戏一个模型的Mesh数量在80-200之间,同屏最多有6个模型,通过UWA GOT测试发现Mesh峰值达到107MB(27380个),Material数量达到10.2MB(14968个),其中Unlit(Instance)数量14563个明显异常。

6bd3c1fdf67bdc5771eb1c0db3ae22ee.png

88f4ed26a1237b92bc21fa6361791689.png

研究CubismRenderer.cs发现如下问题:
1、运行时创建了两份Mesh内存
2、Mesh资源未销毁,导致内存泄漏
3、顶点色每帧都会重新赋值

针对问题1,经测试发现,改成一份Mesh并没有明显的CPU变化,Mesh内存减少一半,由于每一帧都有顶点变化,_meshFilter.mesh = FrontMesh每帧700次的CPU消耗也可以省去。
针对问题2,可以在OnDestroy中调用Destroy接口销毁Mesh。
针对问题3,使用标记控制是否更新顶点色,减少顶点色赋值的0.5ms左右CPU消耗。

另外值得一提的是,Mesh中使用了顶点色处理透明和颜色变化,但是同一个Mesh的顶点色全部相同,可以想到在Shader中控制颜色,实际上为了动态合批不得不使用顶点色,合批之后控制在10个DrawCall左右,不合批每个模型DrawCall数量达到上百个。

3、RenderTexture

CubismMaskTexture.cs中创建的RenderTexture大小默认为1024*1024,项目中遮罩效果大部分使用在眼睛,在实际测试之后有如下优化:
1、将GlobalMaskTexture.asset的Size改成128,实际数值可以根据项目需要修改。
2、CubismMaskTexture.cs中RemoveSource函数加上如下检查,在没有遮罩模型的时候可以释放RenderTexture。

if(Sources.Count == 0)
 {
     if(_renderTexture != null)
     {
         RenderTexture.ReleaseTemporary(_renderTexture);
        _renderTexture = null;
     }
 }

3、CubismMaskTexture.cs中OnDisable()和RefreshRenderTexture()函数中同样添加释放RT的接口。
4、CubismMaskController.cs做如下修改,避免RenderTexture无法释放,同时避免切换模型且未销毁时CubismMaskCommandBuffer.Lateupdate持续CPU消耗

0d7bdffe5546ca4dea73a6529dba85c9.png

0fa8830c318ba7d5c78a1ea8f7132f08.png

修改后:

7f908df6ef274179129ee10ebefe21e2.png

9f5762836f72edd1ecdbe788af4b6661.png

4、Material

通过UWA GOT测试发现材质数量也达到1w多个,研究发现材质有明显的泄漏问题,解决方案如下:

1、CubismRenderer.cs中Material属性

public Material Material
{
     get
    {
         return MeshRenderer.material;
     }
     set
     {
        MeshRenderer.material = value;
     }
 }

修改成:

public Material Material
 {
    get
     {
         return MeshRenderer.sharedMaterial;
     }
     set
     {
         MeshRenderer.sharedMaterial = value;
     }
 }

2、模型导入引擎后,将相同材质的MeshRender缓存,在运行时的Start函数中实例化材质,对所有同材质的MeshRender赋值同一个实例,离线实例化的问题时可能出现两个以上相同的模型共享材质。

ba6d70eb029d5d79dd9d70eecf6c7b91.png

3、模型销毁的时候同时将实例化的材质销毁。

ae7409bca2071c97ab19002283fa606e.png

5、CPU耗时

通过UnityProfiler发现CPU消耗主要在:

1、CubismModel.OnRenderObject[9.92ms]
一部分时间消耗在CubismCoreDll.UpdateModel(Ptr)调用,该接口为Live2D底层封装暂时无法修改,只能通过减少Mesh数量减少CPU时间。

另一部分消耗DynamicDrawableData.ReadFrom(UnmanagedModel),通过分析代码发现这里只是复制数据到DynamicDrawableData,则可以省去该CPU消耗. 同时在使用DynamicDrawableData的逻辑中用UnmanagedModel替代。

2、CubismModel.Update[9.64ms]
这个接口最终调用到CubismRenderController.cs文件的OnDynamicDrawableData接口,其逻辑主要是同步底层数据变化同时更新Mesh信息,优化思路如下:

一方面更新是否可见,渲染顺序,透明度,顶点位置信息. 其中从C++底层获取数据的时候每一次都会进行范围合法性检查,此处可以在循环外对数组进行统一检查,有部分Mesh不可见,可以在Mesh不可见的时候避免更新。

具体逻辑如下:

///CubismUnmanagedArrayView.cs
public unsafe T this[int index]
{
    get
    {
        return Address[index];
    }
}

///CubismRenderController.cs
private void OnDynamicDrawableData(CubismModel sender, CubismUnmanagedModel unmanagedModel)
{
    var dataDrawables = unmanagedModel.Drawables;
    var iLen = dataDrawables.Count;
    var flags = dataDrawables.DynamicFlags;
    var opacities = dataDrawables.Opacities;
    var renderOrders = dataDrawables.RenderOrders;
    var vertexPositions = dataDrawables.VertexPositions;

    if(!flags.IsValid)
    {
        throw new InvalidOperationException("flags Array is empty, or not valid.");
    }
    if (!opacities.IsValid)
    {
        throw new InvalidOperationException("opacities Array is empty, or not valid.");
    }
    if (!renderOrders.IsValid)
    {
        throw new InvalidOperationException("renderOrders Array is empty, or not valid.");
    }
    if (flags.Length < iLen)
    {
        throw new InvalidOperationException(string.Format("flags Array Length[{0}] < iLen[{1}]", flags.Length, iLen));
    }
    if (opacities.Length < iLen)
    {
        throw new InvalidOperationException(string.Format("opacities Array Length[{0}] < iLen[{1}]", opacities.Length, iLen));
    }
    if (renderOrders.Length < iLen)
    {
        throw new InvalidOperationException(string.Format("renderOrders Array Length[{0}] < iLen[{1}]", renderOrders.Length, iLen));
    }

    // Get drawables.
    var renderers = Renderers;
    // Handle render data changes.
    for (var i = 0; i < iLen; ++i)
    {
        var curRenderer = renderers[i];
        var curFlags = flags[i];
        // Skip completely non-dirty data.
        if (curFlags.HasAnyFlag())
        {
            // Update visibility.
            if (curFlags.HasVisibilityDidChangeFlag())
            {
                curRenderer.OnDrawableVisiblityDidChange(curFlags.HasIsVisibleFlag());
            }
            // Update render order.
            if (curFlags.HasRenderOrderDidChangeFlag())
            {
                curRenderer.OnDrawableRenderOrderDidChange(renderOrders[i]);
            }
            // Update opacity.
            if (curFlags.HasOpacityDidChangeFlag())
            {
                curRenderer.OnDrawableOpacityDidChange(opacities[i]);
            }
            // Update vertex positions.
            if (curFlags.HasVertexPositionsDidChangeFlag())
            {
                curRenderer.OnDrawableVertexPositionsDidChange(vertexPositions[i]);
            }
        }
        if (curRenderer.UpdateVisibility())
        {
            curRenderer.UpdateRenderOrder();
            curRenderer.UpdateVertexColors();
            curRenderer.UpdateVertexPositions();
        }
    }
    // Pass draw order changes to handler (if available).
    var drawOrderHandler = DrawOrderHandlerInterface;
    if (drawOrderHandler != null)
    {
        var senderDrawables = sender.Drawables;
        var drawOrders = dataDrawables.DrawOrders;
        for (var i = 0; i < iLen; ++i)
        {
            var curData = flags[i];
            if (curData.HasDrawOrderDidChangeFlag())
            {
                drawOrderHandler.OnDrawOrderDidChange(this, senderDrawables[i], drawOrders[i]);
            }
        }
    }
    dataDrawables.ResetDynamicFlags();
}
///CubismRenderer.cs
internal unsafe void OnDrawableVertexPositionsDidChange(Core.Unmanaged.CubismUnmanagedFloatArrayView newVertexPositions)
        {
            if (!newVertexPositions.IsValid)
            {
                throw new InvalidOperationException("srcVertexPositions Array is empty, or not valid.");
            }
            // Copy vertex positions.
            var iLen = newVertexPositions.Length >> 1;
            if (newVertexPositions.Length < iLen)
            {
                throw new InvalidOperationException(string.Format("newVertexPositions Array Length[{0}] < iLen[{1}]", newVertexPositions.Length, iLen));
            }

            if (VertexPositions.Length != iLen)
            {
                Debug.LogErrorFormat("TranslateVertexPositions dont same length iLen={0}|dstMesh.vertexCount={1}"
                                        , iLen, VertexPositions.Length);
            }
            // Copy vertex positions.
            fixed (Vector3* pDstVertexPositions = VertexPositions)
            {
                for (var v = 0; v < iLen; ++v)
                {
                    var pDst = (pDstVertexPositions + v);
                    var offset = v << 1;


                    pDst->x = newVertexPositions[offset];
                    pDst->y = newVertexPositions[offset | 1];
                }
            }
            // Set swap flag.
            SetNewVertexPositions();
        }

另一方面同步Mesh的信息,源代码使用了双缓冲Mesh优化性能,由于每一帧都有顶点变化,则每帧调用700次左右的Meshes[BackMesh].colors = VertexColors和MeshFilter.mesh = mesh占用大约一半的时间,这里我改成一份Mesh并没有发现明显的CPU变化,Mesh内存减少一半,同时MeshFilter.mesh = mesh的CPU消耗也可以省去。


总结

在低端机红米4X上:
CPU主要耗时优化到45%(30.2ms->13.5ms)
内存优化到32%(13.6MB->4.3MB)
内存和材质泄漏已解决,同时Mesh内存大量减少。

到此Live2D的优化告一段落,在低端机上6个模型已经可以达到30帧。

25f4abbd989697661567b58b665530a2.png

封面图来源于网络https://www.live2d.com/zh-CHS/about/


这是侑虎科技第659篇文章,感谢作者晨星供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在网页上添加Live2D,需要先准备好以下文件: 1. Live2D Core SDK:下载地址为 https://live2d.github.io/#js-core。 2. Live2D 模型文件:可以在网络上搜索到各种免费和付费的Live2D模型,下载后需要将其解压缩到本地。 3. jQuery 库:如果网页中已经用到了jQuery,可以省略这一步。 接下来,按照以下步骤逐步操作: 1. 创建一个 HTML 文件,引入 jQuery 和 Live2D Core SDK: ``` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Live2D Demo</title> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@live2d/[email protected]/dist/Live2DCubismCore.min.js"></script> </head> <body> </body> </html> ``` 2. 在 `body` 中添加一个 `canvas` 元素: ``` <canvas id="live2d" width="300" height="400"></canvas> ``` 3. 在 JavaScript 中加载模型文件: ``` var model; function loadModel() { var modelJson = "模型文件的路径/model.json"; var modelDir = "模型文件的路径/"; Live2D.core.readModel(modelJson, function (buf) { model = Live2D.core.createModel(buf); model.loadTextures(modelDir, function () { draw(); }); }); } ``` 4. 在 `draw` 函数中绘制模型: ``` function draw() { var canvas = document.getElementById("live2d"); if (canvas.getContext) { var gl = canvas.getContext("webgl"); model.setGL(gl); gl.clearColor(0.0, 0.0, 0.0, 0.0); gl.clear(gl.COLOR_BUFFER_BIT); model.draw(); } } ``` 5. 最后调用 `loadModel` 函数即可加载模型并在网页中显示Live2D。 ``` $(document).ready(function () { loadModel(); }); ``` 以上就是给网页添加Live2D的基本步骤,具体实现还需要根据实际情况进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值