Hey guys!
I stumbled across the blueprint solution by @DeathKwonDo, but I think there might be an issue regarding finding the closest point on the mesh when given a spline and a point like this:
The closest point is in between “start” and “middle”, but the blueprint solution would decide for “end” being closer than “start” at its first iteration. It would then go on and only explore the segment “middle” - “end” which would exclude the optimal solution.
Therefore I have created a C++ node with a multistart approach: First testing some sample points along the spline and then resort to the binary search solution presented by @DeathKwonDo. Still not an optimal solution, since you can still miss the closest point with too few sample points, but a bit more robust. Here is the code:
// SplineComponentUtils.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Components/SplineComponent.h"
#include "SplineComponentUtils.generated.h"
/**
*
*/
UCLASS()
class UNREALENGINETOOLS_API USplineComponentUtils : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Spline Utils")
/**
* Projects the specified location to the specified spline and returns the distance on the spline that is the closest point.
* Multistart approach with constant amount `SampleCount` of sample points. Points are evaluated, point with closest distance is chosen.
* Binary Search with depth `MaxIterationCount` is then started with adjacent points.
*/
static float ProjectLocationToSplineDistance(const USplineComponent* Spline, const FVector& Location, const ESplineCoordinateSpace::Type CoordinateSpace, const int SampleCount = 20, const int MaxIterationCount = 10);
};
// SplineComponentUtils.cpp
#include "CustomNodes/Utilities/SplineComponentUtils.h"
/**
* Projects the specified location to the specified spline and returns the distance on the spline that is the closest point.
* Multistart approach with constant amount `SampleCount` of sample points. Points are evaluated, point with closest distance is chosen.
* Binary Search with depth `MaxIterationCount` is then started with adjacent points.
*/
float USplineComponentUtils::ProjectLocationToSplineDistance(const USplineComponent* Spline, const FVector& Location, const ESplineCoordinateSpace::Type CoordinateSpace, const int SampleCount, const int MaxIterationCount)
{
const float Distance = Spline->GetSplineLength();
// Multistart
const float SampleDistanceDelta = Distance / SampleCount;
int ClosestSampleIndex = 0;
float ClosestDistance = Distance;
for (int i = 0; i < SampleCount + 1; i++)
{
const float SampleDistance = SampleDistanceDelta * i;
const FVector SamplePoint = Spline->GetLocationAtDistanceAlongSpline(SampleDistance, CoordinateSpace);
const float SamplePointDistance = FVector::Distance(Location, SamplePoint);
if (SamplePointDistance < ClosestDistance)
{
ClosestSampleIndex = i;
ClosestDistance = SamplePointDistance;
}
}
const int StartIndex = FMath::Max(ClosestSampleIndex - 1, 0);
const int EndIndex = FMath::Min(ClosestSampleIndex + 1, SampleCount + 1);
if (StartIndex == EndIndex)
return SampleDistanceDelta * StartIndex;
// Binary Search
float StartDistance = SampleDistanceDelta * StartIndex;
float EndDistance = SampleDistanceDelta * EndIndex;
FVector StartPoint = Spline->GetLocationAtDistanceAlongSpline(StartDistance, CoordinateSpace);
FVector EndPoint = Spline->GetLocationAtDistanceAlongSpline(EndDistance, CoordinateSpace);
for (int j = 0; j < MaxIterationCount; j++)
{
if (FVector::Distance(Location, StartPoint) < FVector::Distance(Location, EndPoint))
EndDistance = StartDistance + ((EndDistance - StartDistance) / 2);
else
StartDistance = StartDistance + ((EndDistance - StartDistance) / 2);
StartPoint = Spline->GetLocationAtDistanceAlongSpline(StartDistance, CoordinateSpace);
EndPoint = Spline->GetLocationAtDistanceAlongSpline(EndDistance, CoordinateSpace);
}
if (FVector::Distance(Location, StartPoint) < FVector::Distance(Location, EndPoint))
return StartDistance;
else
return EndDistance;
}
Cheers!