スムージング角を用いて法線の再計算を行う

OBJファイルやMqoファイル等のモデリングデータからMeshを生成する際にスムージング角度を用いて法線の再計算を行う必要が出てきます。
UnityEngine.MeshにはRecalculateNormals()と言うメソッドが用意されていますが、こいつはドキュメントにも記載されている通り
滑らかでない法線を生成するとの事なので、やはり独自に計算しなくてはいけない様です。

docs.unity3d.com

と言うわけで、標題の通り、スムージング角を用いて法線の再計算を行うには以下の通り。

    public static void RecalculateNormals(Mesh mesh, float angle)
        {
            var triangles = mesh.triangles;

            var vertices = mesh.vertices;
            var triNormals = new Vector3[triangles.Length / 3];
            var normals = new Vector3[vertices.Length];

            angle = angle * Mathf.Deg2Rad;

            var dictionary = new Dictionary<VertexKey, VertexEntry>(vertices.Length);

            for (var i = 0; i < triangles.Length; i += 3)
            {
                var i1 = triangles[i];
                var i2 = triangles[i + 1];
                var i3 = triangles[i + 2];

                var p1 = vertices[i2] - vertices[i1];
                var p2 = vertices[i3] - vertices[i1];
                var normal = Vector3.Cross(p1, p2).normalized;
                int triIndex = i / 3;
                triNormals[triIndex] = normal;

                VertexEntry entry;
                VertexKey key;

                if (!dictionary.TryGetValue(key = new VertexKey(vertices[i1]), out entry))
                {
                    entry = new VertexEntry();
                    dictionary.Add(key, entry);
                }
                entry.Add(i1, triIndex);

                if (!dictionary.TryGetValue(key = new VertexKey(vertices[i2]), out entry))
                {
                    entry = new VertexEntry();
                    dictionary.Add(key, entry);
                }
                entry.Add(i2, triIndex);

                if (!dictionary.TryGetValue(key = new VertexKey(vertices[i3]), out entry))
                {
                    entry = new VertexEntry();
                    dictionary.Add(key, entry);
                }
                entry.Add(i3, triIndex);
            }
            foreach (var value in dictionary.Values)
            {
                for (var i = 0; i < value.Count; ++i)
                {
                    var sum = new Vector3();
                    for (var j = 0; j < value.Count; ++j)
                    {
                        if (value.VertexIndex[i] == value.VertexIndex[j])
                        {
                            sum += triNormals[value.TriangleIndex[j]];
                        }
                        else
                        {
                            float dot = Vector3.Dot(
                                triNormals[value.TriangleIndex[i]],
                                triNormals[value.TriangleIndex[j]]);
                            dot = Mathf.Clamp(dot, -0.99999f, 0.99999f);
                            float acos = Mathf.Acos(dot);
                            if (acos <= angle)
                            {
                                sum += triNormals[value.TriangleIndex[j]];
                            }
                        }
                    }

                    normals[value.VertexIndex[i]] = sum.normalized;
                }
            }

            mesh.normals = normals;
        }

        private struct VertexKey
        {
            private readonly long x;
            private readonly long y;
            private readonly long z;

            private const int Tolerance = 100000;

            public VertexKey(Vector3 position)
            {
                x = (long) (Mathf.Round(position.x * Tolerance));
                y = (long) (Mathf.Round(position.y * Tolerance));
                z = (long) (Mathf.Round(position.z * Tolerance));
            }

            public override bool Equals(object obj)
            {
                var key = (VertexKey) obj;
                return x == key.x && y == key.y && z == key.z;
            }

            public override int GetHashCode()
            {
                return (x * 7 ^ y * 13 ^ z * 27).GetHashCode();
            }
        }

        private sealed class VertexEntry
        {
            public int[] TriangleIndex = new int[4];
            public int[] VertexIndex = new int[4];

            private int reserved = 4;

            public int Count { get; private set; }

            public void Add(int vertIndex, int triIndex)
            {
                if (reserved == Count)
                {
                    reserved *= 2;
                    Array.Resize(ref TriangleIndex, reserved);
                    Array.Resize(ref VertexIndex, reserved);
                }
                TriangleIndex[Count] = triIndex;
                VertexIndex[Count] = vertIndex;
                ++Count;
            }
        }
    }

UnityEngine.Mesh.RecalculateNormals()を用いた場合
f:id:norimasa-katsumi:20170123081628p:plain

スムージング角を用いた場合
f:id:norimasa-katsumi:20170123081620p:plain