593 lines
20 KiB
C#
593 lines
20 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
|
|
/// <summary>
|
|
/// 统一物理线缆(移植自 Assets/Prototype/2D线缆物理交互 原型):
|
|
/// 固定粒子数 + 可变总长度的 Verlet 绳,一条线覆盖旧 Cable(拖拽)/ PhysicCable(闲置)两套状态。
|
|
/// 拖拽时按需送线、松手自动回收到最短长度、插入插孔时末端钉住。
|
|
/// </summary>
|
|
[RequireComponent(typeof(LineRenderer))]
|
|
public class PhysicCable : MonoBehaviour
|
|
{
|
|
public enum CableState
|
|
{
|
|
Hidden, // 收起,不渲染不模拟
|
|
Free, // 闲置下垂,自动回收到最短长度
|
|
Dragging, // 末端钉在拖拽目标上,按需送线
|
|
Plugged // 末端钉在插孔上
|
|
}
|
|
|
|
[FormerlySerializedAs("StartTranform")] public Transform startTransform; // 出线口
|
|
|
|
[Header("绳体")]
|
|
[SerializeField] private int pointCount = 40;
|
|
[SerializeField] private float minLength = 2f; // 闲置时的自然下垂长度
|
|
[SerializeField, Range(0.05f, 1f)] private float idleLengthScale = 0.165f;
|
|
[SerializeField] private float maxStretch = 4f; // 最大拉出长度(相对 minLength 的倍数)
|
|
[SerializeField] private float gravity = 100f; // 世界单位/s²,向下
|
|
[SerializeField, Range(0.8f, 1f)] private float damping = 0.965f;
|
|
[SerializeField] private int constraintIterations = 24;
|
|
[SerializeField, Range(0f, 1f)] private float bendSmooth = 0.3f; // 弯曲刚度(中点平滑)
|
|
[SerializeField] private float endGravityBoost = 1.8f; // 末端几个点的重力加成,让插头端垂坠
|
|
|
|
[Header("收放")]
|
|
[SerializeField] private float feedRate = 0.45f; // 送线速度
|
|
[SerializeField] private float holdRate = 0.06f; // 拖拽/插接中回收速度
|
|
[SerializeField] private float retractRate = 0.09f; // 松手后回收速度
|
|
[SerializeField] private float lengthMargin = 0.45f; // 需求长度余量
|
|
|
|
[Header("出线口保护段")]
|
|
[SerializeField] private float stubMinOffset = 0.12f; // 出线口处沿出线方向至少伸出的距离
|
|
|
|
[Header("插头朝向")]
|
|
[SerializeField] private float plugRefArcLength = 0.6f; // 取角度参考点时向回走的弧长
|
|
[SerializeField] private float plugTurnRate = 0.18f;
|
|
[SerializeField] private float plugMaxTurnStep = 0.11f; // 每步最大转角(弧度)
|
|
|
|
[Header("可选")]
|
|
[SerializeField] private Transform reelCenter; // 出线方向参考中心,空则用 startTransform.parent
|
|
[SerializeField] private Transform floorLimit; // 地面高度参考,空则不启用地面碰撞
|
|
|
|
[Header("像素风渲染")]
|
|
[SerializeField] private bool usePixelStyle = true;
|
|
[SerializeField] private Color pixelDark = new(0.13f, 0.18f, 0.4f, 1f);
|
|
[SerializeField] private Color pixelBody = new(0.24f, 0.36f, 0.85f, 1f);
|
|
[SerializeField] private Color pixelMid = new(0.43f, 0.55f, 0.96f, 1f);
|
|
[SerializeField] private Color pixelHighlight = new(0.73f, 0.78f, 1f, 1f);
|
|
|
|
|
|
public CableState State { get; private set; } = CableState.Hidden;
|
|
|
|
public Vector3 EndPosition => _pts != null
|
|
? _pts[_n - 1].pos
|
|
: (startTransform != null ? startTransform.position : transform.position);
|
|
|
|
public float CurrentLength => _length;
|
|
|
|
private struct Particle
|
|
{
|
|
public Vector3 pos;
|
|
public Vector3 prev;
|
|
}
|
|
|
|
private Particle[] _pts;
|
|
private int _n;
|
|
private float _length;
|
|
private Vector3 _pointer; // 拖拽指针(世界坐标,驱动送线长度)
|
|
private Vector3 _dragTarget; // 末端钉住目标(重 lerp + 长度钳制,消除拽满时的抖动)
|
|
private Transform _dock; // 当前插入的插孔
|
|
private float _plugAngle = -Mathf.PI / 2f; // 插头朝向(弧度,初始朝下)
|
|
|
|
private LineRenderer _line;
|
|
private LineRenderer _bodyLine;
|
|
private LineRenderer _midLine;
|
|
private LineRenderer _highlightLine;
|
|
private Vector3[] _renderA;
|
|
private Vector3[] _renderB;
|
|
private const int SmoothIterations = 2; // Chaikin 切角细分次数
|
|
|
|
private string _defaultSortingLayer;
|
|
private int _defaultSortingOrder;
|
|
private float _baseWidthMultiplier;
|
|
private AnimationCurve _baseWidthCurve;
|
|
private bool _initialized;
|
|
|
|
private void Awake()
|
|
{
|
|
_line = GetComponent<LineRenderer>();
|
|
_defaultSortingLayer = _line.sortingLayerName;
|
|
_defaultSortingOrder = _line.sortingOrder;
|
|
_baseWidthMultiplier = _line.widthMultiplier;
|
|
_baseWidthCurve = _line.widthCurve;
|
|
_line.enabled = false;
|
|
EnsurePixelLines();
|
|
ApplyPixelStyle();
|
|
}
|
|
|
|
// ------------------------------ 状态 API ------------------------------
|
|
|
|
public void Hide()
|
|
{
|
|
State = CableState.Hidden;
|
|
_dock = null;
|
|
if (_line != null) _line.enabled = false;
|
|
SetPixelLinesEnabled(false);
|
|
}
|
|
|
|
/// <summary>闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。</summary>
|
|
public void ShowFree(bool resetShape = false)
|
|
{
|
|
EnsureInit();
|
|
if (resetShape) ResetHangingShape();
|
|
_dock = null;
|
|
State = CableState.Free;
|
|
SetCableRenderersEnabled(true);
|
|
}
|
|
|
|
public void BeginDrag(Vector3 pointerWorld)
|
|
{
|
|
EnsureInit();
|
|
if (State == CableState.Hidden) ResetHangingShape();
|
|
_dock = null;
|
|
_pointer = FlattenZ(pointerWorld);
|
|
_dragTarget = _pts[_n - 1].pos;
|
|
State = CableState.Dragging;
|
|
SetCableRenderersEnabled(true);
|
|
}
|
|
|
|
public void SetPointer(Vector3 pointerWorld)
|
|
{
|
|
_pointer = FlattenZ(pointerWorld);
|
|
}
|
|
|
|
public void EndDrag()
|
|
{
|
|
if (State == CableState.Dragging)
|
|
{
|
|
State = CableState.Free;
|
|
}
|
|
}
|
|
|
|
/// <summary>末端钉到插孔;snapStraight 用于读档,把绳直接摆成出线口到插孔的直线。</summary>
|
|
public void PlugInto(Transform dock, bool snapStraight = false)
|
|
{
|
|
EnsureInit();
|
|
_dock = dock;
|
|
State = CableState.Plugged;
|
|
SetCableRenderersEnabled(true);
|
|
if (snapStraight && dock != null)
|
|
{
|
|
ResetStraightTo(FlattenZ(dock.position));
|
|
}
|
|
}
|
|
|
|
// ------------------------------ 插头姿态 ------------------------------
|
|
|
|
/// <summary>把插头摆到绳末端并按绳向旋转(角度经过平滑与限速)。</summary>
|
|
public void ApplyPlugPose(Transform plug, Transform plugRoot)
|
|
{
|
|
if (_pts == null || plug == null) return;
|
|
|
|
plug.rotation = Quaternion.Euler(0, 0, _plugAngle * Mathf.Rad2Deg - 90f);
|
|
|
|
Vector3 end = _pts[_n - 1].pos;
|
|
if (plugRoot != null)
|
|
{
|
|
Vector3 delta = end - plugRoot.position;
|
|
delta.z = 0;
|
|
plug.position += delta;
|
|
}
|
|
else
|
|
{
|
|
plug.position = new Vector3(end.x, end.y, plug.position.z);
|
|
}
|
|
}
|
|
|
|
// ------------------------------ 渲染排序 ------------------------------
|
|
|
|
public void SetSorting(string sortingLayerName, int sortingOrder)
|
|
{
|
|
// 主线必须保持在出线口/线盘下方;点击时只提高插头本体层级。
|
|
}
|
|
|
|
public void RestoreDefaultSorting()
|
|
{
|
|
_line.sortingLayerName = _defaultSortingLayer;
|
|
_line.sortingOrder = _defaultSortingOrder;
|
|
}
|
|
|
|
// ------------------------------ 模拟 ------------------------------
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (State == CableState.Hidden || _pts == null || startTransform == null) return;
|
|
|
|
Simulate(Time.fixedDeltaTime);
|
|
Render();
|
|
}
|
|
|
|
private void EnsureInit()
|
|
{
|
|
if (_initialized) return;
|
|
_initialized = true;
|
|
|
|
if (_line == null) _line = GetComponent<LineRenderer>();
|
|
EnsurePixelLines();
|
|
ApplyPixelStyle();
|
|
|
|
_n = Mathf.Max(8, pointCount);
|
|
_pts = new Particle[_n];
|
|
int renderCount = _n << SmoothIterations;
|
|
_renderA = new Vector3[renderCount];
|
|
_renderB = new Vector3[renderCount];
|
|
_line.positionCount = renderCount;
|
|
SetPixelLinePositionCount(renderCount);
|
|
|
|
ResetHangingShape();
|
|
}
|
|
|
|
private void ResetHangingShape()
|
|
{
|
|
Vector3 anchor = startTransform.position;
|
|
float idleLength = GetIdleLength();
|
|
for (int i = 0; i < _n; i++)
|
|
{
|
|
float t = i / (float)(_n - 1);
|
|
Vector3 p = anchor + Vector3.down * (t * idleLength) + Vector3.right * (t * 0.02f);
|
|
_pts[i].pos = p;
|
|
_pts[i].prev = p;
|
|
}
|
|
_length = idleLength;
|
|
_plugAngle = -Mathf.PI / 2f;
|
|
}
|
|
|
|
private void ResetStraightTo(Vector3 end)
|
|
{
|
|
Vector3 anchor = startTransform.position;
|
|
float idleLength = GetIdleLength();
|
|
for (int i = 0; i < _n; i++)
|
|
{
|
|
float t = i / (float)(_n - 1);
|
|
Vector3 p = Vector3.Lerp(anchor, end, t);
|
|
_pts[i].pos = p;
|
|
_pts[i].prev = p;
|
|
}
|
|
_length = Mathf.Max(idleLength, Vector3.Distance(anchor, end) * 1.04f + lengthMargin);
|
|
}
|
|
|
|
private void Simulate(float dt)
|
|
{
|
|
Vector3 anchor = startTransform.position;
|
|
Vector3 outletDir = GetOutletDirection(anchor);
|
|
float idleLength = GetIdleLength();
|
|
|
|
bool dragging = State == CableState.Dragging;
|
|
bool plugged = State == CableState.Plugged && _dock != null;
|
|
bool pinnedEnd = dragging || plugged;
|
|
Vector3 dock = plugged ? FlattenZ(_dock.position) : Vector3.zero;
|
|
|
|
float lMax = minLength * maxStretch;
|
|
if (plugged)
|
|
{
|
|
// 保证够得着插孔
|
|
lMax = Mathf.Max(lMax, Vector3.Distance(dock, anchor) * 1.12f);
|
|
}
|
|
|
|
// --- 拖拽目标:重 lerp + 按当前绳长钳制(拽满时目标跟着绳长走,消除抖动)
|
|
if (dragging)
|
|
{
|
|
_dragTarget += (_pointer - _dragTarget) * 0.3f;
|
|
Vector3 offset = _dragTarget - anchor;
|
|
float dist = offset.magnitude;
|
|
float maxRadius = Mathf.Min(lMax, _length) * 0.99f;
|
|
if (dist > maxRadius)
|
|
{
|
|
_dragTarget = anchor + offset / dist * maxRadius;
|
|
}
|
|
}
|
|
|
|
// --- 送线 / 回收(长度由原始指针驱动,而非被钳制后的目标)
|
|
Vector3 endTarget = dragging ? _pointer : (plugged ? dock : _pts[_n - 1].pos);
|
|
float needed = Vector3.Distance(endTarget, anchor) * 1.04f + lengthMargin;
|
|
float targetLength = pinnedEnd ? Mathf.Clamp(needed, idleLength, lMax) : idleLength;
|
|
float rate = targetLength > _length ? feedRate : (pinnedEnd ? holdRate : retractRate);
|
|
_length += (targetLength - _length) * rate;
|
|
|
|
float seg = _length / (_n - 1);
|
|
float g = gravity * dt * dt;
|
|
|
|
// --- Verlet 积分
|
|
for (int i = 1; i < _n; i++)
|
|
{
|
|
ref Particle p = ref _pts[i];
|
|
Vector3 v = (p.pos - p.prev) * damping;
|
|
p.prev = p.pos;
|
|
float boost = i > _n - 4 ? endGravityBoost : 1f;
|
|
p.pos += v + Vector3.down * (g * boost);
|
|
}
|
|
|
|
_pts[0].pos = anchor;
|
|
_pts[0].prev = anchor;
|
|
|
|
if (dragging)
|
|
{
|
|
_pts[_n - 1].pos = _dragTarget;
|
|
}
|
|
else if (plugged)
|
|
{
|
|
_pts[_n - 1].pos += (dock - _pts[_n - 1].pos) * 0.5f;
|
|
}
|
|
|
|
// --- 约束迭代
|
|
for (int k = 0; k < constraintIterations; k++)
|
|
{
|
|
for (int i = 0; i < _n - 1; i++)
|
|
{
|
|
ref Particle a = ref _pts[i];
|
|
ref Particle b = ref _pts[i + 1];
|
|
Vector3 d = b.pos - a.pos;
|
|
float dist = d.magnitude;
|
|
if (dist < 1e-6f) dist = 1e-6f;
|
|
float diff = (dist - seg) / dist;
|
|
bool pinA = i == 0;
|
|
bool pinB = i == _n - 2 && pinnedEnd;
|
|
float wa = pinA ? 0f : (pinB ? 1f : 0.5f);
|
|
float wb = pinB ? 0f : (pinA ? 1f : 0.5f);
|
|
a.pos += d * (diff * wa);
|
|
b.pos -= d * (diff * wb);
|
|
}
|
|
|
|
// 出线口保护段:靠近出线口的两个点顺着出线方向、横向收紧,
|
|
// 无论绳的其余部分被拽到哪,出线处都不出现折角或自穿插
|
|
ApplyOutletStub(anchor, outletDir);
|
|
|
|
// 弯曲刚度:中点平滑(每 3 次迭代做一次)
|
|
if (bendSmooth > 0f && k % 3 == 0)
|
|
{
|
|
for (int i = 1; i < _n - 1; i++)
|
|
{
|
|
if (i == _n - 2 && pinnedEnd) continue;
|
|
Vector3 mid = (_pts[i - 1].pos + _pts[i + 1].pos) * 0.5f;
|
|
_pts[i].pos += (mid - _pts[i].pos) * (bendSmooth * 0.5f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 地面(被钉住的拖拽末端豁免,避免钉点与地面互相拉扯)
|
|
if (floorLimit != null)
|
|
{
|
|
float floorY = floorLimit.position.y;
|
|
for (int i = 1; i < _n; i++)
|
|
{
|
|
if (i == _n - 1 && dragging) continue;
|
|
if (_pts[i].pos.y < floorY)
|
|
{
|
|
_pts[i].pos.y = floorY;
|
|
_pts[i].prev.x += (_pts[i].pos.x - _pts[i].prev.x) * 0.5f; // 摩擦
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdatePlugAngle(anchor, dragging, plugged);
|
|
}
|
|
|
|
private void ApplyOutletStub(Vector3 anchor, Vector3 dir)
|
|
{
|
|
// P1:横向收紧 0.6,且沿出线方向至少伸出 stubMinOffset
|
|
Vector3 rel = _pts[1].pos - anchor;
|
|
float along = Vector3.Dot(rel, dir);
|
|
Vector3 lateral = (rel - dir * along) * 0.4f;
|
|
if (along < stubMinOffset) along = stubMinOffset;
|
|
_pts[1].pos = anchor + dir * along + lateral;
|
|
|
|
// P2:横向收紧 0.2
|
|
rel = _pts[2].pos - anchor;
|
|
along = Vector3.Dot(rel, dir);
|
|
lateral = (rel - dir * along) * 0.8f;
|
|
_pts[2].pos = anchor + dir * along + lateral;
|
|
}
|
|
|
|
private void UpdatePlugAngle(Vector3 anchor, bool dragging, bool plugged)
|
|
{
|
|
Vector3 end = _pts[_n - 1].pos;
|
|
float targetAngle;
|
|
|
|
if (plugged)
|
|
{
|
|
// 插着时插头精灵隐藏,保持角度即可
|
|
targetAngle = _plugAngle;
|
|
}
|
|
else
|
|
{
|
|
// 拽紧时绳近似出线口到插头的直线,直接沿这条线取向;
|
|
// 局部段方向在高张力下噪声大
|
|
Vector3 toEnd = end - anchor;
|
|
float taut = toEnd.magnitude / Mathf.Max(0.001f, _length);
|
|
if (dragging && taut > 0.92f)
|
|
{
|
|
targetAngle = Mathf.Atan2(toEnd.y, toEnd.x);
|
|
}
|
|
else
|
|
{
|
|
// 角度取自向回走固定弧长的参考点;绳被压缩(末端点堆积)时方向不可靠,
|
|
// 保持上一帧角度而不是乱翻
|
|
int k = _n - 1;
|
|
float arc = 0f;
|
|
while (k > 0 && arc < plugRefArcLength)
|
|
{
|
|
arc += Vector3.Distance(_pts[k].pos, _pts[k - 1].pos);
|
|
k--;
|
|
}
|
|
Vector3 refP = _pts[k].pos;
|
|
targetAngle = Vector3.Distance(end, refP) > plugRefArcLength * 0.3f
|
|
? Mathf.Atan2(end.y - refP.y, end.x - refP.x)
|
|
: _plugAngle;
|
|
}
|
|
}
|
|
|
|
float da = Mathf.DeltaAngle(_plugAngle * Mathf.Rad2Deg, targetAngle * Mathf.Rad2Deg) * Mathf.Deg2Rad;
|
|
float step = Mathf.Clamp(da * plugTurnRate, -plugMaxTurnStep, plugMaxTurnStep);
|
|
_plugAngle += step;
|
|
}
|
|
|
|
private Vector3 GetOutletDirection(Vector3 anchor)
|
|
{
|
|
Transform center = reelCenter != null ? reelCenter : startTransform.parent;
|
|
if (center != null)
|
|
{
|
|
Vector3 radial = anchor - center.position;
|
|
radial.z = 0;
|
|
if (radial.sqrMagnitude > 1e-6f)
|
|
{
|
|
return radial.normalized;
|
|
}
|
|
}
|
|
return Vector3.down;
|
|
}
|
|
|
|
private Vector3 FlattenZ(Vector3 worldPos)
|
|
{
|
|
worldPos.z = startTransform != null ? startTransform.position.z : transform.position.z;
|
|
return worldPos;
|
|
}
|
|
|
|
// ------------------------------ 渲染 ------------------------------
|
|
|
|
private void Render()
|
|
{
|
|
for (int i = 0; i < _n; i++)
|
|
{
|
|
_renderA[i] = _pts[i].pos;
|
|
}
|
|
|
|
int count = _n;
|
|
Vector3[] src = _renderA;
|
|
Vector3[] dst = _renderB;
|
|
for (int it = 0; it < SmoothIterations; it++)
|
|
{
|
|
count = Chaikin(src, count, dst);
|
|
(src, dst) = (dst, src);
|
|
}
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
_line.SetPosition(i, src[i]);
|
|
}
|
|
RenderPixelBands(src, count);
|
|
}
|
|
|
|
private float GetIdleLength()
|
|
{
|
|
return Mathf.Max(0.1f, minLength * idleLengthScale);
|
|
}
|
|
|
|
private void EnsurePixelLines()
|
|
{
|
|
if (!usePixelStyle || _line == null) return;
|
|
|
|
_bodyLine = EnsurePixelLine(_bodyLine, "Pixel Body");
|
|
_midLine = EnsurePixelLine(_midLine, "Pixel Mid");
|
|
_highlightLine = EnsurePixelLine(_highlightLine, "Pixel Highlight");
|
|
}
|
|
|
|
private LineRenderer EnsurePixelLine(LineRenderer renderer, string objectName)
|
|
{
|
|
if (renderer != null) return renderer;
|
|
|
|
Transform existing = transform.Find(objectName);
|
|
if (existing != null && existing.TryGetComponent(out LineRenderer existingRenderer))
|
|
{
|
|
return existingRenderer;
|
|
}
|
|
|
|
var child = new GameObject(objectName);
|
|
child.transform.SetParent(transform, false);
|
|
return child.AddComponent<LineRenderer>();
|
|
}
|
|
|
|
private void ApplyPixelStyle()
|
|
{
|
|
if (!usePixelStyle || _line == null) return;
|
|
|
|
ApplyLineStyle(_line, pixelDark, 1f, _defaultSortingOrder - 3);
|
|
ApplyLineStyle(_bodyLine, pixelBody, 0.72f, _defaultSortingOrder - 2);
|
|
ApplyLineStyle(_midLine, pixelMid, 0.32f, _defaultSortingOrder - 1);
|
|
ApplyLineStyle(_highlightLine, pixelHighlight, 0.14f, _defaultSortingOrder);
|
|
}
|
|
|
|
private void ApplyLineStyle(LineRenderer renderer, Color color, float widthScale, int sortingOrder)
|
|
{
|
|
if (renderer == null || _line == null) return;
|
|
|
|
renderer.useWorldSpace = true;
|
|
renderer.material = _line.material;
|
|
renderer.widthMultiplier = _baseWidthMultiplier * widthScale;
|
|
renderer.widthCurve = _baseWidthCurve;
|
|
renderer.colorGradient = SolidGradient(color);
|
|
renderer.numCornerVertices = 0;
|
|
renderer.numCapVertices = 0;
|
|
renderer.alignment = _line.alignment;
|
|
renderer.textureMode = _line.textureMode;
|
|
renderer.sortingLayerName = _defaultSortingLayer;
|
|
renderer.sortingOrder = sortingOrder;
|
|
}
|
|
|
|
private static Gradient SolidGradient(Color color)
|
|
{
|
|
var gradient = new Gradient();
|
|
gradient.SetKeys(
|
|
new[] { new GradientColorKey(color, 0f), new GradientColorKey(color, 1f) },
|
|
new[] { new GradientAlphaKey(color.a, 0f), new GradientAlphaKey(color.a, 1f) });
|
|
return gradient;
|
|
}
|
|
|
|
private void SetCableRenderersEnabled(bool enabled)
|
|
{
|
|
_line.enabled = enabled;
|
|
SetPixelLinesEnabled(enabled && usePixelStyle);
|
|
}
|
|
|
|
private void SetPixelLinesEnabled(bool enabled)
|
|
{
|
|
if (_bodyLine != null) _bodyLine.enabled = enabled;
|
|
if (_midLine != null) _midLine.enabled = enabled;
|
|
if (_highlightLine != null) _highlightLine.enabled = enabled;
|
|
}
|
|
|
|
private void SetPixelLinePositionCount(int count)
|
|
{
|
|
if (_bodyLine != null) _bodyLine.positionCount = count;
|
|
if (_midLine != null) _midLine.positionCount = count;
|
|
if (_highlightLine != null) _highlightLine.positionCount = count;
|
|
}
|
|
|
|
private void RenderPixelBands(Vector3[] src, int count)
|
|
{
|
|
if (!usePixelStyle) return;
|
|
|
|
SetPixelLinePositionCount(count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (_bodyLine != null) _bodyLine.SetPosition(i, src[i]);
|
|
if (_midLine != null) _midLine.SetPosition(i, src[i]);
|
|
if (_highlightLine != null) _highlightLine.SetPosition(i, src[i]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 一次 Chaikin 切角细分:保留首尾点,每段取 1/4、3/4 两个切分点,输出点数为 2n。
|
|
/// </summary>
|
|
private static int Chaikin(Vector3[] src, int count, Vector3[] dst)
|
|
{
|
|
int idx = 0;
|
|
dst[idx++] = src[0];
|
|
for (int i = 0; i < count - 1; i++)
|
|
{
|
|
dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.25f);
|
|
dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.75f);
|
|
}
|
|
dst[idx++] = src[count - 1];
|
|
return idx;
|
|
}
|
|
}
|