using System; using System.Collections.Generic; using Unity.Collections; using Unity.Mathematics; using UnityEngine.U2D; namespace UnityEngine.Rendering.Universal { internal static class LightUtility { public static bool CheckForChange(Light2D.LightType a, ref Light2D.LightType b) { var changed = a != b; b = a; return changed; } public static bool CheckForChange(int a, ref int b) { var changed = a != b; b = a; return changed; } public static bool CheckForChange(float a, ref float b) { var changed = a != b; b = a; return changed; } public static bool CheckForChange(bool a, ref bool b) { var changed = a != b; b = a; return changed; } private enum PivotType { PivotBase, PivotCurve, PivotIntersect, PivotSkip, PivotClip }; [Serializable] internal struct LightMeshVertex { public Vector3 position; public Color color; public Vector2 uv; public static readonly VertexAttributeDescriptor[] VertexLayout = new[] { new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.Float32, 4), new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), }; } static bool TestPivot(List path, int activePoint, long lastPoint) { for (int i = activePoint; i < path.Count; ++i) { if (path[i].N > lastPoint) return true; } return (path[activePoint].N == -1); } // Degenerate Pivots at the End Points. static List DegeneratePivots(List path, List inPath, ref int interiorStart) { List degenerate = new List(); var minN = path[0].N; var maxN = path[0].N; for (int i = 1; i < path.Count; ++i) { if (path[i].N != -1) { minN = Math.Min(minN, path[i].N); maxN = Math.Max(maxN, path[i].N); } } for (long i = 0; i < minN; ++i) { IntPoint ins = path[(int)minN]; ins.N = i; degenerate.Add(ins); } degenerate.AddRange(path.GetRange(0, path.Count)); interiorStart = degenerate.Count; for (long i = maxN + 1; i < inPath.Count; ++i) { IntPoint ins = inPath[(int)i]; ins.N = i; degenerate.Add(ins); } return degenerate; } // Ensure that we get a valid path from 0. static List SortPivots(List outPath, List inPath) { List sorted = new List(); var min = outPath[0].N; var max = outPath[0].N; var minIndex = 0; bool newMin = true; for (int i = 1; i < outPath.Count; ++i) { if (max > outPath[i].N && newMin && outPath[i].N != -1) { min = max = outPath[i].N; minIndex = i; newMin = false; } else if (outPath[i].N >= max) { max = outPath[i].N; newMin = true; } } sorted.AddRange(outPath.GetRange(minIndex, (outPath.Count - minIndex))); sorted.AddRange(outPath.GetRange(0, minIndex)); return sorted; } // Ensure that all points eliminated due to overlaps and intersections are accounted for Tessellation. static List FixPivots(List outPath, List inPath, ref int interiorStart) { var path = SortPivots(outPath, inPath); long pivotPoint = path[0].N; // Connect Points for Overlaps. for (int i = 1; i < path.Count; ++i) { var j = (i == path.Count - 1) ? 0 : (i + 1); var prev = path[i - 1]; var curr = path[i]; var next = path[j]; if (prev.N > curr.N) { var incr = TestPivot(path, i, pivotPoint); if (incr) { if (prev.N == next.N) curr.N = prev.N; else curr.N = (pivotPoint + 1) < inPath.Count ? (pivotPoint + 1) : 0; curr.D = 3; path[i] = curr; } } pivotPoint = path[i].N; } // Insert Skipped Points. for (int i = 1; i < path.Count - 1;) { var prev = path[i - 1]; var curr = path[i]; var next = path[i + 1]; if (curr.N - prev.N > 1) { if (curr.N == next.N) { IntPoint ins = curr; ins.N = (ins.N - 1); path[i] = ins; } else { IntPoint ins = curr; ins.N = (ins.N - 1); path.Insert(i, ins); } } else { i++; } } path = DegeneratePivots(path, inPath, ref interiorStart); return path; } // Rough shape only used in Inspector for quick preview. internal static List GetOutlinePath(Vector3[] shapePath, float offsetDistance) { const float kClipperScale = 10000.0f; List path = new List(); List output = new List(); for (var i = 0; i < shapePath.Length; ++i) { var newPoint = new Vector2(shapePath[i].x, shapePath[i].y) * kClipperScale; path.Add(new IntPoint((System.Int64)(newPoint.x), (System.Int64)(newPoint.y))); } List> solution = new List>(); ClipperOffset clipOffset = new ClipperOffset(24.0f); clipOffset.AddPath(path, JoinType.jtRound, EndType.etClosedPolygon); clipOffset.Execute(ref solution, kClipperScale * offsetDistance, path.Count); if (solution.Count > 0) { int interiorStart = 0; var outPath = solution[0]; outPath = FixPivots(outPath, path, ref interiorStart); for (int i = 0; i < outPath.Count; ++i) output.Add(new Vector2(outPath[i].X / kClipperScale, outPath[i].Y / kClipperScale)); } return output; } static void TransferToMesh(NativeArray vertices, int vertexCount, NativeArray indices, int indexCount, Light2D light) { var mesh = light.lightMesh; mesh.SetVertexBufferParams(vertexCount, LightMeshVertex.VertexLayout); mesh.SetVertexBufferData(vertices, 0, 0, vertexCount); mesh.SetIndices(indices, 0, indexCount, MeshTopology.Triangles, 0, true); light.vertices = new LightMeshVertex[vertexCount]; NativeArray.Copy(vertices, light.vertices, vertexCount); light.indices = new ushort[indexCount]; NativeArray.Copy(indices, light.indices, indexCount); } public static Bounds GenerateShapeMesh(Light2D light, Vector3[] shapePath, float falloffDistance) { const float kClipperScale = 10000.0f; // todo Revisit this while we do Batching. var meshInteriorColor = new Color(0.0f, 0, 0, 1.0f); var meshExteriorColor = new Color(0.0f, 0, 0, 0.0f); // Create shape geometry based on edges int inEdgeCount = shapePath.Length; NativeArray tessInEdges = new NativeArray(inEdgeCount, Allocator.Temp); NativeArray tessInVertices = new NativeArray(inEdgeCount, Allocator.Temp); for (int i = 0; i < inEdgeCount; ++i) { int edgeEnd = i + 1; if (edgeEnd == inEdgeCount) edgeEnd = 0; int2 edge = new int2(i, edgeEnd); tessInEdges[i] = edge; int index = edge.x; tessInVertices[index] = new float2(shapePath[index].x, shapePath[index].y); } // Do tessellation NativeArray tessOutIndices = new NativeArray(tessInEdges.Length * 8, Allocator.Temp); NativeArray tessOutVertices = new NativeArray(tessInEdges.Length * 8, Allocator.Temp); NativeArray tessOutEdges = new NativeArray(tessInEdges.Length * 8, Allocator.Temp); int tessOutVertexCount = 0; int tessOutIndexCount = 0; int tessOutEdgeCount = 0; UTess.ModuleHandle.Tessellate(Allocator.Temp, tessInVertices, tessInEdges, ref tessOutVertices, ref tessOutVertexCount, ref tessOutIndices, ref tessOutIndexCount, ref tessOutEdges, ref tessOutEdgeCount); // Create falloff geometry with random noise to account for collinear points var inputPointCount = shapePath.Length; List path = new List(); for (var i = 0; i < inputPointCount; ++i) { var newPoint = new Vector2(shapePath[i].x, shapePath[i].y) * kClipperScale; var addPoint = new IntPoint((System.Int64)(newPoint.x) + Random.Range(-100, 100), (System.Int64)(newPoint.y) + Random.Range(-100, 100)); addPoint.N = i; addPoint.D = -1; path.Add(addPoint); } var lastPointIndex = inputPointCount - 1; var interiorStartPoint = 0; // Generate Bevels. List> solution = new List>(); ClipperOffset clipOffset = new ClipperOffset(24.0f); clipOffset.AddPath(path, JoinType.jtRound, EndType.etClosedPolygon); clipOffset.Execute(ref solution, kClipperScale * falloffDistance, path.Count); if (solution.Count > 0) { // Fix path for Pivots. var outPath = solution[0]; var minPath = (long)inputPointCount; for (int i = 0; i < outPath.Count; ++i) minPath = (outPath[i].N != -1) ? Math.Min(minPath, outPath[i].N) : minPath; var containsStart = minPath == 0; outPath = FixPivots(outPath, path, ref interiorStartPoint); // Size accounts for light mesh + falloff geometry(outer + inner) int totalOutVertices = tessOutVertexCount + outPath.Count + inputPointCount; int totalOutIndices = tessOutIndexCount + (outPath.Count * 6) + 6; var outVertices = new NativeArray(totalOutVertices, Allocator.Temp); var outIndices = new NativeArray(totalOutIndices, Allocator.Temp); for (int i = 0; i < tessOutIndexCount; ++i) outIndices[i] = (ushort)tessOutIndices[i]; for (int i = 0; i < tessOutVertexCount; ++i) { outVertices[i] = new LightMeshVertex() { position = new float3(tessOutVertices[i].x, tessOutVertices[i].y, 0), color = meshInteriorColor }; } var vcount = tessOutVertexCount; var icount = tessOutIndexCount; var innerIndices = new ushort[inputPointCount]; // Inner Vertices. (These may or may not be part of the created path. Beware!!) for (int i = 0; i < inputPointCount; ++i) { outVertices[vcount++] = new LightMeshVertex() { position = new float3(shapePath[i].x, shapePath[i].y, 0), color = meshInteriorColor }; innerIndices[i] = (ushort)(vcount - 1); } var saveIndex = (ushort)vcount; var pathStart = saveIndex; var prevIndex = outPath[0].N == -1 ? 0 : outPath[0].N; // Outer Vertices for (int i = 0; i < outPath.Count; ++i) { var curr = outPath[i]; var currPoint = new float2(curr.X / kClipperScale, curr.Y / kClipperScale); var currIndex = curr.N == -1 ? 0 : curr.N; outVertices[vcount++] = new LightMeshVertex() { position = new float3(currPoint.x, currPoint.y, 0), color = (interiorStartPoint > i) ? meshExteriorColor : meshInteriorColor }; if (prevIndex != currIndex) { outIndices[icount++] = innerIndices[prevIndex]; outIndices[icount++] = innerIndices[currIndex]; outIndices[icount++] = (ushort)(vcount - 1); } outIndices[icount++] = innerIndices[prevIndex]; outIndices[icount++] = saveIndex; outIndices[icount++] = saveIndex = (ushort)(vcount - 1); prevIndex = currIndex; } // Close the Loop. { outIndices[icount++] = pathStart; outIndices[icount++] = innerIndices[minPath]; outIndices[icount++] = containsStart ? innerIndices[lastPointIndex] : saveIndex; outIndices[icount++] = containsStart ? pathStart : saveIndex; outIndices[icount++] = containsStart ? saveIndex : innerIndices[minPath]; // Last Triangle. todo: Remove Clipper Usage and use SpriteShape Geometry Generator for falloff potentially. if (containsStart) { var kTolerance = 0.001f; var connectingPoint = innerIndices[lastPointIndex]; // End point detection is tricky and depends on convexity of shape. Simple test is to just check the vertices and detect. var testA = MathF.Abs(outVertices[connectingPoint].position.x - outVertices[outIndices[icount - 1]].position.x) > kTolerance || MathF.Abs(outVertices[connectingPoint].position.y - outVertices[outIndices[icount - 1]].position.y) > kTolerance; var testB = MathF.Abs(outVertices[connectingPoint].position.x - outVertices[outIndices[icount - 2]].position.x) > kTolerance || MathF.Abs(outVertices[connectingPoint].position.y - outVertices[outIndices[icount - 2]].position.y) > kTolerance; if (!testA || !testB) connectingPoint = (ushort)(interiorStartPoint + inputPointCount + tessOutVertexCount - 1); outIndices[icount++] = connectingPoint; } else outIndices[icount++] = innerIndices[minPath - 1]; } TransferToMesh(outVertices, vcount, outIndices, icount, light); } return light.lightMesh.GetSubMesh(0).bounds; } public static Bounds GenerateParametricMesh(Light2D light, float radius, float falloffDistance, float angle, int sides) { var angleOffset = Mathf.PI / 2.0f + Mathf.Deg2Rad * angle; if (sides < 3) { radius = 0.70710678118654752440084436210485f * radius; sides = 4; } if (sides == 4) { angleOffset = Mathf.PI / 4.0f + Mathf.Deg2Rad * angle; } var vertexCount = 1 + 2 * sides; var indexCount = 3 * 3 * sides; var vertices = new NativeArray(vertexCount, Allocator.Temp); var triangles = new NativeArray(indexCount, Allocator.Temp); var centerIndex = (ushort)(2 * sides); var mesh = light.lightMesh; // Only Alpha value in Color channel is ever used. May remove it or keep it for batching params in the future. var color = new Color(0, 0, 0, 1); vertices[centerIndex] = new LightMeshVertex { position = float3.zero, color = color }; var radiansPerSide = 2 * Mathf.PI / sides; var min = new float3(float.MaxValue, float.MaxValue, 0); var max = new float3(float.MinValue, float.MinValue, 0); for (var i = 0; i < sides; i++) { var endAngle = (i + 1) * radiansPerSide; var extrudeDir = new float3(math.cos(endAngle + angleOffset), math.sin(endAngle + angleOffset), 0); var endPoint = radius * extrudeDir; var vertexIndex = (2 * i + 2) % (2 * sides); vertices[vertexIndex] = new LightMeshVertex { position = endPoint, color = new Color(extrudeDir.x, extrudeDir.y, 0, 0) }; vertices[vertexIndex + 1] = new LightMeshVertex { position = endPoint, color = color }; // Triangle 1 (Tip) var triangleIndex = 9 * i; triangles[triangleIndex] = (ushort)(vertexIndex + 1); triangles[triangleIndex + 1] = (ushort)(2 * i + 1); triangles[triangleIndex + 2] = centerIndex; // Triangle 2 (Upper Top Left) triangles[triangleIndex + 3] = (ushort)(vertexIndex); triangles[triangleIndex + 4] = (ushort)(2 * i); triangles[triangleIndex + 5] = (ushort)(2 * i + 1); // Triangle 2 (Bottom Top Left) triangles[triangleIndex + 6] = (ushort)(vertexIndex + 1); triangles[triangleIndex + 7] = (ushort)(vertexIndex); triangles[triangleIndex + 8] = (ushort)(2 * i + 1); min = math.min(min, endPoint + extrudeDir * falloffDistance); max = math.max(max, endPoint + extrudeDir * falloffDistance); } mesh.SetVertexBufferParams(vertexCount, LightMeshVertex.VertexLayout); mesh.SetVertexBufferData(vertices, 0, 0, vertexCount); mesh.SetIndices(triangles, MeshTopology.Triangles, 0, false); light.vertices = new LightMeshVertex[vertexCount]; NativeArray.Copy(vertices, light.vertices, vertexCount); light.indices = new ushort[indexCount]; NativeArray.Copy(triangles, light.indices, indexCount); return new Bounds { min = min, max = max }; } public static Bounds GenerateSpriteMesh(Light2D light, Sprite sprite) { var mesh = light.lightMesh; if (sprite == null) { mesh.Clear(); return new Bounds(Vector3.zero, Vector3.zero); } // this needs to be called before getting UV at the line below. // Venky fixed it, enroute to trunk var uvs = sprite.uv; var srcVertices = sprite.GetVertexAttribute(VertexAttribute.Position); var srcUVs = sprite.GetVertexAttribute(VertexAttribute.TexCoord0); var srcIndices = sprite.GetIndices(); var center = 0.5f * (sprite.bounds.min + sprite.bounds.max); var vertices = new NativeArray(srcIndices.Length, Allocator.Temp); var color = new Color(0, 0, 0, 1); for (var i = 0; i < srcVertices.Length; i++) { vertices[i] = new LightMeshVertex { position = new Vector3(srcVertices[i].x, srcVertices[i].y, 0), color = color, uv = srcUVs[i] }; } mesh.SetVertexBufferParams(vertices.Length, LightMeshVertex.VertexLayout); mesh.SetVertexBufferData(vertices, 0, 0, vertices.Length); mesh.SetIndices(srcIndices, MeshTopology.Triangles, 0, true); light.vertices = new LightMeshVertex[vertices.Length]; NativeArray.Copy(vertices, light.vertices, vertices.Length); light.indices = new ushort[srcIndices.Length]; NativeArray.Copy(srcIndices, light.indices, srcIndices.Length); return mesh.GetSubMesh(0).bounds; } public static int GetShapePathHash(Vector3[] path) { unchecked { int hashCode = (int)2166136261; if (path != null) { foreach (var point in path) hashCode = hashCode * 16777619 ^ point.GetHashCode(); } else { hashCode = 0; } return hashCode; } } } }