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
}
}