#if ENABLE_SPLINES #if ENABLE_COLLECTIONS #if ENABLE_MATHEMATICS #if ENABLE_BURST using System; using System.Collections.Generic; using System.Linq; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEditor; using UnityEngine; using UnityEngine.Splines; namespace UI_Spline_Renderer { [BurstCompile] internal struct SplineExtrudeJob : IJob { [ReadOnly] public NativeSpline spline; [ReadOnly] public NativeCurve widthCurve; [ReadOnly] public float width; [ReadOnly] public bool keepZeroZ; [ReadOnly] public bool keepBillboard; [ReadOnly] public float2 clipRange; [ReadOnly] public float2 uvMultiplier; [ReadOnly] public float2 uvOffset; [ReadOnly] public UVMode uvMode; [ReadOnly] public Color color; [ReadOnly] internal NativeColorGradient colorGradient; [ReadOnly] public int resolution; [ReadOnly] public bool smooth; [ReadOnly] public bool roundEnds; public NativeList vertices; public NativeList triangles; public int addedEdgeCount; int sampleCount => (int)(resolution * length * 0.03f); float v; float length; public void Execute() { if (spline.Count < 2) return; var edgePoints = new NativeList(Allocator.Temp); if(smooth) { SmoothSample(ref edgePoints); Extrude(in edgePoints); } else { UniformEvaluate(ref edgePoints); Extrude(in edgePoints); } if (roundEnds && ((spline.Closed && (clipRange.x > math.EPSILON || clipRange.y < 1)) || !spline.Closed)) { MakeRoundEdges(in edgePoints); } } void SmoothSample(ref NativeList edgePoints) { var samples = new NativeList(Allocator.Temp); // 전체 스플라인 샘플링 length = spline.GetLength(); for (int i = 0; i < spline.Count; i++) { var t = spline.CurveToSplineT(i); var unitT = (GetWidthAt(t) / length) * 0.5f; var knot = spline[i]; add_smooth_sample_point(in spline, samples, t, unitT, math.length(knot.TangentIn), math.length(knot.TangentOut)); } var sampleUnitT = 1f / sampleCount; for (int i = 0; i < spline.Count; i++) { var t = spline.CurveToSplineT(i); var t_1 = spline.CurveToSplineT(i - 1); var t1 = spline.CurveToSplineT(i + 1); var leftT = math.lerp(t, t_1, 0.5f); var rightT = math.lerp(t, t1, 0.5f); var leftCount = (int)(math.abs(t - leftT) / sampleUnitT); var rightCount = (int)(math.abs(rightT - t) / sampleUnitT); if (spline.Closed) { if(i == 0) { t_1 = spline.CurveToSplineT(spline.Count - 1); leftT = math.lerp(1, t_1, 0.5f); leftCount = (int)(math.abs(1 - leftT) / sampleUnitT); } } else { if (i == 0) leftCount = 0; if (i == spline.Count - 1) rightCount = 0; } for (int j = 1; j < leftCount+1; j++) { var tt = t - sampleUnitT*j; if (i == 0 && spline.Closed) tt += 1; add_smooth_sample_point(in spline, samples, tt, sampleUnitT, 1, 1); } for (int j = 1; j < rightCount+1; j++) { var tt = t + sampleUnitT*j; add_smooth_sample_point(in spline, samples, tt, sampleUnitT, 1, 1); } } // SortByT(ref samples); samples.Sort(); // 꼭짓점 스무딩 for (int i = 0; i < samples.Length; i++) { var sample = samples[i]; if(sample.angle < 10) continue; // 열린 스플라인의 시작점과 끝점에선 스무딩을 안함. if(!spline.Closed && (i == 0 || i == samples.Length - 1)) continue; var preSampleIdx = previous_index(i, samples.Length); var preSample = samples[preSampleIdx]; var nextSampleIdx = next_index(i, samples.Length); var nextSample = samples[nextSampleIdx]; var mp0 = sample.pos; var mp1 = math.lerp(preSample.pos, nextSample.pos, 0.5f); var tanLength = (sample.tanInLength + sample.tanOutLength); var curvature = tanLength < math.EPSILON ? 0 : tanLength / GetWidthAt(sample.t); curvature = 1 - math.clamp(curvature, 0, 1); var lerp = InternalUtility.Remap(sample.angle, 0, 180, 0, curvature); var middlePos = math.lerp(mp0, mp1, lerp); sample.pos = middlePos; samples[i] = sample; } var knots = new NativeArray(samples.Length, Allocator.Temp); for (int i = 0; i < samples.Length; i++) { var sample = samples[i]; var preSamplePos = samples[previous_index(i, samples.Length)].pos; var nextSamplePos = samples[next_index(i, samples.Length)].pos; var knot = SplineUtility.GetAutoSmoothKnot(sample.pos, preSamplePos, nextSamplePos, sample.up); knots[i] = knot; } var nSpline = new CopiedNativeSpline(knots, spline.Closed, float4x4.identity); for (int i = 0; i < nSpline.Count; i++) { var knot = nSpline[i]; var t = nSpline.CurveToSplineT(i); // nSpline.Evaluate(t, out var pos, out var tan, out var up); var tan = nSpline.EvaluateTangent(t); var up = keepBillboard ? new float3(0,0,-1) : nSpline.EvaluateUpVector(t); var sample = samples[i]; sample.t = t; sample.pos = knot.Position; sample.tan = tan; sample.up = up; samples[i] = sample; } var tempEdgePoints = new NativeList(Allocator.Temp); // 각 세그먼트의 EdgePoint 계산 for (int i = 0; i < samples.Length; i++) { var isClosingSegment = i == samples.Length - 1 && spline.Closed; var sample0 = samples[i]; var sample1 = isClosingSegment ? samples[0] : samples[i + 1]; // 열린 스플라인의 마지막 세그먼트에선 포인트를 추가하기만 함. // (length - 1)은 마지막 ep라서 열린 스플라인에서 마지막 세그먼트가 아님 if (i == samples.Length - 2 && !spline.Closed) { tempEdgePoints.Add(sample0); tempEdgePoints.Add(sample1); break; } var inTan = sample0.tan; var outTan = sample1.tan; var angle = InternalUtility.Angle(inTan, outTan); var edgePointCount = (int)math.round((angle / 30f) * 5); // 실제 Extrude에 사용할 EdgePoints 추가 tempEdgePoints.Add(sample0); for (int j = 0; j < edgePointCount; j++) { var t = (float)(j + 1) / (edgePointCount + 1); t = t.Remap(0, 1, sample0.t, isClosingSegment ? 1 : sample1.t); var ep = GetEdgePoint(nSpline, t); tempEdgePoints.Add(ep); } if(i == samples.Length - 1 && spline.Closed) tempEdgePoints.Add(sample1); } var clipStartAdded = false; var clipEndAdded = false; var shouldClipping = clipRange.x > math.EPSILON || clipRange.y < 1; for (int i = 0; i < tempEdgePoints.Length; i++) { var point = tempEdgePoints[i]; if (spline.Closed && shouldClipping && point.t == 0 && clipRange.y < 1) { continue; } if(shouldClipping && (point.t < clipRange.x || point.t > clipRange.y)) continue; if (point.t == clipRange.x) { edgePoints.Add(point); clipStartAdded = true; continue; } if (point.t == clipRange.y) { edgePoints.Add(point); clipEndAdded = true; continue; } if (clipRange.x < point.t && point.t < clipRange.y) { if(!clipStartAdded) { var ep = GetEdgePoint(nSpline, clipRange.x); edgePoints.Add(ep); clipStartAdded = true; } else { edgePoints.Add(point); } } } if (!clipEndAdded) { var ep = GetEdgePoint(nSpline, clipRange.y); edgePoints.Add(ep); } } static void SortByT(ref NativeList samples) { for (int i = 0; i < samples.Length - 1; i++) { for (int j = i + 1; j < samples.Length; j++) { if (samples[i].t > samples[j].t) { // 두 객체의 위치를 교환 (samples[i], samples[j]) = (samples[j], samples[i]); } } } } void add_smooth_sample_point(in NativeSpline s, NativeList samples, float t, float unitT, float tanInLength, float tanOutLength) { var preT = InternalUtility.Repeat(t - unitT, 1); var preTTan = s.EvaluateTangent(preT); var nextT = InternalUtility.Repeat(t + unitT, 1); var nextTTan = s.EvaluateTangent(nextT); var angle = InternalUtility.Angle(preTTan, nextTTan); var ep = GetEdgePoint(s, t, angle, tanInLength, tanOutLength); samples.Add(ep); } int previous_index(int i, int maxLength) { if (i == 0) { if (spline.Closed) return maxLength - 1; return 0; } return i - 1; } int next_index(int i, int maxLength) { if (i == maxLength - 1) { if (spline.Closed) return 0; return i; } return i + 1; } bool is_used(float t, float unitT, in NativeList list) { for (int i = 0; i < list.Length; i++) { var diff = t.CircularDistance(list[i].t, list.Length - 1); if (diff < unitT) return true; } return false; } void UniformEvaluate(ref NativeList edgePoints) { length = spline.GetLength(); var clippedLength = length * (clipRange.y - clipRange.x); var edgeCount = math.max((int)math.ceil(clippedLength * resolution * 0.05f), 1) + 2; for (int i = 0; i < edgeCount; i++) { var t = (float)i / (edgeCount - 1); t = t.Remap(0, 1, clipRange.x, clipRange.y); var ep = GetEdgePoint(spline, t); edgePoints.Add(ep); } } EdgePoint GetEdgePoint(in NativeSpline s, float t, float angle = 0, float tanInLength = 0, float tanOutLength = 0) { if (keepBillboard) { var pos = s.EvaluatePosition(t); var tan = s.EvaluateTangent(t); return new EdgePoint(t, angle, pos, tan, new float3(0,0,-1), tanInLength, tanOutLength); } else { s.Evaluate(t, out var pos, out var tan, out var up); return new EdgePoint(t, angle, pos, tan, up, tanInLength, tanOutLength); } } EdgePoint GetEdgePoint(in CopiedNativeSpline s, float t) { if (keepBillboard) { var pos = s.EvaluatePosition(t); var tan = s.EvaluateTangent(t); return new EdgePoint(t, 0, pos, tan, new float3(0,0,-1)); } else { s.Evaluate(t, out var pos, out var tan, out var up); return new EdgePoint(t, 0, pos, tan, up); } } void Extrude(in NativeList edgePoints) { for (int i = 0; i < edgePoints.Length; i++) { var ep = edgePoints[i]; var t = ep.t; var pos = ep.pos; var tan = ep.tan; var up = ep.up; // resolve (0,0,0) tangent if (tan is { x: 0, y: 0 }) { var prev = i == 0 ? pos : edgePoints[^2].pos; var next = i == edgePoints.Length - 1 ? pos : edgePoints[i + 1].pos; tan = next - prev; } InternalUtility.ExtrudeEdge( GetWidthAt(t), GetVAt(t, i), GetColorAt(t), ref pos, tan, up, keepBillboard, keepZeroZ, uvMultiplier, uvOffset, out var v0, out var v1); AddVert(in v0, in v1); if (i > 0) { AddQuadUsingLastVertices(); } } } void AddVert(in UIVertex left, in UIVertex right) { vertices.Add(left); vertices.Add(right); } void AddQuadUsingLastVertices() { var vi = vertices.Length; triangles.Add(new int3 ( vi - 2, vi - 3, vi - 4 )); triangles.Add(new int3 ( vi - 2, vi - 1, vi - 3 )); } float GetWidthAt(float t) { return width * widthCurve.Evaluate(t); } Color GetColorAt(float t) { return color * colorGradient.Evaluate(t); } float GetVAt(float t, int i) { switch (uvMode) { case UVMode.Tile: return length / width * t; case UVMode.RepeatPerSegment: return i; case UVMode.Stretch: return t; default: throw new ArgumentOutOfRangeException(); } } void MakeRoundEdges(in NativeList edgePoints) { var sl = vertices[0]; var sr = vertices[1]; var el = vertices[^2]; var er = vertices[^1]; { // at start var start = edgePoints[0]; var t = start.t; var pos = start.pos; var tan = start.tan; var up = start.up; var V = GetVAt(t, 0); var uv = new float2(0, V) * uvMultiplier - uvOffset; var clr = GetColorAt(t); MakeRoundEdge(in pos, sl.position, sr.position, false, up, uv, clr); } { // at end var end = edgePoints[^1]; var t = end.t; var pos = end.pos; var tan = end.tan; var up = end.up; var V = GetVAt(t, edgePoints.Length - 1); var uv = new float2(0, V) * uvMultiplier - uvOffset; var clr = GetColorAt(t); MakeRoundEdge(in pos, el.position, er.position, true, up, uv, clr); } } void MakeRoundEdge(in float3 center, in float3 left, in float3 right, bool invert, in float3 up, in float2 uv, in Color clr) { var vertexCount = resolution + 7; var marginAngle = 180f / vertexCount; var radius = math.length(center - left); var axis = keepBillboard ? math.back() : up; var arm = invert ? left - center : right - center; var centerVertex = new UIVertex(); centerVertex.position = center; centerVertex.uv0 = new Vector4(0.5f, 0); centerVertex.color = clr; vertices.Add(centerVertex); var startIndex = vertices.Length - 1; var toRadians = #if ENABLE_MATHMATICS__1_3_1_OR_NEWER math.TORADIANS; #else Mathf.Deg2Rad; #endif for (int i = 0; i <= vertexCount; i++) { var vector = math.rotate(quaternion.AxisAngle(axis, marginAngle * i * toRadians), arm); vector = math.normalizesafe(vector); var ratio = (float)i / vertexCount; var vert = new UIVertex { position = center + vector * radius, uv0 = new Vector4(0, ratio), color = clr }; vertices.Add(vert); if(i > 0) triangles.Add(new int3(startIndex, startIndex + i, startIndex + i + 1)); } } } [BurstCompile] public struct EdgePoint : IComparable { public float t; public readonly float angle; public float3 pos; public float3 tan; public float3 up; public readonly float tanInLength; public readonly float tanOutLength; public bool smoothed; public EdgePoint(float t, float angle, float3 pos, float3 tan, float3 up, float tanInLength = 0, float tanOutLength = 0) { this.t = t; this.pos = pos; this.tan = tan; this.up = up; this.angle = angle; this.tanInLength = tanInLength; this.tanOutLength = tanOutLength; smoothed = false; } public int CompareTo(EdgePoint other) { return t.CompareTo(other.t); } } } #endif #endif #endif #endif