update 0.6.5
This commit is contained in:
@@ -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 > tail 是一个区间;循环样条上 head < 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:
|
||||
Reference in New Issue
Block a user