Files
Fishing2/Assets/Scripts/Fishing/New/View/FishingLine/FishingLineRenderer.cs
2026-04-12 18:37:24 +08:00

718 lines
23 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace NBF
{
[RequireComponent(typeof(LineRenderer))]
public class FishingLineRenderer : MonoBehaviour
{
[Header("References")]
[SerializeField] private FishingLineSolver solver;
[SerializeField] private LineRenderer lineRenderer;
[Header("Verlet")]
[Min(1)]
[SerializeField] private int solverIterations = 8;
[Range(0f, 1f)]
[SerializeField] private float damping = 0.98f;
[Min(0f)]
[SerializeField] private float gravityScale = 1f;
[Min(0.001f)]
[SerializeField] private float simulationStep = 0.0166667f;
[Min(0.001f)]
[SerializeField] private float maxDeltaTime = 0.0333333f;
[Header("Water Surface")]
[SerializeField] private bool constrainToWaterSurface = true;
[SerializeField] private Transform waterSurfaceTransform;
[SerializeField] private float waterSurfaceHeight;
[Min(0)]
[SerializeField] private int ignoreHeadNodeCount = 1;
[Min(0)]
[SerializeField] private int ignoreTailNodeCount = 1;
[Min(0f)]
[SerializeField] private float waterSurfaceFollowSpeed = 12f;
[Header("Stability")]
[Min(1)]
[SerializeField] private int maxSubStepsPerFrame = 2;
[Min(0f)]
[SerializeField] private float sleepVelocityThreshold = 0.001f;
[Min(0f)]
[SerializeField] private float sleepDistanceThreshold = 0.002f;
[Min(1)]
[SerializeField] private int stableFramesBeforeSleep = 4;
[Min(0f)]
[SerializeField] private float wakeDistanceThreshold = 0.001f;
[Min(0f)]
[SerializeField] private float tautSegmentThreshold = 0.002f;
[Min(0.001f)]
[SerializeField] private float tautTransitionRange = 0.03f;
[Header("Corner Smoothing")]
[SerializeField] private bool smoothCorners = true;
[Range(0f, 180f)]
[SerializeField] private float minCornerAngle = 12f;
[Min(0f)]
[SerializeField] private float maxCornerSmoothDistance = 0.03f;
[Min(1)]
[SerializeField] private int cornerSmoothSubdivisions = 3;
[Header("Debug")]
[SerializeField] private bool drawDebugSamples;
[SerializeField] private Color debugLogicalSampleColor = Color.cyan;
[SerializeField] private Color debugVirtualSampleColor = new(1f, 0.55f, 0.15f, 1f);
[Min(0.001f)]
[SerializeField] private float debugLogicalSampleRadius = 0.018f;
[Min(0.001f)]
[SerializeField] private float debugVirtualSampleRadius = 0.012f;
private readonly List<Vector3> positions = new();
private readonly List<Vector3> renderPositions = new();
private readonly List<Vector3> previousPositions = new();
private readonly List<long> sampledPointKeys = new();
private readonly List<Vector3> lastPinnedPointPositions = new();
private readonly List<float> lastRestLengths = new();
private bool[] pinnedFlags = System.Array.Empty<bool>();
private float accumulatedTime;
private bool isSleeping;
private int stableFrameCounter;
public int SampleCount => positions.Count;
public float CurrentRenderedLength
{
get
{
var total = 0f;
var source = renderPositions.Count > 0 ? renderPositions : positions;
for (var i = 0; i < source.Count - 1; i++)
{
total += Vector3.Distance(source[i], source[i + 1]);
}
return total;
}
}
private void Reset()
{
TryGetComponent(out lineRenderer);
if (solver == null)
{
TryGetComponent(out solver);
}
}
private void Awake()
{
if (lineRenderer == null)
{
TryGetComponent(out lineRenderer);
}
}
public void Render(FishingLineSolver sourceSolver, float deltaTime)
{
if (lineRenderer == null)
{
return;
}
solver = sourceSolver;
var points = solver.ChainPoints;
var restLengths = solver.RestLengths;
var pinnedIndices = solver.PinnedIndices;
if (points.Count == 0)
{
lineRenderer.positionCount = 0;
return;
}
var topologyChanged = EnsureBuffers(points, pinnedIndices);
if (topologyChanged || ShouldWake(points, restLengths))
{
WakeUp();
}
Simulate(points, restLengths, deltaTime);
ApplyToRenderer();
CacheFrameState(points, restLengths);
}
private bool EnsureBuffers(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<int> pinnedIndices)
{
var topologyChanged = sampledPointKeys.Count != points.Count;
if (!topologyChanged)
{
for (var i = 0; i < points.Count; i++)
{
if (sampledPointKeys[i] == points[i].Key)
{
continue;
}
topologyChanged = true;
break;
}
}
var previousPositionMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
var previousHistoryMap = new Dictionary<long, Vector3>(sampledPointKeys.Count);
for (var i = 0; i < sampledPointKeys.Count; i++)
{
previousPositionMap[sampledPointKeys[i]] = positions[i];
previousHistoryMap[sampledPointKeys[i]] = previousPositions[i];
}
positions.Clear();
previousPositions.Clear();
sampledPointKeys.Clear();
pinnedFlags = new bool[points.Count];
for (var i = 0; i < points.Count; i++)
{
var point = points[i];
sampledPointKeys.Add(point.Key);
if (previousPositionMap.TryGetValue(point.Key, out var preservedPosition))
{
positions.Add(preservedPosition);
previousPositions.Add(previousHistoryMap[point.Key]);
continue;
}
positions.Add(point.Position);
previousPositions.Add(point.Position);
}
for (var i = 0; i < pinnedIndices.Count; i++)
{
var pinnedIndex = pinnedIndices[i];
if (pinnedIndex >= 0 && pinnedIndex < pinnedFlags.Length)
{
pinnedFlags[pinnedIndex] = true;
}
}
return topologyChanged;
}
private void Simulate(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths,
float deltaTime)
{
if (isSleeping)
{
PinLogicalPoints(points);
return;
}
var clampedDelta = Mathf.Clamp(deltaTime, 0f, maxDeltaTime);
accumulatedTime = Mathf.Min(accumulatedTime + clampedDelta, simulationStep * maxSubStepsPerFrame);
var subStepCount = 0;
while (accumulatedTime >= simulationStep && subStepCount < maxSubStepsPerFrame)
{
SimulateStep(points, restLengths, simulationStep);
accumulatedTime -= simulationStep;
subStepCount++;
}
if (subStepCount == 0)
{
PinLogicalPoints(points);
ApplySleep();
}
EvaluateSleepState(restLengths);
}
private void SimulateStep(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths,
float stepDelta)
{
var gravity = Physics.gravity * gravityScale * stepDelta * stepDelta;
for (var i = 0; i < points.Count; i++)
{
if (pinnedFlags[i])
{
positions[i] = points[i].Position;
previousPositions[i] = points[i].Position;
continue;
}
var current = positions[i];
var velocity = (current - previousPositions[i]) * damping;
previousPositions[i] = current;
positions[i] = current + velocity + gravity;
}
SolveDistanceConstraints(points, restLengths);
ApplyWaterSurfaceConstraint(stepDelta);
SolveDistanceConstraints(points, restLengths);
StraightenTautLogicalSegments(points, restLengths);
SolveDistanceConstraints(points, restLengths);
PinLogicalPoints(points);
ApplySleep();
}
private void SolveDistanceConstraints(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{
for (var iteration = 0; iteration < solverIterations; iteration++)
{
PinLogicalPoints(points);
for (var segmentIndex = 0; segmentIndex < restLengths.Count; segmentIndex++)
{
SatisfyDistanceConstraint(segmentIndex, restLengths[segmentIndex]);
}
}
}
private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
{
for (var i = 0; i < points.Count; i++)
{
if (!pinnedFlags[i])
{
continue;
}
positions[i] = points[i].Position;
previousPositions[i] = points[i].Position;
}
}
private void SatisfyDistanceConstraint(int segmentIndex, float restLength)
{
var pointA = positions[segmentIndex];
var pointB = positions[segmentIndex + 1];
var delta = pointB - pointA;
var distance = delta.magnitude;
if (distance <= 0.0001f)
{
return;
}
var correctionScale = (distance - restLength) / distance;
if (Mathf.Approximately(correctionScale, 0f))
{
return;
}
var pointAPinned = pinnedFlags[segmentIndex];
var pointBPinned = pinnedFlags[segmentIndex + 1];
if (pointAPinned && pointBPinned)
{
return;
}
if (pointAPinned)
{
positions[segmentIndex + 1] -= delta * correctionScale;
return;
}
if (pointBPinned)
{
positions[segmentIndex] += delta * correctionScale;
return;
}
var correction = delta * (correctionScale * 0.5f);
positions[segmentIndex] += correction;
positions[segmentIndex + 1] -= correction;
}
private void StraightenTautLogicalSegments(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{
if (points.Count < 2 || restLengths.Count == 0)
{
return;
}
var segmentStartIndex = 0;
while (segmentStartIndex < points.Count - 1)
{
if (!points[segmentStartIndex].IsLogical)
{
segmentStartIndex++;
continue;
}
var segmentEndIndex = segmentStartIndex + 1;
while (segmentEndIndex < points.Count && !points[segmentEndIndex].IsLogical)
{
segmentEndIndex++;
}
if (segmentEndIndex >= points.Count)
{
break;
}
ProjectLogicalSegmentIfTaut(segmentStartIndex, segmentEndIndex, restLengths);
segmentStartIndex = segmentEndIndex;
}
}
private void ProjectLogicalSegmentIfTaut(
int startIndex,
int endIndex,
IReadOnlyList<float> restLengths)
{
if (endIndex - startIndex <= 1)
{
return;
}
var segmentRestLength = 0f;
for (var i = startIndex; i < endIndex; i++)
{
segmentRestLength += restLengths[i];
}
var start = positions[startIndex];
var end = positions[endIndex];
var delta = end - start;
var endpointDistance = delta.magnitude;
if (endpointDistance <= 0.0001f)
{
return;
}
var fullTautDistance = Mathf.Max(0f, segmentRestLength - tautSegmentThreshold);
var blendStartDistance = Mathf.Max(0f, fullTautDistance - tautTransitionRange);
if (endpointDistance < blendStartDistance)
{
return;
}
var straightenBlend = blendStartDistance >= fullTautDistance
? 1f
: Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(blendStartDistance, fullTautDistance, endpointDistance));
var direction = delta / endpointDistance;
var accumulatedDistance = 0f;
for (var pointIndex = startIndex + 1; pointIndex < endIndex; pointIndex++)
{
accumulatedDistance += restLengths[pointIndex - 1];
var projectedPosition = start + direction * accumulatedDistance;
var currentPosition = positions[pointIndex];
var blendedPosition = Vector3.Lerp(currentPosition, projectedPosition, straightenBlend);
positions[pointIndex] = blendedPosition;
previousPositions[pointIndex] = Vector3.Lerp(previousPositions[pointIndex], blendedPosition, straightenBlend);
}
}
private void ApplyWaterSurfaceConstraint(float stepDelta)
{
if (!constrainToWaterSurface || positions.Count == 0)
{
return;
}
var surfaceHeight = waterSurfaceTransform != null ? waterSurfaceTransform.position.y : waterSurfaceHeight;
var startIndex = Mathf.Clamp(ignoreHeadNodeCount, 0, positions.Count);
var endExclusive = Mathf.Clamp(positions.Count - ignoreTailNodeCount, startIndex, positions.Count);
var followFactor = Mathf.Clamp01(waterSurfaceFollowSpeed * stepDelta);
for (var i = startIndex; i < endExclusive; i++)
{
if (pinnedFlags[i])
{
continue;
}
var current = positions[i];
if (current.y >= surfaceHeight)
{
continue;
}
var nextY = Mathf.Lerp(current.y, surfaceHeight, followFactor);
positions[i] = new Vector3(current.x, nextY, current.z);
var previous = previousPositions[i];
previousPositions[i] = new Vector3(
previous.x,
Mathf.Lerp(previous.y, nextY, followFactor),
previous.z);
}
}
private void ApplySleep()
{
for (var i = 0; i < positions.Count; i++)
{
if (pinnedFlags[i])
{
continue;
}
var velocityMagnitude = (positions[i] - previousPositions[i]).magnitude;
if (velocityMagnitude <= sleepVelocityThreshold)
{
previousPositions[i] = positions[i];
}
}
}
private void EvaluateSleepState(IReadOnlyList<float> restLengths)
{
var isStable = true;
for (var i = 0; i < positions.Count; i++)
{
if (pinnedFlags[i])
{
continue;
}
if ((positions[i] - previousPositions[i]).magnitude > sleepVelocityThreshold)
{
isStable = false;
break;
}
}
if (isStable)
{
for (var i = 0; i < restLengths.Count; i++)
{
var error = Mathf.Abs(Vector3.Distance(positions[i], positions[i + 1]) - restLengths[i]);
if (error > sleepDistanceThreshold)
{
isStable = false;
break;
}
}
}
if (!isStable)
{
stableFrameCounter = 0;
return;
}
stableFrameCounter++;
if (stableFrameCounter < stableFramesBeforeSleep)
{
return;
}
isSleeping = true;
accumulatedTime = 0f;
for (var i = 0; i < positions.Count; i++)
{
previousPositions[i] = positions[i];
}
}
private bool ShouldWake(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{
if (!isSleeping)
{
return false;
}
if (lastPinnedPointPositions.Count != points.Count || lastRestLengths.Count != restLengths.Count)
{
return true;
}
for (var i = 0; i < points.Count; i++)
{
if (!pinnedFlags[i])
{
continue;
}
if (Vector3.Distance(points[i].Position, lastPinnedPointPositions[i]) > wakeDistanceThreshold)
{
return true;
}
}
for (var i = 0; i < restLengths.Count; i++)
{
if (Mathf.Abs(restLengths[i] - lastRestLengths[i]) > wakeDistanceThreshold)
{
return true;
}
}
return false;
}
private void CacheFrameState(
IReadOnlyList<FishingLineSolver.ChainPoint> points,
IReadOnlyList<float> restLengths)
{
lastPinnedPointPositions.Clear();
for (var i = 0; i < points.Count; i++)
{
lastPinnedPointPositions.Add(points[i].Position);
}
lastRestLengths.Clear();
for (var i = 0; i < restLengths.Count; i++)
{
lastRestLengths.Add(restLengths[i]);
}
}
private void WakeUp()
{
isSleeping = false;
stableFrameCounter = 0;
accumulatedTime = 0f;
}
private void ApplyToRenderer()
{
BuildRenderPositions();
lineRenderer.positionCount = renderPositions.Count;
for (var i = 0; i < renderPositions.Count; i++)
{
lineRenderer.SetPosition(i, renderPositions[i]);
}
}
private void BuildRenderPositions()
{
renderPositions.Clear();
if (positions.Count == 0)
{
return;
}
if (!smoothCorners || positions.Count < 3)
{
renderPositions.AddRange(positions);
return;
}
renderPositions.Add(positions[0]);
for (var i = 1; i < positions.Count - 1; i++)
{
var previous = positions[i - 1];
var current = positions[i];
var next = positions[i + 1];
if (!TryBuildSmoothedCorner(previous, current, next, out var entry, out var exit))
{
AddRenderPointIfDistinct(current);
continue;
}
AddRenderPointIfDistinct(entry);
for (var subdivision = 1; subdivision <= cornerSmoothSubdivisions; subdivision++)
{
var t = subdivision / (cornerSmoothSubdivisions + 1f);
var pointOnCurve = EvaluateQuadraticBezier(entry, current, exit, t);
AddRenderPointIfDistinct(pointOnCurve);
}
AddRenderPointIfDistinct(exit);
}
AddRenderPointIfDistinct(positions[^1]);
}
private bool TryBuildSmoothedCorner(
Vector3 previous,
Vector3 current,
Vector3 next,
out Vector3 entry,
out Vector3 exit)
{
entry = current;
exit = current;
var incoming = current - previous;
var outgoing = next - current;
var incomingLength = incoming.magnitude;
var outgoingLength = outgoing.magnitude;
if (incomingLength <= 0.0001f || outgoingLength <= 0.0001f)
{
return false;
}
var cornerAngle = Vector3.Angle(incoming, outgoing);
if (cornerAngle < minCornerAngle)
{
return false;
}
var trimDistance = Mathf.Min(
maxCornerSmoothDistance,
incomingLength * 0.5f,
outgoingLength * 0.5f);
if (trimDistance <= 0.0001f)
{
return false;
}
entry = current - (incoming / incomingLength) * trimDistance;
exit = current + (outgoing / outgoingLength) * trimDistance;
return true;
}
private void AddRenderPointIfDistinct(Vector3 point)
{
if (renderPositions.Count > 0 && Vector3.Distance(renderPositions[^1], point) <= 0.0001f)
{
return;
}
renderPositions.Add(point);
}
private static Vector3 EvaluateQuadraticBezier(Vector3 start, Vector3 control, Vector3 end, float t)
{
var oneMinusT = 1f - t;
return (oneMinusT * oneMinusT * start)
+ (2f * oneMinusT * t * control)
+ (t * t * end);
}
private void OnDrawGizmosSelected()
{
if (!drawDebugSamples || positions.Count == 0)
{
return;
}
for (var i = 0; i < positions.Count; i++)
{
var isLogicalPoint = solver != null
&& solver.ChainPoints != null
&& i < solver.ChainPoints.Count
&& solver.ChainPoints[i].IsLogical;
Gizmos.color = isLogicalPoint ? debugLogicalSampleColor : debugVirtualSampleColor;
Gizmos.DrawSphere(
positions[i],
isLogicalPoint ? debugLogicalSampleRadius : debugVirtualSampleRadius);
}
}
}
}