718 lines
23 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|