It’s been over 2 years since I posted about a method to recalculate normals in Unity that fixes on some of the issues of Unity’s default RecalculateNormals() method. I’ve used this algorithm (and similar variations) myself in non-Unity projects and I’ve since made minor adjustments, but I never bothered to update the Unity version of the code. Someone recently reported to me that it fails when working with meshes with multiple materials, so I decided to go ahead and update it.

The most important performance update is that I now use FNV hashing to encode the vertex positions. The first implementation used a very naive hashing which would have many hashing collisions. A mesh with approximately a million vertices would end up with a significant amount of collisions, which was a performance killer.

Using FNV hashing reduced collisions to a negligible amount, even to 0 in most cases. I don’t have the figures now, as this change happened about a year ago, but I might do some newer measurements later (read: probably never).

To summarize, here are the changes:

- Fixed issue with multiple materials not working properly.
- Changed VertexKey to use FNV hashing.
- Other (minor) performance improvements.

And I know you’re here just for the code, so here it is:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
/* * The following code was taken from: http://schemingdeveloper.com * * Visit our game studio website: http://stopthegnomes.com * * License: You may use this code however you see fit, as long as you include this notice * without any modifications. * * You may not publish a paid asset on Unity store if its main function is based on * the following code, but you may publish a paid asset that uses this code. * * If you intend to use this in a Unity store asset or a commercial project, it would * be appreciated, but not required, if you let me know with a link to the asset. If I * don't get back to you just go ahead and use it anyway! */ using System; using System.Collections.Generic; using UnityEngine; public static class NormalSolver { /// <summary> /// Recalculate the normals of a mesh based on an angle threshold. This takes /// into account distinct vertices that have the same position. /// </summary> /// <param name="mesh"></param> /// <param name="angle"> /// The smoothing angle. Note that triangles that already share /// the same vertex will be smooth regardless of the angle! /// </param> public static void RecalculateNormals(this Mesh mesh, float angle) { var cosineThreshold = Mathf.Cos(angle * Mathf.Deg2Rad); var vertices = mesh.vertices; var normals = new Vector3[vertices.Length]; // Holds the normal of each triangle in each sub mesh. var triNormals = new Vector3[mesh.subMeshCount][]; var dictionary = new Dictionary<VertexKey, List<VertexEntry>>(vertices.Length); for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; ++subMeshIndex) { var triangles = mesh.GetTriangles(subMeshIndex); triNormals[subMeshIndex] = new Vector3[triangles.Length / 3]; for (var i = 0; i < triangles.Length; i += 3) { int i1 = triangles[i]; int i2 = triangles[i + 1]; int i3 = triangles[i + 2]; // Calculate the normal of the triangle Vector3 p1 = vertices[i2] - vertices[i1]; Vector3 p2 = vertices[i3] - vertices[i1]; Vector3 normal = Vector3.Cross(p1, p2).normalized; int triIndex = i / 3; triNormals[subMeshIndex][triIndex] = normal; List<VertexEntry> entry; VertexKey key; if (!dictionary.TryGetValue(key = new VertexKey(vertices[i1]), out entry)) { entry = new List<VertexEntry>(4); dictionary.Add(key, entry); } entry.Add(new VertexEntry(subMeshIndex, triIndex, i1)); if (!dictionary.TryGetValue(key = new VertexKey(vertices[i2]), out entry)) { entry = new List<VertexEntry>(); dictionary.Add(key, entry); } entry.Add(new VertexEntry(subMeshIndex, triIndex, i2)); if (!dictionary.TryGetValue(key = new VertexKey(vertices[i3]), out entry)) { entry = new List<VertexEntry>(); dictionary.Add(key, entry); } entry.Add(new VertexEntry(subMeshIndex, triIndex, i3)); } } // Each entry in the dictionary represents a unique vertex position. foreach (var vertList in dictionary.Values) { for (var i = 0; i < vertList.Count; ++i) { var sum = new Vector3(); var lhsEntry = vertList[i]; for (var j = 0; j < vertList.Count; ++j) { var rhsEntry = vertList[j]; if (lhsEntry.VertexIndex == rhsEntry.VertexIndex) { sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; } else { // The dot product is the cosine of the angle between the two triangles. // A larger cosine means a smaller angle. var dot = Vector3.Dot( triNormals[lhsEntry.MeshIndex][lhsEntry.TriangleIndex], triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]); if (dot >= cosineThreshold) { sum += triNormals[rhsEntry.MeshIndex][rhsEntry.TriangleIndex]; } } } normals[lhsEntry.VertexIndex] = sum.normalized; } } mesh.normals = normals; } private struct VertexKey { private readonly long _x; private readonly long _y; private readonly long _z; // Change this if you require a different precision. private const int Tolerance = 100000; // Magic FNV values. Do not change these. private const long FNV32Init = 0x811c9dc5; private const long FNV32Prime = 0x01000193; 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() { long rv = FNV32Init; rv ^= _x; rv *= FNV32Prime; rv ^= _y; rv *= FNV32Prime; rv ^= _z; rv *= FNV32Prime; return rv.GetHashCode(); } } private struct VertexEntry { public int MeshIndex; public int TriangleIndex; public int VertexIndex; public VertexEntry(int meshIndex, int triIndex, int vertIndex) { MeshIndex = meshIndex; TriangleIndex = triIndex; VertexIndex = vertIndex; } } } |

After you include this code in your project, all you have to do is call RecalculateNormals(angle) on your mesh. Make sure you visit the original post for additional information about the algorithm.

Hey, I’m procedurally generating a mesh (convex hull) but I now I want to change the ‘SmoothingAngle’ value, but the Unity API doesn’t let me do this through script, can I solve this with your script?

Yes, just put this script in your project and you’ll be able to use the angle. Keep in mind that when the vertices are already merged (two triangles sharing the exact same vertices, not just distinct vertices at different positions), they will be smoothed together regardless of the angle.

I’m trying to use this to get hard angles after moving vertices, and then reassigning the mesh vertices. The original mesh is just a flat plane imported with a 0 tolerance.

The script appears to work before moving the vertices on any object (tested it with spheres and teapots), but after moving the vertices any change to the smoothing angle results in the exact same smoothing. Results are similar as tolerance is increased as well, unless tolerance is dropped to 1, then smoothing results change as smoothing angle is increased or decreased but not with appropriate results.

The problem is that two triangles can share a vertex, and a vertex can only have one normal. When importing, Unity splits the vertex into two only if any attribute other than position differs (normal, uv, color, etc).

To get around this you import the mesh at 0 tolerance, but then you end up with a larger mesh and this (probably) still won’t work if the triangles are exactly flat to one another.

In other words, any normals that are smooth after calling the original RecalculateNormals() method will not get un-smoothed using my method.

It’s a bit more complicated going the other way, because you’ll have to modify ALL data and not just the normals. So, it’s currently not possible with this script. But seeing how people have been asking for this and that it’s a useful scenario (originally I thought it wasn’t useful enough to bother) I decided to work on this. I can’t give a time estimate yet though.

Hi Charis, firstly, thank you for posting this. It has been very helpful. I am trying to use this with a procedural terrain engine (marching cubes). I’m using chunks of mesh and getting seams along the borders after applying this. Can you give me any clues how i might be able to apply your method to account for triangles and verts in neighboring chunks and smooth across the chunk borders?

I think i got it. It looks to work if i add the verts and triangles from the neighboring chunks when processing each chunk. 🙂

This saved me a lot of headache. Thank you very much for sharing! 🙂