using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; using XericLibrary.Runtime.MacroLibrary; namespace XericLibrary.Runtime.UIGraph { /// /// UILineRenderer 的工具类,提供静态方法用于线条渲染相关的操作 /// internal static class XericRendererUtils { #region 数学计算 /// /// 计算线路的总长度 /// /// 线路的顶点数组 /// 线路是循环 /// 线路的总长度 public static float CalculateTotalLength(this Vector2[] points, bool cycleLoop = false) { if (points.Length < 2) return 0f; var totalLength = 0f; for (int i = 0; i < points.Length - 1; i++) totalLength += Vector2.Distance(points[i], points[i + 1]); if (cycleLoop) totalLength += Vector2.Distance(points[0], points[^1]); return totalLength; } /// /// 计算一个点指向另一个点的角度(以度为单位) /// /// 起始点 /// 目标点 /// 旋转角度,范围为 -180 到 180 度 public static float RotatePointTowards(Vector2 vertex, Vector2 target) { // 使用反正切函数计算两点之间的角度 // 将弧度转换为角度返回 return (Mathf.Atan2(target.y - vertex.y, target.x - vertex.x) * Mathf.Rad2Deg); } /// /// 如果需要居中显示,则偏移到组件中心 /// /// 是否要居中显示 /// 整个绘制矩形范围的尺寸 /// public static Vector2 SwitchCenterOffset(bool center, Vector2 rectSize) => center ? Vector2.zero : -(rectSize / 2); /// /// 获取两个点之间的法线,副法线 /// /// 起点 /// 终点 /// 起点到终点的矢量 /// 两点间距离 /// 起点到终点的法线 /// 垂直与起点到终点的法线(法线左转90) /// 反转矢量 /// private static bool GetNormalizedVector(Vector2 startPoint, Vector2 endPoint, out Vector2 vector, out float distance, out Vector2 normal, out Vector2 subTangent, bool reverseVector = false) { vector = reverseVector ? startPoint - endPoint : endPoint - startPoint; distance = vector.magnitude; normal = Vector2.zero; subTangent = Vector2.zero; // 距离过小时不绘制 if (distance <= float.Epsilon) return false; // 线段的法线和副法线 normal = vector / distance; subTangent = Vector2.Perpendicular(normal); return true; } #endregion #region uv计算 /// /// 计算起点和终点的UV坐标Y值 /// /// 这段线段的起始线路长度 /// 这段线段的长度 /// 总线路的长度 /// 起点处的uv y /// 终点处的uv y private static void GetCurrentLengthUVY(float startLength, float currentLength, float totalLength, out float startUVY, out float endUVY) { startUVY = totalLength > 0 ? startLength / totalLength : 0f; endUVY = totalLength > 0 ? (startLength + currentLength) / totalLength : 0f; } #endregion #region 简易线路绘制 // 在点与点之间绘制给定宽度(thickness)的方片,转角处用两个三角形链接 // 这样会造成一根线会出现5个顶点,所以叫做 DSL5 (直接简单的5点直线 directly simple line 5) /// /// 创建线段的顶点数据 /// 每个线段由 5 个顶点组成: /// - 起点的左右两个顶点 /// - 终点的左右两个顶点 /// - 终点的中心点(用于线段连接) /// /// 顶点容器 /// 线段的起点 /// 线段的距离 /// 线段的终点 /// 线条的厚度 /// 线条的颜色 /// 当前线段起点在线路中的累积长度 /// 线路的总长度 public static List GetSingleLineSegmentVerts_DSL5(List vertices, Vector2 startPoint, Vector2 endPoint, float thickness, Color color, Vector2 normal, Vector2 subTangent, float startUVY, float endUVY, Vector3 offset) { if (vertices == null) throw new Exception("没有指定线段片段顶点容器列表"); vertices.Clear(); // 创建顶点模板,设置颜色为组件的当前颜色 UIVertex vertex = UIVertex.simpleVert; vertex.color = color; // 线段单侧宽度 var width = thickness / 2; vertex.position = (startPoint + subTangent * width); vertex.position += offset; vertex.uv0 = new Vector2(0f, startUVY); // 左侧顶点的UV vertices.Add(vertex); vertex.position = (startPoint + subTangent * -width); vertex.position += offset; vertex.uv0 = new Vector2(1f, startUVY); // 右侧顶点的UV vertices.Add(vertex); vertex.position = (endPoint + subTangent * width); vertex.position += offset; vertex.uv0 = new Vector2(0f, endUVY); // 左侧顶点的UV vertices.Add(vertex); vertex.position = (endPoint + subTangent * -width); vertex.position += offset; vertex.uv0 = new Vector2(1f, endUVY); // 右侧顶点的UV vertices.Add(vertex); // 添加终点的中心点,用于连接后续线段 vertex.position = endPoint; vertex.position += offset; vertex.uv0 = new Vector2(0.5f, endUVY); // 中心点的UV vertices.Add(vertex); return vertices; } /// /// 在网格构建对象上添加这个直线片段 /// /// 构建网格对象 /// 该线路片段索引(比如第一段线应该是0) /// 该片段顶点 public static void AddSingleLineSegmentVertsAndTriangle_DSL5(this VertexHelper vh, int segmentPartIndex, IEnumerable verts) { foreach (var vert in verts) vh.AddVert(vert); int index = segmentPartIndex * 5; // 添加线段的两个三角形(构成矩形线段) vh.AddTriangle(index, index + 1, index + 3); vh.AddTriangle(index + 3, index + 2, index); // 为线段之间添加斜角边缘 // 使用上一个线段的终点和当前线段的起点创建过渡三角形 if (segmentPartIndex != 0) { vh.AddTriangle(index, index - 1, index - 3); vh.AddTriangle(index + 1, index - 1, index - 2); } } /// /// 推送顶点绘制线路 /// /// /// /// /// /// /// /// /// /// 宽x长y /// /// /// /// public static void PopulateLineByPointsArray_DSL5(this VertexHelper vh, Vector2[] points, float thickness, bool cycleLoop, Color lineColor, Vector3 offset, bool enableArrow, Color arrowColor, Vector2 arrowSize, float pointProgress, float arrowPointProgress, bool absProgressPoint = true, bool reverseDir = false) { // 至少需要 2 个点才能绘制线条 if (points.Length < 2) return; // 计算线路总长度 var totalLength = points.CalculateTotalLength(cycleLoop); var currentLength = 0f; var lineVertices = ListPool.Get(); var arrowVertices = ListPool.Get(); var arrowindices = ListPool.Get(); for (int i = 0; i < points.Length - (cycleLoop ? 0 : 1); i++) { var thisPoint = points[i]; var nextPoint = points[(i + 1) % points.Length]; // 先计算线的矢量参数,以及uv。 if (!GetNormalizedVector(thisPoint, nextPoint, out var vector, out var distance, out var normal, out var subTangent)) continue; GetCurrentLengthUVY(currentLength, distance, totalLength, out var startUvy, out var endUvy); vh.AddSingleLineSegmentVertsAndTriangle_DSL5(i, GetSingleLineSegmentVerts_DSL5(lineVertices, thisPoint, nextPoint, thickness, lineColor, normal, subTangent, startUvy, endUvy, offset)); if (enableArrow) { PopulateTriangleArrowByPointArray(arrowVertices, thisPoint, nextPoint, arrowColor, arrowSize.y, arrowSize.x, pointProgress, arrowPointProgress, absProgressPoint, reverseDir); arrowindices.AddFaceByConsecutiveVertices(arrowVertices, i * 3, 3, i * 3); } currentLength += distance; } if (enableArrow) { vh.AddUIVertexStream(arrowVertices, arrowindices); } ListPool.Release(lineVertices); ListPool.Release(arrowVertices); ListPool.Release(arrowindices); } #endregion #region 圆弧过度的线路绘制 // 在简易线路绘制的基础上,修改了转角处的处理 // 线的顶点还是5,但是转角圆弧处理会更细节,叫它 CFAL (中心对齐的圆弧转角直线,center fanshaped arc Line) /// /// 在网格构建对象上添加这个直线片段 /// /// 构建网格对象 /// 该线路片段索引(比如第一段线应该是0) /// 该片段顶点 public static void AddSingleLineSegmentVertsAndTriangle_CFAL(this VertexHelper vh, int segmentPartIndex, IEnumerable verts) { foreach (var vert in verts) vh.AddVert(vert); int index = segmentPartIndex * 5; // 添加线段的两个三角形(构成矩形线段) vh.AddTriangle(index, index + 1, index + 3); vh.AddTriangle(index + 3, index + 2, index); // 为线段之间添加斜角边缘 // 使用上一个线段的终点和当前线段的起点创建过渡三角形 if (segmentPartIndex != 0) { vh.AddTriangle(index, index - 1, index - 3); vh.AddTriangle(index + 1, index - 1, index - 2); } } /// /// 创建带有倒角的线段顶点数据 /// 支持设置圆弧细分数量和内圆弧半径 /// /// 顶点容器 /// 线段的起点 /// 线段的终点 /// 下一个线段的终点(用于计算夹角) /// 上一个线段的起点(用于计算夹角) /// 线条的厚度 /// 线条的颜色 /// 当前线段起点在线路中的累积长度 /// 线路的总长度 /// 偏移量 /// 倒角细分数量 /// 内圆弧半径 /// 返回线段的距离 /// 包含线段顶点的列表 public static List GetSingleLineSegmentVerts_CFAL(List vertices, Vector2 startPoint, Vector2 endPoint, Vector2 nextPoint, Vector2 prevPoint, float thickness, Color color, float startLength, out float distance, float totalLength, Vector3 offset, int chamferSegments = 0, float innerRadius = 0f) { if (vertices == null) throw new Exception("没有指定线段片段顶点容器列表"); vertices.Clear(); // 创建顶点模板,设置颜色为组件的当前颜色 UIVertex vertex = UIVertex.simpleVert; vertex.color = color; if (!GetNormalizedVector(startPoint, endPoint, out var vector, out distance, out var normal, out var subTangent)) return vertices; // 线段单侧宽度 var width = thickness / 2; GetCurrentLengthUVY(startLength, distance, totalLength, out var startUVY, out var endUVY); // 计算起点和终点的夹角 float startAngle = 0f; float endAngle = 0f; if (prevPoint != startPoint) { var prevVector = startPoint - prevPoint; var prevNormal = prevVector.normalized; startAngle = Vector2.Angle(normal, -prevNormal); } if (nextPoint != endPoint) { var nextVector = nextPoint - endPoint; var nextNormal = nextVector.normalized; endAngle = Vector2.Angle(-normal, nextNormal); } // 计算顶点偏移量 float startOffset = 0f; float endOffset = 0f; if (Mathf.Abs(startAngle) > 0.1f) { if (innerRadius > 0f) { startOffset = (width + innerRadius) / Mathf.Sin(startAngle * Mathf.Deg2Rad / 2) - innerRadius; } else { startOffset = width / Mathf.Sin(startAngle * Mathf.Deg2Rad / 2); } } if (Mathf.Abs(endAngle) > 0.1f) { if (innerRadius > 0f) { endOffset = (width + innerRadius) / Mathf.Sin(endAngle * Mathf.Deg2Rad / 2) - innerRadius; } else { endOffset = width / Mathf.Sin(endAngle * Mathf.Deg2Rad / 2); } } // 添加起点的左侧顶点 vertex.position = (startPoint + subTangent * width + normal * startOffset); vertex.position += offset; vertex.uv0 = new Vector2(0f, startUVY); vertices.Add(vertex); // 添加起点的右侧顶点 vertex.position = (startPoint + subTangent * -width + normal * startOffset); vertex.position += offset; vertex.uv0 = new Vector2(1f, startUVY); vertices.Add(vertex); // 添加终点的左侧顶点 vertex.position = (endPoint + subTangent * width + normal * -endOffset); vertex.position += offset; vertex.uv0 = new Vector2(0f, endUVY); vertices.Add(vertex); // 添加终点的右侧顶点 vertex.position = (endPoint + subTangent * -width + normal * -endOffset); vertex.position += offset; vertex.uv0 = new Vector2(1f, endUVY); vertices.Add(vertex); // 添加终点的中心点 vertex.position = (endPoint + normal * -endOffset); vertex.position += offset; vertex.uv0 = new Vector2(0.5f, endUVY); vertices.Add(vertex); return vertices; } /// /// 推送顶点绘制带有倒角的线路 /// /// 网格构建对象 /// 线路顶点数组 /// 线条厚度 /// 是否循环 /// 线条颜色 /// 偏移量 /// 倒角细分数量 /// 内圆弧半径 public static void PopulateLineByPointsArray_CFAL(this VertexHelper vh, Vector2[] points, float thickness, bool cycleLoop, Color color, Vector3 offset, int chamferSegments = 0, float innerRadius = 0f) { // 至少需要 2 个点才能绘制线条 if (points.Length < 2) return; // 计算线路总长度 var totalLength = points.CalculateTotalLength(cycleLoop); var currentLength = 0f; var vertices = ListPool.Get(); for (int i = 0; i < points.Length - (cycleLoop ? 0 : 1); i++) { var startPoint = points[i]; var endPoint = points[(i + 1) % points.Length]; var prevPoint = i > 0 ? points[i - 1] : cycleLoop ? points[^1] : startPoint; var nextPoint = points[(i + 2) % points.Length]; vh.AddSingleLineSegmentVertsAndTriangle_CFAL(i, GetSingleLineSegmentVerts_CFAL(vertices, startPoint, endPoint, nextPoint, prevPoint, thickness, color, currentLength, out var distance, totalLength, offset, chamferSegments, innerRadius)); currentLength += distance; } ListPool.Release(vertices); } #endregion #region 箭头绘制工具 /// /// 绘制一个三角形箭头 /// /// 起点 /// 终点 /// 颜色 /// 箭头的长度 /// 箭头的宽度 /// 箭头所处坐标相对线的进度,使用绝对距离时,进度为世界单位;使用相对距离时,进度为单位距离 /// 箭头的哪部分与所处坐标对齐,0为头部对齐,1为尾部对齐 /// 箭头做出坐标是否为绝对距离 /// 反转方向 /// /// 箭头默认在 /// public static List PopulateTriangleArrowByPointArray(List vertices, Vector2 start, Vector2 end, Color color, float sizeL, float sizeW, float pointProgress, float arrowPointProgress, bool absProgressPoint = true, bool reverseDir = false) { GetNormalizedVector(start, end, out var vector, out var length, out var normal, out var tangent, reverseDir); var progressPosition = absProgressPoint ? pointProgress * normal : pointProgress * vector; // 加上坐标 progressPosition += reverseDir ? end : start; // 加上箭头偏移量 progressPosition += arrowPointProgress * sizeL * normal; // 绘制箭头 UIVertex vertex = UIVertex.simpleVert; vertex.color = color; vertex.position = progressPosition; vertices.Add(vertex); vertex.position = progressPosition + -sizeL * normal + sizeW * 0.5f * tangent; vertices.Add(vertex); vertex.position += (Vector3)(-sizeW * tangent); vertices.Add(vertex); return vertices; } #endregion #region 带有顶合并的线路绘制方法 /// /// 创建线段的顶点数据 /// 每个线段由 5 个顶点组成: /// - 起点的左右两个顶点 /// - 终点的左右两个顶点 /// - 终点的中心点(用于线段连接) /// /// 线段的起点 /// 线段的终点 /// 起点到终点与终点到下一点 /// 起点到终点与终点到下一点 /// 线条的厚度 /// 是否居中显示 /// 矩形变换的尺寸 /// 线条的颜色 /// 用于添加顶点的 VertexHelper 对象 /// 当前线段起点在线路中的累积长度 /// 线路的总长度 public static void CreateLineSegment_CFAL(this VertexHelper vh, Vector2 startPoint, Vector2 endPoint, float angle0, float angle3, float thickness, bool center, Vector2 rectSize, Color color, float startLength, float totalLength) { // 计算偏移量:如果需要居中显示,则偏移到组件中心 Vector2 offset = center ? (rectSize / 2) : Vector2.zero; // 创建顶点模板,设置颜色为组件的当前颜色 UIVertex vertex = UIVertex.simpleVert; vertex.color = color; if (!GetNormalizedVector(startPoint, endPoint, out var vector, out var distance, out var normal, out var subTangent)) return; // 线段单侧宽度 var width = thickness / 2; //顶点偏移量,根据夹角计算内侧交点相对坐标点应该退后的距离 var pointOffset1 = Mathf.Abs(angle0) <= 0.1f ? 0 : width / Mathf.Sin(angle0 * Mathf.Deg2Rad) + (angle0 > 90 ? width : 0); var pointOffset2 = Mathf.Abs(angle3) <= 0.1f ? 0 : width / Mathf.Sin(angle3 * Mathf.Deg2Rad) + (angle3 > 90 ? width : 0); Debug.Log( $"{angle0} = {Mathf.Sin(angle0 * Mathf.Deg2Rad)} , {angle3} = {Mathf.Sin(angle3 * Mathf.Deg2Rad)}"); // 计算起点和终点的UV坐标Y值 float startUVY = totalLength > 0 ? startLength / totalLength : 0f; float endUVY = totalLength > 0 ? (startLength + distance) / totalLength : 0f; vertex.position = (Vector3)(startPoint + subTangent * width + normal * pointOffset1) - (Vector3)offset; vertex.uv0 = new Vector2(0f, startUVY); // 左侧顶点的UV vh.AddVert(vertex); vertex.position = (Vector3)(startPoint + subTangent * -width + normal * pointOffset1) - (Vector3)offset; vertex.uv0 = new Vector2(1f, startUVY); // 右侧顶点的UV vh.AddVert(vertex); vertex.position = (Vector3)(endPoint + subTangent * width + normal * -pointOffset2) - (Vector3)offset; vertex.uv0 = new Vector2(0f, endUVY); // 左侧顶点的UV vh.AddVert(vertex); vertex.position = (Vector3)(endPoint + subTangent * -width + normal * -pointOffset2) - (Vector3)offset; vertex.uv0 = new Vector2(1f, endUVY); // 右侧顶点的UV vh.AddVert(vertex); // 添加终点的中心点,用于连接后续线段 vertex.position = (Vector3)(endPoint + normal * -pointOffset2) - (Vector3)offset; vertex.uv0 = new Vector2(0.5f, endUVY); // 中心点的UV vh.AddVert(vertex); } #endregion #region 顶点工具集 public static void GetUVAtRect(this Vector2 pos, Rect rect, out float u, out float v) { u = (pos.x - rect.xMin) / rect.width; v = (pos.y - rect.yMin) / rect.height; } public static void Get(this IList vertexList, Vector4 uv2) { } #endregion #region 顶点列表工具 private static readonly Color32 s_DefaultColor = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); private static readonly Vector4 s_DefaultTangent = new Vector4(1f, 0.0f, 0.0f, -1f); /// /// 添加一个顶点信息 /// /// /// /// /// public static void AddVert(this ICollection vertexList, Vector3 position, Vector4 uv, Color32 color) { vertexList.Add(new UIVertex() { position = position, normal = Vector3.back, tangent = s_DefaultTangent, color = color, uv0 = uv, uv1 = uv, uv2 = uv, uv3 = uv }); } /// /// 添加一个三角面索引 /// /// /// /// /// public static void AddTriangle(this ICollection indices, int index0, int index1, int index2) { indices.Add(index0); indices.Add(index1); indices.Add(index2); } /// /// 向顶点管理器添加顶点列表中的顶点构成的面 /// /// /// /// 开始连成面的顶点 /// 连成面的顶点范围 /// 作为连接面中心的顶点 public static void AddFaceByConsecutiveVertices(this ICollection indices, IList vertexList, int startVertexIndex, int faceVertexRange, int centerVertexIndex) { if (faceVertexRange < 3) throw new IndexOutOfRangeException("无法创建小于3个点的面"); var length = startVertexIndex + faceVertexRange - 1; for (int i = startVertexIndex + 1; i < length; i++) { if (i < 0 || i > vertexList.Count) throw new IndexOutOfRangeException( $"在向顶点集添加顶点时,从{startVertexIndex}开始的{faceVertexRange}个顶点(以及{centerVertexIndex})可能超出{vertexList.Count}的范围(预定范围{length}),导致构建超出预期"); var next = startVertexIndex + 1; if (next >= vertexList.Count) return; indices.AddTriangle(centerVertexIndex, startVertexIndex, startVertexIndex + 1); } } #endregion } }