update 0.6.1

This commit is contained in:
2026-07-09 17:08:14 +08:00
parent 97d41a1385
commit 53d8d539a3
89 changed files with 4486 additions and 241 deletions
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
fileFormatVersion: 2
guid: c754cc17629bc0f4cac54d1417f746b3
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 43995da0c5deada479d25647709b2854
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b66080c7d48f7f342b79af0fdbedec87
guid: 9a4fde83e9857614fadbbb44cf04d8da
folderAsset: yes
DefaultImporter:
externalObjects: {}
+534
View File
@@ -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,245 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 曲线缓存渲染器基类
/// 管理 CurveCacheEntry 列表 + 扁平控制点/宽度/箭头缓存 + 脏标记增量重建
/// LateUpdate 中只重建脏标记范围内的曲线
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
public abstract class CurveCacheRendererBase : MaskableGraphic
{
#region 默认材质
private static Material s_DefaultMaterial;
private const string DefaultShaderPath = "XericLibrary/UIGraph/XericUICurve";
public override Material defaultMaterial
{
get
{
if (s_DefaultMaterial == null)
{
var shader = Shader.Find(DefaultShaderPath);
if (shader != null)
s_DefaultMaterial = new Material(shader);
else
s_DefaultMaterial = base.defaultMaterial;
}
return s_DefaultMaterial;
}
}
#endregion
#region 数据缓存
/// <summary>曲线条目列表</summary>
protected List<CurveCacheEntry> m_Curves = new List<CurveCacheEntry>();
/// <summary>所有曲线的控制点扁平存储</summary>
protected List<Vector2> m_ControlPoints = new List<Vector2>();
/// <summary>所有曲线的宽度扁平存储(与控制点一一对应)</summary>
protected List<float> m_Widths = new List<float>();
/// <summary>所有曲线的箭头扁平存储</summary>
protected List<ArrowHeadData> m_Arrows = new List<ArrowHeadData>();
/// <summary>模型顶点缓存</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_DirtyStartIndex = -1;
/// <summary>脏标记范围终点(曲线索引)</summary>
protected int m_DirtyEndIndex = -1;
#endregion
#region API
/// <summary>
/// 添加一条曲线
/// </summary>
/// <param name="entry">曲线缓存条目(控制点计数和起始索引需预先设置)</param>
/// <param name="controlPoints">控制点数组</param>
/// <param name="widths">宽度数组(长度需与控制点一致)</param>
/// <param name="arrows">箭头数组(可选)</param>
/// <returns>曲线索引</returns>
public int AddCurve(CurveCacheEntry entry, Vector2[] controlPoints, float[] widths, ArrowHeadData[] arrows = null)
{
int index = m_Curves.Count;
// 填充起始索引
entry.controlPointStartIndex = m_ControlPoints.Count;
entry.widthStartIndex = m_Widths.Count;
entry.arrowStartIndex = (arrows != null && arrows.Length > 0) ? m_Arrows.Count : -1;
entry.arrowCount = arrows?.Length ?? 0;
entry.controlPointCount = controlPoints.Length;
m_Curves.Add(entry);
m_ControlPoints.AddRange(controlPoints);
m_Widths.AddRange(widths);
if (arrows != null)
m_Arrows.AddRange(arrows);
MarkDirty(index);
return index;
}
/// <summary>
/// 移除指定曲线
/// </summary>
public void RemoveCurve(int index)
{
if (index < 0 || index >= m_Curves.Count) return;
m_Curves.RemoveAt(index);
RebuildAll();
}
/// <summary>
/// 标记指定曲线为脏
/// </summary>
public void SetCurveDirty(int index)
{
MarkDirty(index);
}
/// <summary>
/// 清空所有曲线
/// </summary>
public void ClearAll()
{
m_Curves.Clear();
m_ControlPoints.Clear();
m_Widths.Clear();
m_Arrows.Clear();
m_MeshVertexCache.Clear();
m_MeshIndexCache.Clear();
m_DirtyStartIndex = -1;
m_DirtyEndIndex = -1;
SetVerticesDirty();
}
#endregion
#region 脏标记
protected void MarkDirty(int index)
{
if (m_DirtyStartIndex < 0 || index < m_DirtyStartIndex)
m_DirtyStartIndex = index;
if (m_DirtyEndIndex < 0 || index > m_DirtyEndIndex)
m_DirtyEndIndex = index;
}
/// <summary>
/// 标记所有曲线为脏(下次 LateUpdate 全量重绘)
/// 适用于:更新控制点/宽度后,不改变元素数量的重绘
/// </summary>
public void RebuildAll()
{
m_DirtyStartIndex = 0;
m_DirtyEndIndex = m_Curves.Count - 1;
if (m_DirtyEndIndex < 0)
m_DirtyStartIndex = -1;
}
#endregion
#region LateUpdate
protected virtual void LateUpdate()
{
if (m_DirtyStartIndex < 0) return;
int start = m_DirtyStartIndex;
// 统计脏范围前的顶点/索引数(这些无需重建)
int preVertexCount = 0;
int preIndexCount = 0;
for (int i = 0; i < start; i++)
{
preVertexCount += m_Curves[i].vertexCount;
preIndexCount += m_Curves[i].triangleCount * 3;
}
// 从 start 开始重建所有曲线
var tempVerts = new List<UIVertex>();
var tempIndices = new List<int>();
int vAccum = preVertexCount;
int iAccum = preIndexCount;
for (int i = start; i < m_Curves.Count; i++)
{
var entry = m_Curves[i];
int vertBefore = tempVerts.Count;
int idxBefore = tempIndices.Count;
CurveMeshGenerator.GenerateCurve(
entry, m_ControlPoints, m_Widths, m_Arrows,
tempVerts, tempIndices);
var updated = entry;
updated.startVertexIndex = vAccum;
updated.vertexCount = tempVerts.Count - vertBefore;
updated.startIndexIndex = iAccum;
updated.triangleCount = (tempIndices.Count - idxBefore) / 3;
m_Curves[i] = updated;
vAccum += updated.vertexCount;
iAccum += updated.triangleCount * 3;
}
// 合并到主缓存
int totalVertices = vAccum;
int totalIndices = iAccum;
while (m_MeshVertexCache.Count < totalVertices)
m_MeshVertexCache.Add(new UIVertex());
while (m_MeshIndexCache.Count < totalIndices)
m_MeshIndexCache.Add(0);
for (int i = 0; i < tempVerts.Count; i++)
m_MeshVertexCache[preVertexCount + i] = tempVerts[i];
for (int i = 0; i < tempIndices.Count; i++)
m_MeshIndexCache[preIndexCount + i] = tempIndices[i] + preVertexCount;
SetVerticesDirty();
m_DirtyStartIndex = -1;
m_DirtyEndIndex = -1;
}
#endregion
#region OnPopulateMesh
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (m_MeshVertexCache.Count == 0 || m_MeshIndexCache.Count == 0) return;
vh.AddUIVertexStream(m_MeshVertexCache, m_MeshIndexCache);
}
#endregion
#region 生命周期
protected override void OnEnable()
{
base.OnEnable();
RebuildAll();
}
protected override void OnDestroy()
{
base.OnDestroy();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 905c29aca6f80f64f8da8cf8579868ae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+333
View File
@@ -0,0 +1,333 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XericLibrary.Runtime.MacroLibrary;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 曲线网格生成器
/// 将贝塞尔曲线细分为线段带状三角片 + 生成箭头装饰
///
/// 核心算法:
/// 1. 在 t∈[0,1] 上均匀采样 N+1 个点
/// 2. 每个采样点计算位置、切线、宽度、颜色
/// 3. 切线转 90° 得切法线,沿切法线位移 halfWidth 得左右边顶点
/// 4. 顺序连接左右边顶点形成 ribbon 带状:
/// 左[i]──右[i]──左[i+1]──右[i+1]──...
/// 每个四边形拆两个三角形
/// </summary>
public static class CurveMeshGenerator
{
/// <summary>圆圈箭头多边形边数</summary>
private const int CircleArrowSides = 8;
/// <summary>计算切线时的 epsilon 采样偏移</summary>
private const float TangentEpsilon = 0.001f;
/// <summary>
/// 生成单条曲线的网格数据
/// </summary>
public static void GenerateCurve(
in CurveCacheEntry entry,
List<Vector2> controlPointsPool,
List<float> widthsPool,
List<ArrowHeadData> arrowsPool,
List<UIVertex> vertexList,
List<int> indexList)
{
int segCount = entry.tessellationSegments;
if (segCount < 1) segCount = 4;
int sampleCount = segCount + 1; // N+1 个采样点
int cpStart = entry.controlPointStartIndex;
int cpCount = entry.controlPointCount;
int wStart = entry.widthStartIndex;
var ctrlPts = new Vector2[cpCount];
var wPts = new float[cpCount];
for (int i = 0; i < cpCount; i++)
{
ctrlPts[i] = controlPointsPool[cpStart + i];
wPts[i] = widthsPool[wStart + i];
}
int baseVertex = vertexList.Count;
UIVertex vert = new UIVertex
{
normal = Vector3.back,
tangent = new Vector4(1f, 0f, 0f, -1f),
uv1 = new Vector2(1f, 0f),
};
// 1. 采样 + 生成左右边顶点
for (int s = 0; s < sampleCount; s++)
{
float t = (float)s / segCount;
Vector2 pos = MacroCurve.BezierCurve(ctrlPts, t); // 曲线上点
float width = MacroCurve.BezierCurve(wPts, t); // 当前宽度
Vector2 tangent = ComputeTangent(ctrlPts, t); // 切线方向
Vector2 normal = new Vector2(-tangent.y, tangent.x); // 切法线(左转90°)
MacroCurve.BezierCurve1(entry.startColor, entry.endColor, t, out Color col);
Color32 color = col;
float halfW = width * 0.5f;
float uv = (float)s / segCount;
// 左边顶点
vert.position = pos - normal * halfW;
vert.color = color;
vert.uv0 = new Vector2(0f, uv);
vertexList.Add(vert);
// 右边顶点
vert.position = pos + normal * halfW;
vert.color = color;
vert.uv0 = new Vector2(1f, uv);
vertexList.Add(vert);
}
// 2. 连接带状三角形
// 左[i]=v[i*2], 右[i]=v[i*2+1]
// 四边形 i→i+1: (左i, 右i, 左i+1) + (右i, 右i+1, 左i+1)
for (int s = 0; s < segCount; s++)
{
int bv = baseVertex + s * 2;
indexList.Add(bv + 0); // 左i
indexList.Add(bv + 1); // 右i
indexList.Add(bv + 2); // 左i+1
indexList.Add(bv + 1); // 右i
indexList.Add(bv + 3); // 右i+1
indexList.Add(bv + 2); // 左i+1
}
// 3. 生成箭头
if (entry.arrowCount > 0 && entry.arrowStartIndex >= 0)
{
for (int a = 0; a < entry.arrowCount; a++)
{
var arrow = arrowsPool[entry.arrowStartIndex + a];
GenerateArrow(arrow, ctrlPts, vertexList, indexList);
}
}
}
/// <summary>
/// 计算曲线上 t 处的切线方向(差分法)
/// </summary>
private static Vector2 ComputeTangent(Vector2[] ctrlPts, float t)
{
float t0 = Mathf.Max(0f, t - TangentEpsilon);
float t1 = Mathf.Min(1f, t + TangentEpsilon);
Vector2 tangent = MacroCurve.BezierCurve(ctrlPts, t1) - MacroCurve.BezierCurve(ctrlPts, t0);
if (tangent.sqrMagnitude < float.Epsilon)
tangent = Vector2.up;
else
tangent.Normalize();
return tangent;
}
#region 箭头
private static void GenerateArrow(
ArrowHeadData arrow,
Vector2[] controlPoints,
List<UIVertex> vertexList,
List<int> indexList)
{
float t = Mathf.Clamp01(arrow.progress);
Vector2 pos = MacroCurve.BezierCurve(controlPoints, t);
Vector2 tangent = ComputeTangent(controlPoints, t);
if (arrow.reversed)
tangent = -tangent;
// 深度补偿:半长偏移(箭头中心在本地x=0,尖端在+width*0.5,尾部在-width*0.5)
pos += tangent * arrow.width * 0.5f * arrow.depthCompensation;
int baseVert = vertexList.Count;
GenerateArrowShape(arrow.shape, pos, tangent, arrow.width, arrow.height, arrow.color,
vertexList, indexList, baseVert, t);
}
private static void GenerateArrowShape(
ArrowShape shape,
Vector2 position,
Vector2 tangent,
float width,
float height,
Color32 color,
List<UIVertex> vertexList,
List<int> indexList,
int baseVertex,
float progressT)
{
float cos = tangent.x;
float sin = tangent.y;
Vector2 TransformPoint(float ux, float uy)
{
float x = ux * width;
float y = uy * height;
float rx = x * cos - y * sin;
float ry = x * sin + y * cos;
return position + new Vector2(rx, ry);
}
UIVertex vert = new UIVertex
{
color = color,
normal = Vector3.back,
tangent = new Vector4(1f, 0f, 0f, -1f),
uv0 = new Vector2(0.5f, progressT),
uv1 = new Vector2(0f, 1f)
};
switch (shape)
{
case ArrowShape.Triangle:
{
vert.position = TransformPoint(0.5f, 0f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f, -0.3f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f, 0.3f);
vertexList.Add(vert);
indexList.Add(baseVertex + 0);
indexList.Add(baseVertex + 1);
indexList.Add(baseVertex + 2);
break;
}
case ArrowShape.HollowTriangle:
{
float tailOff = -0.1f;
// 外三角
vert.position = TransformPoint(0.5f, 0f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f, -0.3f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f, 0.3f);
vertexList.Add(vert);
// 内三角(缩小 + 向尾部偏移)
vert.position = TransformPoint(0.5f * 0.5f + tailOff, 0f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f * 0.5f + tailOff, -0.3f * 0.5f * 0.6f);
vertexList.Add(vert);
vert.position = TransformPoint(-0.5f * 0.5f + tailOff, 0.3f * 0.5f * 0.6f);
vertexList.Add(vert);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 4); indexList.Add(baseVertex + 5);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 5); indexList.Add(baseVertex + 2);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 4);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 4); indexList.Add(baseVertex + 3);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 3); indexList.Add(baseVertex + 5);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 5); indexList.Add(baseVertex + 2);
break;
}
case ArrowShape.Arrow:
{
float headBaseU = 0.5f - 0.35f; // 头部基座 x=0.15
float shaftEndU = -0.8f; // 杆尾
vert.position = TransformPoint(0.5f, 0f);
vertexList.Add(vert); // 0: 尖端
vert.position = TransformPoint(headBaseU, -0.25f);
vertexList.Add(vert); // 1: 左翼
vert.position = TransformPoint(headBaseU, 0.25f);
vertexList.Add(vert); // 2: 右翼
vert.position = TransformPoint(headBaseU, -0.06f);
vertexList.Add(vert); // 3: 杆起点下
vert.position = TransformPoint(headBaseU, 0.06f);
vertexList.Add(vert); // 4: 杆起点上
vert.position = TransformPoint(shaftEndU, -0.06f);
vertexList.Add(vert); // 5: 杆尾下
vert.position = TransformPoint(shaftEndU, 0.06f);
vertexList.Add(vert); // 6: 杆尾上
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 2);
indexList.Add(baseVertex + 2); indexList.Add(baseVertex + 4); indexList.Add(baseVertex + 6);
indexList.Add(baseVertex + 2); indexList.Add(baseVertex + 6); indexList.Add(baseVertex + 1);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 6); indexList.Add(baseVertex + 5);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 5); indexList.Add(baseVertex + 3);
break;
}
case ArrowShape.Circle:
{
float r = 0.4f;
vert.position = TransformPoint(0f, 0f);
vertexList.Add(vert);
for (int i = 0; i < CircleArrowSides; i++)
{
float angle = i * Mathf.PI * 2f / CircleArrowSides;
vert.position = TransformPoint(Mathf.Cos(angle) * r, Mathf.Sin(angle) * r);
vertexList.Add(vert);
}
for (int i = 0; i < CircleArrowSides; i++)
{
int next = (i + 1) % CircleArrowSides;
indexList.Add(baseVertex + 0);
indexList.Add(baseVertex + 1 + i);
indexList.Add(baseVertex + 1 + next);
}
break;
}
case ArrowShape.Square:
{
float hs = 0.35f;
vert.position = TransformPoint(-hs, -hs);
vertexList.Add(vert);
vert.position = TransformPoint(hs, -hs);
vertexList.Add(vert);
vert.position = TransformPoint(hs, hs);
vertexList.Add(vert);
vert.position = TransformPoint(-hs, hs);
vertexList.Add(vert);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 2);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 2); indexList.Add(baseVertex + 3);
break;
}
case ArrowShape.ReverseArrow:
{
float rHeadBaseU = 0.5f - 0.35f;
float rShaftEndU = -0.8f;
vert.position = TransformPoint(0.5f, 0f);
vertexList.Add(vert);
vert.position = TransformPoint(rHeadBaseU, -0.25f);
vertexList.Add(vert);
vert.position = TransformPoint(rHeadBaseU, 0.25f);
vertexList.Add(vert);
vert.position = TransformPoint(rHeadBaseU, -0.06f);
vertexList.Add(vert);
vert.position = TransformPoint(rHeadBaseU, 0.06f);
vertexList.Add(vert);
vert.position = TransformPoint(rShaftEndU, -0.06f);
vertexList.Add(vert);
vert.position = TransformPoint(rShaftEndU, 0.06f);
vertexList.Add(vert);
indexList.Add(baseVertex + 0); indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 2);
indexList.Add(baseVertex + 2); indexList.Add(baseVertex + 4); indexList.Add(baseVertex + 6);
indexList.Add(baseVertex + 2); indexList.Add(baseVertex + 6); indexList.Add(baseVertex + 1);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 6); indexList.Add(baseVertex + 5);
indexList.Add(baseVertex + 1); indexList.Add(baseVertex + 5); indexList.Add(baseVertex + 3);
break;
}
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b0e0346140ad4847a65df78853dd3dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,86 @@
using System;
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 箭头形状枚举
/// </summary>
public enum ArrowShape : byte
{
/// <summary>实心三角箭头</summary>
Triangle,
/// <summary>空心三角箭头</summary>
HollowTriangle,
/// <summary>箭矢箭头(翼+杆)</summary>
Arrow,
/// <summary>圆形头</summary>
Circle,
/// <summary>方形头</summary>
Square,
/// <summary>反向箭头(箭尾)</summary>
ReverseArrow,
}
/// <summary>
/// 箭头装饰数据
/// </summary>
[Serializable]
public struct ArrowHeadData
{
/// <summary>箭头形状</summary>
public ArrowShape shape;
/// <summary>是否反向</summary>
public bool reversed;
/// <summary>箭头出现在曲线上的位置 (0~1)</summary>
[Range(0f, 1f)]
public float progress;
/// <summary>箭头宽度(沿切线方向)</summary>
public float width;
/// <summary>箭头高度(垂直切线方向)</summary>
public float height;
/// <summary>深度补偿:-1~1,箭头沿切线偏移 width*0.5*depth 距离。
/// -1:尾部对准曲线点(头部向前伸)
/// 0:中心对准曲线点
/// +1:头部对准曲线点(尾部向后拉)</summary>
[Range(-1f, 1f)]
public float depthCompensation;
/// <summary>箭头颜色</summary>
public Color32 color;
}
/// <summary>
/// 曲线缓存条目
/// 控制点、宽度、箭头数据统一存放在外部扁平列表中,
/// 本结构体只记录起始索引和计数。
/// </summary>
[Serializable]
public struct CurveCacheEntry
{
/// <summary>在网格顶点缓存中的起始索引(由 Base 类回填)</summary>
public int startVertexIndex;
/// <summary>该曲线占用的顶点数(由 Base 类回填)</summary>
public int vertexCount;
/// <summary>在网格索引缓存中的起始索引(由 Base 类回填)</summary>
public int startIndexIndex;
/// <summary>三角形数量(由 Base 类回填)</summary>
public int triangleCount;
/// <summary>控制点数量(≥4,默认三阶贝塞尔)</summary>
public int controlPointCount;
/// <summary>在统一控制点缓存中的起始索引</summary>
public int controlPointStartIndex;
/// <summary>在统一宽度缓存中的起始索引</summary>
public int widthStartIndex;
/// <summary>起始颜色</summary>
public Color32 startColor;
/// <summary>结束颜色</summary>
public Color32 endColor;
/// <summary>在统一箭头缓存中的起始索引(-1 表示无箭头)</summary>
public int arrowStartIndex;
/// <summary>箭头数量</summary>
public int arrowCount;
/// <summary>曲线细分段数</summary>
public int tessellationSegments;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6ce7575b6b9e6a45bdba82ae35018e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+274
View File
@@ -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 个模型顶点(串行 &amp; 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:
+58
View File
@@ -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,229 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 图元缓存渲染器基类
/// 管理 PrimitiveCacheEntry 列表 + 脏标记范围更新
/// LateUpdate 中只重建脏标记范围内的图元
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
public abstract class PrimitiveCacheRendererBase : MaskableGraphic
{
#region 默认材质
private static Material s_DefaultMaterial;
private const string DefaultShaderPath = "XericLibrary/UIGraph/UIPattern";
/// <summary>
/// 获取图元渲染器的默认材质(基于 UIPattern.shader)
/// </summary>
public override Material defaultMaterial
{
get
{
if (s_DefaultMaterial == null)
{
var shader = Shader.Find(DefaultShaderPath);
if (shader != null)
s_DefaultMaterial = new Material(shader);
else
s_DefaultMaterial = base.defaultMaterial;
}
return s_DefaultMaterial;
}
}
#endregion
#region 数据缓存
/// <summary>
/// 图元列表
/// </summary>
protected List<PrimitiveCacheEntry> m_Primitives = new List<PrimitiveCacheEntry>();
/// <summary>
/// 模型顶点缓存
/// </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_DirtyStartIndex = -1;
/// <summary>
/// 脏标记范围终点(图元索引)
/// </summary>
protected int m_DirtyEndIndex = -1;
#endregion
#region API
/// <summary>
/// 添加一个图元
/// </summary>
/// <returns>图元索引</returns>
public int AddPrimitive(PrimitiveCacheEntry entry)
{
int index = m_Primitives.Count;
m_Primitives.Add(entry);
MarkDirty(index);
return index;
}
/// <summary>
/// 移除指定图元
/// </summary>
public void RemovePrimitive(int index)
{
if (index < 0 || index >= m_Primitives.Count) return;
m_Primitives.RemoveAt(index);
RebuildAll();
}
/// <summary>
/// 标记指定图元为脏
/// </summary>
public void SetPrimitiveDirty(int index)
{
MarkDirty(index);
}
/// <summary>
/// 清空所有图元
/// </summary>
public void ClearAll()
{
m_Primitives.Clear();
m_MeshVertexCache.Clear();
m_MeshIndexCache.Clear();
m_DirtyStartIndex = -1;
m_DirtyEndIndex = -1;
SetVerticesDirty();
}
#endregion
#region 脏标记
protected void MarkDirty(int index)
{
if (m_DirtyStartIndex < 0 || index < m_DirtyStartIndex)
m_DirtyStartIndex = index;
if (m_DirtyEndIndex < 0 || index > m_DirtyEndIndex)
m_DirtyEndIndex = index;
}
/// <summary>
/// 标记所有图元为脏(下次 LateUpdate 全量重绘)
/// 适用于:更新顶点坐标后,不改变元素数量的重绘
/// </summary>
public void RebuildAll()
{
m_DirtyStartIndex = 0;
m_DirtyEndIndex = m_Primitives.Count - 1;
if (m_DirtyEndIndex < 0)
m_DirtyStartIndex = -1;
}
#endregion
#region LateUpdate
protected virtual void LateUpdate()
{
if (m_DirtyStartIndex < 0) return;
// 全量重建策略:从脏范围起点开始,重绘该条目及后续所有条目
// 保证所有条目 startVertexIndex/startIndexIndex 始终正确
int start = m_DirtyStartIndex;
// 统计脏范围前的顶点/索引数(这些条目无需重建)
int preVertexCount = 0;
int preIndexCount = 0;
for (int i = 0; i < start; i++)
{
preVertexCount += m_Primitives[i].vertexCount;
preIndexCount += m_Primitives[i].triangleCount * 3;
}
// 从 start 开始重建所有条目
var tempVerts = new List<UIVertex>();
var tempIndices = new List<int>();
int vAccum = preVertexCount;
int iAccum = preIndexCount;
for (int i = start; i < m_Primitives.Count; i++)
{
var entry = m_Primitives[i];
int vertBefore = tempVerts.Count;
int idxBefore = tempIndices.Count;
PrimitiveMeshGenerator.GeneratePrimitive(entry, tempVerts, tempIndices);
var updated = entry;
updated.startVertexIndex = vAccum;
updated.vertexCount = tempVerts.Count - vertBefore;
updated.startIndexIndex = iAccum;
updated.triangleCount = (tempIndices.Count - idxBefore) / 3;
m_Primitives[i] = updated;
vAccum += updated.vertexCount;
iAccum += updated.triangleCount * 3;
}
// 合并到主缓存
int totalVertices = vAccum;
int totalIndices = iAccum;
while (m_MeshVertexCache.Count < totalVertices)
m_MeshVertexCache.Add(new UIVertex());
while (m_MeshIndexCache.Count < totalIndices)
m_MeshIndexCache.Add(0);
for (int i = 0; i < tempVerts.Count; i++)
m_MeshVertexCache[preVertexCount + i] = tempVerts[i];
for (int i = 0; i < tempIndices.Count; i++)
m_MeshIndexCache[preIndexCount + i] = tempIndices[i] + preVertexCount;
SetVerticesDirty();
m_DirtyStartIndex = -1;
m_DirtyEndIndex = -1;
}
#endregion
#region OnPopulateMesh
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (m_MeshVertexCache.Count == 0 || m_MeshIndexCache.Count == 0) return;
vh.AddUIVertexStream(m_MeshVertexCache, m_MeshIndexCache);
}
#endregion
#region 生命周期
protected override void OnEnable()
{
base.OnEnable();
RebuildAll();
}
protected override void OnDestroy()
{
base.OnDestroy();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28047425f50534b46a5ad5b7a924bd30
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,390 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XericLibrary.Runtime.MacroLibrary;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 图元网格生成器
/// 为多边形/矩形生成 triangle-fan 网格,支持倒角
/// </summary>
public static class PrimitiveMeshGenerator
{
/// <summary>
/// 生成单个图元的网格顶点和索引
/// </summary>
public static void GeneratePrimitive(
in PrimitiveCacheEntry entry,
List<UIVertex> vertexList,
List<int> indexList)
{
// 将归一化 chamferSize [0,1] 映射为实际像素倒角尺寸
float minDim = Mathf.Min(entry.size.x, entry.size.y);
float scaledChamfer = entry.@params.chamferSize * minDim * 0.5f;
PrimitiveParamHelper helper = new PrimitiveParamHelper
{
center = entry.center,
size = entry.size,
sizeMode = entry.sizeMode,
type = entry.type,
sideCount = (int)entry.@params.sideCount,
chamferSize = scaledChamfer,
chamferSegments = entry.@params.chamferSegments,
faceCount = 0
};
// 1. 计算外围顶点
var corners = ComputeCorners(helper);
if (corners.Count < 3) return;
// 应用角度旋转
if (Mathf.Abs(entry.angle) > 0.001f)
{
float rad = entry.angle * Mathf.Deg2Rad;
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
for (int i = 0; i < corners.Count; i++)
{
Vector2 rel = corners[i] - entry.center;
corners[i] = entry.center + new Vector2(
rel.x * cos - rel.y * sin,
rel.x * sin + rel.y * cos);
}
}
int baseVertex = vertexList.Count;
int typeIndex = (int)entry.type;
// 颜色打包到法线/副法线
// normal: (bg.r/255, bg.g/255, bg.b/255)
// tangent: (border.r/255, border.g/255, border.b/255, border.a/255)
Color32 bg = entry.@params.bgColor;
Vector3 normalCol = new Vector3(bg.r / 255f, bg.g / 255f, bg.b / 255f);
Color32 bc = entry.@params.borderColor;
Vector4 tangentCol = new Vector4(bc.r / 255f, bc.g / 255f, bc.b / 255f, bc.a / 255f);
// 2. 添加中心点
UIVertex centerVert = new UIVertex
{
position = (Vector3)entry.center,
color = entry.@params.centerColor,
normal = normalCol,
tangent = tangentCol,
uv0 = new Vector2(0.5f, 0.5f),
uv1 = new Vector2(0f, 0f),
};
vertexList.Add(centerVert);
// 3. 添加外围顶点 + 创建三角形扇
for (int i = 0; i < corners.Count; i++)
{
Vector2 pos = corners[i];
float dist = Vector2.Distance(pos, entry.center);
float maxDist = Mathf.Max(entry.size.x, entry.size.y) * 0.5f;
float normalizedDist = maxDist > 0.001f ? Mathf.Clamp01(dist / maxDist) : 0f;
UIVertex vert = new UIVertex
{
position = (Vector3)pos,
color = entry.@params.centerColor,
normal = normalCol,
tangent = tangentCol,
uv0 = new Vector2(
(pos.x - entry.center.x) / entry.size.x + 0.5f,
(pos.y - entry.center.y) / entry.size.y + 0.5f),
uv1 = new Vector2(normalizedDist, 0f),
};
vertexList.Add(vert);
// 三角形扇: (中心, i, i+1)
if (i > 0)
{
indexList.Add(baseVertex); // 中心
indexList.Add(baseVertex + i); // 当前角
indexList.Add(baseVertex + i + 1); // 下一个角
}
}
// 闭合:最后一个角 → 第一个角
if (corners.Count > 2)
{
indexList.Add(baseVertex);
indexList.Add(baseVertex + corners.Count);
indexList.Add(baseVertex + 1);
}
// 4. 生成物理边框 — 复制外围顶点并向外偏移一圈,条带拼接
float borderThickness = entry.@params.borderThickness;
if (borderThickness > 0.001f && corners.Count >= 3)
{
int n = corners.Count;
int borderBase = vertexList.Count;
// 计算每个顶点的向外偏移方向(从中心指向顶点的方向)
var outDirs = new Vector2[n];
for (int i = 0; i < n; i++)
{
Vector2 dir = corners[i] - entry.center;
outDirs[i] = dir.sqrMagnitude > 0.001f ? dir.normalized : Vector2.up;
}
// 添加内圈(原始位置)+ 外圈(偏移位置),isBorder=1
for (int i = 0; i < n; i++)
{
Vector2 pos = corners[i];
Vector2 outPos = pos + outDirs[i] * borderThickness;
// 内圈顶点 (borderProgress=0)
UIVertex inner = new UIVertex
{
position = (Vector3)pos,
color = entry.@params.borderColor,
normal = normalCol,
tangent = tangentCol,
uv0 = new Vector2(
(pos.x - entry.center.x) / entry.size.x + 0.5f,
(pos.y - entry.center.y) / entry.size.y + 0.5f),
uv1 = new Vector2(0f, 1f),
};
vertexList.Add(inner);
// 外圈顶点 (borderProgress=1)
UIVertex outer = new UIVertex
{
position = (Vector3)outPos,
color = entry.@params.borderColor,
normal = normalCol,
tangent = tangentCol,
uv0 = new Vector2(
(outPos.x - entry.center.x) / entry.size.x + 0.5f,
(outPos.y - entry.center.y) / entry.size.y + 0.5f),
uv1 = new Vector2(1f, 1f),
};
vertexList.Add(outer);
}
// 连接条带三角形
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
int iI = borderBase + i * 2; // 内圈 i
int oI = borderBase + i * 2 + 1; // 外圈 i
int iN = borderBase + next * 2; // 内圈 next
int oN = borderBase + next * 2 + 1; // 外圈 next
indexList.Add(iI);
indexList.Add(oI);
indexList.Add(iN);
indexList.Add(oI);
indexList.Add(oN);
indexList.Add(iN);
}
}
}
/// <summary>
/// 计算图元的外围顶点列表
/// </summary>
private static List<Vector2> ComputeCorners(in PrimitiveParamHelper helper)
{
if (helper.type == PrimitiveType.Polygon)
return ComputePolygonCorners(helper);
else
return ComputeRectangleCorners(helper);
}
/// <summary>
/// 计算正多边形的外围顶点
/// </summary>
private static List<Vector2> ComputePolygonCorners(in PrimitiveParamHelper helper)
{
int sides = Mathf.Max(3, helper.sideCount);
float angleStep = 360f / sides;
float startAngle = 90f;
// 计算半径
float minSize = Mathf.Min(helper.size.x, helper.size.y);
float radius = minSize * 0.5f;
if (helper.sizeMode == SizeMode.CircumscribedCircle)
{
// 外切圆:半径延伸到各顶点
// 外切圆半径 = minSize/2
// 实际半径需要让多边形的边与外切圆相切
// 对于外切圆,多边形的顶点到中心的距离 = r / cos(π/N)
float apothemAngle = Mathf.PI / sides;
radius = radius / Mathf.Cos(apothemAngle);
}
// InscribedCircle 保持 radius = minSize/2
// 计算基本多边形顶点
var corners = new List<Vector2>(sides);
for (int i = 0; i < sides; i++)
{
float angleDeg = startAngle + i * angleStep;
float angle = angleDeg * Mathf.Deg2Rad;
corners.Add(new Vector2(
helper.center.x + Mathf.Cos(angle) * radius,
helper.center.y + Mathf.Sin(angle) * radius));
}
// 应用 XY 缩放
for (int i = 0; i < corners.Count; i++)
{
Vector2 dir = corners[i] - helper.center;
corners[i] = helper.center + new Vector2(
dir.x * helper.size.x / minSize,
dir.y * helper.size.y / minSize);
}
// 处理倒角
if (helper.chamferSize > 0.001f)
corners = ApplyChamfer(corners, helper.chamferSize, helper.chamferSegments);
return corners;
}
/// <summary>
/// 计算矩形的四个角
/// </summary>
private static List<Vector2> ComputeRectangleCorners(in PrimitiveParamHelper helper)
{
float halfX, halfY;
switch (helper.sizeMode)
{
case SizeMode.InscribedCircle:
{
float minSize = Mathf.Min(helper.size.x, helper.size.y);
halfX = minSize * 0.5f;
halfY = minSize * 0.5f;
break;
}
case SizeMode.CircumscribedCircle:
{
float minSize = Mathf.Min(helper.size.x, helper.size.y);
float radius = minSize * 0.5f;
halfX = radius;
halfY = radius;
break;
}
case SizeMode.InscribedEllipse:
{
halfX = helper.size.x * 0.5f;
halfY = helper.size.y * 0.5f;
break;
}
case SizeMode.CircumscribedRatio:
{
float minSize = Mathf.Min(helper.size.x, helper.size.y);
float radius = minSize * 0.5f;
halfX = radius * (helper.size.x / minSize);
halfY = radius * (helper.size.y / minSize);
break;
}
default:
halfX = helper.size.x * 0.5f;
halfY = helper.size.y * 0.5f;
break;
}
var corners = new List<Vector2>(4)
{
helper.center + new Vector2(-halfX, -halfY),
helper.center + new Vector2( halfX, -halfY),
helper.center + new Vector2( halfX, halfY),
helper.center + new Vector2(-halfX, halfY),
};
if (helper.chamferSize > 0.001f)
corners = ApplyChamfer(corners, helper.chamferSize, helper.chamferSegments);
return corners;
}
/// <summary>
/// 对多边形角应用倒角(Bezier 曲线圆角)
/// </summary>
private static List<Vector2> ApplyChamfer(
List<Vector2> corners, float chamferSize, int segments)
{
int n = corners.Count;
// 计算每条边的长度,取最短边的一半作为倒角上限
float minEdgeHalf = float.MaxValue;
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
float edgeLen = Vector2.Distance(corners[i], corners[next]);
minEdgeHalf = Mathf.Min(minEdgeHalf, edgeLen * 0.5f);
}
float clampedChamfer = Mathf.Min(chamferSize, minEdgeHalf);
if (clampedChamfer < 0.001f) return corners;
var result = new List<Vector2>();
for (int i = 0; i < n; i++)
{
int prev = (i - 1 + n) % n;
int curr = i;
int next = (i + 1) % n;
Vector2 pCurr = corners[curr];
Vector2 pPrev = corners[prev];
Vector2 pNext = corners[next];
float edgeLenIn = Vector2.Distance(pPrev, pCurr);
float edgeLenOut = Vector2.Distance(pCurr, pNext);
float clampIn = Mathf.Min(clampedChamfer, edgeLenIn * 0.5f);
float clampOut = Mathf.Min(clampedChamfer, edgeLenOut * 0.5f);
// p0 在入边 (prev→curr) 上,距角 clampIn
// p3 在出边 (curr→next) 上,距角 clampOut
Vector2 dirFromCurrToPrev = (pPrev - pCurr).normalized;
Vector2 dirFromCurrToNext = (pNext - pCurr).normalized;
Vector2 p0 = pCurr + dirFromCurrToPrev * clampIn;
Vector2 p3 = pCurr + dirFromCurrToNext * clampOut;
// 三次 Bezier 控制点:向角的方向内收,使曲线从 p0 平滑过渡到 p3
// 控制点位于 p0→pCorner 和 p3→pCorner 之间
float c = 0.5522847498f; // 四分之一圆的 Bezier 近似常数
float bezierR = Mathf.Min(clampIn, clampOut);
Vector2 p1 = p0 + (pCurr - p0).normalized * bezierR * c;
Vector2 p2 = p3 + (pCurr - p3).normalized * bezierR * c;
// 添加 p0(每条边的起点)
result.Add(p0);
// 插值 Bezier 曲线上的中间点
for (int j = 1; j < segments; j++)
{
float t = (float)j / segments;
MacroCurve.BezierCurve3(p0, p1, p2, p3, t, out Vector2 pos);
result.Add(pos);
}
// 添加 p3(每条边的终点,也是下条边的起点)
result.Add(p3);
}
return result;
}
/// <summary>
/// 内部辅助结构(避免在 ComputeCorners 中传递过多参数)
/// </summary>
private struct PrimitiveParamHelper
{
public Vector2 center;
public Vector2 size;
public SizeMode sizeMode;
public PrimitiveType type;
public int sideCount;
public float chamferSize;
public int chamferSegments;
public int faceCount; // 输出:实际生成的三角形数
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ae63ff723ba749448c841d5afe21ac2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 图元类型
/// </summary>
public enum PrimitiveType : byte
{
/// <summary>正多边形(内切圆/外切圆)</summary>
Polygon,
/// <summary>矩形(内切椭圆/外切圆比例)</summary>
Rectangle,
}
/// <summary>
/// 尺寸模式
/// </summary>
public enum SizeMode : byte
{
/// <summary>内切圆 — 半径延伸到各边中点</summary>
InscribedCircle,
/// <summary>外切圆 — 半径延伸到各顶点</summary>
CircumscribedCircle,
/// <summary>内切椭圆 — XY各自独立的内切半径</summary>
InscribedEllipse,
/// <summary>外切圆比例 — 外切圆半径 + XY比例</summary>
CircumscribedRatio,
}
/// <summary>
/// 图元参数
/// 顶点通道分配:
/// uv0: 面片内归一化坐标 (0~1)
/// uv1: (centerDist, isBorder)
/// normal: (bg.r, bg.g, bg.b) — 0~1
/// tangent: (border.r, border.g, border.b, border.a) — 0~1
/// </summary>
public struct PrimitiveParams
{
public float axisScaleX;
public float axisScaleY;
public float sideCount;
public float chamferSize;
/// <summary>倒角细分段数(仅网格生成使用)</summary>
public int chamferSegments;
public Color32 bgColor;
public Color32 centerColor;
public Color32 borderColor;
public float borderThickness;
}
/// <summary>
/// 图元缓存条目
/// </summary>
public struct PrimitiveCacheEntry
{
/// <summary>在网格顶点缓存中的起始索引</summary>
public int startVertexIndex;
/// <summary>该图元占用的顶点数</summary>
public int vertexCount;
/// <summary>在网格索引缓存中的起始索引</summary>
public int startIndexIndex;
/// <summary>三角形数量</summary>
public int triangleCount;
public PrimitiveType type;
public SizeMode sizeMode;
/// <summary>图元中心(局部坐标)</summary>
public Vector2 center;
/// <summary>图元尺寸</summary>
public Vector2 size;
/// <summary>旋转角度(度数,0 = 正上方)</summary>
public float angle;
/// <summary>图元参数</summary>
public PrimitiveParams @params;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b233b049f5be67e47a89d1f9bee867b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+121
View File
@@ -0,0 +1,121 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 曲线绘制器组件
/// 支持多阶贝塞尔曲线,可配置控制点、宽度渐变、颜色和箭头装饰
/// </summary>
[AddComponentMenu("Xeric Library/UI/UICurveRenderer", 16)]
public class UICurveRenderer : CurveCacheRendererBase
{
/// <summary>控制点,(x,y)=位置, z=宽度</summary>
public Vector3[] controlPoints = new Vector3[]
{
new Vector3(-150f, 0f, 10f),
new Vector3(-50f, 100f, 10f),
new Vector3(50f, -100f, 10f),
new Vector3(150f, 0f, 10f),
};
public Color32 startColor = Color.white;
public Color32 endColor = Color.white;
[Range(4, 128)]
public int tessellationSegments = 32;
public List<ArrowHeadData> arrows = new List<ArrowHeadData>();
public bool autoRebuild = true;
private CurveCacheEntry? m_CurrentEntry;
protected override void OnEnable()
{
base.OnEnable();
if (autoRebuild) RebuildCurve();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (controlPoints == null || controlPoints.Length < 2)
controlPoints = new Vector3[] { new Vector3(0f, 0f, 10f), new Vector3(100f, 0f, 10f) };
if (tessellationSegments < 4) tessellationSegments = 4;
if (autoRebuild && isActiveAndEnabled)
{
EditorApplication.delayCall += () =>
{
if (this != null && isActiveAndEnabled)
RebuildCurve();
};
}
}
#endif
/// <summary>
/// 重建曲线
/// </summary>
public void RebuildCurve()
{
ClearAll();
if (controlPoints == null || controlPoints.Length < 2) return;
// 从 Vector3[] 中提取 Vector2[] 位置和 float[] 宽度
int count = controlPoints.Length;
var ctrlPts2 = new Vector2[count];
var widths = new float[count];
for (int i = 0; i < count; i++)
{
ctrlPts2[i] = (Vector2)controlPoints[i];
widths[i] = controlPoints[i].z;
}
var entry = new CurveCacheEntry
{
startColor = startColor,
endColor = endColor,
tessellationSegments = tessellationSegments,
};
AddCurve(entry, ctrlPts2, widths, arrows?.ToArray());
m_CurrentEntry = m_Curves.Count > 0 ? m_Curves[0] : (CurveCacheEntry?)null;
}
/// <summary>
/// 设置控制点(z=宽度)
/// </summary>
public void SetControlPoints(Vector3[] points)
{
controlPoints = points;
RebuildCurve();
}
/// <summary>
/// 添加箭头
/// </summary>
public void AddArrow(ArrowHeadData arrow)
{
if (arrows == null) arrows = new List<ArrowHeadData>();
arrows.Add(arrow);
RebuildCurve();
}
/// <summary>
/// 移除箭头
/// </summary>
public void RemoveArrow(int index)
{
if (arrows == null || index < 0 || index >= arrows.Count) return;
arrows.RemoveAt(index);
RebuildCurve();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3189cafe7a5628048b2243986703e3e6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+115
View File
@@ -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:
+144
View File
@@ -0,0 +1,144 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 图元绘制器组件
/// 支持多边形/矩形的内切圆/外切圆/椭圆等尺寸模式
/// 参数写入 UV2-UV7 通道
/// </summary>
[AddComponentMenu("Xeric Library/UI/UIPrimitiveRenderer", 15)]
public class UIPrimitiveRenderer : PrimitiveCacheRendererBase
{
public PrimitiveType primitiveType = PrimitiveType.Polygon;
public SizeMode sizeMode = SizeMode.InscribedCircle;
public Vector2 size = new Vector2(100f, 100f);
public Vector2 centerOffset = Vector2.zero;
[Range(-180f, 180f)]
public float angle = 0f;
[Range(3, 64)]
public int sideCount = 6;
[Range(0f, 1f)]
public float chamferSize = 0f;
[Range(1, 16)]
public int chamferSegments = 4;
public Color bgColor = Color.gray;
public Color centerColor = Color.white;
public Color borderColor = Color.black;
[Range(0f, 50f)]
public float borderThickness = 2f;
public bool autoRebuild = true;
private PrimitiveCacheEntry? m_CurrentEntry;
protected override void OnEnable()
{
base.OnEnable();
if (autoRebuild) RebuildPrimitive();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (sideCount < 3) sideCount = 3;
if (autoRebuild && isActiveAndEnabled)
{
// 使用 EditorApplication.delayCall 避免 OnValidate 中的 SetVerticesDirty 警告
EditorApplication.delayCall += () =>
{
if (this != null && isActiveAndEnabled)
RebuildPrimitive();
};
}
}
#endif
/// <summary>
/// 重建图元
/// </summary>
public void RebuildPrimitive()
{
// 清除旧的图元
ClearAll();
var entry = new PrimitiveCacheEntry
{
type = primitiveType,
sizeMode = sizeMode,
center = centerOffset,
size = size,
angle = angle,
@params = new PrimitiveParams
{
axisScaleX = 1f,
axisScaleY = 1f,
sideCount = sideCount,
chamferSize = chamferSize,
chamferSegments = chamferSegments,
bgColor = bgColor,
centerColor = centerColor,
borderColor = borderColor,
borderThickness = borderThickness,
}
};
AddPrimitive(entry);
m_CurrentEntry = entry;
}
/// <summary>
/// 更新图元参数(不触发完全重建,只标记脏)
/// </summary>
public void UpdateParams(System.Action<PrimitiveCacheEntry> updater)
{
if (m_Primitives.Count == 0) return;
var entry = m_Primitives[0];
updater?.Invoke(entry);
m_Primitives[0] = entry;
SetPrimitiveDirty(0);
}
#region 便捷方法
/// <summary>
/// 快速设置为正多边形
/// </summary>
public void SetPolygon(int sides, float radius, Color? fill = null, Color? border = null)
{
primitiveType = PrimitiveType.Polygon;
sizeMode = SizeMode.InscribedCircle;
size = new Vector2(radius * 2f, radius * 2f);
sideCount = Mathf.Max(3, sides);
if (fill.HasValue) bgColor = fill.Value;
if (border.HasValue) borderColor = border.Value;
RebuildPrimitive();
}
/// <summary>
/// 快速设置为矩形
/// </summary>
public void SetRectangle(float width, float height, Color? fill = null, Color? border = null)
{
primitiveType = PrimitiveType.Rectangle;
sizeMode = SizeMode.InscribedEllipse;
size = new Vector2(width, height);
if (fill.HasValue) bgColor = fill.Value;
if (border.HasValue) borderColor = border.Value;
RebuildPrimitive();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7f57f5609d9f3a46aa9d50dc1a47904
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+478
View File
@@ -0,0 +1,478 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;
using XericLibrary.Runtime.MacroLibrary;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// UILineRenderer 的工具类,提供静态方法用于线条渲染相关的操作
/// </summary>
internal static class XericRendererUtils
{
#region 数学计算
/// <summary>
/// 计算线路的总长度
/// </summary>
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;
}
/// <summary>
/// 计算一个点指向另一个点的角度(以度为单位)
/// </summary>
public static float RotatePointTowards(Vector2 vertex, Vector2 target)
{
return (Mathf.Atan2(target.y - vertex.y, target.x - vertex.x) * Mathf.Rad2Deg);
}
/// <summary>
/// 如果需要居中显示,则偏移到组件中心
/// </summary>
public static Vector2 SwitchCenterOffset(bool center, Vector2 rectSize) =>
center ? Vector2.zero : -(rectSize / 2);
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计算
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 简易线路绘制 DSL5
public static List<UIVertex> GetSingleLineSegmentVerts_DSL5(List<UIVertex> 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);
vertices.Add(vertex);
vertex.position = (startPoint + subTangent * -width);
vertex.position += offset;
vertex.uv0 = new Vector2(1f, startUVY);
vertices.Add(vertex);
vertex.position = (endPoint + subTangent * width);
vertex.position += offset;
vertex.uv0 = new Vector2(0f, endUVY);
vertices.Add(vertex);
vertex.position = (endPoint + subTangent * -width);
vertex.position += offset;
vertex.uv0 = new Vector2(1f, endUVY);
vertices.Add(vertex);
vertex.position = endPoint;
vertex.position += offset;
vertex.uv0 = new Vector2(0.5f, endUVY);
vertices.Add(vertex);
return vertices;
}
public static void AddSingleLineSegmentVertsAndTriangle_DSL5(this VertexHelper vh, int segmentPartIndex,
IEnumerable<UIVertex> 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 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)
{
if (points.Length < 2)
return;
var totalLength = points.CalculateTotalLength(cycleLoop);
var currentLength = 0f;
var lineVertices = ListPool<UIVertex>.Get();
var arrowVertices = ListPool<UIVertex>.Get();
var arrowindices = ListPool<int>.Get();
for (int i = 0; i < points.Length - (cycleLoop ? 0 : 1); i++)
{
var thisPoint = points[i];
var nextPoint = points[(i + 1) % points.Length];
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<UIVertex>.Release(lineVertices);
ListPool<UIVertex>.Release(arrowVertices);
ListPool<int>.Release(arrowindices);
}
#endregion
#region 圆弧过度的线路绘制 CFAL
public static void AddSingleLineSegmentVertsAndTriangle_CFAL(this VertexHelper vh, int segmentPartIndex,
IEnumerable<UIVertex> 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<UIVertex> GetSingleLineSegmentVerts_CFAL(List<UIVertex> 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)
{
if (points.Length < 2)
return;
var totalLength = points.CalculateTotalLength(cycleLoop);
var currentLength = 0f;
var vertices = ListPool<UIVertex>.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<UIVertex>.Release(vertices);
}
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);
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);
vh.AddVert(vertex);
vertex.position = (Vector3)(startPoint + subTangent * -width + normal * pointOffset1) - (Vector3)offset;
vertex.uv0 = new Vector2(1f, startUVY);
vh.AddVert(vertex);
vertex.position = (Vector3)(endPoint + subTangent * width + normal * -pointOffset2) - (Vector3)offset;
vertex.uv0 = new Vector2(0f, endUVY);
vh.AddVert(vertex);
vertex.position = (Vector3)(endPoint + subTangent * -width + normal * -pointOffset2) - (Vector3)offset;
vertex.uv0 = new Vector2(1f, endUVY);
vh.AddVert(vertex);
vertex.position = (Vector3)(endPoint + normal * -pointOffset2) - (Vector3)offset;
vertex.uv0 = new Vector2(0.5f, endUVY);
vh.AddVert(vertex);
}
#endregion
#region 箭头绘制工具
public static List<UIVertex> PopulateTriangleArrowByPointArray(List<UIVertex> 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 顶点工具集
/// <summary>
/// 获取点在矩形中的UV坐标
/// </summary>
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<UIVertex> 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);
/// <summary>
/// 添加一个顶点信息
/// </summary>
public static void AddVert(this ICollection<UIVertex> 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
});
}
/// <summary>
/// 添加一个三角面索引
/// </summary>
public static void AddTriangle(this ICollection<int> indices, int index0, int index1, int index2)
{
indices.Add(index0);
indices.Add(index1);
indices.Add(index2);
}
/// <summary>
/// 向顶点管理器添加顶点列表中的顶点构成的面
/// </summary>
public static void AddFaceByConsecutiveVertices(this ICollection<int> indices, IList<UIVertex> 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
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a46796ddebc719488396e15845faa2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+7 -7
View File
@@ -302,16 +302,16 @@ public static class MacroDXXL
#region 标注 / 标签 / 文本
/// <summary>在 3D 位置绘制文本标签</summary>
public static void Label(Vector3 position, string text, Color? color = null, float size = 0.1f)
/// <summary>在 3D 位置绘制文本标签(rotation 控制朝向,默认 Quaternion.identity 即 xy 平面正向)</summary>
public static void Label(Vector3 position, string text, Color? color = null, float size = 0.1f, Quaternion? rotation = null)
{
DrawText.Write(text, position, color ?? Color.white, size, default(Vector3), default(Vector3));
}
DrawText.Write(text, position, color ?? Color.white, size, rotation ?? Quaternion.identity);
}
/// <summary>在 3D 位置绘制带框文本标签</summary>
public static void LabelFramed(Vector3 position, string text, Color? color = null, float size = 0.1f)
/// <summary>在 3D 位置绘制带框文本标签(rotation 控制朝向,默认 Quaternion.identity 即 xy 平面正向)</summary>
public static void LabelFramed(Vector3 position, string text, Color? color = null, float size = 0.1f, Quaternion? rotation = null)
{
DrawText.WriteFramed(text, position, color ?? Color.white, size, default(Vector3), default(Vector3));
DrawText.WriteFramed(text, position, color ?? Color.white, size, rotation ?? Quaternion.identity);
}
/// <summary>给 GameObject 绘制屏幕空间标签(始终面向相机)</summary>
-1
View File
@@ -30,7 +30,6 @@ namespace Deconstruction.Runtime
{
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTrans);
}
private static HashSet<RectTransform> _rebuildSet = new HashSet<RectTransform>();
/// <summary>
Binary file not shown.
-33
View File
@@ -1,33 +0,0 @@
fileFormatVersion: 2
guid: 39959342f844a444e83cf0f85dcdda9f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.