using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace NBF { /// /// Used for FLine visual rendering. /// Reads rigid body nodes from the cable and simulates extra render points between logic nodes. /// [RequireComponent(typeof(FLine))] [RequireComponent(typeof(LineRenderer))] public class FLineRenderer : MonoBehaviour { private sealed class VisualSegmentState { public int SegmentIndex; public int Subdivisions; public LineRenderer Renderer; public GameObject RendererObject; public Vector3[] Points; public Vector3[] PrevPoints; public float[] EdgeRestLengths; public bool[] SupportedPoints; public Vector3[] SmoothPoints; public Vector3[] SegmentPoints; public int SmoothCapacity; public int SegmentCapacity; public int PointCount => Points != null ? Points.Length : 0; } [Header("Source")] [SerializeField] private FLine cable; [SerializeField, Min(1)] private int visualSubdivisionsPerSegment = 5; [SerializeField] private List visualSubdivisionsBySegment = new List(); [SerializeField, Min(0f)] private float visualLengthTrim = 0.05f; [Header("Visual Physics")] [SerializeField, Min(0f)] private float gravityStrength = 9.81f; [SerializeField, Range(0f, 1f)] private float velocityDamping = 0.985f; [SerializeField, Range(1, 40)] private int solverIterations = 12; [SerializeField, Range(0f, 1f)] private float stiffness = 1f; [SerializeField] private Vector3 windVelocity = Vector3.zero; [SerializeField, Min(0f)] private float windResponse = 0.5f; [SerializeField, Min(0f)] private float maxWindAcceleration = 30f; [SerializeField, Min(0f)] private float maxPointSpeed = 25f; [Header("Ground")] [SerializeField] private bool stopOnGround = false; [SerializeField] private LayerMask groundMask = 0; [SerializeField, Min(0f)] private float groundRadius = 0.01f; [SerializeField, Min(0f)] private float groundCastHeight = 1f; [SerializeField, Min(0.01f)] private float groundCastDistance = 3f; [Header("Water")] [SerializeField] private bool stopOnWaterSurface = false; [SerializeField] private float waterSurfaceIgnoreNodeCount = 3; [SerializeField] private float waterLevelY = 0f; [SerializeField, Min(0f)] private float waterSurfaceOffset = 0.002f; [Header("Line Renderer")] [SerializeField, Min(0.0001f)] private float lineWidth = 0.01f; [SerializeField] private bool smoothLine = true; [SerializeField, Range(0, 4)] private int smoothingIterations = 2; [SerializeField, Range(0.05f, 0.45f)] private float smoothingStrength = 0.25f; [SerializeField, Min(0f)] private float lengthTolerance = 0.0005f; [Header("Taut Stabilization")] [SerializeField, Min(0f)] private float tautSlackThreshold = 0.01f; [SerializeField, Range(0f, 1f)] private float tautPositionBlend = 0.85f; [SerializeField, Range(0f, 1f)] private float tautVelocityRetention = 0.05f; [Header("Debug")] [SerializeField] private bool drawVisualNodes = false; [SerializeField] private float gizmoNodeRadius = 0.015f; private readonly List _bodies = new List(); private readonly List _segments = new List(); private LineRenderer _lineRenderer; private bool _needsRebuild = true; private const float MinDistance = 0.0001f; private const float MinDeltaTime = 0.0001f; private const string SegmentRendererNamePrefix = "FLineRenderer Segment "; private void Awake() { cable = cable ? cable : GetComponent(); _lineRenderer = GetComponent(); ConfigureLineRenderer(_lineRenderer); } private void OnEnable() { _needsRebuild = true; } private void OnDisable() { ClearLine(); } private void OnDestroy() { DestroyExtraSegmentRenderers(); } private void OnValidate() { visualSubdivisionsPerSegment = Mathf.Max(1, visualSubdivisionsPerSegment); gravityStrength = Mathf.Max(0f, gravityStrength); visualLengthTrim = Mathf.Max(0f, visualLengthTrim); velocityDamping = Mathf.Clamp01(velocityDamping); solverIterations = Mathf.Clamp(solverIterations, 1, 40); stiffness = Mathf.Clamp01(stiffness); windResponse = Mathf.Max(0f, windResponse); maxWindAcceleration = Mathf.Max(0f, maxWindAcceleration); maxPointSpeed = Mathf.Max(0f, maxPointSpeed); groundRadius = Mathf.Max(0f, groundRadius); groundCastHeight = Mathf.Max(0f, groundCastHeight); groundCastDistance = Mathf.Max(0.01f, groundCastDistance); waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset); lineWidth = Mathf.Max(0.0001f, lineWidth); smoothingIterations = Mathf.Clamp(smoothingIterations, 0, 4); smoothingStrength = Mathf.Clamp(smoothingStrength, 0.05f, 0.45f); lengthTolerance = Mathf.Max(0f, lengthTolerance); tautSlackThreshold = Mathf.Max(0f, tautSlackThreshold); tautPositionBlend = Mathf.Clamp01(tautPositionBlend); tautVelocityRetention = Mathf.Clamp01(tautVelocityRetention); gizmoNodeRadius = Mathf.Max(0f, gizmoNodeRadius); if (visualSubdivisionsBySegment == null) visualSubdivisionsBySegment = new List(); for (int i = 0; i < visualSubdivisionsBySegment.Count; i++) visualSubdivisionsBySegment[i] = Mathf.Max(0, visualSubdivisionsBySegment[i]); _needsRebuild = true; if (_lineRenderer == null) _lineRenderer = GetComponent(); ConfigureLineRenderer(_lineRenderer); SyncSegmentRendererStyles(); } private void FixedUpdate() { if (!PrepareSimulation()) return; float dt = Mathf.Max(Time.fixedDeltaTime, MinDeltaTime); if (!stopOnGround && !stopOnWaterSurface) ClearSupportedPoints(); LockCableNodes(); SimulateFreePoints(dt); RefreshRestLengths(); for (int i = 0; i < solverIterations; i++) { LockCableNodes(); SolveDistanceConstraints(); ApplySurfaceConstraints(); } EnforceExactSegmentLengths(); StabilizeTautSegments(); LockCableNodes(); } private void LateUpdate() { if (!PrepareSimulation()) { ClearLine(); return; } LockCableNodes(); DrawLine(); } public void Rebuild() { _needsRebuild = true; } public void SetWind(Vector3 velocity, float response) { windVelocity = velocity; windResponse = Mathf.Max(0f, response); } private bool PrepareSimulation() { if (cable == null) cable = GetComponent(); if (_lineRenderer == null) { _lineRenderer = GetComponent(); ConfigureLineRenderer(_lineRenderer); } if (cable == null || !RefreshBodies()) return false; EnsureSegmentStates(); return _segments.Count > 0; } private bool RefreshBodies() { List sourceBodies = cable.GetConnectedBodies(); if (sourceBodies == null || sourceBodies.Count < 2) return false; bool changed = sourceBodies.Count != _bodies.Count; if (!changed) { for (int i = 0; i < sourceBodies.Count; i++) { if (sourceBodies[i] != _bodies[i]) { changed = true; break; } } } if (changed) { _bodies.Clear(); _bodies.AddRange(sourceBodies); _needsRebuild = true; } for (int i = 0; i < _bodies.Count; i++) { if (_bodies[i] == null) return false; } return true; } private void EnsureSegmentStates() { int segmentCount = _bodies.Count - 1; EnsureSegmentRendererCount(segmentCount); for (int segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) { VisualSegmentState state = _segments[segmentIndex]; state.SegmentIndex = segmentIndex; state.Renderer = GetOrCreateSegmentRenderer(segmentIndex, state); int subdivisions = GetVisualSubdivisionsForSegment(segmentIndex); int pointCount = subdivisions + 1; if (state.Points == null || state.PointCount != pointCount || state.Subdivisions != subdivisions || _needsRebuild) { state.Subdivisions = subdivisions; state.Points = new Vector3[pointCount]; state.PrevPoints = new Vector3[pointCount]; state.EdgeRestLengths = new float[subdivisions]; state.SupportedPoints = new bool[pointCount]; state.SmoothPoints = null; state.SegmentPoints = null; state.SmoothCapacity = 0; state.SegmentCapacity = 0; InitializeSegmentPoints(state); } } _needsRebuild = false; } private void EnsureSegmentRendererCount(int segmentCount) { while (_segments.Count < segmentCount) _segments.Add(new VisualSegmentState()); for (int i = _segments.Count - 1; i >= segmentCount; i--) { DestroySegmentRenderer(_segments[i]); _segments.RemoveAt(i); } } private LineRenderer GetOrCreateSegmentRenderer(int segmentIndex, VisualSegmentState state) { if (segmentIndex == 0) return _lineRenderer; if (state.Renderer != null) return state.Renderer; string rendererName = SegmentRendererNamePrefix + segmentIndex; GameObject rendererObject; LineRenderer renderer; Transform child = transform.Find(rendererName); if (child != null) { rendererObject = child.gameObject; renderer = child.GetComponent(); } else { rendererObject = new GameObject(rendererName); rendererObject.transform.SetParent(transform, false); renderer = null; } if (renderer == null) renderer = rendererObject.AddComponent(); state.RendererObject = rendererObject; state.Renderer = renderer; ConfigureLineRenderer(renderer); return renderer; } private void DestroyExtraSegmentRenderers() { for (int i = 1; i < _segments.Count; i++) DestroySegmentRenderer(_segments[i]); } private void DestroySegmentRenderer(VisualSegmentState state) { if (state == null) return; if (state.Renderer == _lineRenderer) { state.Renderer = null; state.RendererObject = null; return; } GameObject rendererObject = state.RendererObject; if (rendererObject == null && state.Renderer != null) rendererObject = state.Renderer.gameObject; if (state.Renderer != null) state.Renderer.positionCount = 0; state.Renderer = null; state.RendererObject = null; if (rendererObject == null) return; if (Application.isPlaying) Destroy(rendererObject); else DestroyImmediate(rendererObject); } private void InitializeSegmentPoints(VisualSegmentState state) { int subdivisions = Mathf.Max(1, state.Subdivisions); Vector3 start = _bodies[state.SegmentIndex].position; Vector3 end = _bodies[state.SegmentIndex + 1].position; for (int i = 0; i <= subdivisions; i++) { float t = i / (float)subdivisions; Vector3 point = Vector3.Lerp(start, end, t); state.Points[i] = point; state.PrevPoints[i] = point; state.SupportedPoints[i] = false; } RefreshRestLengths(state); } private void RefreshRestLengths() { for (int i = 0; i < _segments.Count; i++) RefreshRestLengths(_segments[i]); } private void RefreshRestLengths(VisualSegmentState state) { float currentDistance = Vector3.Distance( _bodies[state.SegmentIndex].position, _bodies[state.SegmentIndex + 1].position ); float maxLength = cable.GetSegmentMaxLength(state.SegmentIndex); if (maxLength <= 0f) maxLength = currentDistance; float visualRest = Mathf.Max(maxLength - visualLengthTrim, 0f); float totalRest = Mathf.Max(visualRest, currentDistance); float edgeRest = totalRest / Mathf.Max(1, state.Subdivisions); for (int i = 0; i < state.EdgeRestLengths.Length; i++) state.EdgeRestLengths[i] = edgeRest; } private void ConfigureLineRenderer(LineRenderer renderer) { if (renderer == null) return; if (_lineRenderer != null && renderer != _lineRenderer) CopyLineRendererTemplate(_lineRenderer, renderer); renderer.useWorldSpace = true; renderer.loop = false; renderer.startWidth = lineWidth; renderer.endWidth = lineWidth; } private void CopyLineRendererTemplate(LineRenderer template, LineRenderer target) { if (template == null || target == null || template == target) return; target.alignment = template.alignment; target.textureMode = template.textureMode; target.shadowBias = template.shadowBias; target.generateLightingData = template.generateLightingData; target.numCapVertices = template.numCapVertices; target.numCornerVertices = template.numCornerVertices; target.startColor = template.startColor; target.endColor = template.endColor; target.colorGradient = template.colorGradient; target.widthCurve = template.widthCurve; target.widthMultiplier = template.widthMultiplier; target.sharedMaterial = template.sharedMaterial; target.sortingLayerID = template.sortingLayerID; target.sortingOrder = template.sortingOrder; target.shadowCastingMode = template.shadowCastingMode; target.receiveShadows = template.receiveShadows; target.enabled = template.enabled; } private void SyncSegmentRendererStyles() { for (int i = 0; i < _segments.Count; i++) ConfigureLineRenderer(_segments[i].Renderer); } private void ClearLine() { if (_lineRenderer != null) _lineRenderer.positionCount = 0; for (int i = 1; i < _segments.Count; i++) { LineRenderer renderer = _segments[i].Renderer; if (renderer != null) renderer.positionCount = 0; } } private void LockCableNodes() { for (int i = 0; i < _segments.Count; i++) LockCableNodes(_segments[i]); } private void LockCableNodes(VisualSegmentState state) { if (state.Points == null || state.PrevPoints == null || state.PointCount < 2) return; int lastIndex = state.PointCount - 1; Vector3 start = _bodies[state.SegmentIndex].position; Vector3 end = _bodies[state.SegmentIndex + 1].position; state.Points[0] = start; state.PrevPoints[0] = start; state.SupportedPoints[0] = false; state.Points[lastIndex] = end; state.PrevPoints[lastIndex] = end; state.SupportedPoints[lastIndex] = false; } private void SimulateFreePoints(float dt) { for (int i = 0; i < _segments.Count; i++) SimulateFreePoints(_segments[i], dt); } private void SimulateFreePoints(VisualSegmentState state, float dt) { if (state.PointCount <= 2) return; float dt2 = dt * dt; float maxDisp = maxPointSpeed > 0f ? maxPointSpeed * dt : float.PositiveInfinity; float maxDispSq = maxDisp * maxDisp; Vector3 gravity = Vector3.down * gravityStrength; bool useWind = windResponse > 0f && windVelocity.sqrMagnitude > 0.0001f; float chordLength = Vector3.Distance( _bodies[state.SegmentIndex].position, _bodies[state.SegmentIndex + 1].position ); float tautness = GetSegmentTautness(state.SegmentIndex, chordLength); float mobility = 1f - tautness; for (int i = 1; i < state.PointCount - 1; i++) { if (state.SupportedPoints[i]) { state.PrevPoints[i] = state.Points[i]; continue; } Vector3 current = state.Points[i]; Vector3 displacement = current - state.PrevPoints[i]; if (maxPointSpeed > 0f && displacement.sqrMagnitude > maxDispSq) displacement = displacement.normalized * maxDisp; displacement *= Mathf.Lerp(tautVelocityRetention, 1f, mobility); Vector3 acceleration = gravity; if (useWind) { Vector3 velocity = displacement / dt; Vector3 windAcceleration = (windVelocity - velocity) * windResponse; if (maxWindAcceleration > 0f && windAcceleration.sqrMagnitude > maxWindAcceleration * maxWindAcceleration) windAcceleration = windAcceleration.normalized * maxWindAcceleration; acceleration += windAcceleration; } acceleration *= mobility; state.PrevPoints[i] = current; state.Points[i] = current + displacement * velocityDamping + acceleration * dt2; } } private void SolveDistanceConstraints() { float solveStiffness = Mathf.Clamp01(stiffness); for (int i = 0; i < _segments.Count; i++) SolveDistanceConstraints(_segments[i], solveStiffness); } private void SolveDistanceConstraints(VisualSegmentState state, float solveStiffness) { if (state.EdgeRestLengths == null || state.EdgeRestLengths.Length == 0) return; SolveSweep(state, 0, state.EdgeRestLengths.Length, 1, solveStiffness); SolveSweep(state, state.EdgeRestLengths.Length - 1, -1, -1, solveStiffness); } private void SolveSweep(VisualSegmentState state, int start, int endExclusive, int step, float solveStiffness) { float chordLength = Vector3.Distance(state.Points[0], state.Points[state.PointCount - 1]); bool useMaxOnly = ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state); for (int edge = start; edge != endExclusive; edge += step) { int aIndex = edge; int bIndex = edge + 1; Vector3 a = state.Points[aIndex]; Vector3 b = state.Points[bIndex]; Vector3 delta = b - a; float sqrMagnitude = delta.sqrMagnitude; if (sqrMagnitude < 1e-10f) continue; float distance = Mathf.Sqrt(sqrMagnitude); float rest = Mathf.Max(state.EdgeRestLengths[edge], MinDistance); if (useMaxOnly && distance <= rest + lengthTolerance) continue; float error = useMaxOnly ? Mathf.Max(distance - rest, 0f) : distance - rest; Vector3 correction = delta * (error / distance * solveStiffness); bool aLocked = aIndex == 0; bool bLocked = bIndex == state.PointCount - 1; if (!aLocked && !bLocked) { state.Points[aIndex] = a + correction * 0.5f; state.Points[bIndex] = b - correction * 0.5f; } else if (aLocked && !bLocked) { state.Points[bIndex] = b - correction; } else if (!aLocked && bLocked) { state.Points[aIndex] = a + correction; } } } private void ApplySurfaceConstraints() { if (!stopOnGround && !stopOnWaterSurface) return; for (int i = 0; i < _segments.Count; i++) ApplySurfaceConstraints(_segments[i]); } private void ApplySurfaceConstraints(VisualSegmentState state) { if (state.PointCount <= 2) return; for (int i = 0; i < state.SupportedPoints.Length; i++) state.SupportedPoints[i] = false; for (int i = 1; i < state.PointCount - 1; i++) { float minY = SampleSurfaceMinY(state, i); if (float.IsNegativeInfinity(minY)) continue; Vector3 point = state.Points[i]; if (point.y < minY) { point.y = minY; state.Points[i] = point; Vector3 prev = state.PrevPoints[i]; if (prev.y < minY) { prev.y = minY; state.PrevPoints[i] = prev; } } if (state.Points[i].y <= minY + lengthTolerance) state.SupportedPoints[i] = true; } } private void EnforceExactSegmentLengths() { for (int i = 0; i < _segments.Count; i++) EnforceExactSegmentLengths(_segments[i]); ApplySurfaceConstraints(); } private void EnforceExactSegmentLengths(VisualSegmentState state) { if (state.Subdivisions <= 1 || state.PointCount <= 2) return; Vector3 start = state.Points[0]; Vector3 end = state.Points[state.PointCount - 1]; float chordLength = Vector3.Distance(start, end); if (ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state)) return; float targetLength = GetTargetSegmentLength(state.SegmentIndex, chordLength); float currentLength = ComputeSegmentLength(state); if (currentLength <= targetLength + lengthTolerance) return; float blend = FindSegmentCompressionBlend(state, start, end, targetLength); ApplySegmentCompression(state, start, end, blend); } private void StabilizeTautSegments() { for (int i = 0; i < _segments.Count; i++) StabilizeTautSegments(_segments[i]); ApplySurfaceConstraints(); } private void StabilizeTautSegments(VisualSegmentState state) { if (state.Subdivisions <= 1 || state.PointCount <= 2) return; Vector3 start = state.Points[0]; Vector3 end = state.Points[state.PointCount - 1]; float chordLength = Vector3.Distance(start, end); if (ShouldUseMaxOnlyConstraint(state.SegmentIndex, chordLength, state)) return; float tautness = GetSegmentTautness(state.SegmentIndex, chordLength); if (tautness <= 0f) return; float positionBlend = tautPositionBlend * tautness; bool fullyTaut = tautness >= 0.999f; for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++) { float t = pointIndex / (float)state.Subdivisions; Vector3 linear = Vector3.Lerp(start, end, t); if (fullyTaut) { state.Points[pointIndex] = linear; state.PrevPoints[pointIndex] = linear; continue; } Vector3 stabilized = Vector3.Lerp(state.Points[pointIndex], linear, positionBlend); state.Points[pointIndex] = stabilized; state.PrevPoints[pointIndex] = Vector3.Lerp(state.PrevPoints[pointIndex], stabilized, positionBlend); } } private float ComputeSegmentLength(VisualSegmentState state) { float length = 0f; for (int i = 0; i < state.PointCount - 1; i++) length += Vector3.Distance(state.Points[i], state.Points[i + 1]); return length; } private float FindSegmentCompressionBlend(VisualSegmentState state, Vector3 start, Vector3 end, float targetLength) { float minBlend = 0f; float maxBlend = 1f; for (int iteration = 0; iteration < 10; iteration++) { float midBlend = (minBlend + maxBlend) * 0.5f; float midLength = ComputeCompressedSegmentLength(state, start, end, midBlend); if (midLength > targetLength) maxBlend = midBlend; else minBlend = midBlend; } return minBlend; } private float ComputeCompressedSegmentLength(VisualSegmentState state, Vector3 start, Vector3 end, float blend) { Vector3 previous = start; float length = 0f; for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++) { float t = pointIndex / (float)state.Subdivisions; Vector3 linear = Vector3.Lerp(start, end, t); Vector3 current = state.Points[pointIndex]; Vector3 blended = Vector3.Lerp(linear, current, blend); length += Vector3.Distance(previous, blended); previous = blended; } length += Vector3.Distance(previous, end); return length; } private void ApplySegmentCompression(VisualSegmentState state, Vector3 start, Vector3 end, float blend) { for (int pointIndex = 1; pointIndex < state.PointCount - 1; pointIndex++) { float t = pointIndex / (float)state.Subdivisions; Vector3 linear = Vector3.Lerp(start, end, t); Vector3 compressed = Vector3.Lerp(linear, state.Points[pointIndex], blend); state.Points[pointIndex] = compressed; state.PrevPoints[pointIndex] = compressed; } } private float SampleSurfaceMinY(VisualSegmentState state, int pointIndex) { Vector3 point = state.Points[pointIndex]; float minY = float.NegativeInfinity; if (stopOnGround && groundMask.value != 0) { Vector3 origin = point + Vector3.up * groundCastHeight; if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, QueryTriggerInteraction.Ignore)) { minY = Mathf.Max(minY, hit.point.y + groundRadius); } } if (stopOnWaterSurface && state.SegmentIndex == 0) { if (pointIndex > waterSurfaceIgnoreNodeCount && pointIndex < state.PointCount - waterSurfaceIgnoreNodeCount) minY = Mathf.Max(minY, waterLevelY + waterSurfaceOffset); } return minY; } private void ClearSupportedPoints() { for (int i = 0; i < _segments.Count; i++) { bool[] supportedPoints = _segments[i].SupportedPoints; if (supportedPoints == null) continue; for (int pointIndex = 0; pointIndex < supportedPoints.Length; pointIndex++) supportedPoints[pointIndex] = false; } } private float GetTargetSegmentLength(int segmentIndex, float chordLength) { float maxLength = cable.GetSegmentMaxLength(segmentIndex); if (maxLength <= 0f) maxLength = chordLength; return Mathf.Max(maxLength - visualLengthTrim, chordLength); } private float GetSegmentTautness(int segmentIndex, float chordLength) { float targetLength = GetTargetSegmentLength(segmentIndex, chordLength); float slack = Mathf.Max(targetLength - chordLength, 0f); if (tautSlackThreshold <= 0f) return slack <= lengthTolerance ? 1f : 0f; return 1f - Mathf.Clamp01(slack / tautSlackThreshold); } private bool ShouldUseMaxOnlyConstraint(int segmentIndex, float chordLength, VisualSegmentState state) { if (IsSegmentSupported(state)) return true; return GetSegmentTautness(segmentIndex, chordLength) < 0.999f; } private bool IsSegmentSupported(VisualSegmentState state) { if (state.SupportedPoints == null || state.PointCount <= 2) return false; for (int i = 1; i < state.PointCount - 1; i++) { if (state.SupportedPoints[i]) return true; } return false; } private int GetVisualSubdivisionsForSegment(int segmentIndex) { if (segmentIndex >= 0 && segmentIndex < visualSubdivisionsBySegment.Count) { int overrideValue = visualSubdivisionsBySegment[segmentIndex]; if (overrideValue > 0) return overrideValue; } return Mathf.Max(1, visualSubdivisionsPerSegment); } private void DrawLine() { SyncSegmentRendererStyles(); for (int i = 0; i < _segments.Count; i++) DrawSegment(_segments[i]); } private void DrawSegment(VisualSegmentState state) { LineRenderer renderer = state.Renderer; if (renderer == null) return; if (state.Points == null || state.PointCount < 2) { renderer.positionCount = 0; return; } if (!smoothLine || smoothingIterations <= 0 || state.PointCount <= 2) { ApplyRendererPositions(renderer, state.Points, state.PointCount); return; } int count = BuildSmoothedSegment(state); ApplyRendererPositions(renderer, state.SegmentPoints, count); } private int BuildSmoothedSegment(VisualSegmentState state) { int sourceCount = state.PointCount; EnsureSegmentCapacity(state, sourceCount); for (int i = 0; i < sourceCount; i++) state.SegmentPoints[i] = state.Points[i]; Vector3[] source = state.SegmentPoints; int segmentPointCount = sourceCount; for (int iteration = 0; iteration < smoothingIterations; iteration++) { int needed = segmentPointCount * 2; Vector3[] target; if (source == state.SegmentPoints) { EnsureSmoothCapacity(state, needed); target = state.SmoothPoints; } else { EnsureSegmentCapacity(state, needed); target = state.SegmentPoints; } int index = 0; target[index++] = source[0]; for (int i = 0; i < segmentPointCount - 1; i++) { Vector3 a = source[i]; Vector3 b = source[i + 1]; target[index++] = Vector3.Lerp(a, b, smoothingStrength); target[index++] = Vector3.Lerp(a, b, 1f - smoothingStrength); } target[index++] = source[segmentPointCount - 1]; source = target; segmentPointCount = index; } if (source != state.SegmentPoints) { EnsureSegmentCapacity(state, segmentPointCount); for (int i = 0; i < segmentPointCount; i++) state.SegmentPoints[i] = source[i]; } return segmentPointCount; } private void EnsureSmoothCapacity(VisualSegmentState state, int needed) { if (state.SmoothPoints != null && state.SmoothCapacity >= needed) return; state.SmoothCapacity = needed; state.SmoothPoints = new Vector3[state.SmoothCapacity]; } private void EnsureSegmentCapacity(VisualSegmentState state, int needed) { if (state.SegmentPoints != null && state.SegmentCapacity >= needed) return; state.SegmentCapacity = needed; state.SegmentPoints = new Vector3[state.SegmentCapacity]; } private void ApplyRendererPositions(LineRenderer renderer, Vector3[] positions, int count) { if (renderer == null) return; renderer.positionCount = count; for (int i = 0; i < count; i++) renderer.SetPosition(i, positions[i]); } private void OnDrawGizmosSelected() { if (!drawVisualNodes) return; for (int segmentIndex = 0; segmentIndex < _segments.Count; segmentIndex++) { VisualSegmentState state = _segments[segmentIndex]; if (state.Points == null) continue; for (int pointIndex = 0; pointIndex < state.PointCount; pointIndex++) { bool isLogicNode = pointIndex == 0 || pointIndex == state.PointCount - 1; Gizmos.color = isLogicNode ? Color.yellow : Color.cyan; Gizmos.DrawSphere(state.Points[pointIndex], gizmoNodeRadius); #if UNITY_EDITOR Handles.Label( state.Points[pointIndex] + Vector3.up * gizmoNodeRadius, $"{segmentIndex}:{pointIndex}" ); #endif } } } } }