udpate 0.6.3.3
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
#if ENABLE_BURST
|
||||
using Unity.Jobs;
|
||||
using Unity.Collections;
|
||||
#endif
|
||||
|
||||
namespace XericLibrary.Runtime.UIGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于缓存结构的线段渲染器基类
|
||||
/// 数据由 NativeArray 管理,支持脏标记范围更新和 Jobs 并行生成
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasRenderer))]
|
||||
public abstract class CacheLineRendererBase : MaskableGraphic
|
||||
{
|
||||
#region 数据缓存
|
||||
|
||||
/// <summary>
|
||||
/// 所有线段端点的扁平缓存
|
||||
/// </summary>
|
||||
protected List<LineVertexData> m_VertexData = new List<LineVertexData>();
|
||||
|
||||
/// <summary>
|
||||
/// 线段范围数组
|
||||
/// </summary>
|
||||
protected List<LineSegmentRange> m_SegmentRanges = new List<LineSegmentRange>();
|
||||
|
||||
/// <summary>
|
||||
/// 待添加的端点(Begin/End API 模式用)
|
||||
/// </summary>
|
||||
protected List<LineVertexData> m_PendingVertices = new List<LineVertexData>();
|
||||
|
||||
/// <summary>
|
||||
/// 当前 Begin/End 模式的 uvMode
|
||||
/// </summary>
|
||||
protected UVMode m_PendingUVMode = UVMode.ByDistance;
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在 Begin/End 会话中
|
||||
/// </summary>
|
||||
protected bool m_IsBuilding;
|
||||
|
||||
/// <summary>
|
||||
/// 模型顶点缓存 (segmentCount × 5)
|
||||
/// </summary>
|
||||
protected List<UIVertex> m_MeshVertexCache = new List<UIVertex>();
|
||||
|
||||
/// <summary>
|
||||
/// 三角形索引缓存
|
||||
/// </summary>
|
||||
protected List<int> m_MeshIndexCache = new List<int>();
|
||||
|
||||
/// <summary>
|
||||
/// 脏标记范围起点(线段范围索引),-1 表示无脏标记
|
||||
/// </summary>
|
||||
protected int m_DirtyStartSegment = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 脏标记范围终点(线段范围索引)
|
||||
/// </summary>
|
||||
protected int m_DirtyEndSegment = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Jobs 模式
|
||||
/// </summary>
|
||||
protected static bool UseJobs =>
|
||||
#if ENABLE_BURST
|
||||
SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.Null &&
|
||||
Application.platform != RuntimePlatform.WebGLPlayer;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region 线段添加
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加一根完整线段
|
||||
/// </summary>
|
||||
/// <param name="vertices">线段端点列表(至少2个)</param>
|
||||
/// <param name="uvMode">UV1 均分模式</param>
|
||||
/// <returns>线段范围索引</returns>
|
||||
public int AddLine(LineVertexData[] vertices, UVMode uvMode = UVMode.ByDistance)
|
||||
{
|
||||
if (vertices == null || vertices.Length < 2)
|
||||
return -1;
|
||||
|
||||
int startIndex = m_VertexData.Count;
|
||||
m_VertexData.AddRange(vertices);
|
||||
|
||||
var range = new LineSegmentRange
|
||||
{
|
||||
startVertexIndex = startIndex,
|
||||
vertexCount = vertices.Length,
|
||||
uvMode = uvMode
|
||||
};
|
||||
|
||||
int rangeIndex = m_SegmentRanges.Count;
|
||||
m_SegmentRanges.Add(range);
|
||||
|
||||
MarkDirty(rangeIndex);
|
||||
return rangeIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始画线(记录模式)
|
||||
/// </summary>
|
||||
public void BeginLine(UVMode uvMode = UVMode.ByDistance)
|
||||
{
|
||||
m_IsBuilding = true;
|
||||
m_PendingUVMode = uvMode;
|
||||
m_PendingVertices.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加画线节点到当前记录
|
||||
/// </summary>
|
||||
public void AddLineNode(Vector2 position, Color32 color, float thickness, Vector4 userData = default)
|
||||
{
|
||||
if (!m_IsBuilding) return;
|
||||
m_PendingVertices.Add(new LineVertexData
|
||||
{
|
||||
position = position,
|
||||
color = color,
|
||||
thickness = thickness,
|
||||
userData = userData
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加画线节点
|
||||
/// </summary>
|
||||
public void AddLineNode(LineVertexData node)
|
||||
{
|
||||
if (!m_IsBuilding) return;
|
||||
m_PendingVertices.Add(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束画线,将挂起列表写入缓存
|
||||
/// </summary>
|
||||
public void EndLine()
|
||||
{
|
||||
if (!m_IsBuilding) return;
|
||||
m_IsBuilding = false;
|
||||
|
||||
if (m_PendingVertices.Count < 2)
|
||||
{
|
||||
m_PendingVertices.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = m_VertexData.Count;
|
||||
m_VertexData.AddRange(m_PendingVertices);
|
||||
|
||||
var range = new LineSegmentRange
|
||||
{
|
||||
startVertexIndex = startIndex,
|
||||
vertexCount = m_PendingVertices.Count,
|
||||
uvMode = m_PendingUVMode
|
||||
};
|
||||
|
||||
int rangeIndex = m_SegmentRanges.Count;
|
||||
m_SegmentRanges.Add(range);
|
||||
m_PendingVertices.Clear();
|
||||
|
||||
MarkDirty(rangeIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 线段管理
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定线段
|
||||
/// </summary>
|
||||
public void RemoveLine(int segmentRangeIndex)
|
||||
{
|
||||
if (segmentRangeIndex < 0 || segmentRangeIndex >= m_SegmentRanges.Count)
|
||||
return;
|
||||
|
||||
m_SegmentRanges.RemoveAt(segmentRangeIndex);
|
||||
// 移除后后续所有线段索引发生变化,需完全重建
|
||||
RebuildAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有线段
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
m_VertexData.Clear();
|
||||
m_SegmentRanges.Clear();
|
||||
m_MeshVertexCache.Clear();
|
||||
m_MeshIndexCache.Clear();
|
||||
m_DirtyStartSegment = -1;
|
||||
m_DirtyEndSegment = -1;
|
||||
SetVerticesDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记指定线段为脏(需要重建)
|
||||
/// </summary>
|
||||
public void SetSegmentDirty(int segmentRangeIndex)
|
||||
{
|
||||
MarkDirty(segmentRangeIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 脏标记缓存
|
||||
|
||||
/// <summary>
|
||||
/// 标记脏范围(合并到现有范围)
|
||||
/// </summary>
|
||||
protected void MarkDirty(int segmentIndex)
|
||||
{
|
||||
if (m_DirtyStartSegment < 0 || segmentIndex < m_DirtyStartSegment)
|
||||
m_DirtyStartSegment = segmentIndex;
|
||||
if (m_DirtyEndSegment < 0 || segmentIndex > m_DirtyEndSegment)
|
||||
m_DirtyEndSegment = segmentIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记所有线段为脏(下次 LateUpdate 全量重绘)
|
||||
/// 适用于:更新顶点数据后,不改变元素数量的重绘
|
||||
/// </summary>
|
||||
public void RebuildAll()
|
||||
{
|
||||
m_DirtyStartSegment = 0;
|
||||
m_DirtyEndSegment = m_SegmentRanges.Count - 1;
|
||||
if (m_DirtyEndSegment < 0)
|
||||
m_DirtyStartSegment = -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 顶点生成
|
||||
|
||||
protected virtual void LateUpdate()
|
||||
{
|
||||
if (m_DirtyStartSegment < 0)
|
||||
return;
|
||||
|
||||
// 1) 统计脏范围内所有子段数(一个 range 有 vertexCount-1 个子段)
|
||||
int totalDirtySegments = 0;
|
||||
for (int ri = m_DirtyStartSegment; ri <= m_DirtyEndSegment; ri++)
|
||||
totalDirtySegments += m_SegmentRanges[ri].vertexCount - 1;
|
||||
|
||||
if (totalDirtySegments == 0)
|
||||
{
|
||||
m_DirtyStartSegment = -1;
|
||||
m_DirtyEndSegment = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 确保 mesh 顶点缓存足够大
|
||||
int totalSegments = GetTotalSegmentCount();
|
||||
int neededVertices = totalSegments * LineMeshGenerator.VerticesPerSegment;
|
||||
if (m_MeshVertexCache.Count < neededVertices)
|
||||
{
|
||||
int growBy = neededVertices - m_MeshVertexCache.Count;
|
||||
for (int i = 0; i < growBy; i++)
|
||||
m_MeshVertexCache.Add(new UIVertex());
|
||||
}
|
||||
|
||||
m_MeshIndexCache.Clear();
|
||||
|
||||
// 3) 按子段构建 SegmentJobInput(同时计算 ByDistance 的 totalDist 缓存)
|
||||
int globalSegBase = GetGlobalSegmentOffset(m_DirtyStartSegment);
|
||||
var inputs = new List<SegmentJobInput>(totalDirtySegments);
|
||||
int inSegIdx = 0;
|
||||
for (int ri = m_DirtyStartSegment; ri <= m_DirtyEndSegment; ri++)
|
||||
{
|
||||
var range = m_SegmentRanges[ri];
|
||||
float cachedTotalDist = (range.uvMode == UVMode.ByDistance)
|
||||
? CalculateRangeTotalDistance(range)
|
||||
: -1f;
|
||||
int subSegCount = range.vertexCount - 1;
|
||||
for (int si = 0; si < subSegCount; si++)
|
||||
{
|
||||
inputs.Add(BuildSegmentInput(range, si,
|
||||
globalSegBase + inSegIdx, totalSegments, cachedTotalDist));
|
||||
inSegIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 生成顶点(串行或 Jobs)
|
||||
if (UseJobs)
|
||||
{
|
||||
#if ENABLE_BURST
|
||||
GenerateMeshJobs(inputs, globalSegBase);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerateMeshSerial(inputs, globalSegBase);
|
||||
}
|
||||
|
||||
// 5) 生成三角形索引
|
||||
GenerateIndicesForRange(m_DirtyStartSegment, m_DirtyEndSegment,
|
||||
globalSegBase);
|
||||
|
||||
// 清除脏标记
|
||||
SetVerticesDirty();
|
||||
m_DirtyStartSegment = -1;
|
||||
m_DirtyEndSegment = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 串行生成顶点
|
||||
/// </summary>
|
||||
private void GenerateMeshSerial(List<SegmentJobInput> inputs, int globalSegBase)
|
||||
{
|
||||
var tempVerts = new UIVertex[LineMeshGenerator.VerticesPerSegment];
|
||||
for (int i = 0; i < inputs.Count; i++)
|
||||
{
|
||||
int meshVertStart = (globalSegBase + i) * LineMeshGenerator.VerticesPerSegment;
|
||||
LineMeshGenerator.GenerateSingleSegment(inputs[i], tempVerts, 0);
|
||||
for (int v = 0; v < LineMeshGenerator.VerticesPerSegment; v++)
|
||||
{
|
||||
int cacheIdx = meshVertStart + v;
|
||||
if (cacheIdx < m_MeshVertexCache.Count)
|
||||
m_MeshVertexCache[cacheIdx] = tempVerts[v];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if ENABLE_BURST
|
||||
/// <summary>
|
||||
/// Jobs 并行生成顶点
|
||||
/// </summary>
|
||||
private void GenerateMeshJobs(List<SegmentJobInput> inputs, int globalSegBase)
|
||||
{
|
||||
int count = inputs.Count;
|
||||
int outputSize = count * LineMeshGenerator.VerticesPerSegment;
|
||||
|
||||
var nativeInputs = new NativeArray<SegmentJobInput>(count, Allocator.TempJob);
|
||||
var nativeOutputs = new NativeArray<UIVertex>(outputSize, Allocator.TempJob);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
nativeInputs[i] = inputs[i];
|
||||
|
||||
var handle = LineMeshGenerator.GenerateSegmentsJobs(nativeInputs, nativeOutputs);
|
||||
handle.Complete();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int srcStart = i * LineMeshGenerator.VerticesPerSegment;
|
||||
int dstStart = (globalSegBase + i) * LineMeshGenerator.VerticesPerSegment;
|
||||
for (int v = 0; v < LineMeshGenerator.VerticesPerSegment; v++)
|
||||
{
|
||||
if (dstStart + v < m_MeshVertexCache.Count)
|
||||
m_MeshVertexCache[dstStart + v] = nativeOutputs[srcStart + v];
|
||||
}
|
||||
}
|
||||
|
||||
nativeInputs.Dispose();
|
||||
nativeOutputs.Dispose();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 生成指定 range 范围中所有子段的三角形索引
|
||||
/// 每条线的首个子段不生成转角连接三角形
|
||||
/// </summary>
|
||||
private void GenerateIndicesForRange(int startRange, int endRange,
|
||||
int globalSegBase)
|
||||
{
|
||||
int segIdx = globalSegBase;
|
||||
for (int ri = startRange; ri <= endRange; ri++)
|
||||
{
|
||||
int subSegCount = m_SegmentRanges[ri].vertexCount - 1;
|
||||
for (int si = 0; si < subSegCount; si++)
|
||||
{
|
||||
// 子段 si == 0 是该条线的首段 → 无前驱同线段 → hasPrevInLine = false
|
||||
bool hasPrevInLine = (si > 0);
|
||||
LineMeshGenerator.GenerateSegmentIndices(segIdx, hasPrevInLine, m_MeshIndexCache);
|
||||
segIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算某条线段包含的段数(端点数量 - 1)
|
||||
/// </summary>
|
||||
protected static int GetSegmentCountForRange(in LineSegmentRange range) => range.vertexCount - 1;
|
||||
|
||||
/// <summary>
|
||||
/// 获取总段数(所有线段累加)
|
||||
/// </summary>
|
||||
protected int GetTotalSegmentCount()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < m_SegmentRanges.Count; i++)
|
||||
count += GetSegmentCountForRange(m_SegmentRanges[i]);
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定线段范围的全局段偏移
|
||||
/// </summary>
|
||||
protected int GetGlobalSegmentOffset(int rangeIndex)
|
||||
{
|
||||
int offset = 0;
|
||||
for (int i = 0; i < rangeIndex; i++)
|
||||
offset += GetSegmentCountForRange(m_SegmentRanges[i]);
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算指定 range 的总距离(用于 ByDistance UV 模式)
|
||||
/// </summary>
|
||||
private float CalculateRangeTotalDistance(in LineSegmentRange range)
|
||||
{
|
||||
float total = 0f;
|
||||
int segCount = range.vertexCount - 1;
|
||||
for (int i = 0; i < segCount; i++)
|
||||
{
|
||||
int idx = range.startVertexIndex + i;
|
||||
total += Vector2.Distance(m_VertexData[idx].position,
|
||||
m_VertexData[idx + 1].position);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建指定范围内第 inSegmentIndex 段的输入参数
|
||||
/// </summary>
|
||||
protected SegmentJobInput BuildSegmentInput(
|
||||
in LineSegmentRange range, int inSegmentIndex,
|
||||
int globalSegmentIndex, int totalSegments,
|
||||
float cachedTotalDist = -1f)
|
||||
{
|
||||
int startIdx = range.startVertexIndex + inSegmentIndex;
|
||||
var start = m_VertexData[startIdx];
|
||||
var end = m_VertexData[startIdx + 1];
|
||||
|
||||
var input = new SegmentJobInput
|
||||
{
|
||||
start = start,
|
||||
end = end,
|
||||
segmentIndex = globalSegmentIndex,
|
||||
totalSegmentCount = totalSegments,
|
||||
uvMode = range.uvMode,
|
||||
hasPrev = inSegmentIndex > 0 ? (byte)1 : (byte)0,
|
||||
hasNext = (inSegmentIndex < range.vertexCount - 2) ? (byte)1 : (byte)0
|
||||
};
|
||||
|
||||
if (input.hasPrev == 1)
|
||||
input.prevEnd = m_VertexData[startIdx - 1];
|
||||
if (input.hasNext == 1)
|
||||
input.nextStart = m_VertexData[startIdx + 2];
|
||||
|
||||
// 计算 startLineUV / endLineUV
|
||||
int segCount = GetSegmentCountForRange(range);
|
||||
if (range.uvMode == UVMode.BySegment)
|
||||
{
|
||||
float t0 = (float)inSegmentIndex / segCount;
|
||||
float t1 = (float)(inSegmentIndex + 1) / segCount;
|
||||
input.startLineUV = t0;
|
||||
input.endLineUV = t1;
|
||||
}
|
||||
else // ByDistance
|
||||
{
|
||||
float totalDist = cachedTotalDist >= 0f
|
||||
? cachedTotalDist
|
||||
: CalculateRangeTotalDistance(range);
|
||||
|
||||
if (totalDist < float.Epsilon)
|
||||
{
|
||||
input.startLineUV = (float)inSegmentIndex / segCount;
|
||||
input.endLineUV = (float)(inSegmentIndex + 1) / segCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
float distSoFar = 0f;
|
||||
for (int i = 0; i < inSegmentIndex; i++)
|
||||
{
|
||||
int idx = range.startVertexIndex + i;
|
||||
distSoFar += Vector2.Distance(m_VertexData[idx].position,
|
||||
m_VertexData[idx + 1].position);
|
||||
}
|
||||
float segDist = Vector2.Distance(start.position, end.position);
|
||||
input.startLineUV = distSoFar / totalDist;
|
||||
input.endLineUV = (distSoFar + segDist) / totalDist;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region OnPopulateMesh
|
||||
|
||||
protected override void OnPopulateMesh(VertexHelper vh)
|
||||
{
|
||||
vh.Clear();
|
||||
|
||||
if (m_MeshVertexCache.Count == 0 || m_MeshIndexCache.Count == 0)
|
||||
return;
|
||||
|
||||
// 将缓存的顶点和索引写入 VertexHelper
|
||||
vh.AddUIVertexStream(m_MeshVertexCache, m_MeshIndexCache);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
RebuildAll();
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad4b412c6e5cfe14194c22997929cdee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
#if ENABLE_BURST
|
||||
using Unity.Burst;
|
||||
using Unity.Jobs;
|
||||
using Unity.Collections;
|
||||
#endif
|
||||
|
||||
namespace XericLibrary.Runtime.UIGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// 线段→模型顶点生成器
|
||||
/// 核心算法:每段线生成 5 个模型顶点 + 3 个三角形
|
||||
/// </summary>
|
||||
public static class LineMeshGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 每段线的模型顶点数
|
||||
/// </summary>
|
||||
public const int VerticesPerSegment = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 每段线的三角形索引数(3 个三角形 × 3 索引)
|
||||
/// </summary>
|
||||
public const int IndicesPerSegment = 9;
|
||||
|
||||
/// <summary>
|
||||
/// 计算单段线段对应的 5 个模型顶点(串行 & Job 共用)
|
||||
/// </summary>
|
||||
/// <param name="input">线段生成参数</param>
|
||||
/// <param name="outputVertices">输出顶点数组,须预留 VerticesPerSegment 个位置</param>
|
||||
/// <param name="outputStart">写入 outputVertices 的起始偏移</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GenerateSingleSegment(in SegmentJobInput input,
|
||||
UIVertex[] outputVertices, int outputStart = 0)
|
||||
{
|
||||
Vector2 startPos = input.start.position;
|
||||
Vector2 endPos = input.end.position;
|
||||
Vector2 dir = endPos - startPos;
|
||||
float length = dir.magnitude;
|
||||
if (length < float.Epsilon)
|
||||
{
|
||||
// 退化为零长度线段,生成空顶点
|
||||
for (int i = 0; i < VerticesPerSegment; i++)
|
||||
{
|
||||
outputVertices[outputStart + i] = new UIVertex
|
||||
{
|
||||
position = startPos,
|
||||
color = Color.clear,
|
||||
uv0 = Vector2.zero,
|
||||
uv1 = Vector2.zero,
|
||||
uv2 = Vector4.zero,
|
||||
uv3 = Vector4.zero
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 normal = dir / length;
|
||||
Vector2 subTangent = Vector2.Perpendicular(normal);
|
||||
|
||||
// 起点和终点的实际宽度(两端可不同)
|
||||
float halfW0 = input.start.thickness * 0.5f;
|
||||
float halfW1 = input.end.thickness * 0.5f;
|
||||
|
||||
// 5 顶点布局
|
||||
// V0 = start + normal * -halfW (左下)
|
||||
// V1 = start + normal + halfW (左上)
|
||||
// V2 = end + normal * -halfW (右下)
|
||||
// V3 = end + normal + halfW (右上)
|
||||
// V4 = end (中心 → 转角连接)
|
||||
Vector3 v0 = startPos - subTangent * halfW0;
|
||||
Vector3 v1 = startPos + subTangent * halfW0;
|
||||
Vector3 v2 = endPos - subTangent * halfW1;
|
||||
Vector3 v3 = endPos + subTangent * halfW1;
|
||||
Vector3 v4 = endPos;
|
||||
|
||||
Color32 c0 = Color32.Lerp(input.start.color, input.end.color, 0f);
|
||||
Color32 c1 = c0;
|
||||
Color32 c2 = Color32.Lerp(input.start.color, input.end.color, 1f);
|
||||
Color32 c3 = c2;
|
||||
Color32 c4 = c2;
|
||||
|
||||
float uvSX = 0f;
|
||||
float uvEX = 1f;
|
||||
float uvSY = input.startLineUV;
|
||||
float uvEY = input.endLineUV;
|
||||
|
||||
UIVertex vert = new UIVertex
|
||||
{
|
||||
normal = Vector3.back,
|
||||
tangent = new Vector4(1f, 0f, 0f, -1f)
|
||||
};
|
||||
|
||||
Vector4 userDataStart = input.start.userData;
|
||||
Vector4 userDataEnd = input.end.userData;
|
||||
|
||||
// V0
|
||||
vert.position = v0;
|
||||
vert.color = c0;
|
||||
vert.uv0 = new Vector2(uvSX, uvSY);
|
||||
vert.uv1 = new Vector2(0f, uvSY); // 整线UV (u=0 表示线的起点侧)
|
||||
vert.uv2 = userDataStart;
|
||||
vert.uv3 = Vector4.zero;
|
||||
outputVertices[outputStart + 0] = vert;
|
||||
|
||||
// V1
|
||||
vert.position = v1;
|
||||
vert.color = c1;
|
||||
vert.uv0 = new Vector2(uvEX, uvSY);
|
||||
vert.uv1 = new Vector2(1f, uvSY); // 整线UV (u=1 表示线的终点侧)
|
||||
vert.uv2 = userDataStart;
|
||||
vert.uv3 = Vector4.zero;
|
||||
outputVertices[outputStart + 1] = vert;
|
||||
|
||||
// V2
|
||||
vert.position = v2;
|
||||
vert.color = c2;
|
||||
vert.uv0 = new Vector2(uvSX, uvEY);
|
||||
vert.uv1 = new Vector2(0f, uvEY);
|
||||
vert.uv2 = userDataEnd;
|
||||
vert.uv3 = Vector4.zero;
|
||||
outputVertices[outputStart + 2] = vert;
|
||||
|
||||
// V3
|
||||
vert.position = v3;
|
||||
vert.color = c3;
|
||||
vert.uv0 = new Vector2(uvEX, uvEY);
|
||||
vert.uv1 = new Vector2(1f, uvEY);
|
||||
vert.uv2 = userDataEnd;
|
||||
vert.uv3 = Vector4.zero;
|
||||
outputVertices[outputStart + 3] = vert;
|
||||
|
||||
// V4
|
||||
vert.position = v4;
|
||||
vert.color = c4;
|
||||
vert.uv0 = new Vector2(0.5f, uvEY);
|
||||
vert.uv1 = new Vector2(0.5f, uvEY);
|
||||
vert.uv2 = userDataEnd;
|
||||
vert.uv3 = Vector4.zero;
|
||||
outputVertices[outputStart + 4] = vert;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成单段线段的三角形索引(写入 indexList)
|
||||
/// </summary>
|
||||
/// <param name="segmentIndex">子段全局索引</param>
|
||||
/// <param name="hasPrevInLine">上一子段是否属于同一条线(控制转角连接三角形)</param>
|
||||
/// <param name="indexList">输出的三角形索引列表</param>
|
||||
public static void GenerateSegmentIndices(int segmentIndex, bool hasPrevInLine, List<int> indexList)
|
||||
{
|
||||
int baseVert = segmentIndex * VerticesPerSegment;
|
||||
|
||||
// 三角形 1: V0-V1-V3 (矩形上半)
|
||||
indexList.Add(baseVert + 0);
|
||||
indexList.Add(baseVert + 1);
|
||||
indexList.Add(baseVert + 3);
|
||||
|
||||
// 三角形 2: V3-V2-V0 (矩形下半)
|
||||
indexList.Add(baseVert + 3);
|
||||
indexList.Add(baseVert + 2);
|
||||
indexList.Add(baseVert + 0);
|
||||
|
||||
// 三角形 3: 转角连接三角形
|
||||
// 仅在「同一折线上的相邻子段」之间绘制
|
||||
// 连接上一段的 V4(上一段终点中心)到当前段的 V0, V1
|
||||
if (hasPrevInLine)
|
||||
{
|
||||
int prevV4 = (segmentIndex - 1) * VerticesPerSegment + 4;
|
||||
indexList.Add(prevV4);
|
||||
indexList.Add(baseVert + 0);
|
||||
indexList.Add(baseVert + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs 模式
|
||||
|
||||
#if ENABLE_BURST
|
||||
/// <summary>
|
||||
/// Burst 编译的 IJobParallelFor:并行生成各段线的模型顶点
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
public struct GenerateVerticesJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<SegmentJobInput> Inputs;
|
||||
[WriteOnly] public NativeArray<UIVertex> OutputVertices;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
int start = index * VerticesPerSegment;
|
||||
var input = Inputs[index];
|
||||
Vector2 startPos = input.start.position;
|
||||
Vector2 endPos = input.end.position;
|
||||
Vector2 dir = endPos - startPos;
|
||||
float length = dir.magnitude;
|
||||
if (length < float.Epsilon)
|
||||
{
|
||||
for (int i = 0; i < VerticesPerSegment; i++)
|
||||
OutputVertices[start + i] = new UIVertex();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 normal = dir / length;
|
||||
Vector2 subTangent = new Vector2(-normal.y, normal.x);
|
||||
|
||||
float halfW0 = input.start.thickness * 0.5f;
|
||||
float halfW1 = input.end.thickness * 0.5f;
|
||||
|
||||
Vector3 v0 = startPos - subTangent * halfW0;
|
||||
Vector3 v1 = startPos + subTangent * halfW0;
|
||||
Vector3 v2 = endPos - subTangent * halfW1;
|
||||
Vector3 v3 = endPos + subTangent * halfW1;
|
||||
Vector3 v4 = endPos;
|
||||
|
||||
Color32 c0 = Color32.Lerp(input.start.color, input.end.color, 0f);
|
||||
Color32 c2 = Color32.Lerp(input.start.color, input.end.color, 1f);
|
||||
|
||||
float uvSY = input.startLineUV;
|
||||
float uvEY = input.endLineUV;
|
||||
Vector4 userDataStart = input.start.userData;
|
||||
Vector4 userDataEnd = input.end.userData;
|
||||
|
||||
UIVertex vert = new UIVertex
|
||||
{
|
||||
normal = Vector3.back,
|
||||
tangent = new Vector4(1f, 0f, 0f, -1f)
|
||||
};
|
||||
|
||||
vert.position = v0; vert.color = c0;
|
||||
vert.uv0 = new Vector2(0f, uvSY); vert.uv1 = new Vector2(0f, uvSY); vert.uv2 = userDataStart;
|
||||
OutputVertices[start + 0] = vert;
|
||||
|
||||
vert.position = v1; vert.color = c0;
|
||||
vert.uv0 = new Vector2(1f, uvSY); vert.uv1 = new Vector2(1f, uvSY); vert.uv2 = userDataStart;
|
||||
OutputVertices[start + 1] = vert;
|
||||
|
||||
vert.position = v2; vert.color = c2;
|
||||
vert.uv0 = new Vector2(0f, uvEY); vert.uv1 = new Vector2(0f, uvEY); vert.uv2 = userDataEnd;
|
||||
OutputVertices[start + 2] = vert;
|
||||
|
||||
vert.position = v3; vert.color = c2;
|
||||
vert.uv0 = new Vector2(1f, uvEY); vert.uv1 = new Vector2(1f, uvEY); vert.uv2 = userDataEnd;
|
||||
OutputVertices[start + 3] = vert;
|
||||
|
||||
vert.position = v4; vert.color = c2;
|
||||
vert.uv0 = new Vector2(0.5f, uvEY); vert.uv1 = new Vector2(0.5f, uvEY); vert.uv2 = userDataEnd;
|
||||
OutputVertices[start + 4] = vert;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Jobs 并行生成线段顶点
|
||||
/// </summary>
|
||||
public static JobHandle GenerateSegmentsJobs(
|
||||
NativeArray<SegmentJobInput> inputs,
|
||||
NativeArray<UIVertex> outputVertices,
|
||||
JobHandle dependsOn = default)
|
||||
{
|
||||
var job = new GenerateVerticesJob
|
||||
{
|
||||
Inputs = inputs,
|
||||
OutputVertices = outputVertices
|
||||
};
|
||||
return job.Schedule(inputs.Length, 1, dependsOn);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24c8f213d27953c419e17ce974bbf594
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericLibrary.Runtime.UIGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// 线段端点数据(所有字段需为 blittable 类型以支持 NativeArray/Job)
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct LineVertexData
|
||||
{
|
||||
public Vector2 position; // 端点位置(局部坐标)
|
||||
public Color32 color; // 端点颜色
|
||||
public float thickness; // 端点宽度
|
||||
public Vector4 userData; // 自定义扩展数据 → 映射到 UV2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UV1 均分模式(每条线段独立)
|
||||
/// </summary>
|
||||
public enum UVMode : byte
|
||||
{
|
||||
/// <summary>按实际线路距离均分</summary>
|
||||
ByDistance,
|
||||
/// <summary>按线段数量均分</summary>
|
||||
BySegment,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线段范围记录
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct LineSegmentRange
|
||||
{
|
||||
public int startVertexIndex; // 在 vertexData 中的起始索引
|
||||
public int vertexCount; // 该线段端点数量(>= 2)
|
||||
public UVMode uvMode; // UV1 均分模式
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Job 输入:单段线段的顶点生成参数
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct SegmentJobInput
|
||||
{
|
||||
public LineVertexData start;
|
||||
public LineVertexData end;
|
||||
public LineVertexData prevEnd; // 上个线段终点(仅 hasPrev=1 时有效)
|
||||
public LineVertexData nextStart; // 下个线段起点(仅 hasNext=1 时有效)
|
||||
public byte hasPrev; // 0=无效, 1=有效
|
||||
public byte hasNext; // 0=无效, 1=有效
|
||||
public int segmentIndex;
|
||||
public int totalSegmentCount;
|
||||
public float startLineUV;
|
||||
public float endLineUV;
|
||||
public UVMode uvMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba184a89add023d42b93dbc715acc1c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace XericLibrary.Runtime.UIGraph
|
||||
{
|
||||
/// <summary>
|
||||
/// 新一代线段渲染器组件
|
||||
/// 基于 CacheLineRendererBase 的缓存 + Jobs 架构
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric Library/UI/UILineRenderer V2", 15)]
|
||||
public class UILineRendererV2 : CacheLineRendererBase
|
||||
{
|
||||
[Header("全局默认设置")]
|
||||
[Range(0.01f, 200f)]
|
||||
[Tooltip("默认线段宽度")]
|
||||
public float defaultThickness = 10f;
|
||||
|
||||
[Tooltip("默认线段颜色")]
|
||||
public Color defaultColor = Color.white;
|
||||
|
||||
[Tooltip("默认 UV1 均分模式")]
|
||||
public UVMode defaultUVMode = UVMode.ByDistance;
|
||||
|
||||
/// <summary>
|
||||
/// 绘制一条简单线段(2个端点)
|
||||
/// </summary>
|
||||
public void DrawLine(Vector2 from, Vector2 to)
|
||||
{
|
||||
DrawLine(from, to, defaultColor, defaultThickness, defaultUVMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制一条简单线段(指定颜色和宽度)
|
||||
/// </summary>
|
||||
public void DrawLine(Vector2 from, Vector2 to, Color color, float thickness, UVMode uvMode = UVMode.ByDistance)
|
||||
{
|
||||
BeginLine(uvMode);
|
||||
AddLineNode(from, color, thickness);
|
||||
AddLineNode(to, color, thickness);
|
||||
EndLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制折线(多点连接)
|
||||
/// </summary>
|
||||
public void DrawPolyline(Vector2[] points, Color color, float thickness, UVMode uvMode = UVMode.ByDistance)
|
||||
{
|
||||
if (points == null || points.Length < 2) return;
|
||||
|
||||
LineVertexData[] vertices = new LineVertexData[points.Length];
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
vertices[i] = new LineVertexData
|
||||
{
|
||||
position = points[i],
|
||||
color = color,
|
||||
thickness = thickness,
|
||||
userData = Vector4.zero
|
||||
};
|
||||
}
|
||||
AddLine(vertices, uvMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制折线(每个端点独立颜色/宽度)
|
||||
/// </summary>
|
||||
public void DrawPolyline(LineVertexData[] vertices, UVMode uvMode = UVMode.ByDistance)
|
||||
{
|
||||
if (vertices == null || vertices.Length < 2) return;
|
||||
AddLine(vertices, uvMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简单绘制矩形框
|
||||
/// </summary>
|
||||
public void DrawRect(Rect rect, Color color, float thickness)
|
||||
{
|
||||
Vector2 center = rect.center;
|
||||
float hw = rect.width * 0.5f;
|
||||
float hh = rect.height * 0.5f;
|
||||
|
||||
var pts = new Vector2[]
|
||||
{
|
||||
new Vector2(center.x - hw, center.y - hh),
|
||||
new Vector2(center.x + hw, center.y - hh),
|
||||
new Vector2(center.x + hw, center.y + hh),
|
||||
new Vector2(center.x - hw, center.y + hh),
|
||||
new Vector2(center.x - hw, center.y - hh),
|
||||
};
|
||||
DrawPolyline(pts, color, thickness, UVMode.ByDistance);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
if (defaultThickness < 0.01f)
|
||||
defaultThickness = 0.01f;
|
||||
}
|
||||
|
||||
[MenuItem("GameObject/Xeric Library/UI/UILineRenderer V2", false, 10)]
|
||||
private static void CreateGameObject()
|
||||
{
|
||||
var go = new GameObject(nameof(UILineRendererV2), typeof(UILineRendererV2));
|
||||
if (Selection.activeGameObject != null)
|
||||
go.transform.SetParent(Selection.activeGameObject.transform);
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Create {nameof(UILineRendererV2)}");
|
||||
Selection.activeObject = go;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d43989c17d4c7d640988acb51713390a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user