update 0.6.5

This commit is contained in:
2026-09-30 08:56:49 +08:00
parent d9f9b2a90f
commit 016aee57ee
30 changed files with 2471 additions and 7 deletions
@@ -0,0 +1,81 @@
using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime.UIGraph;
namespace XericLibraryEditor.XericComponent
{
/// <summary>
/// UISplineCurveRenderer 的 Inspector:在默认字段上面加一条烘焙工具栏。
/// 烘焙系数(采样数、触发时机)本来就是公开字段,这里只补"手动烘一次"和状态读数。
/// </summary>
[CustomEditor(typeof(UISplineCurveRenderer))]
public class UISplineCurveRendererEditor : Editor
{
private UISplineCurveRenderer script;
private void OnEnable()
{
script = (UISplineCurveRenderer)target;
}
public override void OnInspectorGUI()
{
DrawBakeBar();
serializedObject.Update();
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// 烘焙状态条:读数 + 手动烘焙按钮。
/// 烘焙结果落在 XericRectSpline 的序列化字段里,所以按完要标脏,
/// 否则存不进场景,下次打开还得重烘。
/// </summary>
private void DrawBakeBar()
{
if (script == null)
return;
var path = script.splinePath;
var spline = path != null ? path.xericSpline : null;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("弧长烘焙", EditorStyles.boldLabel);
EditorGUILayout.LabelField("采样点",
spline != null ? spline.BakeCount.ToString() : "未绑定样条");
EditorGUILayout.LabelField("曲线长度",
spline != null ? spline.SplineLength.ToString("0.###") : "-");
EditorGUILayout.LabelField("已烘焙次数", script.BakeRunCount.ToString());
EditorGUILayout.LabelField("校正状态",
script.progressMapping == SplineProgressMapping.BakedDistance
? (spline != null && spline.BakeCount >= script.bakeSubdivide
? "按烘焙距离校正"
: "烘焙数据不够,暂时退回参数映射")
: "未启用(直接按参数)");
using (new EditorGUI.DisabledScope(spline == null))
{
if (GUILayout.Button("烘焙一次", GUILayout.Height(24)))
{
// XericRectSpline 是普通 [Serializable] 类,不是 Object,
// 能登记撤销/标脏的只有承载它的那个路径组件
Undo.RecordObject(path, "烘焙样条");
script.BakeNow();
EditorUtility.SetDirty(path);
EditorUtility.SetDirty(script);
SceneView.RepaintAll();
}
}
if (script.bakeTrigger == SplineBakeTrigger.Manual)
EditorGUILayout.HelpBox(
"触发时机是 Manual:几何怎么改都不会自动烘,进度贴不贴得准就看上面这个按钮按没按。",
MessageType.Info);
EditorGUILayout.EndVertical();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4cb828489da70e091cbdc53afe9881ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3dd3579dda7928297735f22b46528170
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+212
View File
@@ -0,0 +1,212 @@
using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime;
using XericLibrary.Runtime.UIGraph;
using XericLibrary.Runtime.XericComponent;
namespace XericLibraryEditor.Spline
{
/// <summary>
/// 样条曲线路径的 Hierarchy 右键创建菜单。
/// 此文件直接放在 Release/Editor/ 下由 Unity 编译,
/// 不经过外部 DLL 管线,确保菜单立即生效。
/// </summary>
public static class SplinePathCreateMenuItems
{
private const string MenuRoot = "GameObject/Xeric Library/Spline/";
/// <summary>
/// 创建带 XericRectSplinePath 的 UI 节点(二维样条)
/// </summary>
[MenuItem(MenuRoot + "2D Spline", false, 20)]
private static void CreateRectSplinePath(MenuCommand menuCommand)
{
var go = CreateUiGameObject("2D Spline", new Vector2(300f, 150f));
var path = go.AddComponent<XericRectSplinePath>();
// 给两个默认控制点,创建后场景里立刻有点可选中
path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.25f, 0.5f)));
path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.75f, 0.5f)));
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Finish(go, "Create 2D Spline");
}
/// <summary>
/// 创建带 XericSplinePath 的场景节点(三维样条)
/// </summary>
[MenuItem(MenuRoot + "3D Spline", false, 21)]
private static void CreateSplinePath(MenuCommand menuCommand)
{
var go = new GameObject("3D Spline");
var path = go.AddComponent<XericSplinePath>();
path.xericSpline.Add(new XericSpline.SplinePoint(Vector3.left));
path.xericSpline.Add(new XericSpline.SplinePoint(Vector3.right));
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Finish(go, "Create 3D Spline");
}
/// <summary>
/// 创建由二维样条驱动的 UI 曲线渲染节点,并自动绑定同物体上的样条路径
/// </summary>
[MenuItem(MenuRoot + "Spline Curve Renderer", false, 22)]
private static void CreateSplineCurveRenderer(MenuCommand menuCommand)
{
var go = CreateUiGameObject("Spline Curve Renderer", new Vector2(300f, 150f));
var path = go.AddComponent<XericRectSplinePath>();
path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.1f, 0.5f)));
path.xericSpline.Add(MakeDefaultPoint(new Vector2(0.9f, 0.5f)));
var renderer = go.AddComponent<UISplineCurveRenderer>();
renderer.splinePath = path;
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Finish(go, "Create Spline Curve Renderer");
}
/// <summary>
/// 建一条能直接跑起来的曲线进度条:底轨铺满 + 填充到 60%,两条面片共用同一条样条
/// </summary>
[MenuItem(MenuRoot + "Progress Bar Demo", false, 23)]
private static void CreateProgressBarDemo(MenuCommand menuCommand)
{
var go = CreateUiGameObject("Spline Progress Bar", new Vector2(520f, 220f));
var path = go.AddComponent<XericRectSplinePath>();
// S 形:弧长校正在这条形状上才看得出差别(匀速直线段上校正前后一模一样)
AddDemoPoint(path, new Vector2(0.08f, 0.3f), new Vector2(0.28f, 0f));
AddDemoPoint(path, new Vector2(0.5f, 0.78f), new Vector2(0.3f, 0f));
AddDemoPoint(path, new Vector2(0.92f, 0.22f), new Vector2(0.26f, 0f));
var renderer = go.AddComponent<UISplineCurveRenderer>();
renderer.splinePath = path;
// 面片0=底轨铺满并压暗,面片1=填充到 60% 保持全亮
renderer.headTailPairs = new[] { 1f, 0f, 0.6f, 0f };
renderer.ribbonTints = new[]
{
new Color32(90, 90, 90, 255),
new Color32(255, 255, 255, 255),
};
renderer.curveWidth = 14f;
renderer.fillRibbonIndex = 1;
go.AddComponent<UISplineProgressDriver>();
ApplyProgressMaterial(renderer);
PlaceUnderCanvas(go, menuCommand);
Finish(go, "Create Spline Progress Bar");
}
/// <summary>
/// 换上真正消费 uv0.y 的材质,并配一张沿路径方向的手风琴条纹,这样一进来就能看见流动。
/// 纹理是运行时生成的(不落盘、不进版本库),要长期用请自己把图拖到 progressTexture 上。
/// </summary>
private static void ApplyProgressMaterial(UISplineCurveRenderer renderer)
{
var shader = Shader.Find("XericLibrary/UIGraph/XericUICurveProgress");
if (shader == null || !shader.isSupported)
{
// 必须写全:本文件在 XericLibraryEditor.Spline 里,裸 Debug 会被
// 外层命名空间 XericLibraryEditor.Debug 抢走
UnityEngine.Debug.LogWarning("[Spline Progress] 找不到 XericUICurveProgress 着色器,演示沿用默认材质(默认材质不读 uv0.y,也没有采贴图)");
return;
}
var texture = new Texture2D(4, 128, TextureFormat.RGBA32, false);
texture.wrapMode = TextureWrapMode.Repeat;
for (int y = 0; y < texture.height; y++)
{
// 沿 uv.y(也就是路径进度)方向的亮带 + 暗谷
float wave = 0.5f + 0.5f * Mathf.Sin(y * Mathf.PI * 2f / texture.height * 3f);
var color = new Color(1f, 1f, 1f, Mathf.Lerp(0.25f, 1f, wave));
for (int x = 0; x < texture.width; x++)
texture.SetPixel(x, y, color);
}
texture.Apply();
var material = new Material(shader);
material.SetFloat("_Tile", 5f);
material.SetFloat("_Scroll", 0.45f);
material.SetFloat("_EdgeExp", 0.85f);
material.SetFloat("_EdgeStepRange", 0.12f);
renderer.progressTexture = texture;
renderer.material = material;
}
// 工具方法
/// <summary>默认手柄偏移(锚点空间):手柄全零时贝塞尔段退化成直线,看不出这是一条曲线</summary>
private const float DefaultHandleOffset = 0.1f;
/// <summary>
/// 造一个带默认手柄的端点。与 2D 编辑器里插入端点用的是同一套默认值,
/// 两边各自实现(编辑器在 DLL 里,这里要当场编译,不能等它重新构建)。
/// </summary>
private static XericRectSpline.RectSplinePoint MakeDefaultPoint(Vector2 anchor)
{
var point = new XericRectSpline.RectSplinePoint(anchor);
// Local 是相对锚点的偏移;StartHandle/EndHandle 是叠在锚点上的绝对值
point.StartHandleLocal = new Vector2(-DefaultHandleOffset, 0f);
point.EndHandleLocal = new Vector2(DefaultHandleOffset, 0f);
return point;
}
private static void AddDemoPoint(XericRectSplinePath path, Vector2 anchor, Vector2 handleOffset)
{
var point = new XericRectSpline.RectSplinePoint(anchor);
// Start/EndHandle 是"叠在锚点上的"绝对锚点值,不是偏移量
point.EndHandle = anchor + handleOffset;
point.StartHandle = anchor - handleOffset;
path.xericSpline.Add(point);
}
/// <summary>
/// Graphic 不在 Canvas 下就画不出来,演示物必须落到某个 Canvas 里;
/// 场景里没有就现建一个(与 BlueprintMenuItems 同一套路)
/// </summary>
private static void PlaceUnderCanvas(GameObject go, MenuCommand menuCommand)
{
var parent = menuCommand.context as GameObject;
if (parent != null && parent.GetComponentInParent<Canvas>() != null)
{
GameObjectUtility.SetParentAndAlign(go, parent);
return;
}
var canvas = Object.FindObjectOfType<Canvas>();
if (canvas == null)
{
var canvasGo = new GameObject("Spline Demo Canvas",
typeof(Canvas), typeof(UnityEngine.UI.CanvasScaler), typeof(UnityEngine.UI.GraphicRaycaster));
canvasGo.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
Undo.RegisterCreatedObjectUndo(canvasGo, "Create Canvas For Spline Demo");
}
GameObjectUtility.SetParentAndAlign(go, canvas.gameObject);
}
/// <summary>
/// 创建带 RectTransform 的 UI 节点;界面样条的计算范围就是这个矩形
/// </summary>
private static GameObject CreateUiGameObject(string name, Vector2 size)
{
var go = new GameObject(name, typeof(RectTransform));
var rt = go.GetComponent<RectTransform>();
rt.sizeDelta = size;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
return go;
}
private static void Finish(GameObject go, string undoName)
{
Undo.RegisterCreatedObjectUndo(go, undoName);
Selection.activeObject = go;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 543e1ea7a6089fd6a835ee1d69cd721c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -7,4 +7,4 @@ ScriptedImporter:
userData:
assetBundleName:
assetBundleVariant:
script: {instanceID: 0}
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
@@ -48,6 +48,6 @@ Material:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _GridBackgroundColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1}
- _GridColor: {r: 0.8666667, g: 0.8862745, b: 0.93333334, a: 1}
- _Transform: {r: 9.787723, g: 8.369877, b: -4.381715, a: -3.846801}
- _Transform: {r: 14.84472, g: 12.69432, b: 32.392414, a: -6.6925216}
- _Vector0: {r: 0.95, g: 1, b: 0, a: 0}
m_BuildTextureStacks: []
@@ -7,4 +7,4 @@ ScriptedImporter:
userData:
assetBundleName:
assetBundleVariant:
script: {instanceID: 0}
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
@@ -0,0 +1,159 @@
Shader "XericLibrary/UIGraph/XericUICurveProgress"
{
Properties
{
[PerRendererData] _MainTex ("Progress Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_Tile ("Tile Count Along Path", Float) = 1
_Scroll ("Scroll Speed (along +progress)", Float) = 0
_ScrollPhase ("Scroll Phase", Range(0, 1)) = 0
_Cutoff ("Progress Cutoff", Range(0, 1)) = 1
_CutoffSoftness ("Cutoff Softness", Range(0, 0.5)) = 0.02
_EdgeExp ("Edge Exp", Range(0.001, 1)) = 0.8671591
_EdgeStep ("Edge Step", Range(0, 1)) = 0
_EdgeStepRange ("Edge Step Range", Range(0, 0.5)) = 0.1
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" }
Stencil
{
Ref [_Stencil]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
Comp [_StencilComp]
Pass [_StencilOp]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend One OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 uv0 : TEXCOORD0;
float4 uv1 : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 uv0 : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
float4 mask : TEXCOORD2;
float featherOn : TEXCOORD3;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
float4 _MainTex_ST;
float4 _ClipRect;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
float _Tile;
float _Scroll;
float _ScrollPhase;
float _Cutoff;
float _CutoffSoftness;
float _EdgeExp;
float _EdgeStep;
float _EdgeStepRange;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex;
OUT.vertex = vPosition;
float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw,
0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
OUT.uv0 = v.uv0;
OUT.featherOn = v.uv1.x;
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
// 与 UI 默认着色器同样的处理:把插值出来的 alpha 对齐到 1/256,
// 否则预乘混合下容易抖
const half alphaPrecision = half(0xff);
const half invAlphaPrecision = half(1.0 / alphaPrecision);
IN.color.a = round(IN.color.a * alphaPrecision) * invAlphaPrecision;
// uv0.y 已经是"沿整条面片的进度",横向不重复,纵向按 _Tile 平铺并可随时间流动
float2 sampleUV = float2(
IN.uv0.x,
IN.uv0.y * _Tile - _Time.y * _Scroll + _ScrollPhase);
fixed4 tex = tex2D(_MainTex, sampleUV);
float edge = smoothstep(
_EdgeStep - _EdgeStepRange * 2.0,
_EdgeStep + _EdgeStepRange,
sin(IN.uv0.x * UNITY_PI));
float4 color = lerp(IN.color, IN.color * pow(saturate(edge), _EdgeExp), IN.featherOn);
color *= tex;
float soft = max(_CutoffSoftness, 1e-4);
color.a *= smoothstep(_Cutoff + soft, _Cutoff - soft, IN.uv0.y);
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif
color.rgb *= color.a;
return color;
}
ENDCG
}
}
Fallback Off
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 6e11e3a532c926db17c9a5d958de6591
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:
@@ -5,6 +5,7 @@ using UnityEngine.InputSystem;
#endif
using XericLibrary.Runtime.Blueprint.Canvas;
using XericLibrary.Runtime.Blueprint.Element;
using XericLibrary.Runtime.SuperStyleSheet;
namespace XericLibrary.Runtime.Blueprint
{
@@ -33,7 +34,7 @@ namespace XericLibrary.Runtime.Blueprint
[Header("Input")]
[Tooltip("新输入系统 InputActionAsset。留空则仅支持鼠标基础操作(中键拖动平移、滚轮缩放)。")]
[SerializeField] private Object _inputActions;
public Object InputActions
{
get { return _inputActions; }
Binary file not shown.
@@ -0,0 +1,504 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 一条面片切出来的一个贝塞尔小块:几何、逐点宽度、端点颜色、uv 进度区间。
/// 位置都在 Graphic 的局部平面内,与渲染器同一坐标约定。
/// </summary>
internal struct RibbonPiece
{
public Vector2 c0;
public Vector2 c1;
public Vector2 c2;
public Vector2 c3;
public float w0;
public float w1;
public float w2;
public float w3;
public Color32 startColor;
public Color32 endColor;
public float uvYStart;
public float uvYEnd;
public int ribbonIndex;
/// <summary>本小块占它所在样条段的比例,用来折算细分数</summary>
public float segmentFraction;
}
/// <summary>面片上 uv0.y 代表什么进度</summary>
public enum SplineUvProgressSpace
{
/// <summary>每条面片各自 0→1:尾部 0、头部 1,效果跟着已填充部分伸缩</summary>
RibbonLocal,
/// <summary>钉在整条样条上:贴图位置不随进度变化</summary>
SplineGlobal,
}
/// <summary>
/// 样条面片切分器:把若干条 (头部,尾部) 面片切成逐段贝塞尔小块。
/// <code>
/// 纯函数:不碰 MonoBehaviour,也不碰 Debug.Log —— 后者在 Unity 之外会抛
/// "ECall could not be defined",那样就没法在离线门里直接跑出数来验证。
/// 几何切分用贝塞尔在 [t0,t1] 上的精确限制(Restrict);
/// t0=0,t1=1 时逐点还原原始四个控制点,所以整段不产生任何漂移。
/// </code>
/// </summary>
internal static class SplineRibbonBuilder
{
internal struct Request
{
/// <summary>各控制点锚点(局部坐标),长度 = pointCount</summary>
public Vector2[] anchors;
/// <summary>各点的出手柄(局部坐标世界式,即已含锚点)</summary>
public Vector2[] outHandles;
/// <summary>各点的入手柄(局部坐标世界式,即已含锚点)</summary>
public Vector2[] inHandles;
/// <summary>各点颜色</summary>
public Color[] pointColors;
/// <summary>各点宽度</summary>
public float[] pointWidths;
public int pointCount;
public int segmentCount;
public bool curveLoop;
/// <summary>偶数下标=头部、奇数下标=尾部,成对;取值 [0,1]</summary>
public float[] headTailPairs;
/// <summary>可空;与面片同序时逐条叠乘到样条颜色上</summary>
public Color32[] ribbonTints;
/// <summary>进度 → 参数 t(距离校正就落在这个回调里)</summary>
public Func<float, float> progressToParametric;
public SplineUvProgressSpace uvSpace;
/// <summary>每条面片至少切成几块;决定 uv0.y 与视觉距离对齐的精度</summary>
public int chunksPerRibbon;
}
/// <summary>
/// 切分所有面片。无效输入返回空表而不是抛异常。
/// </summary>
internal static List<RibbonPiece> Build(Request req)
{
var pieces = new List<RibbonPiece>();
if (req.anchors == null || req.outHandles == null || req.inHandles == null)
return pieces;
if (req.headTailPairs == null || req.progressToParametric == null)
return pieces;
if (req.pointCount < 2 || req.segmentCount < 1 || req.headTailPairs.Length < 2)
return pieces;
int chunks = Mathf.Max(1, req.chunksPerRibbon);
int ribbonCount = req.headTailPairs.Length / 2;
for (int r = 0; r < ribbonCount; r++)
{
float head = Mathf.Clamp01(req.headTailPairs[r * 2]);
float tail = Mathf.Clamp01(req.headTailPairs[r * 2 + 1]);
if (Mathf.Abs(head - tail) <= 1e-6f)
continue;
BuildRibbon(req, r, tail, head, chunks, pieces);
}
return pieces;
}
/// <summary>
/// 面片沿路径的一段连续进度区间(起点进度、终点进度、起终点的参数 t)
/// </summary>
private struct Span
{
public float fromProgress;
public float toProgress;
public float fromParam;
public float toParam;
public float ArcLength;
}
private static void BuildRibbon(Request req, int ribbonIndex, float tail, float head,
int chunks, List<RibbonPiece> output)
{
var spans = ResolveSpans(req, tail, head);
if (spans.Count == 0)
return;
float segments = req.segmentCount;
Color32 tint = (req.ribbonTints != null && ribbonIndex < req.ribbonTints.Length)
? req.ribbonTints[ribbonIndex]
: new Color32(255, 255, 255, 255);
bool global = req.uvSpace == SplineUvProgressSpace.SplineGlobal;
int firstOfRibbon = output.Count;
// 与本次新增的 piece 一一对应的实测弧长:RibbonLocal 的 uv 分母就用这一份加总
var arcs = new List<float>();
foreach (var span in spans)
{
if (span.ArcLength <= 0f)
continue;
var cuts = CollectCuts(req, span);
int intervalCount = cuts.Count - 1;
float spanWalked = 0f;
for (int i = 0; i < intervalCount; i++)
{
float a = cuts[i];
float b = cuts[i + 1];
float arc = MeasureArc(req, a, b);
int seg = Mathf.Clamp(Mathf.FloorToInt(a * segments), 0, req.segmentCount - 1);
float localA = Mathf.Clamp01(a * segments - seg);
float localB = Mathf.Clamp01(b * segments - seg);
if (localB - localA <= 1e-6f)
continue;
Vector2 p0, p1, p2, p3;
GetSegment(req, seg, out p0, out p1, out p2, out p3);
float q0, q1, q2, q3;
GetWidthSegment(req, seg, out q0, out q1, out q2, out q3);
var piece = new RibbonPiece();
piece.ribbonIndex = ribbonIndex;
piece.segmentFraction = localB - localA;
Restrict(p0, p1, p2, p3, localA, localB,
out piece.c0, out piece.c1, out piece.c2, out piece.c3);
Restrict(q0, q1, q2, q3, localA, localB,
out piece.w0, out piece.w1, out piece.w2, out piece.w3);
piece.startColor = Tint(ColorAt(req, a), tint);
piece.endColor = Tint(ColorAt(req, b), tint);
// SplineGlobal:落在路径上的绝对进度,环绕时在 1→0 处天然断开。
// spanWalked 只在这里用 —— 它要的是"在本区间内走了多长"。
// RibbonLocal 的 uv 留给下面那一趟,用和分子同一份来源的弧长来分配。
if (global)
{
piece.uvYStart = ProgressAt(span, spanWalked / span.ArcLength);
piece.uvYEnd = ProgressAt(span, (spanWalked + arc) / span.ArcLength);
spanWalked += arc;
output.Add(piece);
continue;
}
spanWalked += arc;
output.Add(piece);
arcs.Add(arc);
}
}
if (global)
return;
// RibbonLocal:整条面片(含环绕的两段)合成一个连续的 0→1。
// <code>
// 分子分母必须同源。以前分母用 MeasureSpanArc(整个区间一次量完的折线长),
// 分子用逐小块的 MeasureArc(切得更细、量得更长),两者能差 0.2%,
// 于是中间的小块算出 uv > 1,末片又被钉成 1 ⇒ 出现"某片 uv 非增"。
// 更糟的是原先靠"emit 计数 == 预计块数"来认最后一块:被跳过的退化小块
// 也已经计过一次数 ⇒ 1 被压到中间某块上,下一块又从 0.6 起重来 ——
// 真机门量到的"跨尾面片 uv 回跳 4 次"就是这么来的。
// 现在出完片再按这份弧长统一分配,两端自然落在 0 与 1,中间跳过几块都不影响单调。
// </code>
float totalArc = 0f;
for (int i = 0; i < arcs.Count; i++)
totalArc += arcs[i];
if (arcs.Count == 0 || totalArc <= 0f)
{
output.RemoveRange(firstOfRibbon, output.Count - firstOfRibbon);
return;
}
float cursor = 0f;
for (int i = 0; i < arcs.Count; i++)
{
int pieceIndex = firstOfRibbon + i;
var piece = output[pieceIndex];
float from = cursor;
cursor += arcs[i];
piece.uvYStart = from / totalArc;
piece.uvYEnd = i == arcs.Count - 1 ? 1f : cursor / totalArc;
output[pieceIndex] = piece;
}
}
/// <summary>
/// 把一对 (头部,尾部) 展开成沿路径的区间。
/// head &gt; tail 是一个区间;循环样条上 head &lt; tail 表示"跨过终点绕回去",
/// 拆成 [tail,1] 与 [0,head] 两个区间,面片仍然连续。非循环样条不做环绕,直接不出片。
/// </summary>
private static List<Span> ResolveSpans(Request req, float tail, float head)
{
var spans = new List<Span>(req.curveLoop ? 2 : 1);
if (head > tail)
{
AddSpan(spans, req, tail, head);
return spans;
}
if (!req.curveLoop)
return spans;
AddSpan(spans, req, tail, 1f);
AddSpan(spans, req, 0f, head);
return spans;
}
private static void AddSpan(List<Span> spans, Request req, float fromProgress, float toProgress)
{
if (toProgress - fromProgress <= 1e-6f)
return;
// 退化:校正后端点仍然重合(长度为零的样条会出现),这个区间不出
float fromParam = Mathf.Clamp01(req.progressToParametric(fromProgress));
float toParam = Mathf.Clamp01(req.progressToParametric(toProgress));
if (toParam - fromParam <= 1e-6f)
return;
var span = new Span
{
fromProgress = fromProgress,
toProgress = toProgress,
fromParam = fromParam,
toParam = toParam,
};
span.ArcLength = MeasureSpanArc(req, fromParam, toParam);
spans.Add(span);
}
/// <summary>
/// 跨段的区间弧长:先按样条段边界切开,再逐段量。
/// MeasureArc 只能量"同一段内"的两点,直接拿来量整段会静默少算。
/// </summary>
private static float MeasureSpanArc(Request req, float a, float b)
{
float segments = req.segmentCount;
float sum = 0f;
float cursor = a;
while (cursor < b - 1e-6f)
{
int seg = Mathf.Clamp(Mathf.FloorToInt(cursor * segments), 0, req.segmentCount - 1);
float nextBoundary = (seg + 1f) / segments;
float stop = Mathf.Min(b, nextBoundary);
sum += MeasureArc(req, cursor, stop);
cursor = stop;
}
return sum;
}
/// <summary>区间内按弧长占比折算出该处的进度值</summary>
private static float ProgressAt(Span span, float fraction)
{
return Mathf.Clamp01(span.fromProgress + (span.toProgress - span.fromProgress) * fraction);
}
/// <summary>
/// 切分点:等进度网格 + 所有落在区间内的样条段边界,去重后升序。
/// 有了段边界,每个小块必然完整落在同一段贝塞尔里。
/// </summary>
private static List<float> CollectCuts(Request req, Span span)
{
int chunks = Mathf.Max(1, req.chunksPerRibbon);
var cuts = new List<float>(chunks + req.segmentCount + 2) { span.fromParam, span.toParam };
for (int s = 1; s < req.segmentCount; s++)
{
float boundary = (float)s / req.segmentCount;
if (boundary > span.fromParam && boundary < span.toParam)
cuts.Add(boundary);
}
for (int k = 1; k < chunks; k++)
{
float progress = span.fromProgress
+ (span.toProgress - span.fromProgress) * k / chunks;
cuts.Add(Mathf.Clamp01(req.progressToParametric(progress)));
}
cuts.Sort();
var unique = new List<float>(cuts.Count);
for (int i = 0; i < cuts.Count; i++)
{
if (unique.Count > 0 && cuts[i] - unique[unique.Count - 1] <= 1e-6f)
continue;
unique.Add(cuts[i]);
}
return unique;
}
/// <summary>段 seg 的四个控制点:本点锚、本点出手柄、下一点入手柄、下一点锚</summary>
private static void GetSegment(Request req, int seg,
out Vector2 p0, out Vector2 p1, out Vector2 p2, out Vector2 p3)
{
int next = NextPoint(req, seg);
p0 = req.anchors[seg];
p1 = req.outHandles[seg];
p2 = req.inHandles[next];
p3 = req.anchors[next];
}
/// <summary>
/// 宽度也走同一条贝塞尔:本段两端取本点宽、下段两端取下点宽。
/// 与几何用同一个 [t0,t1] 限制,带宽过渡才不会和形状错位。
/// </summary>
private static void GetWidthSegment(Request req, int seg,
out float w0, out float w1, out float w2, out float w3)
{
int next = NextPoint(req, seg);
w0 = WidthAt(req, seg);
w1 = WidthAt(req, seg);
w2 = WidthAt(req, next);
w3 = WidthAt(req, next);
}
private static int NextPoint(Request req, int seg)
{
return req.curveLoop
? PositiveMod(seg + 1, req.pointCount)
: Mathf.Min(seg + 1, req.pointCount - 1);
}
private static float WidthAt(Request req, int index)
{
if (req.pointWidths == null || req.pointWidths.Length == 0)
return 0f;
return req.pointWidths[Mathf.Clamp(index, 0, req.pointWidths.Length - 1)];
}
/// <summary>按渐层取色;索引越界一律夹紧,绝不裸索引(渐层列表可能比点少)</summary>
private static Color ColorAt(Request req, float parametric)
{
if (req.pointColors == null || req.pointColors.Length == 0)
return Color.white;
int count = req.pointColors.Length;
// 与 XericRectSpline.GetDeltaT 同一套映射(那个是 internal,Release 侧够不到),
// 所以取色和几何形状保持同相
float usable = req.curveLoop ? count : Mathf.Max(1, count - 1);
float scaled = Mathf.Clamp01(parametric) * usable;
int index = Mathf.FloorToInt(scaled);
float dt = Mathf.Clamp01(scaled - index);
int a = req.curveLoop ? PositiveMod(index, count) : Mathf.Clamp(index, 0, count - 1);
int b = req.curveLoop ? PositiveMod(index + 1, count) : Mathf.Clamp(index + 1, 0, count - 1);
return Color.Lerp(req.pointColors[a], req.pointColors[b], dt);
}
private static Color32 Tint(Color baseColor, Color32 tint)
{
var t = (Color)tint;
var r = new Color(baseColor.r * t.r, baseColor.g * t.g,
baseColor.b * t.b, baseColor.a * t.a);
return r;
}
private static int PositiveMod(int value, int modulus)
{
if (modulus <= 0)
return 0;
return ((value % modulus) + modulus) % modulus;
}
// ---------------------------------------------------------------- 贝塞尔裁剪
/// <summary>
/// 三次贝塞尔在 [t0,t1] 上的精确限制:
/// Q0=B(t0),Q1=B(t0)+(t1-t0)/3·B'(t0),Q2=B(t1)-(t1-t0)/3·B'(t1),Q3=B(t1)。
/// </summary>
internal static void Restrict(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3,
float t0, float t1, out Vector2 q0, out Vector2 q1, out Vector2 q2, out Vector2 q3)
{
float span = t1 - t0;
q0 = Eval(p0, p1, p2, p3, t0);
q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f);
q3 = Eval(p0, p1, p2, p3, t1);
q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f);
}
internal static void Restrict(float p0, float p1, float p2, float p3,
float t0, float t1, out float q0, out float q1, out float q2, out float q3)
{
float span = t1 - t0;
q0 = Eval(p0, p1, p2, p3, t0);
q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f);
q3 = Eval(p0, p1, p2, p3, t1);
q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f);
}
internal static Vector2 Eval(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
var a = Vector2.Lerp(p0, p1, t);
var b = Vector2.Lerp(p1, p2, t);
var c = Vector2.Lerp(p2, p3, t);
var d = Vector2.Lerp(a, b, t);
var e = Vector2.Lerp(b, c, t);
return Vector2.Lerp(d, e, t);
}
internal static float Eval(float p0, float p1, float p2, float p3, float t)
{
var a = Mathf.Lerp(p0, p1, t);
var b = Mathf.Lerp(p1, p2, t);
var c = Mathf.Lerp(p2, p3, t);
var d = Mathf.Lerp(a, b, t);
var e = Mathf.Lerp(b, c, t);
return Mathf.Lerp(d, e, t);
}
/// <summary>导数 = 3 × 一阶差分控制点的二次贝塞尔</summary>
internal static Vector2 Derivative(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
var d0 = p1 - p0;
var d1 = p2 - p1;
var d2 = p3 - p2;
var a = Vector2.Lerp(d0, d1, t);
var b = Vector2.Lerp(d1, d2, t);
return Vector2.Lerp(a, b, t) * 3f;
}
internal static float Derivative(float p0, float p1, float p2, float p3, float t)
{
var d0 = p1 - p0;
var d1 = p2 - p1;
var d2 = p3 - p2;
var a = Mathf.Lerp(d0, d1, t);
var b = Mathf.Lerp(d1, d2, t);
return Mathf.Lerp(a, b, t) * 3f;
}
/// <summary>小块弧长(折线近似,够 uv 分配用)</summary>
private static float MeasureArc(Request req, float a, float b)
{
const int samples = 5;
int seg = Mathf.Clamp(Mathf.FloorToInt(a * req.segmentCount), 0, req.segmentCount - 1);
Vector2 p0, p1, p2, p3;
GetSegment(req, seg, out p0, out p1, out p2, out p3);
float la = Mathf.Clamp01(a * req.segmentCount - seg);
float lb = Mathf.Clamp01(b * req.segmentCount - seg);
var last = Eval(p0, p1, p2, p3, la);
float sum = 0f;
for (int i = 1; i <= samples; i++)
{
var next = Eval(p0, p1, p2, p3, Mathf.Lerp(la, lb, (float)i / samples));
sum += Vector2.Distance(last, next);
last = next;
}
return sum;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2849f073cdc1b8028779b7b6c5fedfe3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using XericLibrary.Runtime.XericComponent;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 由界面样条(XericRectSplinePath)驱动的曲线绘制组件。
/// <code>
/// 一条样条上可以同时铺多条"面片",每条由一对 (头部, 尾部) 进度值决定覆盖范围,
/// 于是 {1,0} 是铺满整条的底轨,{0.6f,0} 就是填到 60% 的进度条本体。
/// 进度默认经样条烘焙后的弧长重映射校正,画面上的推进长度才和进度值成正比;
/// 面片上的 uv0.y 被重映射成整条面片的 0→1,材质按 UV 就能做流动/遮罩类效果。
/// </code>
/// </summary>
[AddComponentMenu("Xeric Library/UI/UISplineCurveRenderer", 17)]
public class UISplineCurveRenderer : CurveCacheRendererBase
{
#region 字段属性
/// <summary>驱动本曲线的界面样条</summary>
public XericRectSplinePath splinePath;
/// <summary>
/// 面片对:偶数下标=头部、奇数下标=尾部,取值 [0,1]。
/// 例:{1,0} 一条铺满;{1,0, 0.6f,0} 两条=底轨 + 60% 填充。
/// </summary>
public float[] headTailPairs = { 1f, 0f };
/// <summary>可空;与面片同序时逐条叠乘到样条颜色上(底轨压暗、填充提亮)</summary>
public Color32[] ribbonTints;
/// <summary>进度值的含义:直接按参数,还是先经烘焙做弧长校正</summary>
public SplineProgressMapping progressMapping = SplineProgressMapping.BakedDistance;
/// <summary>烘焙采样数,越大校正越细;同时压住库侧 1/(N+1) 的系统偏差</summary>
[Range(8, 4096)]
public int bakeSubdivide = 256;
/// <summary>
/// 什么时候做弧长烘焙。默认 OnEditEnd:几何停止变化后补烘一次。
/// 烘焙是 O(subdivide) 的采样,放在每帧会把编辑器拖到卡。
/// </summary>
public SplineBakeTrigger bakeTrigger = SplineBakeTrigger.OnEditEnd;
/// <summary>已经烘过几次(排查用:编辑器里拖动一整轮应该只加 1)</summary>
public int BakeRunCount => m_BakeRunCount;
/// <summary>面片上 uv0.y 代表条内进度还是全样条进度</summary>
public SplineUvProgressSpace uvProgressSpace = SplineUvProgressSpace.RibbonLocal;
/// <summary>每条面片至少切成几块,决定 uv 与视觉距离对齐的精度</summary>
[Range(1, 64)]
public int chunksPerRibbon = 8;
/// <summary>基准带宽(样条所在矩形的局部像素)</summary>
[Min(0.01f)]
public float curveWidth = 10f;
/// <summary>带宽来源:常量,还是乘上样条的缩放渐层 Y</summary>
public SplineWidthSource widthSource = SplineWidthSource.ScaleGradientY;
/// <summary>单段贝塞尔的细分段数</summary>
[Range(2, 128)]
public int tessellationSegments = 32;
/// <summary>样条颜色渐层全透明时的兜底颜色</summary>
public Color fallbackColor = Color.white;
/// <summary>
/// 推给材质 _MainTex 的贴图。
/// Canvas 是用 MaterialPropertyBlock 把 Graphic.mainTexture 顶到 _MainTex 上的,
/// 所以必须走这个口子交出去;只改 material 的贴图会被默认白图覆盖掉。
/// </summary>
public Texture progressTexture;
/// <summary>SetProgress 作用在哪条面片的头部;-1 表示不指定</summary>
public int fillRibbonIndex = 1;
/// <summary>每帧跟随样条变化重建(关掉后只能靠 RebuildCurve 手动触发)</summary>
public bool followSplineChanges = true;
/// <summary>与曲线缓存同序的 uv 区间,x=起点 y=终点</summary>
private readonly List<Vector2> m_PieceUv = new List<Vector2>();
private bool m_NeedUvPatch;
private int m_LastBakeSignature = int.MinValue;
private bool m_BakePending;
private int m_BakeRunCount;
#endregion
#region 生命周期
public override Texture mainTexture
=> progressTexture != null ? progressTexture : base.mainTexture;
protected override void OnEnable()
{
base.OnEnable();
RebuildCurve();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (curveWidth < 0.01f) curveWidth = 0.01f;
if (tessellationSegments < 2) tessellationSegments = 2;
if (followSplineChanges && isActiveAndEnabled)
{
EditorApplication.delayCall += () =>
{
if (this != null && isActiveAndEnabled)
RebuildCurve();
};
}
}
#endif
protected override void LateUpdate()
{
if (followSplineChanges)
RebuildCurve();
base.LateUpdate();
// 只能在"刚刚重建过缓存"之后改写一次:
// 改写后 uv0.y 就不再是生成器写的段内 t,重复改写会把映射套两遍
if (m_NeedUvPatch)
{
m_NeedUvPatch = false;
ApplyRibbonUv();
}
}
#endregion
#region API
/// <summary>
/// 把指定面片的头部推到 value(进度条前进就调这个)
/// </summary>
public void SetProgress(float value)
{
int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0;
if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2)
return;
headTailPairs[index * 2] = Mathf.Clamp01(value);
RebuildCurve();
}
/// <summary>
/// 读取指定面片的头部进度
/// </summary>
public float GetProgress()
{
int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0;
if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2)
return 0f;
return headTailPairs[index * 2];
}
/// <summary>
/// 按当前样条与面片配置重建曲线
/// </summary>
public void RebuildCurve()
{
ClearAll();
m_PieceUv.Clear();
m_NeedUvPatch = false;
if (splinePath == null || rectTransform == null)
return;
var spline = splinePath.xericSpline;
if (spline == null || spline.Count < 2)
return;
// 锚点→世界的换算依赖矩形角点
splinePath.UpdateRectDepend();
var rect = rectTransform.rect;
if (Mathf.Abs(rect.width) < 1e-6f || Mathf.Abs(rect.height) < 1e-6f)
return;
int pointCount = spline.Count;
var anchors = new Vector2[pointCount];
var outHandles = new Vector2[pointCount];
var inHandles = new Vector2[pointCount];
var colors = new Color[pointCount];
var widths = new float[pointCount];
int signature = pointCount;
for (int i = 0; i < pointCount; i++)
{
anchors[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetPointPosition(i));
outHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetEndHandlePosition(i));
inHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetStartHandlePosition(i));
colors[i] = ColorAt(spline, i);
widths[i] = WidthAt(spline, i);
unchecked
{
signature = signature * 31 + anchors[i].GetHashCode()
+ outHandles[i].GetHashCode() * 7 + inHandles[i].GetHashCode() * 13;
}
}
// 非均匀缩放矩形会改变世界空间弧长的比例,签名要把矩形尺寸也算进去
unchecked
{
signature = signature * 31 + rect.width.GetHashCode() + rect.height.GetHashCode() * 3;
}
var request = new SplineRibbonBuilder.Request
{
anchors = anchors,
outHandles = outHandles,
inHandles = inHandles,
pointColors = colors,
pointWidths = widths,
pointCount = pointCount,
segmentCount = spline.useablePointCount,
curveLoop = spline.CurveLoop,
headTailPairs = headTailPairs,
ribbonTints = ribbonTints,
uvSpace = uvProgressSpace,
chunksPerRibbon = chunksPerRibbon,
progressToParametric = BuildMapping(spline, signature),
};
var pieces = SplineRibbonBuilder.Build(request);
if (pieces.Count == 0)
return;
var ctrl = new Vector2[4];
var pieceWidths = new float[4];
foreach (var piece in pieces)
{
ctrl[0] = piece.c0; ctrl[1] = piece.c1; ctrl[2] = piece.c2; ctrl[3] = piece.c3;
pieceWidths[0] = piece.w0; pieceWidths[1] = piece.w1;
pieceWidths[2] = piece.w2; pieceWidths[3] = piece.w3;
var entry = new CurveCacheEntry
{
startColor = piece.startColor,
endColor = piece.endColor,
tessellationSegments = PieceTessellation(piece),
};
AddCurve(entry, (Vector2[])ctrl.Clone(), (float[])pieceWidths.Clone());
m_PieceUv.Add(new Vector2(piece.uvYStart, piece.uvYEnd));
}
m_NeedUvPatch = m_PieceUv.Count > 0;
}
#endregion
#region 取数
/// <summary>
/// 造出"进度 → 参数 t"的映射。校正走样条烘焙的弧长重映射,
/// 库侧要求先烘焙且长度不足时会抛,所以这里自己把关并回落。
/// </summary>
private Func<float, float> BuildMapping(XericRectSpline spline, int signature)
{
if (progressMapping != SplineProgressMapping.BakedDistance)
return ParametricIdentity;
if (!EnsureBaked(spline, signature))
return ParametricIdentity;
return ParametricFromBakedDistance;
}
private float ParametricIdentity(float progress) => progress;
/// <summary>
/// 进度→参数 t。两端是边界真值:进度 0 就是曲线起点、1 就是终点,
/// 所以先把两端钉死再走重映射 —— 库里的弧长重映射原本按"采样点数"而不是"段数"
/// 做分母,进度 1 会落在 (N-1)/N,{1,0} 的面片就永远短一小截贴不到末尾。
/// 库侧已经修了分母,这里钉端点既是兜底也让旧 DLL 下也能立刻闭合。
/// </summary>
private float ParametricFromBakedDistance(float progress)
{
if (progress <= 0f) return 0f;
if (progress >= 1f) return 1f;
return Mathf.Clamp01(splinePath.RemappingTimeValue(progress));
}
/// <summary>
/// 按 <see cref="bakeTrigger"/> 决定这一趟烘不烘,返回 false 表示当前用不了弧长校正。
/// <code>
/// 烘焙是 O(subdivide) 的采样:以前几何签名一变就烘,编辑器里拖动控制点等于每帧烘一次。
/// OnEditEnd 的做法是"几何正在线上变"时先挂起、继续用上一次那份烘焙数据,
/// 等下一趟签名不再变化(编辑停了)才补烘那一次。
/// </code>
/// </summary>
private bool EnsureBaked(XericRectSpline spline, int signature)
{
int target = Mathf.Max(bakeSubdivide, spline.Count);
bool geometryChanged = signature != m_LastBakeSignature;
m_LastBakeSignature = signature;
switch (bakeTrigger)
{
case SplineBakeTrigger.Manual:
// 一次都不自动烘,等 BakeNow
return HasUsableBake(spline, target);
case SplineBakeTrigger.EveryRebuild:
if (!geometryChanged && HasUsableBake(spline, target))
return true;
return RunBake(spline, target);
default:
if (geometryChanged)
{
// 几何正在线上变:这一趟不烘,也**不用**上一份烘焙数据。
// 结构大改(清点重铺、切循环)后那份表对应的已经不是这条曲线了,
// 拿它做校正会把面片排到错处 —— 宁可这一趟退回参数映射,
// 等编辑停下来的下一趟烘完再恢复校正。
m_BakePending = true;
return false;
}
if (!m_BakePending)
return HasUsableBake(spline, target);
m_BakePending = false;
return RunBake(spline, target);
}
}
/// <summary>烘焙数据存在且长度够用(旧数据只要长度够就继续用,别再烘一次)</summary>
private static bool HasUsableBake(XericRectSpline spline, int target)
=> spline.BakeCount >= target;
private bool RunBake(XericRectSpline spline, int target)
{
splinePath.BakePoint(target);
m_BakeRunCount++;
return spline.BakeCount >= target;
}
/// <summary>
/// 手动烘一次(bakeTrigger 为 Manual 时用这个,或改完曲线想立刻精确时按)。
/// 烘完直接重建,按下就能看见贴合结果。
/// </summary>
public void BakeNow()
{
if (splinePath == null || splinePath.xericSpline == null)
return;
var spline = splinePath.xericSpline;
RunBake(spline, Mathf.Max(bakeSubdivide, spline.Count));
m_BakePending = false;
RebuildCurve();
}
private Color ColorAt(XericRectSpline spline, int index)
{
var gradient = spline.ColorGradient;
if (gradient == null || index < 0 || index >= gradient.Count)
return fallbackColor;
var color = gradient[index];
return color.a <= 0f ? fallbackColor : color;
}
private float WidthAt(XericRectSpline spline, int index)
{
switch (widthSource)
{
case SplineWidthSource.ScaleGradientY:
return curveWidth * Mathf.Abs(GradientAt(spline.ScaleGradient, index, Vector3.one).y);
case SplineWidthSource.FloatGradient:
// 额外参数本来就是"每点多大"的语义时用它最直观;
// 注意它默认全 0,不填就什么都画不出来
return curveWidth * Mathf.Max(0f, GradientAt(spline.FloatGradient, index, 1f));
default:
return curveWidth;
}
}
/// <summary>渐层可能比点数短(在 Inspector 里直接改 ControlPoint 长度就会这样),一律夹紧</summary>
private static T GradientAt<T>(SplineGradientBase<T> gradient, int index, T fallback)
{
if (gradient == null || index < 0 || index >= gradient.Count)
return fallback;
return gradient[index];
}
private int PieceTessellation(RibbonPiece piece)
{
int tess = Mathf.RoundToInt(tessellationSegments * Mathf.Clamp01(piece.segmentFraction));
return Mathf.Clamp(tess, 2, 512);
}
#endregion
#region uv 重写
/// <summary>
/// 把生成器写在每个小块里的"段内 t"重映射成整条面片的 0→1。
/// 默认材质只用到 uv0.x(边缘羽化),uv0.y 留给换上去的材质消费。
/// </summary>
private void ApplyRibbonUv()
{
if (m_PieceUv.Count == 0)
return;
for (int i = 0; i < m_Curves.Count && i < m_PieceUv.Count; i++)
{
var curve = m_Curves[i];
var range = m_PieceUv[i];
int end = curve.startVertexIndex + curve.vertexCount;
if (end > m_MeshVertexCache.Count)
end = m_MeshVertexCache.Count;
for (int v = curve.startVertexIndex; v < end; v++)
{
var vert = m_MeshVertexCache[v];
vert.uv0.y = Mathf.LerpUnclamped(range.x, range.y, vert.uv0.y);
m_MeshVertexCache[v] = vert;
}
}
SetVerticesDirty();
}
#endregion
}
/// <summary>进度值如何换算成曲线参数</summary>
public enum SplineProgressMapping
{
/// <summary>进度直接当参数 t:段与段的视觉长度不等</summary>
Parametric,
/// <summary>进度按烘焙后的弧长重映射:画面上推进的长度和进度成正比</summary>
BakedDistance,
}
/// <summary>弧长烘焙的触发时机(校正的数据源就是这份烘焙数据)</summary>
public enum SplineBakeTrigger
{
/// <summary>从不自动烘,只认 BakeNow / Inspector 上的烘焙按钮</summary>
Manual,
/// <summary>几何停止变化后补烘一次:编辑器里拖动控制点不会逐帧烘</summary>
OnEditEnd,
/// <summary>几何一变就烘(每帧一变烘一次,贵;只在需要立刻精确时用)</summary>
EveryRebuild,
}
/// <summary>带宽来源</summary>
public enum SplineWidthSource
{
/// <summary>恒用 curveWidth</summary>
Constant,
/// <summary>curveWidth × 该点缩放渐层的 Y(渐层默认是 1,不填也等价于常量)</summary>
ScaleGradientY,
/// <summary>curveWidth × 该点的额外参数。默认全 0,不填什么都画不出来</summary>
FloatGradient,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91cbc8bc694b85a8f15fefce8d3a72b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 曲线进度条的播放驱动器:把时间推成 UISplineCurveRenderer 的进度。
/// 只负责"进度从哪来",几何与着色全在渲染器那一侧。
/// </summary>
[AddComponentMenu("Xeric Library/UI/UISplineProgressDriver", 18)]
[RequireComponent(typeof(UISplineCurveRenderer))]
public class UISplineProgressDriver : MonoBehaviour
{
/// <summary>自动推进(关掉就只认手动 SetProgress)</summary>
public bool autoDrive = true;
/// <summary>从 0 走到 1 所需秒数</summary>
[Min(0.01f)]
public float duration = 2.5f;
/// <summary>用 scaledTime,随引擎暂停/缩放</summary>
public bool useUnscaledTime = false;
/// <summary>走到尽头后往返还是从头再来</summary>
public bool pingPong = false;
/// <summary>起始进度</summary>
[Range(0f, 1f)]
public float startProgress = 0f;
private UISplineCurveRenderer m_Renderer;
private float m_Elapsed;
private void Awake()
{
m_Renderer = GetComponent<UISplineCurveRenderer>();
m_Elapsed = startProgress * duration;
if (m_Renderer != null)
m_Renderer.SetProgress(startProgress);
}
private void Update()
{
if (!autoDrive || m_Renderer == null)
return;
m_Elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
float cycle = m_Elapsed / duration;
float progress = pingPong
? Mathf.PingPong(cycle, 1f)
: cycle - Mathf.Floor(cycle);
m_Renderer.SetProgress(progress);
}
/// <summary>手动设进度,同时让自动推进从该处继续</summary>
public void SetProgress(float value)
{
value = Mathf.Clamp01(value);
m_Elapsed = value * duration;
if (m_Renderer != null)
m_Renderer.SetProgress(value);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f851c7a6653a78879ce665e643f5a28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+127
View File
@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 65b82257fc9da6b4ba046b02555dd0d6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
@@ -1,6 +1,6 @@
fileFormatVersion: 2
guid: 9abe5f919a1840a4dace5de52d721a06
TextScriptImporter:
guid: affe7d56dc01a48439aedc888552bf47
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
+638
View File
@@ -0,0 +1,638 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 705507994}
m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641275, b: 0.5748172, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &246206081
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 246206082}
- component: {fileID: 246206084}
- component: {fileID: 246206083}
m_Layer: 5
m_Name: LineRenderer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &246206082
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.5, y: 1.5, z: 1.5}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 387643621}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 1000, y: 300}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &246206083
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b33b2e663e78774c9f0c9af55018725, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
points:
- {x: 0, y: 0}
- {x: 58.7, y: 138.4}
- {x: 125.1, y: 80.6}
- {x: 238.9, y: 300}
- {x: 354, y: 100}
- {x: 512.2, y: 70}
- {x: 716.1, y: 192.5}
- {x: 938.8, y: 3.85}
- {x: 1000, y: 133}
thickness: 20
center: 1
--- !u!222 &246206084
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_CullTransparentMesh: 1
--- !u!1 &387643617
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 387643621}
- component: {fileID: 387643620}
- component: {fileID: 387643619}
- component: {fileID: 387643618}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &387643618
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &387643619
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 1920, y: 1080}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &387643620
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &387643621
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1231462426}
- {fileID: 246206082}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!1 &705507993
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 705507995}
- component: {fileID: 705507994}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &705507994
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_Enabled: 1
serializedVersion: 10
m_Type: 1
m_Shape: 0
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &705507995
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 705507993}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1 &963194225
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963194228}
- component: {fileID: 963194227}
- component: {fileID: 963194226}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &963194226
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
--- !u!20 &963194227
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &963194228
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1231462425
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1231462426}
- component: {fileID: 1231462428}
- component: {fileID: 1231462427}
m_Layer: 5
m_Name: Background
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1231462426
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 387643621}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1231462427
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 980e274b7ed2de945bdefcc1d0423472, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1231462428
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_CullTransparentMesh: 1
--- !u!1 &1721653935
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1721653938}
- component: {fileID: 1721653937}
- component: {fileID: 1721653936}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1721653936
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &1721653937
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1721653938
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 963194228}
- {fileID: 705507995}
- {fileID: 387643621}
- {fileID: 1721653938}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eb3b7561320c3df409493f5b073278f0
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
using UnityEngine;
using UnityEngine.UI;
namespace Radishmouse
{
[RequireComponent(typeof(CanvasRenderer))]
public class XericSimpleUILineRenderer : MaskableGraphic
{
public Vector2[] points;
public float thickness = 10f;
public bool center = true;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (points.Length < 2)
return;
for (int i = 0; i < points.Length-1; i++)
{
// Create a line segment between the next two points
CreateLineSegment(points[i], points[i+1], vh);
int index = i * 5;
// Add the line segment to the triangles array
vh.AddTriangle(index, index+1, index+3);
vh.AddTriangle(index+3, index+2, index);
// These two triangles create the beveled edges
// between line segments using the end point of
// the last line segment and the start points of this one
if (i != 0)
{
vh.AddTriangle(index, index-1, index-3);
vh.AddTriangle(index+1, index-1, index-2);
}
}
}
/// <summary>
/// Creates a rect from two points that acts as a line segment
/// </summary>
/// <param name="point1">The starting point of the segment</param>
/// <param name="point2">The endint point of the segment</param>
/// <param name="vh">The vertex helper that the segment is added to</param>
private void CreateLineSegment(Vector3 point1, Vector3 point2, VertexHelper vh)
{
Vector3 offset = center ? (rectTransform.sizeDelta / 2) : Vector2.zero;
// Create vertex template
UIVertex vertex = UIVertex.simpleVert;
vertex.color = color;
// Create the start of the segment
Quaternion point1Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point1, point2) + 90);
vertex.position = point1Rotation * new Vector3(-thickness / 2, 0);
vertex.position += point1 - offset;
vh.AddVert(vertex);
vertex.position = point1Rotation * new Vector3(thickness / 2, 0);
vertex.position += point1 - offset;
vh.AddVert(vertex);
// Create the end of the segment
Quaternion point2Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point2, point1) - 90);
vertex.position = point2Rotation * new Vector3(-thickness / 2, 0);
vertex.position += point2 - offset;
vh.AddVert(vertex);
vertex.position = point2Rotation * new Vector3(thickness / 2, 0);
vertex.position += point2 - offset;
vh.AddVert(vertex);
// Also add the end point
vertex.position = point2 - offset;
vh.AddVert(vertex);
}
/// <summary>
/// Gets the angle that a vertex needs to rotate to face target vertex
/// </summary>
/// <param name="vertex">The vertex being rotated</param>
/// <param name="target">The vertex to rotate towards</param>
/// <returns>The angle required to rotate vertex towards target</returns>
private float RotatePointTowards(Vector2 vertex, Vector2 target)
{
return (float)(Mathf.Atan2(target.y - vertex.y, target.x - vertex.x) * (180 / Mathf.PI));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e20d14972d02c04eba084112d510af7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
Binary file not shown.
+1 -1
View File
File diff suppressed because one or more lines are too long