Code/RailComponent/RailComponent.Utility.cs

Utility functions for the RailComponent that compute transform frames along a spline. It provides two frame-generation methods: one that orients frames using a fixed up direction, and one that computes rotation-minimizing frames (parallel transport) to avoid twisting. It also includes a helper to compute the rotation-minimizing normal between two spline samples.

Native Interop
using System;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RailComponent
{
	private static Transform[] CalculateTangentFramesUsingUpDir(Spline _Spline, int _FrameCount)
	{
		var frames = new Transform[_FrameCount];
		float totalSplineLength = _Spline.Length;

		var sample = _Spline.SampleAtDistance(0.0f);
		sample.Up = Vector3.Up;

		// Fall back to a different up if the tangent runs (nearly) straight up.
		if (MathF.Abs(Vector3.Dot(sample.Tangent, sample.Up)) > 0.999f)
			sample.Up = Vector3.Right;

		for (int i = 0; i < _FrameCount; i++)
		{
			float t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;
			float distance = t * totalSplineLength;

			sample = _Spline.SampleAtDistance(distance);

			var up = Rotation.FromAxis(sample.Tangent, sample.Roll) * sample.Up;
			Rotation rotation = Rotation.LookAt(sample.Tangent, up);

			frames[i] = new Transform(sample.Position, rotation, sample.Scale);
		}

		return frames;
	}



	private static Transform[] CalculateRotationMinimizingTangentFrames(Spline _Spline, int _FrameCount)
	{
		var frames = new Transform[_FrameCount];
		float totalSplineLength = _Spline.Length;

		var previousSample = _Spline.SampleAtDistance(0.0f);
		Vector3 up = Vector3.Up;

		if (MathF.Abs(Vector3.Dot(previousSample.Tangent, up)) > 0.999f)
			up = Vector3.Right;

		up = Rotation.FromAxis(previousSample.Tangent, previousSample.Roll) * up;

		frames[0] = new Transform(previousSample.Position, Rotation.LookAt(previousSample.Tangent, up), previousSample.Scale);

		for (int i = 1; i < _FrameCount; i++)
		{
			float t = _FrameCount > 1 ? (float)i / (_FrameCount - 1) : 0.0f;
			float distance = t * totalSplineLength;

			var sample = _Spline.SampleAtDistance(distance);

			// Parallel-transport the up vector so the profile doesn't twist through 3D curves.
			up = GetRotationMinimizingNormal(previousSample.Position, previousSample.Tangent, up, sample.Position, sample.Tangent);

			float deltaRoll = sample.Roll - previousSample.Roll;
			up = Rotation.FromAxis(sample.Tangent, deltaRoll) * up;

			Rotation rotation = Rotation.LookAt(sample.Tangent, up);
			frames[i] = new Transform(sample.Position, rotation, sample.Scale);

			previousSample = sample;
		}

		return frames;
	}



	private static Vector3 GetRotationMinimizingNormal(Vector3 _PosA, Vector3 _TangentA, Vector3 _NormalA, Vector3 _PosB, Vector3 _TangentB)
	{
		// Source: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf
		Vector3 v1 = _PosB - _PosA;

		float v1DotV1Half = Vector3.Dot(v1, v1) / 2.0f;

		if (v1DotV1Half <= 0.0001f)
			return _NormalA;

		float r1 = Vector3.Dot(v1, _NormalA) / v1DotV1Half;
		float r2 = Vector3.Dot(v1, _TangentA) / v1DotV1Half;

		Vector3 nL = _NormalA - r1 * v1;
		Vector3 tL = _TangentA - r2 * v1;
		Vector3 v2 = _TangentB - tL;

		float r3 = Vector3.Dot(v2, nL) / Vector3.Dot(v2, v2);

		return (nL - 2.0f * r3 * v2).Normal;
	}
}