SmoothNormals
Static utility class that computes averaged (smooth) normals for mesh vertices and stores them in the UV3 channel. Used by the Outline system to eliminate hard-edge artifacts at mesh seams where vertices share positions but have different normals. Results are cached per Mesh instance to avoid redundant computation.
Definition
Namespace: Paragon.Townskeep.OutlineSystem
Assembly: Townskeep.dll
public static class SmoothNormalsRemarks
Why Smooth Normals?
When a mesh has split normals at edges (hard edges), the outline shader would render visible seams because adjacent vertices have different normal directions. By averaging normals across all vertices that share the same position and storing the result in UV3, the outline shader can read smooth normals for a continuous outline silhouette.
Baking Pipeline
Bake() processes two types of renderers:
MeshFilter — Computes averaged normals by grouping vertices by position, averaging their normals, and storing the result as
Vector3[]in UV3 viamesh.SetUVs(3, smoothNormals).SkinnedMeshRenderer — Stores empty
Vector2[]in UV3 (smooth normals are not computed for skinned meshes, but UV3 must be populated for shader compatibility).
After writing UV3, if the mesh has multiple submeshes, an extra submesh is appended containing all triangles. This combined submesh is used by the outline materials to render the outline across the entire mesh as one draw call.
Caching
A static Dictionary<Mesh, Vector3[]> caches computed normals per mesh instance. IsBaked() checks the cache before processing, so shared meshes (e.g., mesh instancing) are only computed once.
Quick Lookup
Bake all meshes on a GameObject
SmoothNormals.Bake(gameObject)
Check if a mesh is already baked
SmoothNormals.IsBaked(mesh)
Methods
IsBaked
Checks whether smooth normals have already been computed and cached for this mesh.
mesh
Mesh
The mesh to check
Returns: true if the mesh has been baked.
Bake
Discovers all MeshFilter and SkinnedMeshRenderer components in the hierarchy (including inactive children) and bakes smooth normals into UV3 for each unbaked mesh.
gameObject
GameObject
The root GameObject to process
Pipeline for each MeshFilter:
Pipeline for each SkinnedMeshRenderer:
Internal Mechanism
GenerateSmoothNormals (private)
Groups all mesh vertices by position using LINQ GroupBy. For each group with more than one vertex, the normals are summed and normalized, then assigned back to every vertex in the group. Single-vertex groups retain their original normal.
CombineSubMeshes (private)
If a mesh has multiple submeshes (and fewer submeshes than materials), appends an additional submesh containing all mesh triangles. This allows the outline material to render across all submeshes in a single pass. Skipped for single-submesh meshes.
Cache
Initialized in the static constructor. Persists for the application lifetime. Each mesh's computed smooth normals are stored once and reused across all GameObjects sharing that mesh.
Common Pitfalls
Modifies meshes in-place
Bake() calls mesh.SetUVs(3, ...) and potentially mesh.SetTriangles(...) on sharedMesh. This modifies the mesh asset permanently for the session. If the same mesh is used elsewhere without outlines, the UV3 data is still present (but harmless unless a shader reads UV3).
Cache persists across scenes The static cache is never cleared. This is intentional (avoids re-baking) but means mesh references are held, preventing garbage collection of unloaded mesh assets.
Skinned meshes get empty UV3
SkinnedMeshRenderer meshes receive empty Vector2[] in UV3 instead of computed normals. This means outline quality on skinned meshes may show seam artifacts.
SubMesh count validation
CombineSubMeshes() skips meshes where subMeshCount > materials.Count. This avoids processing meshes that already have an extra submesh from a previous bake.
See Also
Outline — the rendering component that calls
Bake()
Last updated