├── .gitignore ├── ClippingJob.cs ├── ClippingJob.cs.meta ├── DecalExtensions.cs ├── DecalExtensions.cs.meta ├── DecalGizmo.cs ├── DecalGizmo.cs.meta ├── DecalProjector.cs ├── DecalProjector.cs.meta ├── Editor.meta ├── Editor ├── DecalProjectorEditor.cs └── DecalProjectorEditor.cs.meta ├── Images.meta ├── Images ├── ContextMenu.PNG ├── ContextMenu.PNG.meta ├── DecalDemo.PNG └── DecalDemo.PNG.meta ├── Primitives.meta ├── Primitives ├── Edge.cs ├── Edge.cs.meta ├── Plane.cs ├── Plane.cs.meta ├── Ray.cs ├── Ray.cs.meta ├── TRS.cs ├── TRS.cs.meta ├── Triangle.cs └── Triangle.cs.meta ├── README.md ├── README.md.meta ├── Resources.meta ├── Resources ├── DefaultDecal.png └── DefaultDecal.png.meta ├── TrianglePointComparer.cs ├── TrianglePointComparer.cs.meta ├── UnitCube.cs └── UnitCube.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | -------------------------------------------------------------------------------- /ClippingJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unity.Burst; 4 | using Unity.Collections; 5 | using Unity.Jobs; 6 | using Unity.Mathematics; 7 | 8 | namespace SimpleDecal 9 | { 10 | // This class is the job that takes a list of triangles, and clips them against a unit cube 11 | [BurstCompile] 12 | public struct ClippingJob : IJob 13 | { 14 | // Input and Output 15 | [ReadOnly] public NativeArray SourceTriangles; 16 | [ReadOnly] public int NumSourceTriangles; 17 | [WriteOnly] public NativeArray GeneratedTriangles; 18 | [WriteOnly] public NativeArray NumGeneratedTriangles; // size of 1, to return value 19 | 20 | // Scratch buffers 21 | public NativeArray ScratchPoints; 22 | public NativeArray ScratchTriangleEdges; 23 | public NativeArray ScratchTrianglePlanes; 24 | [ReadOnly] public NativeArray UnitCubeEdges; 25 | [ReadOnly] public NativeArray UnitCubePlanes; 26 | 27 | enum TestPointMode 28 | { 29 | UnitCube, 30 | Triangle 31 | } 32 | 33 | public void Execute() 34 | { 35 | int generatedTriangleCount = 0; 36 | int scratchPointsCount; 37 | 38 | Triangle sourceTriangle = Triangle.zero; 39 | 40 | // Comparer for sorting new points around a normal 41 | TrianglePointComparer comparer = TrianglePointComparer.zero; 42 | 43 | // For each source triangle, find a new set of points against the unit cube 44 | for (int tIndex = 0; tIndex < NumSourceTriangles; tIndex++) 45 | { 46 | if (generatedTriangleCount >= DecalProjector.MaxDecalTriangles) 47 | break; 48 | 49 | // We need to build our own version of T, otherwise burst complains 50 | sourceTriangle.CopyFrom(SourceTriangles[tIndex]); 51 | 52 | // Start over from the beginning of the reused array 53 | scratchPointsCount = 0; 54 | 55 | // Some verts may actually be directly inside the projector 56 | if (UnitCubeContains(sourceTriangle.Vertex0)) ScratchPoints[scratchPointsCount++] = sourceTriangle.Vertex0; 57 | if (UnitCubeContains(sourceTriangle.Vertex1)) ScratchPoints[scratchPointsCount++] = sourceTriangle.Vertex1; 58 | if (UnitCubeContains(sourceTriangle.Vertex2)) ScratchPoints[scratchPointsCount++] = sourceTriangle.Vertex2; 59 | 60 | // Early out 61 | // If all three points of the triangle were in the projector, we don't need to do anything. 62 | if (scratchPointsCount == 3) 63 | { 64 | GeneratedTriangles[generatedTriangleCount++] = sourceTriangle; 65 | continue; 66 | } 67 | 68 | // Test each triangle edge against the projector planes 69 | ScratchTriangleEdges[0] = sourceTriangle.Edge0; 70 | ScratchTriangleEdges[1] = sourceTriangle.Edge1; 71 | ScratchTriangleEdges[2] = sourceTriangle.Edge2; 72 | ClipToPlanes(sourceTriangle, ScratchTriangleEdges, UnitCubePlanes, ScratchPoints, TestPointMode.UnitCube, scratchPointsCount, out scratchPointsCount); 73 | 74 | // Test each projector edge against the triangle plane 75 | ScratchTrianglePlanes[0] = sourceTriangle.Plane; 76 | ClipToPlanes(sourceTriangle, UnitCubeEdges, ScratchTrianglePlanes, ScratchPoints, TestPointMode.Triangle, scratchPointsCount, out scratchPointsCount); 77 | 78 | // No points 79 | if (scratchPointsCount == 0) 80 | continue; 81 | 82 | // Sort the points by dot product around the median. 83 | float4 middle = MiddlePoint(ScratchPoints, scratchPointsCount); 84 | float4 orientation = middle - ScratchPoints[0]; 85 | 86 | // Provide the comparer with some state, and sort the array 87 | comparer.Update(orientation, middle, sourceTriangle.Normal); 88 | NativeArraySort(ScratchPoints, 0, scratchPointsCount, comparer); 89 | 90 | Triangle newTriangle = Triangle.zero; 91 | // Create triangles from the points 92 | for (int p = 0; p < scratchPointsCount; p++) 93 | { 94 | newTriangle.SetFrom( 95 | middle, ScratchPoints[p], ScratchPoints[(p + 1) % scratchPointsCount] 96 | ); 97 | 98 | GeneratedTriangles[generatedTriangleCount++] = newTriangle; 99 | 100 | if (generatedTriangleCount >= DecalProjector.MaxDecalTriangles) 101 | break; 102 | } 103 | } 104 | 105 | NumGeneratedTriangles[0] = generatedTriangleCount; 106 | } 107 | 108 | public static float Tolerant(float v) 109 | { 110 | return v > 0f ? v + DecalProjector.ErrorTolerance : v - DecalProjector.ErrorTolerance; 111 | } 112 | 113 | static bool UnitCubeContains(float4 v) 114 | { 115 | if ((v.x >= Tolerant(-0.5f) && v.x <= Tolerant(0.5f)) && 116 | (v.y >= Tolerant(-0.5f) && v.y <= Tolerant(0.5f)) && 117 | (v.z >= Tolerant(-0.5f) && v.z <= Tolerant(0.5f)) 118 | ) 119 | { 120 | return true; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | 127 | // Find the middle point of a set of points 128 | float4 MiddlePoint(NativeArray points, int length) 129 | { 130 | float4 sum = float4.zero; 131 | for(int i = 0; i < length; i++) 132 | { 133 | sum = sum + points[i]; 134 | } 135 | 136 | return sum / length; 137 | } 138 | 139 | // Iterate over the edges, comparing each to every plane, and find if the intersection is usable. 140 | void ClipToPlanes(Triangle sourceTriangle, NativeArray edges, NativeArray planes, NativeArray points, TestPointMode mode, int startingPointsCount, out int m_scratchPointsCount) 141 | { 142 | Ray r = Ray.zero; 143 | m_scratchPointsCount = startingPointsCount; 144 | 145 | for(int e = 0; e < edges.Length; e++) 146 | { 147 | Edge edge = edges[e]; 148 | r.SetFrom(edge.Vertex0, edge.Vertex1 - edge.Vertex0); 149 | 150 | for(int p = 0; p < planes.Length; p++) 151 | { 152 | Plane plane = planes[p]; 153 | float dist; 154 | plane.Raycast(r, out dist); 155 | float absDist = math.abs(dist); 156 | if (absDist > 0f) 157 | { 158 | float4 pt = r.GetPoint(dist); 159 | if (edge.Contains(pt)) 160 | { 161 | if (mode == TestPointMode.UnitCube) 162 | { 163 | if (UnitCubeContains(pt)) 164 | { 165 | points[m_scratchPointsCount++] = pt; 166 | } 167 | } 168 | else { 169 | if (sourceTriangle.Contains(pt)) 170 | { 171 | points[m_scratchPointsCount++] = pt; 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | 180 | // Bubble sort, because the arrays are small and NativeArray doesn't have a sort method 181 | void NativeArraySort(NativeArray a, int start, int length, TrianglePointComparer comparer) 182 | { 183 | int upperBound = start + length; 184 | float4 t = 0f; 185 | for (int p = start; p <= upperBound - 2; p++) 186 | { 187 | for (int i = start; i <= upperBound - 2; i++) 188 | { 189 | if (comparer.Compare(a[i], a[i + 1]) > 0) 190 | { 191 | t = a[i + 1]; 192 | a[i + 1] = a[i]; 193 | a[i] = t; 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /ClippingJob.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e7f262f2bb8df3439f3150bb42b9cd0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /DecalExtensions.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | using UnityEngine; 3 | 4 | namespace SimpleDecal 5 | { 6 | public static class VectorExtensions 7 | { 8 | public static bool Approximately(this Quaternion a, Quaternion b) 9 | { 10 | return Mathf.Approximately(a.w, b.w) && 11 | Mathf.Approximately(a.x, b.x) && 12 | Mathf.Approximately(a.y, b.y) && 13 | Mathf.Approximately(a.z, b.z); 14 | 15 | } 16 | 17 | public static bool Approximately(this Vector3 a, Vector3 b) 18 | { 19 | return Mathf.Approximately(a.x, b.x) && 20 | Mathf.Approximately(a.y, b.y) && 21 | Mathf.Approximately(a.z, b.z); 22 | } 23 | 24 | public static float4 ToFloat4(this Vector3 v) 25 | { 26 | float4 f = float4.zero; 27 | f.xyz = v; 28 | return f; 29 | } 30 | 31 | public static float Angle(float4 a, float4 b) 32 | { 33 | float num = math.sqrt(math.lengthsq(a) * math.lengthsq(b)); 34 | if (num < 1.00000000362749E-15) 35 | return 0.0f; 36 | return math.acos(math.clamp(math.dot(a, b) / num, -1f, 1f)) * 57.29578f; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /DecalExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b43b5243dbcede74f9ccc4029aa85d00 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /DecalGizmo.cs: -------------------------------------------------------------------------------- 1 | using Unity.Collections; 2 | using Unity.Mathematics; 3 | using UnityEngine; 4 | 5 | namespace SimpleDecal 6 | { 7 | public static class DecalGizmo 8 | { 9 | public static void DrawGizmos(TRS trs) 10 | { 11 | NativeArray unitCubeEdges; 12 | NativeArray unitCubePlanes; 13 | UnitCube.GenerateStructures(out unitCubeEdges, out unitCubePlanes); 14 | 15 | foreach( var e in unitCubeEdges) 16 | GizmoLine(e, trs); 17 | 18 | Gizmos.color = Color.green; 19 | GizmoLine(UnitCube.Edge0, trs); 20 | GizmoLine(UnitCube.Edge1, trs); 21 | GizmoLine(UnitCube.Edge2, trs); 22 | GizmoLine(UnitCube.Edge3, trs); 23 | 24 | float4 extrapolate0 = GizmoLineExtrapolate(UnitCube.Edge0.Vertex0, trs); 25 | float4 extrapolate1 = GizmoLineExtrapolate(UnitCube.Edge1.Vertex0, trs); 26 | float4 extrapolate2 = GizmoLineExtrapolate(UnitCube.Edge2.Vertex0, trs); 27 | float4 extrapolate3 = GizmoLineExtrapolate(UnitCube.Edge3.Vertex0, trs); 28 | 29 | GizmoLine(extrapolate0, extrapolate1, trs); 30 | GizmoLine(extrapolate1, extrapolate2, trs); 31 | GizmoLine(extrapolate2, extrapolate3, trs); 32 | GizmoLine(extrapolate3, extrapolate0, trs); 33 | 34 | unitCubeEdges.Dispose(); 35 | unitCubePlanes.Dispose(); 36 | } 37 | 38 | static float4 GizmoLineExtrapolate(float4 v, TRS trs) 39 | { 40 | float4 extrapolatedV = v + (math.normalize(v) * 0.2f); 41 | GizmoLine(v, extrapolatedV, trs); 42 | return extrapolatedV; 43 | } 44 | 45 | static void GizmoLine(Edge e, TRS trs) 46 | { 47 | GizmoLine(e.Vertex0, e.Vertex1, trs); 48 | } 49 | 50 | static void GizmoLine(float4 a, float4 b, TRS trs) 51 | { 52 | Gizmos.DrawLine( 53 | trs.LocalToWorld(a).xyz, trs.LocalToWorld(b).xyz 54 | ); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /DecalGizmo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 80d9bf17bfc69f14e8f4763edf7ea771 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /DecalProjector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.Collections; 3 | using Unity.Jobs; 4 | using Unity.Mathematics; 5 | using UnityEngine; 6 | using EventProvider = System.Diagnostics.Eventing.EventProvider; 7 | 8 | namespace SimpleDecal 9 | { 10 | [ExecuteInEditMode] 11 | [RequireComponent(typeof(MeshRenderer))] 12 | [RequireComponent(typeof(MeshFilter))] 13 | public class DecalProjector : MonoBehaviour 14 | { 15 | public static readonly float ErrorTolerance = 0.005f; 16 | public static readonly int MaxDecalTriangles = 1000; 17 | 18 | [SerializeField] 19 | float m_displacement = 0.0001f; 20 | [SerializeField] 21 | LayerMask m_layerMask = int.MaxValue; // Everything 22 | [SerializeField] 23 | bool m_cullBackfaces = true; 24 | [SerializeField] 25 | bool m_bakeOnStart; 26 | 27 | Mesh m_mesh; 28 | Bounds m_bounds; 29 | TRS m_TRS; 30 | MeshFilter m_meshFilter; 31 | 32 | // The results of the clipping job will be written to these buffers. The mesh will then be generated using them. 33 | static Vector3[] m_scratchVertices; 34 | static int m_scratchVerticesCount; 35 | static int[] m_scratchIndices; 36 | static int m_scratchIndicesCount; 37 | static Vector3[] m_scratchNormals; 38 | static int m_scratchNormalsCount; 39 | static Vector2[] m_scratchUVs; 40 | static int m_scratchUVCount; 41 | 42 | // Job info 43 | JobHandle m_clippingJobHandle; 44 | bool m_outstandingClippingJob; 45 | ClippingJob m_clippingJob; 46 | bool m_wantsToScheduleNewJob; 47 | 48 | 49 | 50 | // Start is called before the first frame update 51 | void Start() 52 | { 53 | m_meshFilter = GetComponent(); 54 | if(m_bakeOnStart) 55 | Bake(); 56 | } 57 | 58 | void OnDestroy() 59 | { 60 | DestroyMesh(); 61 | } 62 | 63 | void GenerateScratchBuffers() 64 | { 65 | if (_lastMaxTriangles != MaxDecalTriangles || 66 | m_scratchVertices == null || 67 | m_scratchNormals == null || 68 | m_scratchIndices == null || 69 | m_scratchUVs == null 70 | ) 71 | { 72 | m_scratchVertices = new Vector3[MaxDecalTriangles * 3]; 73 | m_scratchNormals = new Vector3[MaxDecalTriangles * 3]; 74 | m_scratchIndices = new int[MaxDecalTriangles * 3]; 75 | m_scratchUVs = new Vector2[MaxDecalTriangles * 3]; 76 | for (int i = 0; i < MaxDecalTriangles * 3; i++) 77 | { 78 | m_scratchIndices[i] = i; 79 | } 80 | 81 | _lastMaxTriangles = MaxDecalTriangles; 82 | } 83 | } 84 | 85 | // Editor-only logic for detecting if the user is moving the projector around in the scene. Currently, 86 | // automatic baking is only supported outside of playmode, in the Editor, for potential performance reasons. 87 | // These restrictions can be lifted if you want. 88 | #if UNITY_EDITOR 89 | Quaternion _lastRotation; 90 | Vector3 _lastPosition; 91 | Vector3 _lastScale; 92 | float _lastDisplacement; 93 | Material _lastMaterial; 94 | int _lastLayerMask; 95 | int _lastMaxTriangles; 96 | 97 | // Update is called once per frame 98 | void Update() 99 | { 100 | if (!Application.isPlaying) 101 | { 102 | if (IsDirty()) 103 | { 104 | m_wantsToScheduleNewJob = true; 105 | 106 | _lastRotation = transform.rotation; 107 | _lastPosition = transform.position; 108 | _lastScale = transform.lossyScale; 109 | _lastDisplacement = m_displacement; 110 | _lastLayerMask = m_layerMask; 111 | } 112 | } 113 | 114 | if (m_wantsToScheduleNewJob && !m_outstandingClippingJob) 115 | { 116 | Bake(); 117 | m_wantsToScheduleNewJob = false; 118 | } 119 | } 120 | 121 | // Has the GameObject or projector been configured differently or moved? 122 | bool IsDirty() 123 | { 124 | return !_lastRotation.Approximately(transform.rotation) || 125 | !_lastPosition.Approximately(transform.position) || 126 | !_lastScale.Approximately(transform.lossyScale) || 127 | !Mathf.Approximately(_lastDisplacement, m_displacement) || 128 | _lastLayerMask != m_layerMask; 129 | } 130 | 131 | void OnDrawGizmos() 132 | { 133 | UpdateSelfTRS(); 134 | 135 | if (UnityEditor.Selection.activeGameObject != gameObject) 136 | return; 137 | 138 | DecalGizmo.DrawGizmos(m_TRS); 139 | } 140 | #endif 141 | 142 | void UpdateSelfTRS() 143 | { 144 | m_TRS.Update(transform.worldToLocalMatrix, transform.localToWorldMatrix, transform.position.ToFloat4()); 145 | } 146 | 147 | public void Bake() 148 | { 149 | m_bounds = new Bounds(transform.position, transform.lossyScale); 150 | 151 | UpdateSelfTRS(); 152 | GenerateScratchBuffers(); 153 | bool jobCreated = CreateMeshJob(); 154 | if (!jobCreated) 155 | { 156 | return; 157 | } 158 | 159 | m_outstandingClippingJob = true; 160 | m_clippingJobHandle = m_clippingJob.Schedule(); 161 | // Wait for the job to complete in LateUpdate 162 | } 163 | 164 | // Create the job that will perform the clipping 165 | bool CreateMeshJob() 166 | { 167 | NativeArray sourceTriangleArray = new NativeArray(MaxDecalTriangles, Allocator.TempJob); 168 | int numSourceTriangles = GatherTriangles(sourceTriangleArray); 169 | if (numSourceTriangles == 0) 170 | { 171 | sourceTriangleArray.Dispose(); 172 | return false; 173 | } 174 | 175 | NativeArray numGeneratedTriangles = new NativeArray(1, Allocator.TempJob); 176 | NativeArray generatedTriangleArray = new NativeArray(MaxDecalTriangles, Allocator.TempJob); 177 | 178 | m_clippingJob = new ClippingJob(); 179 | m_clippingJob.NumSourceTriangles = numSourceTriangles; 180 | m_clippingJob.SourceTriangles = sourceTriangleArray; 181 | m_clippingJob.GeneratedTriangles = generatedTriangleArray; 182 | m_clippingJob.NumGeneratedTriangles = numGeneratedTriangles; 183 | 184 | m_clippingJob.ScratchPoints = new NativeArray(10, Allocator.TempJob); 185 | m_clippingJob.ScratchTriangleEdges = new NativeArray(3, Allocator.TempJob); 186 | m_clippingJob.ScratchTrianglePlanes = new NativeArray(1, Allocator.TempJob); 187 | UnitCube.GenerateStructures(out m_clippingJob.UnitCubeEdges, out m_clippingJob.UnitCubePlanes); 188 | 189 | return true; 190 | } 191 | 192 | void CleanUpMeshJob() 193 | { 194 | m_clippingJob.SourceTriangles.Dispose(); 195 | m_clippingJob.GeneratedTriangles.Dispose(); 196 | m_clippingJob.NumGeneratedTriangles.Dispose(); 197 | 198 | m_clippingJob.ScratchPoints.Dispose(); 199 | m_clippingJob.ScratchTriangleEdges.Dispose(); 200 | m_clippingJob.ScratchTrianglePlanes.Dispose(); 201 | m_clippingJob.UnitCubeEdges.Dispose(); 202 | m_clippingJob.UnitCubePlanes.Dispose(); 203 | } 204 | 205 | // Process a clipping job that has completed 206 | void HandleJob() 207 | { 208 | int numGeneratedTriangles = m_clippingJob.NumGeneratedTriangles[0]; 209 | 210 | if (numGeneratedTriangles >= MaxDecalTriangles) 211 | { 212 | Debug.LogError($"Decal triangles exceeds max triangles {MaxDecalTriangles}."); 213 | } 214 | 215 | BuildMesh(m_clippingJob.GeneratedTriangles, numGeneratedTriangles); 216 | 217 | CleanUpMeshJob(); 218 | } 219 | 220 | // Scan for MeshRenderers that overlap the projector, and collect all of their triangles to be clipped 221 | // into the projector 222 | int GatherTriangles(NativeArray sourceTriangleArray) 223 | { 224 | TRS meshTRS = new TRS(); 225 | int sourceTriangles = 0; 226 | float4 up = new float4(0f,1f,0f,0f); 227 | 228 | foreach (var meshFilter in FindObjectsOfType()) 229 | { 230 | // Filter out object by layer mask 231 | int mask = 1 << meshFilter.gameObject.layer; 232 | if ((mask & m_layerMask) != mask) 233 | continue; 234 | 235 | // Filter out objects that are themselves decal projectors 236 | if (meshFilter.GetComponent() != null) 237 | continue; 238 | 239 | // Filter out objects by render bounds 240 | Renderer r = meshFilter.GetComponent(); 241 | if (!r.bounds.Intersects(m_bounds)) 242 | { 243 | continue; 244 | } 245 | 246 | // Filter out objects with no mesh 247 | Mesh m = meshFilter.sharedMesh; 248 | if (m == null) 249 | continue; 250 | 251 | Transform meshTransform = meshFilter.transform; 252 | meshTRS.Update(meshTransform.worldToLocalMatrix, meshTransform.localToWorldMatrix, meshTransform.position.ToFloat4()); 253 | 254 | Vector3[] meshVertices = m.vertices; 255 | 256 | // Iterate over the submeshes 257 | for (int submeshIndex = 0; submeshIndex < m.subMeshCount; submeshIndex++) 258 | { 259 | // Iterate over every group of 3 indices that form triangles 260 | int[] meshIndices = m.GetIndices(submeshIndex); 261 | for (int meshIndex = 0; meshIndex < meshIndices.Length; meshIndex += 3) 262 | { 263 | // TODO, make triangle Transform modify the triangle instead of making a new one 264 | Triangle tInMeshLocal = Triangle.zero; 265 | tInMeshLocal.SetFrom( 266 | meshVertices[meshIndices[meshIndex]].ToFloat4(), 267 | meshVertices[meshIndices[meshIndex + 1]].ToFloat4(), 268 | meshVertices[meshIndices[meshIndex + 2]].ToFloat4()); 269 | Triangle tInWorld = tInMeshLocal.LocalToWorld(meshTRS); 270 | 271 | if (m_cullBackfaces) 272 | { 273 | if (math.dot(tInWorld.Normal, up) < 0f) 274 | continue; 275 | } 276 | 277 | // If the bounds of the individual triangle don't intersect with the unit cube bounds, we can 278 | // ignore it 279 | Bounds triangleBounds = BoundsFromTriangle(tInWorld); 280 | if (!triangleBounds.Intersects(m_bounds)) 281 | continue; 282 | 283 | Triangle tInProjectorLocal = tInWorld.WorldToLocal(m_TRS); 284 | 285 | sourceTriangleArray[sourceTriangles++] = tInProjectorLocal; 286 | 287 | if (sourceTriangles >= MaxDecalTriangles) 288 | { 289 | Debug.LogError($"Decal triangles exceeds max trianges {MaxDecalTriangles}."); 290 | return sourceTriangles; 291 | } 292 | } 293 | } 294 | } 295 | 296 | return sourceTriangles; 297 | } 298 | 299 | Bounds BoundsFromTriangle(Triangle t) 300 | { 301 | float4 center = (t.Vertex0 + t.Vertex1 + t.Vertex2) / 3f; 302 | 303 | float4 d0 = (center - t.Vertex0) * 2f; 304 | float4 d1 = (center - t.Vertex1) * 2f; 305 | float4 d2 = (center - t.Vertex2) * 2f; 306 | 307 | Bounds b = new Bounds(center.xyz, 308 | new Vector3( 309 | Mathf.Max(math.abs(d0.x), math.abs(d1.x), math.abs(d2.x)), 310 | Mathf.Max(math.abs(d0.y), math.abs(d1.y), math.abs(d2.y)), 311 | Mathf.Max(math.abs(d0.z), math.abs(d1.z), math.abs(d2.z)) 312 | )); 313 | 314 | 315 | return b; 316 | } 317 | 318 | 319 | void LateUpdate() 320 | { 321 | // Complete and handle results of clipping job, if there was one 322 | if (m_outstandingClippingJob) 323 | { 324 | m_outstandingClippingJob = false; 325 | 326 | m_clippingJobHandle.Complete(); // Sync on the job 327 | 328 | HandleJob(); 329 | } 330 | } 331 | 332 | // Construct the mesh from the job data 333 | void BuildMesh(NativeArray triangleBuffer, int numTriangles) 334 | { 335 | DestroyMesh(); 336 | 337 | if (numTriangles == 0) 338 | { 339 | Debug.LogWarning("Unable to generate mesh with zero triangles."); 340 | return; 341 | } 342 | 343 | m_scratchVerticesCount = 0; 344 | m_scratchIndicesCount = 0; 345 | m_scratchNormalsCount = 0; 346 | m_scratchUVCount = 0; 347 | 348 | for( int i = 0; i < numTriangles; i++) 349 | { 350 | Triangle t = triangleBuffer[i]; 351 | AppendTriangleToScratchBuffers(t.Offset(m_displacement)); 352 | } 353 | 354 | m_mesh = new Mesh(); 355 | m_mesh.SetVertices(m_scratchVertices, 0, m_scratchVerticesCount); 356 | m_mesh.SetIndices(m_scratchIndices, 0, m_scratchIndicesCount, MeshTopology.Triangles, 0); 357 | m_mesh.SetNormals(m_scratchNormals, 0, m_scratchNormalsCount); 358 | m_mesh.SetUVs(0, m_scratchUVs, 0, m_scratchUVCount); 359 | m_mesh.UploadMeshData(true); 360 | 361 | if (m_meshFilter != null) 362 | { 363 | m_meshFilter.sharedMesh = m_mesh; 364 | } 365 | } 366 | 367 | void DestroyMesh() 368 | { 369 | if (m_mesh != null) 370 | #if UNITY_EDITOR 371 | DestroyImmediate(m_mesh); 372 | #else 373 | Destroy(_mesh); 374 | #endif 375 | 376 | m_mesh = null; 377 | if (m_meshFilter != null) 378 | { 379 | m_meshFilter.sharedMesh = null; 380 | } 381 | } 382 | 383 | // Fill out the scratch buffers with a triangle's data 384 | void AppendTriangleToScratchBuffers(Triangle t) 385 | { 386 | m_scratchIndicesCount++; // Already set 387 | m_scratchVertices[m_scratchVerticesCount++] = t.Vertex0.xyz; 388 | m_scratchNormals[m_scratchNormalsCount++] = t.Normal.xyz; 389 | m_scratchUVs[m_scratchUVCount].x = t.Vertex0.x + 0.5f; 390 | m_scratchUVs[m_scratchUVCount++].y = t.Vertex0.z + 0.5f; 391 | 392 | m_scratchIndicesCount++; // Already set 393 | m_scratchVertices[m_scratchVerticesCount++] = t.Vertex1.xyz; 394 | m_scratchNormals[m_scratchNormalsCount++] = t.Normal.xyz; 395 | m_scratchUVs[m_scratchUVCount].x = t.Vertex1.x + 0.5f; 396 | m_scratchUVs[m_scratchUVCount++].y = t.Vertex1.z + 0.5f; 397 | 398 | m_scratchIndicesCount++; // Already set 399 | m_scratchVertices[m_scratchVerticesCount++] = t.Vertex2.xyz; 400 | m_scratchNormals[m_scratchNormalsCount++] = t.Normal.xyz; 401 | m_scratchUVs[m_scratchUVCount].x = t.Vertex2.x + 0.5f; 402 | m_scratchUVs[m_scratchUVCount++].y = t.Vertex2.z + 0.5f; 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /DecalProjector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 50298d3115846434a9a6cd2975c60d64 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8b4282d4c59cf294495c02bd1e0a0b36 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/DecalProjectorEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using SimpleDecal; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.Rendering; 7 | 8 | namespace SimpleDecal 9 | { 10 | [InitializeOnLoad] 11 | public class DecalProjectorEditor 12 | { 13 | [MenuItem("GameObject/3D Object/Decal Projector", false, 0)] 14 | public static void AddProjector() 15 | { 16 | GameObject newGO = new GameObject("Decal Projector"); 17 | if (Selection.activeGameObject != null) 18 | { 19 | newGO.transform.parent = Selection.activeGameObject.transform; 20 | newGO.transform.localPosition = Vector3.zero; 21 | newGO.transform.localRotation = Quaternion.identity; 22 | } 23 | 24 | newGO.AddComponent(); 25 | MeshRenderer rend = newGO.AddComponent(); 26 | rend.shadowCastingMode = ShadowCastingMode.Off; 27 | 28 | DecalProjector proj = newGO.AddComponent(); 29 | 30 | // Build Material for current pipeline 31 | var pipeline = GraphicsSettings.renderPipelineAsset; 32 | 33 | Material m; 34 | Texture2D defaultTex = Resources.Load("DefaultDecal"); 35 | if (pipeline == null) 36 | { 37 | // Built-in renderer 38 | Shader s = Shader.Find("Mobile/BumpedDiffuse"); 39 | m = new Material(s); 40 | m.SetTexture("_MainTex", defaultTex); 41 | m.SetColor("_Color", Color.white); 42 | } 43 | else 44 | { 45 | Shader s = Shader.Find("HDRP/Decal"); 46 | if (s == null) 47 | { 48 | s = Shader.Find(""); 49 | } 50 | else 51 | { 52 | Debug.LogWarning("HDRP has its own decal system, which you should use instead of SimpleDecal."); 53 | } 54 | 55 | if (s == null) 56 | { 57 | Debug.LogWarning("Unable to find default shader for either URP or HDRP. Using pipeline default."); 58 | s = pipeline.defaultShader; 59 | } 60 | 61 | m = new Material(s); 62 | 63 | m.SetTexture("_BaseMap", defaultTex); // URP 64 | m.SetTexture("_BaseColorMap", defaultTex); // HDRP 65 | m.SetColor("_BaseColor", Color.white); 66 | m.SetColor("_Color", Color.white); 67 | } 68 | rend.material = m; 69 | proj.Bake(); 70 | 71 | Selection.activeGameObject = newGO; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Editor/DecalProjectorEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 49092067b15fd304283ffee3e125a1c7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Images.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6f82740e7dec9974790535b48176bf54 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Images/ContextMenu.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jconstable/SimpleDecal/0bd164e3559f575dc23ffd9e81a88af98f4f0b45/Images/ContextMenu.PNG -------------------------------------------------------------------------------- /Images/ContextMenu.PNG.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fcafbd13d3536994aa495ac8422f9183 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: -1 36 | aniso: 2 37 | mipBias: -100 38 | wrapU: 0 39 | wrapV: 0 40 | wrapW: 0 41 | nPOTScale: 1 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 0 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 0 54 | spriteTessellationDetail: -1 55 | textureType: 0 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 8192 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | - serializedVersion: 3 77 | buildTarget: Standalone 78 | maxTextureSize: 8192 79 | resizeAlgorithm: 0 80 | textureFormat: -1 81 | textureCompression: 1 82 | compressionQuality: 50 83 | crunchedCompression: 0 84 | allowsAlphaSplitting: 0 85 | overridden: 0 86 | androidETC2FallbackOverride: 0 87 | forceMaximumCompressionQuality_BC6H_BC7: 0 88 | - serializedVersion: 3 89 | buildTarget: iPhone 90 | maxTextureSize: 8192 91 | resizeAlgorithm: 0 92 | textureFormat: -1 93 | textureCompression: 1 94 | compressionQuality: 50 95 | crunchedCompression: 0 96 | allowsAlphaSplitting: 0 97 | overridden: 0 98 | androidETC2FallbackOverride: 0 99 | forceMaximumCompressionQuality_BC6H_BC7: 0 100 | - serializedVersion: 3 101 | buildTarget: Android 102 | maxTextureSize: 8192 103 | resizeAlgorithm: 0 104 | textureFormat: -1 105 | textureCompression: 1 106 | compressionQuality: 50 107 | crunchedCompression: 0 108 | allowsAlphaSplitting: 0 109 | overridden: 0 110 | androidETC2FallbackOverride: 0 111 | forceMaximumCompressionQuality_BC6H_BC7: 0 112 | - serializedVersion: 3 113 | buildTarget: Windows Store Apps 114 | maxTextureSize: 8192 115 | resizeAlgorithm: 0 116 | textureFormat: -1 117 | textureCompression: 1 118 | compressionQuality: 50 119 | crunchedCompression: 0 120 | allowsAlphaSplitting: 0 121 | overridden: 0 122 | androidETC2FallbackOverride: 0 123 | forceMaximumCompressionQuality_BC6H_BC7: 0 124 | spriteSheet: 125 | serializedVersion: 2 126 | sprites: [] 127 | outline: [] 128 | physicsShape: [] 129 | bones: [] 130 | spriteID: 131 | internalID: 0 132 | vertices: [] 133 | indices: 134 | edges: [] 135 | weights: [] 136 | secondaryTextures: [] 137 | spritePackingTag: 138 | pSDRemoveMatte: 0 139 | pSDShowRemoveMatteOption: 0 140 | userData: 141 | assetBundleName: 142 | assetBundleVariant: 143 | -------------------------------------------------------------------------------- /Images/DecalDemo.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jconstable/SimpleDecal/0bd164e3559f575dc23ffd9e81a88af98f4f0b45/Images/DecalDemo.PNG -------------------------------------------------------------------------------- /Images/DecalDemo.PNG.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b79130a6a5cc58b44b584c3dbacb5b60 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: -1 36 | aniso: 2 37 | mipBias: -100 38 | wrapU: 0 39 | wrapV: 0 40 | wrapW: 0 41 | nPOTScale: 1 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 0 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 0 54 | spriteTessellationDetail: -1 55 | textureType: 0 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | maxTextureSizeSet: 0 59 | compressionQualitySet: 0 60 | textureFormatSet: 0 61 | ignorePngGamma: 0 62 | applyGammaDecoding: 0 63 | platformSettings: 64 | - serializedVersion: 3 65 | buildTarget: DefaultTexturePlatform 66 | maxTextureSize: 8192 67 | resizeAlgorithm: 0 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | androidETC2FallbackOverride: 0 75 | forceMaximumCompressionQuality_BC6H_BC7: 0 76 | - serializedVersion: 3 77 | buildTarget: Standalone 78 | maxTextureSize: 8192 79 | resizeAlgorithm: 0 80 | textureFormat: -1 81 | textureCompression: 1 82 | compressionQuality: 50 83 | crunchedCompression: 0 84 | allowsAlphaSplitting: 0 85 | overridden: 0 86 | androidETC2FallbackOverride: 0 87 | forceMaximumCompressionQuality_BC6H_BC7: 0 88 | - serializedVersion: 3 89 | buildTarget: iPhone 90 | maxTextureSize: 8192 91 | resizeAlgorithm: 0 92 | textureFormat: -1 93 | textureCompression: 1 94 | compressionQuality: 50 95 | crunchedCompression: 0 96 | allowsAlphaSplitting: 0 97 | overridden: 0 98 | androidETC2FallbackOverride: 0 99 | forceMaximumCompressionQuality_BC6H_BC7: 0 100 | - serializedVersion: 3 101 | buildTarget: Android 102 | maxTextureSize: 8192 103 | resizeAlgorithm: 0 104 | textureFormat: -1 105 | textureCompression: 1 106 | compressionQuality: 50 107 | crunchedCompression: 0 108 | allowsAlphaSplitting: 0 109 | overridden: 0 110 | androidETC2FallbackOverride: 0 111 | forceMaximumCompressionQuality_BC6H_BC7: 0 112 | - serializedVersion: 3 113 | buildTarget: Windows Store Apps 114 | maxTextureSize: 8192 115 | resizeAlgorithm: 0 116 | textureFormat: -1 117 | textureCompression: 1 118 | compressionQuality: 50 119 | crunchedCompression: 0 120 | allowsAlphaSplitting: 0 121 | overridden: 0 122 | androidETC2FallbackOverride: 0 123 | forceMaximumCompressionQuality_BC6H_BC7: 0 124 | spriteSheet: 125 | serializedVersion: 2 126 | sprites: [] 127 | outline: [] 128 | physicsShape: [] 129 | bones: [] 130 | spriteID: 131 | internalID: 0 132 | vertices: [] 133 | indices: 134 | edges: [] 135 | weights: [] 136 | secondaryTextures: [] 137 | spritePackingTag: 138 | pSDRemoveMatte: 0 139 | pSDShowRemoveMatteOption: 0 140 | userData: 141 | assetBundleName: 142 | assetBundleVariant: 143 | -------------------------------------------------------------------------------- /Primitives.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2a3aa1ed0cdf98642a10a12cfc5305c7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Primitives/Edge.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | 3 | namespace SimpleDecal 4 | { 5 | // Small class representing an Edge 6 | public struct Edge 7 | { 8 | public static readonly Edge zero = new Edge(0f, 0f); 9 | 10 | public float4 Vertex0; 11 | public float4 Vertex1; 12 | public float length; 13 | 14 | public Edge(float4 a, float4 b) 15 | { 16 | this = default; 17 | SetFrom(a,b); 18 | } 19 | 20 | public void SetFrom(float4 a, float4 b) 21 | { 22 | Vertex0 = a; 23 | Vertex1 = b; 24 | 25 | float4 dir = a - b; 26 | length = math.length(dir); 27 | } 28 | 29 | public bool Contains(float4 point) 30 | { 31 | float testLength = math.length(Vertex0 - point) + math.length(Vertex1 - point); 32 | return math.abs(length - testLength) < DecalProjector.ErrorTolerance; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Primitives/Edge.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cf35b229ccb8cb2499c110b49bcedbb5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Primitives/Plane.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | using UnityEditor.Build.Pipeline; 3 | 4 | namespace SimpleDecal 5 | { 6 | public struct Plane 7 | { 8 | public static readonly Plane zero = new Plane(0f, 0f,0f); 9 | 10 | public float4 Normal; 11 | public float Distance; 12 | 13 | public Plane(float4 a, float4 b, float4 c) 14 | { 15 | this = default; 16 | SetFrom(a,b,c); 17 | } 18 | 19 | public void SetFrom(float4 a, float4 b, float4 c) 20 | { 21 | float3 n3 = math.normalize(math.cross((b - a).xyz, (c - a).xyz)); 22 | Normal = float4.zero; 23 | Normal.xyz = n3.xyz; 24 | Distance = -math.dot(Normal, a); 25 | } 26 | 27 | public bool Raycast(Ray r, out float dist) 28 | { 29 | float a = math.dot(r.Direction, Normal); 30 | float num = -math.dot(r.Origin, Normal) - Distance; 31 | if (math.abs(a - 0.0f) < DecalProjector.ErrorTolerance) 32 | { 33 | dist = 0f; 34 | return false; 35 | } 36 | dist = num / a; 37 | return (double) dist > 0.0; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Primitives/Plane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ac67f0c70a35274ea17fc8e5ff2ce1c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Primitives/Ray.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | 3 | namespace SimpleDecal 4 | { 5 | // Small class that represent a ray 6 | public struct Ray 7 | { 8 | public static readonly Ray zero = new Ray(0f, 0f); 9 | public float4 Origin; 10 | public float4 Direction; 11 | 12 | public Ray(float4 origin, float4 direction) 13 | { 14 | this = default; 15 | SetFrom(origin, direction); 16 | } 17 | 18 | public void SetFrom(float4 origin, float4 direction) 19 | { 20 | Origin = origin; 21 | Direction = math.normalize(direction); 22 | } 23 | 24 | public float4 GetPoint(float dist) 25 | { 26 | return Origin + (Direction * dist); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Primitives/Ray.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b8c2c5c1bf28dd47bf379a5efd2a1b9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Primitives/TRS.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | 3 | namespace SimpleDecal 4 | { 5 | // A small class that handles Translation, Rotation, and Scale for dealing with Triangle type 6 | public struct TRS 7 | { 8 | float4x4 m_worldToLocal; 9 | float4x4 m_localToWorld; 10 | float4 m_translation; 11 | 12 | public void Update(float4x4 w2l, float4x4 l2w, float4 offset) 13 | { 14 | m_worldToLocal = w2l; 15 | m_localToWorld = l2w; 16 | m_translation = offset; 17 | } 18 | 19 | public float4 LocalToWorld(float4 point) 20 | { 21 | return math.mul(m_localToWorld, point) + m_translation; 22 | } 23 | 24 | public float4 WorldToLocal(float4 point) 25 | { 26 | return math.mul(m_worldToLocal, point + -m_translation); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Primitives/TRS.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c849c8dff0f5d1d4f8d377ffd6af3cc4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Primitives/Triangle.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | 3 | namespace SimpleDecal 4 | { 5 | // Our representation of a Triangle, with some convenience functions 6 | public struct Triangle 7 | { 8 | public Edge Edge0; 9 | public Edge Edge1; 10 | public Edge Edge2; 11 | public float4 Normal; 12 | public Plane Plane; 13 | public float4 Vertex0; 14 | public float4 Vertex1; 15 | public float4 Vertex2; 16 | 17 | bool m_hasCalculatedArea; 18 | float m_area; 19 | 20 | public static readonly Triangle zero = new Triangle() 21 | { 22 | Vertex0 = 0f, 23 | Vertex1 = 0f, 24 | Vertex2 = 0f, 25 | Normal = 0f, 26 | Plane = Plane.zero, 27 | Edge0 = Edge.zero, 28 | Edge1 = Edge.zero, 29 | Edge2 = Edge.zero 30 | }; 31 | 32 | public void SetFrom(float4 aIn, float4 bIn, float4 cIn) 33 | { 34 | Edge0 = Edge.zero; 35 | Edge1 = Edge.zero; 36 | Edge2 = Edge.zero; 37 | 38 | Edge0.SetFrom(aIn, bIn); 39 | Edge1.SetFrom(bIn, cIn); 40 | Edge2.SetFrom(cIn, aIn); 41 | 42 | Vertex0 = aIn; 43 | Vertex1 = bIn; 44 | Vertex2 = cIn; 45 | 46 | Plane = Plane.zero; 47 | Plane.SetFrom(aIn, bIn, cIn); 48 | Normal = Plane.Normal; 49 | m_hasCalculatedArea = false; 50 | m_area = 0f; 51 | } 52 | 53 | public void CopyFrom(Triangle t) 54 | { 55 | Edge0 = t.Edge0; 56 | Edge1 = t.Edge1; 57 | Edge2 = t.Edge2; 58 | 59 | Vertex0 = t.Vertex0; 60 | Vertex1 = t.Vertex1; 61 | Vertex2 = t.Vertex2; 62 | 63 | Plane = t.Plane; 64 | Normal = t.Normal; 65 | m_hasCalculatedArea = false; 66 | m_area = 0f; 67 | } 68 | 69 | public Triangle LocalToWorld(TRS trs) 70 | { 71 | Triangle t = Triangle.zero; 72 | t.SetFrom( 73 | trs.LocalToWorld(Vertex0), 74 | trs.LocalToWorld(Vertex1), 75 | trs.LocalToWorld(Vertex2) 76 | ); 77 | return t; 78 | } 79 | 80 | public Triangle WorldToLocal(TRS trs) 81 | { 82 | Triangle t = Triangle.zero; 83 | t.SetFrom( 84 | trs.WorldToLocal(Vertex0), 85 | trs.WorldToLocal(Vertex1), 86 | trs.WorldToLocal(Vertex2) 87 | ); 88 | return t; 89 | } 90 | 91 | public Triangle Offset(float distance) 92 | { 93 | float4 offset = Normal * distance; 94 | Triangle t = Triangle.zero; 95 | t.SetFrom( 96 | Vertex0 + offset, 97 | Vertex1 + offset, 98 | Vertex2 + offset 99 | ); 100 | return t; 101 | } 102 | 103 | public float Area() 104 | { 105 | if (m_hasCalculatedArea) 106 | return m_area; 107 | m_area = Area(Vertex0, Vertex1, Vertex2); 108 | m_hasCalculatedArea = true; 109 | return m_area; 110 | } 111 | 112 | public float Area(float4 a, float4 b, float4 c) 113 | { 114 | float4 ab = (a - b); 115 | float4 ac = (a - c); 116 | float abLength = math.length(ab); 117 | float acLength = math.length(ac); 118 | float theta = math.acos(math.dot(ab, ac) / (abLength * acLength)); 119 | 120 | return 0.5f * abLength * acLength * math.sin(theta); 121 | } 122 | 123 | public bool Contains(float4 position) 124 | { 125 | float area = Area(); 126 | 127 | float a1 = Area(Vertex0, Vertex1, position); 128 | float a2 = Area(Vertex1, Vertex2, position); 129 | float a3 = Area(Vertex2, Vertex0, position); 130 | 131 | // Position is inside triangle of the sum of the areas of the three new triangle made using position 132 | // equals the area of the whole triangle 133 | return math.abs(area - (a1 + a2 + a3)) < DecalProjector.ErrorTolerance; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /Primitives/Triangle.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 758c71341d9e597499bdec8baa2833f5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleDecal 2 | A simple solution to adding decals to a Unity Scene. 3 | 4 | ![Three decal projectors on crates](https://github.com/jconstable/SimpleDecal/blob/master/Images/DecalDemo.PNG) 5 | 6 | Decals can be added to an object by using the context menu in the hierarchy, and creating a new Decal Project under "3D Objects". 7 | 8 | ![Creating a new decal projector](https://github.com/jconstable/SimpleDecal/blob/master/Images/ContextMenu.PNG) 9 | 10 | Any material can be used in the decal's MeshRenderer. It is simply new geometry, copied from the intersecting geometry and clipped to the Decal Projector space. 11 | 12 | Clipping work is done via a job, and is extremely fast. The job itself is allocation-free, though Mesh generation has a cost. 13 | 14 | TODO: 15 | * In Edit mode, if the mesh is not read/write, load the aset from disk instead. Obviously, this won't be supported at runtime. 16 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d47a8eb331c5a244ab6905dcb6c159b9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35faacde8f9ed824bac37382b0a7f253 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Resources/DefaultDecal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jconstable/SimpleDecal/0bd164e3559f575dc23ffd9e81a88af98f4f0b45/Resources/DefaultDecal.png -------------------------------------------------------------------------------- /Resources/DefaultDecal.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5be03e9b8ba6e0447928b4a8736a75bf 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /TrianglePointComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Unity.Mathematics; 3 | 4 | namespace SimpleDecal 5 | { 6 | // IComparer that can sort points in rotational position around a normal 7 | struct TrianglePointComparer : IComparer 8 | { 9 | public static readonly TrianglePointComparer zero = new TrianglePointComparer 10 | { 11 | m_orientation = 0f, 12 | m_middle = 0f, 13 | m_normal = 0f 14 | }; 15 | 16 | float4 m_orientation; 17 | float4 m_middle; 18 | float4 m_normal; 19 | 20 | public void Update(float4 orientation, float4 middle, float4 normal) 21 | { 22 | m_orientation = orientation; 23 | m_middle = middle; 24 | m_normal = normal; 25 | } 26 | 27 | public int Compare(float4 a, float4 b) 28 | { 29 | return SignedAngle(m_orientation, m_middle - a, m_normal).CompareTo(SignedAngle(m_orientation, m_middle - b, m_normal)); 30 | } 31 | 32 | public static float SignedAngle(float4 a, float4 b, float4 normal) 33 | { 34 | float angle = VectorExtensions.Angle(a, b); 35 | float3 cross = math.cross(a.xyz, b.xyz); 36 | if (math.dot(normal.xyz, cross) < 0f) 37 | { 38 | return -angle; 39 | } 40 | 41 | return angle; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TrianglePointComparer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d67f013b11ae0c545a8774ac2e054a44 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /UnitCube.cs: -------------------------------------------------------------------------------- 1 | using Unity.Collections; 2 | using Unity.Mathematics; 3 | 4 | namespace SimpleDecal 5 | { 6 | public struct UnitCube 7 | { 8 | // 0---------1 9 | // |\ A |\ 10 | // | 2---------3 11 | // |B| C |D| 12 | // 4-|-------5 | 13 | // \ | E \ | 14 | // 6---------7 15 | 16 | public static void GenerateStructures(out NativeArray Edges, out NativeArray Planes) 17 | { 18 | Edges = new NativeArray(12, Allocator.TempJob); 19 | Edges[0] = Edge0; 20 | Edges[1] = Edge1; 21 | Edges[2] = Edge2; 22 | Edges[3] = Edge3; 23 | Edges[4] = Edge4; 24 | Edges[5] = Edge5; 25 | Edges[6] = Edge6; 26 | Edges[7] = Edge7; 27 | Edges[8] = Edge8; 28 | Edges[9] = Edge9; 29 | Edges[10] = Edge10; 30 | Edges[11] = Edge11; 31 | 32 | Planes = new NativeArray(6, Allocator.TempJob); 33 | Planes[0] = Plane0; 34 | Planes[1] = Plane1; 35 | Planes[2] = Plane2; 36 | Planes[3] = Plane3; 37 | Planes[4] = Plane4; 38 | Planes[5] = Plane5; 39 | } 40 | 41 | static float4 Vertex0 = new float4(-0.5f, 0.5f, -0.5f, 0f); 42 | static float4 Vertex1 = new float4(0.5f, 0.5f, -0.5f, 0f); 43 | static float4 Vertex2 = new float4(-0.5f, 0.5f, 0.5f, 0f); 44 | static float4 Vertex3 = new float4(0.5f, 0.5f, 0.5f, 0f); 45 | static float4 Vertex4 = new float4(-0.5f, -0.5f, -0.5f, 0f); 46 | static float4 Vertex5 = new float4(0.5f, -0.5f, -0.5f, 0f); 47 | static float4 Vertex6 = new float4(-0.5f, -0.5f, 0.5f, 0f); 48 | static float4 Vertex7 = new float4(0.5f, -0.5f, 0.5f, 0f); 49 | 50 | static Plane Plane0 = new Plane(Vertex0, Vertex3, Vertex1); 51 | static Plane Plane1 = new Plane(Vertex0, Vertex6, Vertex2); 52 | static Plane Plane2 = new Plane(Vertex0, Vertex1, Vertex5); 53 | static Plane Plane3 = new Plane(Vertex1, Vertex3, Vertex7); 54 | static Plane Plane4 = new Plane(Vertex4, Vertex5, Vertex7); 55 | static Plane Plane5 = new Plane(Vertex2, Vertex6, Vertex3); 56 | 57 | public static Edge Edge0 = new Edge(Vertex0, Vertex1); 58 | public static Edge Edge1 = new Edge(Vertex1, Vertex3); 59 | public static Edge Edge2 = new Edge(Vertex3, Vertex2); 60 | public static Edge Edge3 = new Edge(Vertex2, Vertex0); 61 | public static Edge Edge4 = new Edge(Vertex4, Vertex5); 62 | public static Edge Edge5 = new Edge(Vertex5, Vertex7); 63 | public static Edge Edge6 = new Edge(Vertex7, Vertex6); 64 | public static Edge Edge7 = new Edge(Vertex6, Vertex4); 65 | public static Edge Edge8 = new Edge(Vertex0, Vertex4); 66 | public static Edge Edge9 = new Edge(Vertex1, Vertex5); 67 | public static Edge Edge10 = new Edge(Vertex3, Vertex7); 68 | public static Edge Edge11 = new Edge(Vertex2, Vertex6); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /UnitCube.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d5d609d3c68856144996c6b27e05993c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------