From 3092c3554ba7f0c4ec8f658b0d9e04c9996c394d Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Tue, 7 Jul 2026 22:21:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(fix-system):=20=E4=BC=98=E5=8C=96=E7=BA=BF?= =?UTF-8?q?=E7=BC=86=E6=B8=B2=E6=9F=93=E4=B8=8E=E6=8B=96=E6=8B=BD=E6=8E=92?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Scripts/FixSystemNew/BodyModule/Cable.cs | 317 +++++++++++++++++- Assets/Scripts/FixSystemNew/Cable/Plug.cs | 3 +- .../Screen System/FixPanel/CablePanel.cs | 4 + 3 files changed, 309 insertions(+), 15 deletions(-) diff --git a/Assets/Scripts/FixSystemNew/BodyModule/Cable.cs b/Assets/Scripts/FixSystemNew/BodyModule/Cable.cs index 60b664e9d..9cec05583 100644 --- a/Assets/Scripts/FixSystemNew/BodyModule/Cable.cs +++ b/Assets/Scripts/FixSystemNew/BodyModule/Cable.cs @@ -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.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; } + /// + /// 计算生效配置:新加的字段在旧序列化数据里是 0,取兜底默认;并做范围保护。 + /// + 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().sortingLayerName; _stiffLineRenderer.sortingOrder = _cablePanel.CableReelRef.GetComponent().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]); + } + } + + /// + /// 一次 Chaikin 切角细分:保留首尾点,每段取 1/4、3/4 两个切分点,输出点数为 2n。 + /// + 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); + } + + /// + /// 插头轴向:从插头中心指向线缆接点(PlugRoot)、并继续向外的方向。 + /// 仅当末端连接点是 PlugRoot 时有效;连接到 Socket 时返回 false。 + /// + 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; + } + /// /// 为线缆设置末端点 /// @@ -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 }; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/FixSystemNew/Cable/Plug.cs b/Assets/Scripts/FixSystemNew/Cable/Plug.cs index bfde86d9c..ed6f8390a 100644 --- a/Assets/Scripts/FixSystemNew/Cable/Plug.cs +++ b/Assets/Scripts/FixSystemNew/Cable/Plug.cs @@ -188,6 +188,7 @@ namespace AibisDream.FixSystem transform.DOMove(_startPos, 0.1f).OnComplete(() => { cablePanel.CableReelRef.ResetRotation(); + cablePanel.CableRef.RestoreDefaultSorting(); cablePanel.SetCableMode(CableMode.Physic, transform); }); } @@ -217,4 +218,4 @@ namespace AibisDream.FixSystem return _startPos; } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs b/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs index f0d5fa3e1..1a7e3910a 100644 --- a/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs +++ b/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs @@ -14,7 +14,9 @@ namespace AibisDream.FixSystem public class CablePanel : MonoBehaviour, IOperatorPanel { + private const string SortingLayerDragging = "Tools"; private const int SortingOrderRetracted = 41; + private const int CableSortingOrderDragging = 47; #region 组件索引 @@ -214,6 +216,7 @@ namespace AibisDream.FixSystem var isSameSocket = curSocket == socket; curSocket = socket; CableRef.SetEndPos(socket.GetSocketPos()); + CableRef.RestoreDefaultSorting(); AudioManager.Instance.PlaySfx("event:/FollowInput/plugin"); if (!isSameSocket @@ -251,6 +254,7 @@ namespace AibisDream.FixSystem // 点击时切换到视觉线缆 SetCableMode(CableMode.Visual); + CableRef.SetSorting(SortingLayerDragging, CableSortingOrderDragging); // 打开插孔 BodyModuleSystem.OpenModuleSlots();