feat(fix-system): 优化线缆渲染与拖拽排序
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FixSystem
|
||||
@@ -13,7 +13,21 @@ namespace AibisDream.FixSystem
|
||||
private LineRenderer _stiffLineRenderer; // 用于渲染刚性部分
|
||||
private Vector3[] _points;
|
||||
|
||||
// 渲染平滑:物理点数少,直接连线转折生硬,渲染前做切角细分
|
||||
private Vector3[] _smoothBufferA;
|
||||
private Vector3[] _smoothBufferB;
|
||||
|
||||
// 配置兜底后的生效值(旧场景/预制体上新字段是 0 时取默认)
|
||||
private float _headStiffLength;
|
||||
private float _headRenderBlend;
|
||||
private int _tailStiffCount;
|
||||
private float _tailStiffLength;
|
||||
private float _tailRenderBlend;
|
||||
private int _smoothIterations;
|
||||
|
||||
private CablePanel _cablePanel;
|
||||
private RendererSortingState _defaultLineSorting;
|
||||
private RendererSortingState _defaultStiffLineSorting;
|
||||
|
||||
[SerializeField] private CableConfig config = CableConfig.GeneDefaultConfig();
|
||||
|
||||
@@ -31,9 +45,17 @@ namespace AibisDream.FixSystem
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveConfig();
|
||||
|
||||
_lineRenderer = GetComponent<LineRenderer>();
|
||||
_lineRenderer.positionCount = config.resolution - config.stiffCount;
|
||||
|
||||
|
||||
// 每次 Chaikin 细分点数翻倍(保留首尾 + 每段两个切分点)
|
||||
int flexCount = config.resolution - config.stiffCount;
|
||||
int smoothedCount = flexCount << _smoothIterations;
|
||||
_lineRenderer.positionCount = smoothedCount;
|
||||
_smoothBufferA = new Vector3[smoothedCount];
|
||||
_smoothBufferB = new Vector3[smoothedCount];
|
||||
|
||||
// 创建第二个LineRenderer用于刚性部分
|
||||
GameObject stiffObj = new GameObject("StiffLineRenderer");
|
||||
stiffObj.transform.SetParent(transform);
|
||||
@@ -41,6 +63,24 @@ namespace AibisDream.FixSystem
|
||||
_stiffLineRenderer.positionCount = config.stiffCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算生效配置:新加的字段在旧序列化数据里是 0,取兜底默认;并做范围保护。
|
||||
/// </summary>
|
||||
private void ResolveConfig()
|
||||
{
|
||||
_headStiffLength = config.headStiffLength <= 0f ? 0.055f : config.headStiffLength;
|
||||
_headRenderBlend = config.headRenderBlend <= 0f ? 0.55f : Mathf.Clamp01(config.headRenderBlend);
|
||||
|
||||
_tailStiffCount = config.tailStiffCount < 2 ? 2 : config.tailStiffCount;
|
||||
// 至少给物理留 2 个自由点
|
||||
_tailStiffCount = Mathf.Min(_tailStiffCount, config.resolution - config.stiffCount - 2);
|
||||
_tailStiffCount = Mathf.Max(_tailStiffCount, 2);
|
||||
|
||||
_tailStiffLength = config.tailStiffLength <= 0f ? 0.024f : config.tailStiffLength;
|
||||
_tailRenderBlend = config.tailRenderBlend <= 0f ? 0.45f : Mathf.Clamp01(config.tailRenderBlend);
|
||||
_smoothIterations = config.smoothIterations <= 0 ? 2 : Mathf.Min(config.smoothIterations, 4);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitComponentRefs();
|
||||
@@ -57,9 +97,23 @@ namespace AibisDream.FixSystem
|
||||
_stiffLineRenderer.material = _lineRenderer.material;
|
||||
_stiffLineRenderer.startColor = _lineRenderer.startColor;
|
||||
_stiffLineRenderer.endColor = _lineRenderer.startColor;
|
||||
_stiffLineRenderer.numCornerVertices = _lineRenderer.numCornerVertices;
|
||||
_stiffLineRenderer.alignment = _lineRenderer.alignment;
|
||||
_stiffLineRenderer.textureMode = _lineRenderer.textureMode;
|
||||
_stiffLineRenderer.sortingLayerName =
|
||||
_cablePanel.CableReelRef.GetComponent<SpriteRenderer>().sortingLayerName;
|
||||
_stiffLineRenderer.sortingOrder = _cablePanel.CableReelRef.GetComponent<SpriteRenderer>().sortingOrder - 1;
|
||||
_defaultLineSorting = RendererSortingState.From(_lineRenderer);
|
||||
_defaultStiffLineSorting = RendererSortingState.From(_stiffLineRenderer);
|
||||
|
||||
// 两端端帽分开处理:
|
||||
// 刚性段贴着出线口,用方口端帽(0)保证切面与出线口齐平不凸出;
|
||||
// 柔性段末端在插头/插孔处,用圆头端帽包进贴图,拖拽转向时不露缝。
|
||||
_stiffLineRenderer.numCapVertices = 0;
|
||||
if (_lineRenderer.numCapVertices < 1)
|
||||
{
|
||||
_lineRenderer.numCapVertices = 6;
|
||||
}
|
||||
|
||||
// 连线起止点(末端用 PlugRoot,避免连到插头精灵中心)
|
||||
_start = _cablePanel.CableRootPos;
|
||||
@@ -84,18 +138,109 @@ namespace AibisDream.FixSystem
|
||||
void DrawLine()
|
||||
{
|
||||
var points = UpdateRope();
|
||||
Vector3 zOffset = Vector3.forward * -.3f;
|
||||
|
||||
// 刚性段走向(出线口 -> 接缝点)与接缝方向(接缝点 -> 下一个柔性点)
|
||||
Vector3 stiffDir = (points[config.stiffCount - 1] - points[0]).normalized;
|
||||
Vector3 seamOffset = points[config.stiffCount + 1] - points[config.stiffCount];
|
||||
Vector3 seamDir = seamOffset.sqrMagnitude > 1e-6f ? seamOffset.normalized : stiffDir;
|
||||
float halfWidth = _lineRenderer.startWidth * 0.5f;
|
||||
|
||||
// 渲染刚性部分
|
||||
for (int i = 0; i < config.stiffCount; i++)
|
||||
{
|
||||
_stiffLineRenderer.SetPosition(i, points[i] + Vector3.forward * -.3f);
|
||||
_stiffLineRenderer.SetPosition(i, points[i] + zOffset);
|
||||
}
|
||||
|
||||
// 渲染非刚性部分
|
||||
for (int i = 0; i < config.resolution - config.stiffCount; i++)
|
||||
// 刚性段末点沿接缝方向多画半个线宽,垫在柔性段圆头下方,避免接缝两侧出现缺口。
|
||||
// 仅在接缝方向大致顺延刚性段时外延;线松弛折返时跳过,
|
||||
// 否则条带在折角处被斜切,会闪现细长尖刺。
|
||||
if (Vector3.Dot(stiffDir, seamDir) > 0.2f)
|
||||
{
|
||||
_lineRenderer.SetPosition(i, points[i + config.stiffCount] + Vector3.forward * -.3f);
|
||||
_stiffLineRenderer.SetPosition(config.stiffCount - 1,
|
||||
points[config.stiffCount - 1] + seamDir * halfWidth + zOffset);
|
||||
}
|
||||
|
||||
// 拷贝柔性段准备渲染
|
||||
int flexCount = config.resolution - config.stiffCount;
|
||||
for (int i = 0; i < flexCount; i++)
|
||||
{
|
||||
_smoothBufferA[i] = points[i + config.stiffCount] + zOffset;
|
||||
}
|
||||
|
||||
// 柔性段首点内缩半个线宽:圆头端帽顶点正好落在接缝处,
|
||||
// 不会越过接缝、盖到出线口贴图上(柔性段排序在出线口贴图之上)。
|
||||
// 内缩量不超过首段长度的一半,防止首段退化成零长度产生扭折。
|
||||
float insetLimit = Mathf.Min(halfWidth, seamOffset.magnitude * 0.5f);
|
||||
_smoothBufferA[0] += seamDir * insetLimit;
|
||||
if (TryGetHeadDir(out Vector3 headRenderDir))
|
||||
{
|
||||
BlendHeadRenderControls(flexCount, headRenderDir);
|
||||
}
|
||||
if (TryGetTailDir(out Vector3 tailRenderDir))
|
||||
{
|
||||
BlendTailRenderControls(flexCount, tailRenderDir);
|
||||
}
|
||||
|
||||
// Chaikin 切角细分:物理点直接连线在转折处(尤其插头端刚性段与曲线的交界)
|
||||
// 过于生硬,细分把折角切圆;首尾端点保留,两端的进出方向不变。
|
||||
Vector3[] src = _smoothBufferA;
|
||||
Vector3[] dst = _smoothBufferB;
|
||||
int count = flexCount;
|
||||
for (int it = 0; it < _smoothIterations; it++)
|
||||
{
|
||||
count = Chaikin(src, count, dst);
|
||||
(src, dst) = (dst, src);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_lineRenderer.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;
|
||||
}
|
||||
|
||||
private void BlendHeadRenderControls(int flexCount, Vector3 headDir)
|
||||
{
|
||||
if (flexCount < 2) return;
|
||||
|
||||
Vector3 start = _smoothBufferA[0];
|
||||
Vector3 next = _smoothBufferA[1];
|
||||
float blendLength = Mathf.Min(_headStiffLength, Vector3.Distance(start, next));
|
||||
Vector3 tangentTarget = start + headDir * blendLength;
|
||||
_smoothBufferA[1] = Vector3.Lerp(next, tangentTarget, _headRenderBlend);
|
||||
}
|
||||
|
||||
private void BlendTailRenderControls(int flexCount, Vector3 tailDir)
|
||||
{
|
||||
int endIndex = flexCount - 1;
|
||||
int prevIndex = endIndex - 1;
|
||||
if (prevIndex < 0) return;
|
||||
|
||||
Vector3 end = _smoothBufferA[endIndex];
|
||||
Vector3 previous = _smoothBufferA[prevIndex];
|
||||
Vector3 incoming = previous - end;
|
||||
if (incoming.sqrMagnitude < 1e-6f) return;
|
||||
|
||||
float angleWeight = Mathf.InverseLerp(-0.1f, 0.7f, Vector3.Dot(incoming.normalized, tailDir));
|
||||
float blendLength = Mathf.Min(_tailStiffLength, Vector3.Distance(end, previous));
|
||||
Vector3 tangentTarget = end + tailDir * blendLength;
|
||||
_smoothBufferA[prevIndex] = Vector3.Lerp(previous, tangentTarget, _tailRenderBlend * angleWeight);
|
||||
}
|
||||
|
||||
Vector3[] UpdateRope()
|
||||
@@ -110,19 +255,36 @@ namespace AibisDream.FixSystem
|
||||
for (int i = 0; i < config.stiffCount; i++)
|
||||
{
|
||||
float t_stiff = (float)i / (config.stiffCount - 1);
|
||||
_points[i] = _start.position + dir * (config.stiffLength * t_stiff);
|
||||
_points[i] = _start.position + dir * (_headStiffLength * t_stiff);
|
||||
}
|
||||
|
||||
// 设置末端点
|
||||
_points[^1] = _end.position;
|
||||
|
||||
// 插头端刚性段:让线先顺着插头轴向(插头中心 -> 线缆接点)伸出一小段再弯曲,
|
||||
// 否则线会以任意角度横着搭在插头尾部,看起来没有"插进去"。
|
||||
// 末端挂在 Socket 上时接点没有可靠的轴向,保持原有纯物理表现。
|
||||
bool pinTail = TryGetTailDir(out Vector3 tailDir);
|
||||
int tailPinned = 1;
|
||||
if (pinTail)
|
||||
{
|
||||
tailPinned = _tailStiffCount;
|
||||
for (int j = 0; j < tailPinned; j++)
|
||||
{
|
||||
float tTail = (float)j / (tailPinned - 1);
|
||||
_points[_points.Length - 1 - j] = _end.position + tailDir * (_tailStiffLength * tTail);
|
||||
}
|
||||
}
|
||||
|
||||
int simEndExclusive = _points.Length - tailPinned;
|
||||
|
||||
// 只对非刚性部分进行物理模拟
|
||||
for (int ik = 0; ik < config.k; ik++)
|
||||
{
|
||||
// 确保非刚性部分的第一个点与刚性部分的最后一个点重合
|
||||
_points[config.stiffCount] = _points[config.stiffCount - 1];
|
||||
|
||||
for (int i = config.stiffCount + 1; i < _points.Length - 1; i++)
|
||||
for (int i = config.stiffCount + 1; i < simEndExclusive; i++)
|
||||
{
|
||||
Vector3 offsetPrev = _points[i - 1] - _points[i];
|
||||
Vector3 offsetNext = _points[i + 1] - _points[i];
|
||||
@@ -131,15 +293,58 @@ namespace AibisDream.FixSystem
|
||||
_points[i] += velocity * Time.deltaTime / config.k;
|
||||
}
|
||||
|
||||
for (int i = config.stiffCount + 1; i < _points.Length - 1; i++)
|
||||
for (int i = config.stiffCount + 1; i < simEndExclusive; i++)
|
||||
{
|
||||
_points[i] += Vector3.down * (9.8f * Time.deltaTime) / config.k;
|
||||
}
|
||||
}
|
||||
|
||||
if (pinTail)
|
||||
{
|
||||
RelaxTailApproach(tailPinned, tailDir);
|
||||
}
|
||||
|
||||
return _points;
|
||||
}
|
||||
|
||||
private bool TryGetHeadDir(out Vector3 headDir)
|
||||
{
|
||||
headDir = Vector3.zero;
|
||||
if (_start == null || _cablePanel == null || _cablePanel.CableReelRef == null) return false;
|
||||
|
||||
Vector3 offset = _start.position - _cablePanel.CableReelRef.transform.position;
|
||||
if (offset.sqrMagnitude < 1e-6f) return false;
|
||||
|
||||
headDir = offset.normalized;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RelaxTailApproach(int tailPinned, Vector3 tailDir)
|
||||
{
|
||||
int firstTailIndex = _points.Length - tailPinned;
|
||||
int approachIndex = firstTailIndex - 1;
|
||||
if (approachIndex <= config.stiffCount) return;
|
||||
|
||||
Vector3 target = _points[firstTailIndex] + tailDir * _tailStiffLength;
|
||||
_points[approachIndex] = Vector3.Lerp(_points[approachIndex], target, 0.22f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插头轴向:从插头中心指向线缆接点(PlugRoot)、并继续向外的方向。
|
||||
/// 仅当末端连接点是 PlugRoot 时有效;连接到 Socket 时返回 false。
|
||||
/// </summary>
|
||||
private bool TryGetTailDir(out Vector3 tailDir)
|
||||
{
|
||||
tailDir = Vector3.zero;
|
||||
if (_end == null || _end != _cablePanel.PlugRef.PlugRoot || _end.parent == null) return false;
|
||||
|
||||
Vector3 offset = _end.position - _end.parent.position;
|
||||
if (offset.sqrMagnitude < 1e-6f) return false;
|
||||
|
||||
tailDir = offset.normalized;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为线缆设置末端点
|
||||
/// </summary>
|
||||
@@ -149,23 +354,101 @@ namespace AibisDream.FixSystem
|
||||
_end = endPos;
|
||||
}
|
||||
|
||||
public void SetSorting(string sortingLayerName, int sortingOrder)
|
||||
{
|
||||
ApplySorting(_lineRenderer, sortingLayerName, sortingOrder);
|
||||
ApplySorting(_stiffLineRenderer, sortingLayerName, sortingOrder);
|
||||
}
|
||||
|
||||
public void RestoreDefaultSorting()
|
||||
{
|
||||
_defaultLineSorting.ApplyTo(_lineRenderer);
|
||||
_defaultStiffLineSorting.ApplyTo(_stiffLineRenderer);
|
||||
}
|
||||
|
||||
public Vector3 GetDirection()
|
||||
{
|
||||
return _points[^1] - _points[^2];
|
||||
}
|
||||
|
||||
private static void ApplySorting(LineRenderer lineRenderer, string sortingLayerName, int sortingOrder)
|
||||
{
|
||||
if (lineRenderer == null) return;
|
||||
|
||||
lineRenderer.sortingLayerName = sortingLayerName;
|
||||
lineRenderer.sortingOrder = sortingOrder;
|
||||
}
|
||||
|
||||
private readonly struct RendererSortingState
|
||||
{
|
||||
private readonly string _sortingLayerName;
|
||||
private readonly int _sortingOrder;
|
||||
|
||||
private RendererSortingState(string sortingLayerName, int sortingOrder)
|
||||
{
|
||||
_sortingLayerName = sortingLayerName;
|
||||
_sortingOrder = sortingOrder;
|
||||
}
|
||||
|
||||
public static RendererSortingState From(LineRenderer lineRenderer)
|
||||
{
|
||||
return new RendererSortingState(lineRenderer.sortingLayerName, lineRenderer.sortingOrder);
|
||||
}
|
||||
|
||||
public void ApplyTo(LineRenderer lineRenderer)
|
||||
{
|
||||
if (lineRenderer == null) return;
|
||||
|
||||
lineRenderer.sortingLayerName = _sortingLayerName;
|
||||
lineRenderer.sortingOrder = _sortingOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct CableConfig
|
||||
{
|
||||
[Tooltip("物理采样点总数。越大曲线越顺滑,插头端/出线口端刚性点也从这里扣除")]
|
||||
public int resolution;
|
||||
|
||||
[Tooltip("两端距离小于该值时张力取 forceMin(线最松弛)")]
|
||||
public float dstMin;
|
||||
|
||||
[Tooltip("两端距离大于该值时张力取 forceMax(线完全绷直)。想让线平时带点垂坠感就调大它")]
|
||||
public float dstMax;
|
||||
|
||||
[Tooltip("松弛时的张力。越小越软、下垂越明显")]
|
||||
public float forceMin;
|
||||
|
||||
[Tooltip("绷紧时的张力。越大线越像直棍,调小会保留一点弧度")]
|
||||
public float forceMax;
|
||||
|
||||
[Tooltip("每帧物理迭代次数,影响收敛稳定性,一般不用动")]
|
||||
public int k;
|
||||
public int stiffCount; // 刚性部分点数
|
||||
public float stiffLength; // 刚性部分长度
|
||||
|
||||
[Tooltip("出线口端刚性点数:线离开出线口时保持笔直的采样点数量")]
|
||||
public int stiffCount;
|
||||
|
||||
[Tooltip("出线口端刚性段长度(世界单位)")]
|
||||
public float stiffLength;
|
||||
|
||||
[Tooltip("Outlet-side rigid section length. Zero uses the tuned default.")]
|
||||
public float headStiffLength;
|
||||
|
||||
[Tooltip("Render-only blend for the first soft control point after the outlet-side rigid section.")]
|
||||
public float headRenderBlend;
|
||||
|
||||
[Tooltip("插头端刚性点数(含末端点,最小 2)。0 = 用默认值 3")]
|
||||
public int tailStiffCount;
|
||||
|
||||
[Tooltip("插头端刚性段长度:线沿插头轴向笔直伸出多远再开始弯。0 = 用 stiffLength 的 2 倍")]
|
||||
public float tailStiffLength;
|
||||
|
||||
[Tooltip("Render-only blend for the first soft control point after the plug-side rigid section.")]
|
||||
public float tailRenderBlend;
|
||||
|
||||
[Tooltip("渲染切角细分次数(1~4):越大转折越圆。0 = 用默认值 2")]
|
||||
public int smoothIterations;
|
||||
|
||||
public static CableConfig GeneDefaultConfig()
|
||||
{
|
||||
@@ -178,8 +461,14 @@ namespace AibisDream.FixSystem
|
||||
forceMax = 150f,
|
||||
k = 10,
|
||||
stiffCount = 3,
|
||||
stiffLength = 0.1f
|
||||
stiffLength = 0.1f,
|
||||
headStiffLength = 0f,
|
||||
headRenderBlend = 0f,
|
||||
tailStiffCount = 0,
|
||||
tailStiffLength = 0f,
|
||||
tailRenderBlend = 0f,
|
||||
smoothIterations = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user