Files
Fishing2/Assets/Scripts/Fishing/New/View/FishingLine/FLineRenderer.cs
Bob.Song d432c468b1 接入新逻辑
# Conflicts:
#	Assets/Scenes/RopeTest.unity
#	Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs
#	Assets/Scripts/Fishing/Rope/Rope.cs
#	Assets/Scripts/Fishing/Rope/Rope.cs.meta
2026-04-26 14:44:06 +08:00

1006 lines
36 KiB
C#

using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace NBF
{
/// <summary>
/// Used for FLine visual rendering.
/// Reads rigid body nodes from the cable and simulates extra render points between logic nodes.
/// </summary>
[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<int> visualSubdivisionsBySegment = new List<int>();
[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<Rigidbody> _bodies = new List<Rigidbody>();
private readonly List<VisualSegmentState> _segments = new List<VisualSegmentState>();
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<FLine>();
_lineRenderer = GetComponent<LineRenderer>();
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<int>();
for (int i = 0; i < visualSubdivisionsBySegment.Count; i++)
visualSubdivisionsBySegment[i] = Mathf.Max(0, visualSubdivisionsBySegment[i]);
_needsRebuild = true;
if (_lineRenderer == null)
_lineRenderer = GetComponent<LineRenderer>();
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<FLine>();
if (_lineRenderer == null)
{
_lineRenderer = GetComponent<LineRenderer>();
ConfigureLineRenderer(_lineRenderer);
}
if (cable == null || !RefreshBodies())
return false;
EnsureSegmentStates();
return _segments.Count > 0;
}
private bool RefreshBodies()
{
List<Rigidbody> 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<LineRenderer>();
}
else
{
rendererObject = new GameObject(rendererName);
rendererObject.transform.SetParent(transform, false);
renderer = null;
}
if (renderer == null)
renderer = rendererObject.AddComponent<LineRenderer>();
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
}
}
}
}
}