();
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 |
--------------------------------------------------------------------------------