📌

Unityの動的なMesh作成を高速化する

2024/11/02に公開

前置き

Meshオブジェクト動的な変更処理を行っていたのだけど、作成更新に割と処理時間が取られる.
高速化の実験を行ったので、メモしておく.

結論

  1. Meshは毎回newせずに使い回す
  2. MarkDynamic + SetVertices(Triangles,UV,Normals)でList要素を割り当てる
  3. RecalculateNormalsせずに自前のnormalsを使う
  4. mesh.Clear()は毎回呼び出す

上記を実施することで実行時間が15.10 -> 5.37で35.5%に短縮できた.

PerformanceTest

image

UnityのPerformacneTestingを利用して比較した.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.PerformanceTesting;
using UnityEngine;

namespace PerformanceTest
{
    public class TrianglePerformanceTest
    {
        private const int REPEAT = 500;
        private const int MEASURE_COUNT = 100;

        [Test, Performance]
        public void RunBaseline()
        {
            Measure.Method(() =>
                {
                    var vertices = new List<Vector3>();
                    var triangles = new List<int>();
                    var uv = new List<Vector2>();
                    var normals = new List<Vector3>();

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        var (nextVertices, nextTriangles, nextUV, nextNormals) = CreateTriangle(offset, i * 3);
                        vertices.AddRange(nextVertices);
                        triangles.AddRange(nextTriangles);
                        uv.AddRange(nextUV);
                        normals.AddRange(nextNormals);
                        var mesh = CreateMeshPlain(vertices.ToArray(), triangles.ToArray(), uv.ToArray(), normals.ToArray());
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        [Test, Performance]
        public void RunUpdateBaseline()
        {
            Measure.Method(() =>
                {
                    var vertices = new List<Vector3>();
                    var triangles = new List<int>();
                    var uv = new List<Vector2>();
                    var normals = new List<Vector3>();
                    var mesh = new Mesh();
                    mesh.MarkDynamic();

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        var (nextVertices, nextTriangles, nextUV, nextNormals) = CreateTriangle(offset, i * 3);
                        vertices.AddRange(nextVertices);
                        triangles.AddRange(nextTriangles);
                        uv.AddRange(nextUV);
                        normals.AddRange(nextNormals);
                        UpdateMeshPlain(mesh, vertices.ToArray(), triangles.ToArray(), uv.ToArray(), normals.ToArray());
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        [Test, Performance]
        public void RunUpdateList()
        {
            Measure.Method(() =>
                {
                    var vertices = new List<Vector3>();
                    var triangles = new List<int>();
                    var uv = new List<Vector2>();
                    var normals = new List<Vector3>();
                    var mesh = new Mesh();
                    mesh.MarkDynamic();

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        var (nextVertices, nextTriangles, nextUV, nextNormals) = CreateTriangle(offset, i * 3);
                        vertices.AddRange(nextVertices);
                        triangles.AddRange(nextTriangles);
                        uv.AddRange(nextUV);
                        normals.AddRange(nextNormals);
                        UpdateMeshList(mesh, vertices, triangles, uv, normals);
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        [Test, Performance]
        public void RunUpdateListNoRecalculation()
        {
            Measure.Method(() =>
                {
                    var vertices = new List<Vector3>();
                    var triangles = new List<int>();
                    var uv = new List<Vector2>();
                    var normals = new List<Vector3>();
                    var mesh = new Mesh();
                    mesh.MarkDynamic();

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        var (nextVertices, nextTriangles, nextUV, nextNormals) = CreateTriangle(offset, i * 3);
                        vertices.AddRange(nextVertices);
                        triangles.AddRange(nextTriangles);
                        uv.AddRange(nextUV);
                        normals.AddRange(nextNormals);
                        UpdateMeshListNoRecalculation(mesh, vertices, triangles, uv, normals);
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        [Test, Performance]
        public void RunUpdateListNoRecalculationNoClear()
        {
            Measure.Method(() =>
                {
                    var vertices = new List<Vector3>();
                    var triangles = new List<int>();
                    var uv = new List<Vector2>();
                    var normals = new List<Vector3>();
                    var mesh = new Mesh();
                    mesh.MarkDynamic();

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        var (nextVertices, nextTriangles, nextUV, nextNormals) = CreateTriangle(offset, i * 3);
                        vertices.AddRange(nextVertices);
                        triangles.AddRange(nextTriangles);
                        uv.AddRange(nextUV);
                        normals.AddRange(nextNormals);
                        UpdateMeshListNoRecalculationNoClear(mesh, vertices, triangles, uv, normals);
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        [Test, Performance]
        public void RunUpdateListNoRecalculationWithAllocated()
        {
            Measure.Method(() =>
                {
                    var vertices = new Vector3[REPEAT * 3];
                    var triangles = new int[REPEAT * 3];
                    var uv = new Vector2[REPEAT * 3];
                    var normals = new Vector3[REPEAT * 3];
                    var mesh = new Mesh();
                    mesh.MarkDynamic();
                    Array.Fill(vertices, Vector3.zero);
                    Array.Fill(triangles, 0);
                    Array.Fill(uv, Vector2.zero);
                    Array.Fill(normals, Vector3.up);

                    for (var i = 0; i < REPEAT; i++)
                    {
                        var offset = new Vector3(i, 0, 0);
                        CreateTriangleWithAllocated(
                            offset,
                            i * 3,
                            vertices, triangles, uv, normals,
                            i * 3,
                            i * 3,
                            i * 3,
                            i * 3
                        );
                        UpdateMeshListWithAllocated(mesh, vertices, triangles, uv, normals);
                    }
                })
                .MeasurementCount(MEASURE_COUNT)
                .Run();
        }

        private Mesh CreateMeshPlain(Vector3[] vertices, int[] triangles, Vector2[] uv, Vector3[] normals)
        {
            var mesh = new Mesh();
            mesh.vertices = vertices;
            mesh.triangles = triangles;
            mesh.uv = uv;
            mesh.normals = normals;
            mesh.RecalculateNormals();
            return mesh;
        }

        private void UpdateMeshPlain(Mesh mesh, Vector3[] vertices, int[] triangles, Vector2[] uv, Vector3[] normals)
        {
            mesh.Clear();
            mesh.vertices = vertices;
            mesh.triangles = triangles;
            mesh.uv = uv;
            mesh.normals = normals;
            mesh.RecalculateNormals();
        }

        private void UpdateMeshList(Mesh mesh, List<Vector3> vertices, List<int> triangles, List<Vector2> uv, List<Vector3> normals)
        {
            mesh.Clear();
            mesh.SetVertices(vertices);
            mesh.SetTriangles(triangles, 0);
            mesh.SetUVs(0, uv);
            mesh.SetNormals(normals);
            mesh.RecalculateNormals();
        }

        private void UpdateMeshListNoRecalculation(Mesh mesh, List<Vector3> vertices, List<int> triangles, List<Vector2> uv, List<Vector3> normals)
        {
            mesh.Clear();
            mesh.SetVertices(vertices);
            mesh.SetTriangles(triangles, 0);
            mesh.SetUVs(0, uv);
            mesh.SetNormals(normals);
        }

        private void UpdateMeshListNoRecalculationNoClear(Mesh mesh, List<Vector3> vertices, List<int> triangles, List<Vector2> uv, List<Vector3> normals)
        {
            mesh.SetVertices(vertices);
            mesh.SetTriangles(triangles, 0);
            mesh.SetUVs(0, uv);
            mesh.SetNormals(normals);
        }

        private void UpdateMeshListWithAllocated(Mesh mesh, Vector3[] vertices, int[] triangles, Vector2[] uv, Vector3[] normals)
        {
            mesh.SetVertices(vertices);
            mesh.SetTriangles(triangles, 0);
            mesh.SetUVs(0, uv);
            mesh.SetNormals(normals);
        }

        private (Vector3[], int[], Vector2[], Vector3[]) CreateTriangle(Vector3 verticesOffset, int triangleOffset)
        {
            var vertices = new Vector3[3];
            var triangles = new int[3];
            var uv = new Vector2[3];
            var normals = new Vector3[3];

            vertices[0] = new Vector3(-1, 0, 0) + verticesOffset;
            vertices[1] = new Vector3(0, 0, 1) + verticesOffset;
            vertices[2] = new Vector3(1, 0, 0) + verticesOffset;

            triangles[0] = 0 + triangleOffset;
            triangles[1] = 1 + triangleOffset;
            triangles[2] = 2 + triangleOffset;

            uv[0] = new Vector2(0, 0);
            uv[1] = new Vector2(0, 1);
            uv[2] = new Vector2(1, 0);

            normals[0] = Vector3.up;
            normals[1] = Vector3.up;
            normals[2] = Vector3.up;

            return (vertices, triangles, uv, normals);
        }

        private void CreateTriangleWithAllocated(
            Vector3 verticesOffset,
            int triangleOffset,
            IList<Vector3> vertices, IList<int> triangles, IList<Vector2> uv, IList<Vector3> normals,
            int verticesIndex, int trianglesIndex, int uvIndex, int normalsIndex
        )
        {
            vertices[verticesIndex + 0] = new Vector3(-1, 0, 0) + verticesOffset;
            vertices[verticesIndex + 1] = new Vector3(0, 0, 1) + verticesOffset;
            vertices[verticesIndex + 2] = new Vector3(1, 0, 0) + verticesOffset;

            triangles[trianglesIndex + 0] = 0 + triangleOffset;
            triangles[trianglesIndex + 1] = 1 + triangleOffset;
            triangles[trianglesIndex + 2] = 2 + triangleOffset;

            uv[uvIndex + 0] = new Vector2(0, 0);
            uv[uvIndex + 1] = new Vector2(0, 1);
            uv[uvIndex + 2] = new Vector2(1, 0);

            normals[normalsIndex + 0] = Vector3.up;
            normals[normalsIndex + 1] = Vector3.up;
            normals[normalsIndex + 2] = Vector3.up;
        }
    }
}

Discussion