├── Editor.meta ├── Editor ├── SmoothNormalConfig.cs ├── SmoothNormalConfig.cs.meta ├── SmoothNormalConfigEditor.cs ├── SmoothNormalConfigEditor.cs.meta ├── SmoothNormalHelper.cs ├── SmoothNormalHelper.cs.meta ├── SmoothNormalPostprocessor.cs ├── SmoothNormalPostprocessor.cs.meta ├── Unity.SmoothNormalTool.Editor.asmdef └── Unity.SmoothNormalTool.Editor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── DrawNormalGizmos.cs ├── DrawNormalGizmos.cs.meta ├── SmoothNormalAsset.asset ├── SmoothNormalAsset.asset.meta ├── Unity.SmoothNormalTool.Runtime.asmdef └── Unity.SmoothNormalTool.Runtime.asmdef.meta ├── Shaders.meta ├── Shaders ├── SmoothNormalGPU.compute └── SmoothNormalGPU.compute.meta ├── package.json └── package.json.meta /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d74a4ba3a4e998d4e8141c88ec3df692 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/SmoothNormalConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditorInternal; 4 | using UnityEngine; 5 | using UnityEngine.Rendering; 6 | 7 | #if UNITY_EDITOR 8 | namespace UnityEditor.SmoothNormalTool 9 | { 10 | /// 11 | /// SmoothNormalConfig, default config is fixed asset in packagePath. 12 | /// User can create only one unique config file in the Assets/. 13 | /// 14 | public class SmoothNormalConfig : ScriptableObject 15 | { 16 | [Serializable, ReloadGroup] 17 | public sealed class ShaderResources 18 | { 19 | [Reload("Shaders/SmoothNormalGPU.compute")] 20 | public ComputeShader smoothNormalCS; 21 | } 22 | 23 | /// 24 | /// Import file use matching. 25 | /// 26 | public enum MatchingMethod 27 | { 28 | NameSuffix, 29 | FilePath 30 | } 31 | 32 | /// 33 | /// Write smoothNormal data to. 34 | /// 35 | public enum WriteTarget 36 | { 37 | VertexColorRGB, 38 | VertexColorRG, 39 | TangentXYZ, 40 | TangentXY, 41 | } 42 | 43 | /// 44 | /// User custom config. 45 | /// 46 | public bool useUserConfig = false; 47 | public string userConfigGUID = null; 48 | 49 | public ShaderResources shaders; 50 | 51 | public MatchingMethod matchingMethod = MatchingMethod.NameSuffix; 52 | public string matchingNameSuffix = "_SN"; 53 | public string matchingFilePath = "Assets/SmoothNormal/"; 54 | 55 | public WriteTarget writeTarget = WriteTarget.VertexColorRGB; 56 | 57 | [Range(1e-20f, 1e-8f)] 58 | public float vertDistThresold = 1e-14f; 59 | 60 | public static readonly string packagePath = "Packages/com.danbaidong.smoothnormal"; 61 | public static readonly string editorAssetGUID = "ddc6b06df6aa2f441a30311bae4b8d7c"; 62 | 63 | /// 64 | /// Only one global instance. 65 | /// 66 | private static SmoothNormalConfig s_instance; 67 | public static SmoothNormalConfig instance 68 | { 69 | get 70 | { 71 | if (s_instance != null) 72 | return s_instance; 73 | 74 | SmoothNormalConfig globalConfig = null; 75 | 76 | string resourcePath = AssetDatabase.GUIDToAssetPath(editorAssetGUID); 77 | var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath); 78 | globalConfig = objs != null && objs.Length > 0 ? objs.First() as SmoothNormalConfig : null; 79 | s_instance = globalConfig; 80 | 81 | if (globalConfig.useUserConfig) 82 | { 83 | SmoothNormalConfig userConfig = null; 84 | string path = AssetDatabase.GUIDToAssetPath(globalConfig.userConfigGUID); 85 | userConfig = AssetDatabase.LoadAssetAtPath(path); 86 | if (userConfig != null) 87 | { 88 | s_instance = userConfig; 89 | } 90 | else 91 | { 92 | Debug.LogError("User Config not found, back to default Config."); 93 | s_instance.useUserConfig = false; 94 | s_instance.userConfigGUID = null; 95 | InternalEditorUtility.SaveToSerializedFileAndForget(new[] { s_instance }, resourcePath, true); 96 | } 97 | } 98 | 99 | return s_instance; 100 | } 101 | } 102 | 103 | /// 104 | /// Check default file and create userConfigFile if there is no other userConfigFiles. 105 | /// Ping userConfigFile is there exist an userConfigFile. 106 | /// 107 | [MenuItem("Assets/Create/SmoothNormalGlobalConfig")] 108 | public static void CreateSmoothNormalGlobalConfig() 109 | { 110 | string resourcePath = AssetDatabase.GUIDToAssetPath(editorAssetGUID); 111 | var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath); 112 | SmoothNormalConfig defaultGlobalConfig = objs != null && objs.Length > 0 ? objs.First() as SmoothNormalConfig : null; 113 | 114 | if (defaultGlobalConfig == null) 115 | { 116 | Debug.LogError("Can't get defaultGlobalConfig guid: " + editorAssetGUID); 117 | return; 118 | } 119 | 120 | if (defaultGlobalConfig.useUserConfig == true && !string.IsNullOrEmpty(defaultGlobalConfig.userConfigGUID)) 121 | { 122 | var userConfigFile = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(defaultGlobalConfig.userConfigGUID)); 123 | if (userConfigFile != null) 124 | { 125 | EditorGUIUtility.PingObject(userConfigFile); 126 | Debug.LogError("The user SmoothNormalGlobalConfig file can only have one instance."); 127 | return; 128 | } 129 | } 130 | 131 | string currentFolder = AssetDatabase.GetAssetPath(Selection.activeObject); 132 | var instance = CreateInstance(); 133 | var path = string.Format(currentFolder + "/{0}.asset", typeof(SmoothNormalConfig).Name); 134 | ResourceReloader.ReloadAllNullIn(instance, packagePath); 135 | AssetDatabase.CreateAsset(instance, path); 136 | 137 | // Set package's defaultConfig properties; 138 | defaultGlobalConfig.useUserConfig = true; 139 | defaultGlobalConfig.userConfigGUID = AssetDatabase.AssetPathToGUID(path); 140 | InternalEditorUtility.SaveToSerializedFileAndForget(new[] { defaultGlobalConfig }, resourcePath, true); 141 | s_instance = null; 142 | } 143 | 144 | } 145 | } 146 | #endif -------------------------------------------------------------------------------- /Editor/SmoothNormalConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48c7feeafff94a04c92fbfd1164af7eb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/SmoothNormalConfigEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | #if UNITY_EDITOR 4 | namespace UnityEditor.SmoothNormalTool 5 | { 6 | /// 7 | /// SmoothNormalConfig CustomEditor 8 | /// 9 | [CustomEditor(typeof(SmoothNormalConfig))] 10 | public class SmoothNormalConfigEditor : Editor 11 | { 12 | private SerializedProperty m_Shaders; 13 | private SerializedProperty m_MatchingMethod; 14 | private SerializedProperty m_MatchingNameSuffix; 15 | private SerializedProperty m_MatchingFilePath; 16 | private SerializedProperty m_WriteTarget; 17 | private SerializedProperty m_VertDistThreshold; 18 | private SerializedProperty m_UserConfig; 19 | private SerializedProperty m_UserConfigGUID; 20 | 21 | private string curGuid = null; 22 | private bool isDirty = false; 23 | 24 | private void OnEnable() 25 | { 26 | m_Shaders = serializedObject.FindProperty(nameof(SmoothNormalConfig.shaders)); 27 | m_MatchingMethod = serializedObject.FindProperty(nameof(SmoothNormalConfig.matchingMethod)); 28 | m_MatchingNameSuffix = serializedObject.FindProperty(nameof(SmoothNormalConfig.matchingNameSuffix)); 29 | m_MatchingFilePath = serializedObject.FindProperty(nameof(SmoothNormalConfig.matchingFilePath)); 30 | m_WriteTarget = serializedObject.FindProperty(nameof(SmoothNormalConfig.writeTarget)); 31 | m_VertDistThreshold = serializedObject.FindProperty(nameof(SmoothNormalConfig.vertDistThresold)); 32 | m_UserConfig = serializedObject.FindProperty(nameof(SmoothNormalConfig.useUserConfig)); 33 | m_UserConfigGUID = serializedObject.FindProperty(nameof(SmoothNormalConfig.userConfigGUID)); 34 | 35 | string assetPath = AssetDatabase.GetAssetPath(target); 36 | curGuid = AssetDatabase.AssetPathToGUID(assetPath); 37 | 38 | isDirty = false; 39 | } 40 | private void OnDisable() 41 | { 42 | isDirty = false; 43 | } 44 | public override void OnInspectorGUI() 45 | { 46 | // Base Properties 47 | EditorGUI.BeginChangeCheck(); 48 | 49 | EditorGUILayout.PropertyField(m_Shaders); 50 | EditorGUILayout.PropertyField(m_MatchingMethod); 51 | if (m_MatchingMethod.intValue == (int)SmoothNormalConfig.MatchingMethod.NameSuffix) 52 | { 53 | EditorGUILayout.PropertyField(m_MatchingNameSuffix); 54 | } 55 | else 56 | { 57 | EditorGUILayout.PropertyField(m_MatchingFilePath); 58 | } 59 | EditorGUILayout.PropertyField(m_WriteTarget); 60 | EditorGUILayout.PropertyField(m_VertDistThreshold); 61 | if (!string.IsNullOrEmpty(curGuid) && curGuid.Equals(SmoothNormalConfig.editorAssetGUID)) 62 | { 63 | EditorGUILayout.PropertyField(m_UserConfig); 64 | if (m_UserConfig.boolValue) 65 | { 66 | EditorGUILayout.PropertyField(m_UserConfigGUID); 67 | } 68 | } 69 | 70 | if (EditorGUI.EndChangeCheck()) 71 | { 72 | isDirty = true; 73 | } 74 | 75 | // End Button 76 | GUILayout.Space(10); 77 | GUILayout.BeginHorizontal(); 78 | 79 | GUILayout.FlexibleSpace(); 80 | 81 | EditorGUI.BeginDisabledGroup(!isDirty); 82 | if (GUILayout.Button("Revert")) 83 | { 84 | serializedObject.Update(); 85 | isDirty = false; 86 | } 87 | 88 | if (GUILayout.Button("Apply")) 89 | { 90 | serializedObject.ApplyModifiedProperties(); 91 | serializedObject.Update(); 92 | 93 | isDirty = false; 94 | } 95 | EditorGUI.EndDisabledGroup(); 96 | 97 | GUILayout.EndHorizontal(); 98 | } 99 | } 100 | } 101 | #endif -------------------------------------------------------------------------------- /Editor/SmoothNormalConfigEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 089de212fa015f6468d7d773c532b6a5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/SmoothNormalHelper.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | using Unity.Mathematics; 4 | 5 | #if UNITY_EDITOR 6 | namespace UnityEditor.SmoothNormalTool 7 | { 8 | public class SmoothNormalHelper 9 | { 10 | public struct TriangleData 11 | { 12 | public Vector4 faceNormal; 13 | public Vector4 vertWeights; 14 | public Vector4 vertIndices; 15 | } 16 | 17 | internal static int DivRoundUp(int x, int y) => (x + y - 1) / y; 18 | 19 | private static Dictionary CreateVertexNormalsDictionary(Mesh mesh) 20 | { 21 | Dictionary vartNormalsDictionary = new Dictionary(); 22 | 23 | for (int i = 0; i <= mesh.triangles.Length - 3; i += 3) 24 | { 25 | // Get edges, cal faceNormal, angleWeights. 26 | Vector3 a = mesh.vertices[mesh.triangles[i + 1]] - mesh.vertices[mesh.triangles[i]];a = a.normalized; 27 | Vector3 b = mesh.vertices[mesh.triangles[i + 2]] - mesh.vertices[mesh.triangles[i]];b = b.normalized; 28 | Vector3 c = mesh.vertices[mesh.triangles[i + 2]] - mesh.vertices[mesh.triangles[i + 1]];c = c.normalized; 29 | Vector3 faceNormal = Vector3.Cross(a, b).normalized; 30 | float[] angleWeight = { Vector3.Angle(a, b), 31 | Vector3.Angle(-a, c), 32 | Vector3.Angle(b, c) }; 33 | 34 | for (int j = 0; j < 3; j++) 35 | { 36 | Vector3 weightedNormal; 37 | int tri = mesh.triangles[i + j]; 38 | Vector3 vertPos = mesh.vertices[tri]; 39 | 40 | if (!vartNormalsDictionary.ContainsKey(vertPos)) 41 | { 42 | vartNormalsDictionary.Add(vertPos, faceNormal * angleWeight[j]); 43 | } 44 | else 45 | { 46 | if (vartNormalsDictionary.TryGetValue(vertPos, out weightedNormal)) 47 | { 48 | weightedNormal += (faceNormal * angleWeight[j]); 49 | vartNormalsDictionary[vertPos] = weightedNormal; 50 | } 51 | } 52 | } 53 | } 54 | 55 | return vartNormalsDictionary; 56 | } 57 | 58 | private static Vector3[] CalculateAngleWeightedNormal(Dictionary surfaceNormalDictionary, Mesh mesh) 59 | { 60 | List averageNoramls = new List(); 61 | for (int i = 0; i < mesh.vertices.Length; i++) 62 | { 63 | Vector3 weightedNormal = surfaceNormalDictionary[mesh.vertices[i]]; 64 | 65 | averageNoramls.Add(weightedNormal.normalized); 66 | } 67 | 68 | return averageNoramls.ToArray(); 69 | } 70 | 71 | public static Vector3[] CalculateSmoothNormal(Mesh mesh) 72 | { 73 | Dictionary dic = CreateVertexNormalsDictionary(mesh); 74 | 75 | Vector3[] normals = CalculateAngleWeightedNormal(dic, mesh); 76 | 77 | //ObjectSpace to TangentSpace 78 | for (int i = 0; i < mesh.normals.Length; i++) 79 | { 80 | var tangent = mesh.tangents[i]; 81 | var normal = mesh.normals[i]; 82 | var binormal = (Vector3.Cross(normal, tangent) * tangent.w).normalized; 83 | var TBNMatrix = new Matrix4x4(tangent, binormal, normal, Vector4.zero); 84 | TBNMatrix = TBNMatrix.transpose; 85 | 86 | normals[i] = TBNMatrix.MultiplyVector(normals[i]).normalized; 87 | normals[i] = normals[i] * 0.5f + Vector3.one * 0.5f; 88 | } 89 | 90 | return normals; 91 | } 92 | 93 | public static Vector3[] ComputeSmoothNormal(Mesh mesh, ComputeShader cs) 94 | { 95 | // Compute 96 | float4[] smoothNormalsArray = new float4[mesh.normals.Length]; 97 | float4[] vertPosArray = new float4[mesh.vertices.Length]; 98 | float4[] vertNormalsArray = new float4[mesh.normals.Length]; 99 | float4[] vertTangentsArray = new float4[mesh.tangents.Length]; 100 | TriangleData[] triangleDataArray = new TriangleData[mesh.triangles.Length / 3]; 101 | 102 | for (int i = 0; i < mesh.vertices.Length; i++) 103 | { 104 | vertPosArray[i] = new float4(mesh.vertices[i], i); 105 | vertNormalsArray[i] = new float4(mesh.normals[i], 0); 106 | vertTangentsArray[i] = mesh.tangents[i]; 107 | } 108 | 109 | ComputeBuffer vertPosBuffer = new ComputeBuffer(vertPosArray.Length, sizeof(float) * 4); 110 | ComputeBuffer vertNormalsBuffer = new ComputeBuffer(vertNormalsArray.Length, sizeof(float) * 4); 111 | ComputeBuffer vertTangentsBuffer = new ComputeBuffer(vertTangentsArray.Length, sizeof(float) * 4); 112 | ComputeBuffer trianglesBuffer = new ComputeBuffer(mesh.triangles.Length, sizeof(int)); 113 | ComputeBuffer triangleDataBuffer = new ComputeBuffer(triangleDataArray.Length, sizeof(float) * 4 * 3); 114 | ComputeBuffer smoothNormalsBuffer = new ComputeBuffer(mesh.vertices.Length, sizeof(float) * 4); 115 | 116 | vertPosBuffer.SetData(vertPosArray); 117 | vertNormalsBuffer.SetData(vertNormalsArray); 118 | vertTangentsBuffer.SetData(vertTangentsArray); 119 | trianglesBuffer.SetData(mesh.triangles); 120 | triangleDataBuffer.SetData(triangleDataArray); 121 | smoothNormalsBuffer.SetData(smoothNormalsArray); 122 | 123 | cs.SetInt("_VerticesCounts", mesh.vertices.Length); 124 | cs.SetInt("_TrianglesCounts", mesh.triangles.Length); 125 | 126 | cs.SetBuffer(0, "_VertPosBuffer", vertPosBuffer); 127 | cs.SetBuffer(0, "_TrianglesBuffer", trianglesBuffer); 128 | cs.SetBuffer(0, "_TriangleDataBuffer", triangleDataBuffer); 129 | cs.Dispatch(0, DivRoundUp(mesh.triangles.Length / 3, 64), 1, 1); 130 | 131 | cs.SetBuffer(1, "_VertPosBuffer", vertPosBuffer); 132 | cs.SetBuffer(1, "_VertNormalsBuffer", vertNormalsBuffer); 133 | cs.SetBuffer(1, "_VertTangentsBuffer", vertTangentsBuffer); 134 | cs.SetBuffer(1, "_TriangleDataBuffer", triangleDataBuffer); 135 | cs.SetBuffer(1, "_SmoothNormalsBuffer", smoothNormalsBuffer); 136 | cs.Dispatch(1, DivRoundUp(mesh.vertices.Length, 64), 1, 1); 137 | 138 | smoothNormalsBuffer.GetData(smoothNormalsArray); 139 | 140 | //string debugstr = "TriNum: " + mesh.triangles.Length + ", DispatchNum: " + DivRoundUp(mesh.triangles.Length / 3, 64) 141 | // + ", vertNum: " + mesh.vertices.Length + "\n"; 142 | //foreach (float4 f3 in smoothNormalsArray) 143 | //{ 144 | // debugstr += f3 + "\n"; 145 | //} 146 | 147 | //Debug.Log(debugstr); 148 | 149 | // Release Resources 150 | vertPosBuffer?.Release(); 151 | vertNormalsBuffer.Release(); 152 | vertTangentsBuffer.Release(); 153 | trianglesBuffer?.Release(); 154 | triangleDataBuffer?.Release(); 155 | smoothNormalsBuffer?.Release(); 156 | 157 | Vector3[] smoothNormals = new Vector3[mesh.normals.Length]; 158 | for (int i = 0; i < smoothNormals.Length; i++) 159 | { 160 | smoothNormals[i] = new Vector3(smoothNormalsArray[i].x, smoothNormalsArray[i].y, smoothNormalsArray[i].z); 161 | } 162 | 163 | return smoothNormals; 164 | } 165 | 166 | public static void CopyVector3NormalsToColorRGB(ref Vector3[] smoothNormals, ref Color[] vertexColors, int size, bool retainColorA) 167 | { 168 | if (retainColorA) 169 | { 170 | for (int i = 0; i < size; i++) 171 | { 172 | vertexColors[i] = new Vector4(smoothNormals[i].x, smoothNormals[i].y, smoothNormals[i].z, vertexColors[i].a); 173 | } 174 | } 175 | else 176 | { 177 | for (int i = 0; i < size; i++) 178 | { 179 | vertexColors[i] = new Vector4(smoothNormals[i].x, smoothNormals[i].y, smoothNormals[i].z, 1); 180 | } 181 | } 182 | } 183 | 184 | public static void CopyVector3NormalsToColorRG(ref Vector3[] smoothNormals, ref Color[] vertexColors, int size, bool retainColorA) 185 | { 186 | if (retainColorA) 187 | { 188 | for (int i = 0; i < size; i++) 189 | { 190 | vertexColors[i] = new Vector4(smoothNormals[i].x, smoothNormals[i].y, vertexColors[i].b, vertexColors[i].a); 191 | } 192 | } 193 | else 194 | { 195 | for (int i = 0; i < size; i++) 196 | { 197 | vertexColors[i] = new Vector4(smoothNormals[i].x, smoothNormals[i].y, 1, 1); 198 | } 199 | } 200 | } 201 | 202 | public static void CopyVector3NormalsToTangentXYZ(ref Vector3[] smoothNormals, ref Vector4[] vertexTangents, int size) 203 | { 204 | for (int i = 0; i < size; i++) 205 | { 206 | vertexTangents[i].x = smoothNormals[i].x; 207 | vertexTangents[i].y = smoothNormals[i].y; 208 | vertexTangents[i].z = smoothNormals[i].z; 209 | } 210 | } 211 | 212 | public static void CopyVector3NormalsToTangentXY(ref Vector3[] smoothNormals, ref Vector4[] vertexTangents, int size) 213 | { 214 | for (int i = 0; i < size; i++) 215 | { 216 | vertexTangents[i].x = smoothNormals[i].x; 217 | vertexTangents[i].y = smoothNormals[i].y; 218 | } 219 | } 220 | } 221 | 222 | } 223 | #endif -------------------------------------------------------------------------------- /Editor/SmoothNormalHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 953748fb0c6b5bc4c8cc843e969eb37b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/SmoothNormalPostprocessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | 4 | #if UNITY_EDITOR 5 | namespace UnityEditor.SmoothNormalTool 6 | { 7 | /// 8 | /// Hook into the import pipeline, compute smoothNormal and write it to mesh data. 9 | /// Note that we only changed UNITY's mesh data, the original modelfile has not changed. 10 | /// 11 | public class SmoothNormalPostprocessor : AssetPostprocessor 12 | { 13 | private static int s_DISTANCE_THRESHOLD = Shader.PropertyToID("_DISTANCE_THRESHOLD"); 14 | /// 15 | /// After importing model. 16 | /// 17 | /// 18 | private void OnPostprocessModel(GameObject gameObject) 19 | { 20 | SmoothNormalConfig config = SmoothNormalConfig.instance; 21 | 22 | // Matching file 23 | switch (config.matchingMethod) 24 | { 25 | case SmoothNormalConfig.MatchingMethod.NameSuffix: 26 | if (!gameObject.name.Contains(config.matchingNameSuffix)) 27 | return; 28 | break; 29 | case SmoothNormalConfig.MatchingMethod.FilePath: 30 | string path = assetImporter.assetPath; 31 | if (!path.Contains(config.matchingFilePath)) 32 | return; 33 | break; 34 | default: 35 | return; 36 | } 37 | 38 | ComputeShader smoothNormalCS = config.shaders.smoothNormalCS; 39 | smoothNormalCS.SetFloat(s_DISTANCE_THRESHOLD, config.vertDistThresold); 40 | 41 | System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); 42 | stopwatch.Start(); 43 | long lastTimeStamp = 0; 44 | 45 | List meshes = new List(); 46 | // Get all meshes 47 | { 48 | MeshFilter[] meshFilters = gameObject.GetComponentsInChildren(); 49 | foreach (MeshFilter meshFilter in meshFilters) 50 | { 51 | meshes.Add(meshFilter.sharedMesh); 52 | } 53 | 54 | SkinnedMeshRenderer[] skinnedMeshs = gameObject.GetComponentsInChildren(); 55 | foreach (SkinnedMeshRenderer skinnedMesh in skinnedMeshs) 56 | { 57 | meshes.Add(skinnedMesh.sharedMesh); 58 | } 59 | } 60 | 61 | // Compute smoothNormals 62 | { 63 | foreach (Mesh mesh in meshes) 64 | { 65 | // Init vert Color 66 | Color[] vertexColors; 67 | bool retainColorA = false; 68 | if (mesh.colors != null && mesh.colors.Length != 0) 69 | { 70 | vertexColors = mesh.colors; 71 | retainColorA = true; 72 | } 73 | else 74 | { 75 | vertexColors = new Color[mesh.vertexCount]; 76 | } 77 | 78 | Vector3[] smoothNormals = SmoothNormalHelper.ComputeSmoothNormal(mesh, smoothNormalCS); 79 | Vector4[] tangents = mesh.tangents; 80 | switch (config.writeTarget) 81 | { 82 | case SmoothNormalConfig.WriteTarget.VertexColorRGB: 83 | SmoothNormalHelper.CopyVector3NormalsToColorRGB(ref smoothNormals, ref vertexColors, vertexColors.Length, retainColorA); 84 | mesh.SetColors(vertexColors); 85 | break; 86 | case SmoothNormalConfig.WriteTarget.VertexColorRG: 87 | SmoothNormalHelper.CopyVector3NormalsToColorRG(ref smoothNormals, ref vertexColors, vertexColors.Length, retainColorA); 88 | mesh.SetColors(vertexColors); 89 | break; 90 | case SmoothNormalConfig.WriteTarget.TangentXYZ: 91 | SmoothNormalHelper.CopyVector3NormalsToTangentXYZ(ref smoothNormals, ref tangents, vertexColors.Length); 92 | mesh.SetTangents(tangents); 93 | break; 94 | case SmoothNormalConfig.WriteTarget.TangentXY: 95 | SmoothNormalHelper.CopyVector3NormalsToTangentXY(ref smoothNormals, ref tangents, vertexColors.Length); 96 | mesh.SetTangents(tangents); 97 | break; 98 | } 99 | 100 | } 101 | } 102 | 103 | stopwatch.Stop(); 104 | Debug.Log("Generate " + gameObject.name + " smoothNormal use: " + ((stopwatch.ElapsedMilliseconds - lastTimeStamp) * 0.001).ToString("F3") + "s"); 105 | } 106 | } 107 | } 108 | #endif -------------------------------------------------------------------------------- /Editor/SmoothNormalPostprocessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6ad69c73e81efe344ab94c8a976b34f5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Unity.SmoothNormalTool.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.SmoothNormalTool.Editor", 3 | "rootNamespace": "", 4 | "references": [ 5 | "Unity.RenderPipelines.Universal.Runtime", 6 | "Unity.RenderPipelines.Core.Runtime", 7 | "Unity.RenderPipelines.Core.Editor", 8 | "Unity.Mathematics.Editor", 9 | "Unity.Mathematics", 10 | "Unity.SmoothNormalTool.Runtime" 11 | ], 12 | "includePlatforms": [ 13 | "Editor" 14 | ], 15 | "excludePlatforms": [], 16 | "allowUnsafeCode": true, 17 | "overrideReferences": false, 18 | "precompiledReferences": [], 19 | "autoReferenced": true, 20 | "defineConstraints": [], 21 | "versionDefines": [], 22 | "noEngineReferences": false 23 | } -------------------------------------------------------------------------------- /Editor/Unity.SmoothNormalTool.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d401f7d7eb2dea41ae94e8b1eee46ba 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 danbaidong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 936f8d4b95debf44fbb623e470966e61 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # SmoothNormal 4 | 5 |
6 | 7 | # Purpose 8 | 9 | The purpose of this Unity package is to calculate smooth normals for objects that require outlining. In cartoon rendering, smooth normal information is often needed to achieve a good outlining effect. 10 | 11 | # Algorithm 12 | 13 | The smooth normal algorithm employs angle-weighted averaging and is accelerated using a `compute shader`. However, it's important to note that the current algorithm has a time complexity of O(n^2). Therefore, I recommend `splitting your models` based on materials rather than using submeshes to optimize performance. 14 | 15 | This tool treats nearby vertices in the model as a single vertex, so make sure that the vertices on adjacent surfaces of your model are either `merged or close enough` to each other. 16 | 17 | # Usage 18 | 19 | You can add this package by UPM (Unity Packages Manager), url like: `https://github.com/danbaidong1111/SmoothNormal.git`. 20 | 21 | The SmoothNormal package filters objects during model import based on the model's name or import path. When a matching model is imported, smooth normals are calculated and ultimately stored in vertex colors or tangents. Users can create their own custom config file by right-clicking in UnityAsset -> Create -> SmoothNormalGlobalConfig. This user-defined config file is globally unique. The default config file is located in the package directory under the "Runtime" folder, named "SmoothNormalAsset." You can determine the current configuration used by checking the "Use User Config" field here. 22 | 23 | Config Properties: 24 | 25 | * Matching Method: Defines which model will compute smoothnormal. 26 | * Mathing Name Suffix: Match suffix, default is "_SN". 27 | * Write Target: Write smoothnormal data to `vertetx color` RGB, RG or `tangent` RGB, RG. 28 | * Vert Dist Thresold: Vertex diatance squre < value will treate as a single vertex. See SmoothNormalGPU computeshader. 29 | 30 | # Important 31 | Model vertexes should merged by distance. 32 | 33 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 14f446f7a0214cc48b4772c05b9dfb68 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a7ec3c6fdf034854eb5b120f3b756694 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/DrawNormalGizmos.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | #if UNITY_EDITOR 6 | namespace UnityEditor.SmoothNormalTool 7 | { 8 | [ExecuteAlways] 9 | public class DrawNormalGizmos : MonoBehaviour 10 | { 11 | public ShowNormalDataFrom normalDataFrom = ShowNormalDataFrom.VertexColor; 12 | public float lineLength = 0.1f; 13 | private float _lineLengthCache = 0.1f; 14 | private Mesh _mesh; 15 | private Color[] m_ColorCache; 16 | private ShowNormalDataFrom normalDataFromCache; 17 | 18 | public enum ShowNormalDataFrom 19 | { 20 | VertexNormal, 21 | VertexColor, 22 | } 23 | struct NormalLine 24 | { 25 | public Vector3 posFrom; 26 | public Vector3 normalWS; 27 | } 28 | 29 | private List _normalLines; 30 | 31 | void CalculateNormalLine() 32 | { 33 | _normalLines.Clear(); 34 | if (_mesh != null) 35 | { 36 | for (int i = 0; i < _mesh.colors.Length; i++) 37 | { 38 | var normalLine = new NormalLine(); 39 | var mat = transform.localToWorldMatrix; 40 | 41 | Vector3 normalOS = _mesh.normals[i]; 42 | if (normalDataFrom == ShowNormalDataFrom.VertexColor) 43 | { 44 | Vector3 normalTS = new Vector3(_mesh.colors[i].r, _mesh.colors[i].g, _mesh.colors[i].b) * 2.0f - Vector3.one; 45 | 46 | var tangent = _mesh.tangents[i]; 47 | var normal = _mesh.normals[i]; 48 | var binormal = (Vector3.Cross(normal, tangent) * tangent.w).normalized; 49 | var TBNMatrix = new Matrix4x4(tangent, binormal, normal, Vector4.zero); 50 | normalOS = TBNMatrix.MultiplyVector(normalTS).normalized; 51 | } 52 | 53 | normalLine.posFrom = mat.MultiplyPoint(_mesh.vertices[i]); 54 | normalLine.normalWS = mat.MultiplyVector(normalOS); 55 | _normalLines.Add(normalLine); 56 | } 57 | } 58 | normalDataFromCache = normalDataFrom; 59 | _lineLengthCache = lineLength; 60 | m_ColorCache = _mesh.colors; 61 | } 62 | void OnEnable() 63 | { 64 | _normalLines = new List(); 65 | if (TryGetComponent(out MeshFilter filter)) 66 | _mesh = filter.sharedMesh; 67 | else 68 | _mesh = GetComponent().sharedMesh; 69 | 70 | CalculateNormalLine(); 71 | } 72 | 73 | private void Update() 74 | { 75 | 76 | } 77 | 78 | private void OnDisable() 79 | { 80 | _mesh = null; 81 | _normalLines = null; 82 | } 83 | 84 | private void OnDrawGizmosSelected() 85 | { 86 | Gizmos.color = Color.magenta; 87 | if (_mesh != null) 88 | { 89 | if (normalDataFromCache != normalDataFrom) 90 | CalculateNormalLine(); 91 | 92 | foreach (var normalLine in _normalLines) 93 | { 94 | Gizmos.DrawLine(normalLine.posFrom, normalLine.posFrom + normalLine.normalWS * lineLength); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | #endif -------------------------------------------------------------------------------- /Runtime/DrawNormalGizmos.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 30f96aa8c0de4f4458907c76efbebc37 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/SmoothNormalAsset.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &1 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 48c7feeafff94a04c92fbfd1164af7eb, type: 3} 13 | m_Name: SmoothNormalAsset 14 | m_EditorClassIdentifier: 15 | useUserConfig: 0 16 | userConfigGUID: 17 | shaders: 18 | smoothNormalCS: {fileID: 7200000, guid: d60bc63a74d3e3143877ef942ac2fe51, type: 3} 19 | matchingMethod: 0 20 | matchingNameSuffix: _SN 21 | matchingFilePath: Assets/SmoothNormal/ 22 | writeTarget: 0 23 | vertDistThresold: 1e-14 24 | -------------------------------------------------------------------------------- /Runtime/SmoothNormalAsset.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddc6b06df6aa2f441a30311bae4b8d7c 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Unity.SmoothNormalTool.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.SmoothNormalTool.Runtime", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:df380645f10b7bc4b97d4f5eb6303d95" 6 | ], 7 | "includePlatforms": [ 8 | "Editor", 9 | "WindowsStandalone32", 10 | "WindowsStandalone64" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": true, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /Runtime/Unity.SmoothNormalTool.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a5522d9f8c725724994417eba8304a66 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Shaders.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 36b1b56070e1cb94c971c45487f22c02 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Shaders/SmoothNormalGPU.compute: -------------------------------------------------------------------------------- 1 | #pragma kernel ComputeTriangles 2 | #pragma kernel ComputeSmoothNormals 3 | 4 | #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" 5 | 6 | // Two points square-distance smaller than this value will be treated as the same point 7 | float _DISTANCE_THRESHOLD; 8 | 9 | struct TriangleData 10 | { 11 | float4 faceNormal; 12 | float4 vertWeights; 13 | float4 vertIndices; 14 | }; 15 | 16 | uint _TrianglesCounts; 17 | uint _VerticesCounts; 18 | RWBuffer _VertPosBuffer; 19 | RWBuffer _VertNormalsBuffer; 20 | RWBuffer _VertTangentsBuffer; 21 | 22 | RWBuffer _TrianglesBuffer; 23 | RWBuffer _SmoothNormalsBuffer; 24 | 25 | RWStructuredBuffer _TriangleDataBuffer; 26 | 27 | [numthreads(64,1,1)] 28 | void ComputeTriangles (uint3 id : SV_DispatchThreadID) 29 | { 30 | uint triangleIndex = id.x * 3; 31 | if (triangleIndex + 2 > _TrianglesCounts) 32 | return; 33 | 34 | float3 a = _VertPosBuffer[_TrianglesBuffer[triangleIndex + 1]].xyz - _VertPosBuffer[_TrianglesBuffer[triangleIndex]].xyz; 35 | float3 b = _VertPosBuffer[_TrianglesBuffer[triangleIndex + 2]].xyz - _VertPosBuffer[_TrianglesBuffer[triangleIndex + 1]].xyz; 36 | float3 c = _VertPosBuffer[_TrianglesBuffer[triangleIndex]].xyz - _VertPosBuffer[_TrianglesBuffer[triangleIndex + 2]].xyz; 37 | 38 | a = SafeNormalize(a); 39 | b = SafeNormalize(b); 40 | c = SafeNormalize(c); 41 | 42 | float3 faceNormal = cross(a, -c); 43 | faceNormal = SafeNormalize(faceNormal); 44 | 45 | float3 dotProduct = float3(dot(a, -c), dot(b, -a), dot(c, -b)); 46 | dotProduct = clamp(dotProduct, 0, 1); 47 | 48 | TriangleData triData = (TriangleData)0; 49 | triData.faceNormal = float4(faceNormal, 1); 50 | triData.vertWeights = float4(acos(dotProduct.x), 51 | acos(dotProduct.y), 52 | acos(dotProduct.z), 0); 53 | triData.vertIndices = float4(_TrianglesBuffer[triangleIndex], 54 | _TrianglesBuffer[triangleIndex + 1], 55 | _TrianglesBuffer[triangleIndex + 2], 0); 56 | _TriangleDataBuffer[id.x] = triData; 57 | } 58 | 59 | [numthreads(64,1,1)] 60 | void ComputeSmoothNormals (uint3 id : SV_DispatchThreadID) 61 | { 62 | uint curVertIndex = id.x; 63 | float3 averageNormal = 0; 64 | float3 positionOS = _VertPosBuffer[curVertIndex].xyz; 65 | 66 | for (uint triIndex = 0; triIndex < _TrianglesCounts / 3; triIndex++) 67 | { 68 | uint triVertIndices[3] = {_TriangleDataBuffer[triIndex].vertIndices.x, 69 | _TriangleDataBuffer[triIndex].vertIndices.y, 70 | _TriangleDataBuffer[triIndex].vertIndices.z}; 71 | float vertWeights[3] = {_TriangleDataBuffer[triIndex].vertWeights.x, 72 | _TriangleDataBuffer[triIndex].vertWeights.y, 73 | _TriangleDataBuffer[triIndex].vertWeights.z}; 74 | 75 | for (uint triVertIndex = 0; triVertIndex < 3; triVertIndex++) 76 | { 77 | float3 triVertPositionDir = _VertPosBuffer[triVertIndices[triVertIndex]].xyz - positionOS.xyz; 78 | 79 | if (dot(triVertPositionDir, triVertPositionDir) < _DISTANCE_THRESHOLD) 80 | { 81 | averageNormal += _TriangleDataBuffer[triIndex].faceNormal.xyz * vertWeights[triVertIndex]; 82 | } 83 | } 84 | } 85 | 86 | 87 | averageNormal = SafeNormalize(averageNormal); 88 | 89 | float3 normalOS = _VertNormalsBuffer[curVertIndex].xyz; 90 | float3 tangentOS = _VertTangentsBuffer[curVertIndex].xyz; 91 | float3 biTangentOS = cross(normalOS, tangentOS.xyz) * _VertTangentsBuffer[curVertIndex].w; 92 | 93 | float3x3 TBN_TSOS= float3x3(tangentOS, biTangentOS, normalOS); 94 | float3 smoothNormalOS = mul(TBN_TSOS, averageNormal); 95 | smoothNormalOS = SafeNormalize(smoothNormalOS) * 0.5 + 0.5; 96 | 97 | _SmoothNormalsBuffer[curVertIndex] = float4(smoothNormalOS, 0); 98 | } 99 | 100 | -------------------------------------------------------------------------------- /Shaders/SmoothNormalGPU.compute.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d60bc63a74d3e3143877ef942ac2fe51 3 | ComputeShaderImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.danbaidong.smoothnormal", 3 | "version": "1.0.2", 4 | "displayName": "Danbaidong RP SmoothNormal", 5 | "unity": "2022.2", 6 | "documentationUrl": "https://github.com/danbaidong1111/SmoothNormal", 7 | "changelogUrl": "https://github.com/danbaidong1111/SmoothNormal/commits/main", 8 | "dependencies": { 9 | "com.unity.mathematics": "1.2.1", 10 | "com.unity.burst": "1.8.4", 11 | "com.unity.render-pipelines.core": "14.0.8" 12 | }, 13 | "keywords": [ 14 | "smooth", 15 | "normal" 16 | ], 17 | "author": { 18 | "name": "Danbaidong", 19 | "email": "", 20 | "url": "" 21 | }, 22 | "description": "This script computes smooth normals for resolving outline rendering artifacts when import models." 23 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb7eacecf2cc5684da86663871a3a2af 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------