Code/Waymaker/WaymakerMeshBuilder.cs

The file defines a static mesh builder for spline geometry. It samples a 3D curve, calculates vertices and UV coordinates for quadrangular strips, and assembles them into a polygon mesh.

Native Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace FluidPlay.Waymaker;

internal struct WaymakerMeshSettings
{
	public Material SourceMaterial;
	public Material StartCapMaterial;
	public Material EndCapMaterial;
	public float SurfaceWidth;
	public bool DynamicVTiling;
	public float DynamicVTileLength;
	public int VerticalTiles;
	public float HorizontalTiling;
	public float HorizontalPadding;
	public bool UseCaps;
	public float CapLength;
	public float MinimumEdgeLength;
}

internal readonly record struct WaymakerFrame(Vector3 Position, Rotation Rotation, float SplineDistance)
{
	public Vector3 Right => Rotation.Right;
}

/// <summary>
/// Samples Waymaker's spline and builds the textured quad strip, including UVs and optional endpoint cap materials.
/// </summary>
internal static class WaymakerMeshBuilder
{
	private const float Epsilon = 0.001f;

	internal static (float Min, float Max) GetMainURange(float horizontalTiling, float horizontalPadding)
	{
		float tiling = Math.Max(0.01f, horizontalTiling);
		float padding = Math.Min(Math.Max(0.0f, horizontalPadding), tiling);
		return (padding * 0.5f, tiling - padding * 0.5f);
	}

	internal static (float Min, float Max) GetDynamicVRange(float startDistance, float endDistance, float tileLength)
	{
		float length = Math.Max(Epsilon, tileLength);
		return (startDistance / length, endDistance / length);
	}

	internal static (float Min, float Max) GetFixedVRange(float startDistance, float endDistance, float totalLength, int verticalTiles)
	{
		float length = Math.Max(Epsilon, totalLength);
		float tiles = Math.Max(1, verticalTiles);
		return (startDistance / length * tiles, endDistance / length * tiles);
	}

	internal static bool CanUseCaps(bool dynamicVTiling, bool isLoop) => !dynamicVTiling && !isLoop;

	internal static float GetEffectiveCapLength(float totalLength, float requestedLength, bool useCaps, bool dynamicVTiling, bool isLoop)
	{
		if (!useCaps || !CanUseCaps(dynamicVTiling, isLoop))
			return 0.0f;

		return Math.Clamp(requestedLength, 0.0f, Math.Max(0.0f, totalLength) * 0.5f);
	}

	public static PolygonMesh Build(Spline spline, WaymakerMeshSettings settings, float sampleSpacing)
	{
		if (spline is null || spline.PointCount < 2 || spline.Length <= Epsilon)
			return null;

		float totalLength = spline.Length;
		bool useCaps = settings.UseCaps && CanUseCaps(settings.DynamicVTiling, spline.IsLoop);
		float capLength = GetEffectiveCapLength(totalLength, settings.CapLength, useCaps, settings.DynamicVTiling, spline.IsLoop);

		var distances = CreateSampleDistances(spline, sampleSpacing, capLength);
		if (distances.Count < 2)
			return null;

		var sampledFrames = distances.Select(distance => SampleFrame(spline, distance)).ToArray();
		float halfWidth = Math.Max(0.0f, settings.SurfaceWidth) * 0.5f;
		var frames = MergeShortEdges(sampledFrames, halfWidth, Math.Max(0.0f, settings.MinimumEdgeLength),
			capLength > Epsilon ? capLength : -1.0f,
			capLength > Epsilon ? totalLength - capLength : -1.0f);
		if (frames.Length < 2)
			return null;

		var cumulativeCenterlineLengths = GetCumulativeCenterlineLengths(frames);

		var mesh = new PolygonMesh();
		var vertices = new HalfEdgeMesh.VertexHandle[frames.Length][];

		for (int i = 0; i < frames.Length; i++)
		{
			Vector3 halfSpan = frames[i].Right * halfWidth;
			vertices[i] = mesh.AddVertices(frames[i].Position - halfSpan, frames[i].Position + halfSpan);
		}

		var mainURange = GetMainURange(settings.HorizontalTiling, settings.HorizontalPadding);
		float mainU0 = mainURange.Min;
		float mainU1 = mainURange.Max;
		int verticalTiles = Math.Max(1, settings.VerticalTiles);
		float dynamicTileLength = Math.Max(Epsilon, settings.DynamicVTileLength);
		float interiorStart = capLength;
		float interiorEnd = Math.Max(interiorStart, totalLength - capLength);
		float interiorLength = Math.Max(0.0f, interiorEnd - interiorStart);

		for (int i = 0; i < frames.Length - 1; i++)
		{
			float distance0 = frames[i].SplineDistance;
			float distance1 = frames[i + 1].SplineDistance;
			float centerline0 = cumulativeCenterlineLengths[i];
			float centerline1 = cumulativeCenterlineLengths[i + 1];
			float segmentMidpoint = (distance0 + distance1) * 0.5f;

			bool isStartCap = capLength > Epsilon && segmentMidpoint < capLength;
			bool isEndCap = capLength > Epsilon && segmentMidpoint > totalLength - capLength;

			Material material = isStartCap
				? settings.StartCapMaterial ?? settings.SourceMaterial
				: isEndCap
					? settings.EndCapMaterial ?? settings.SourceMaterial
					: settings.SourceMaterial;

			float v0;
			float v1;
			if (isStartCap)
			{
				v0 = (distance0 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);
				v1 = (distance1 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);
			}
			else if (isEndCap)
			{
				v0 = ((distance0 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);
				v1 = ((distance1 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);
			}
			else if (settings.DynamicVTiling)
			{
				var vRange = GetDynamicVRange(centerline0, centerline1, dynamicTileLength);
				v0 = vRange.Min;
				v1 = vRange.Max;
			}
			else if (useCaps && interiorLength > Epsilon)
			{
				var vRange = GetFixedVRange(distance0 - interiorStart, distance1 - interiorStart, interiorLength, verticalTiles);
				v0 = vRange.Min;
				v1 = vRange.Max;
			}
			else
			{
				var vRange = GetFixedVRange(centerline0, centerline1, cumulativeCenterlineLengths[^1], verticalTiles);
				v0 = vRange.Min;
				v1 = vRange.Max;
			}

			WaymakerMeshUtility.AddTexturedQuad(mesh, material,
				vertices[i][0], vertices[i][1], vertices[i + 1][1], vertices[i + 1][0],
				new Vector2(mainU0, v0), new Vector2(mainU1, v0),
				new Vector2(mainU1, v1), new Vector2(mainU0, v1));
		}

		mesh.RemoveBadGeometry();

		// Translucent shaders multiply texture alpha by vertex alpha. Explicitly initialize every half-edge color;
		// leaving PolygonMesh's color stream sparse can make some translucent face corners resolve to zero alpha.
		foreach (var halfEdge in mesh.HalfEdgeHandles)
		{
			mesh.SetVertexColor(halfEdge, Color32.White);
			if (halfEdge.OppositeEdge.IsValid)
				mesh.SetVertexColor(halfEdge.OppositeEdge, Color32.White);
		}

		return mesh;
	}

	internal static List<float> CreateSampleDistances(Spline spline, float sampleSpacing, float capLength)
	{
		float totalLength = spline.Length;
		float spacing = Math.Max(1.0f, sampleSpacing);
		var samples = new List<float> { 0.0f, totalLength };

		int subdivisions = Math.Max(2, (int)MathF.Ceiling(totalLength / spacing));
		for (int i = 1; i < subdivisions; i++)
			samples.Add(totalLength * i / subdivisions);

		for (int i = 0; i < spline.PointCount; i++)
			samples.Add(spline.GetDistanceAtPoint(i).Clamp(0.0f, totalLength));

		if (capLength > Epsilon)
		{
			samples.Add(capLength);
			samples.Add(totalLength - capLength);
		}

		samples.Sort();
		var unique = new List<float>(samples.Count);
		foreach (float sample in samples)
		{
			if (unique.Count == 0 || sample - unique[^1] > Epsilon)
				unique.Add(sample);
		}

		if (unique.Count == 1)
			unique.Add(totalLength);

		return unique;
	}

	private static WaymakerFrame SampleFrame(Spline spline, float distance)
	{
		var sample = spline.SampleAtDistance(distance);
		Vector3 tangent = sample.Tangent.Normal;
		if (tangent.IsNearZeroLength)
			tangent = Vector3.Forward;

		Vector3 up = sample.Up;
		if (up.IsNearZeroLength)
			up = Vector3.Up;
		if (MathF.Abs(Vector3.Dot(tangent, up.Normal)) > 0.999f)
			up = Vector3.Right;

		up = Rotation.FromAxis(tangent, sample.Roll) * up;
		Rotation rotation = Rotation.LookAt(tangent, up);
		return new WaymakerFrame(sample.Position, rotation, distance);
	}

	internal static float[] GetCumulativeCenterlineLengths(WaymakerFrame[] frames)
	{
		var lengths = new float[frames.Length];
		for (int i = 1; i < frames.Length; i++)
			lengths[i] = lengths[i - 1] + Vector3.DistanceBetween(frames[i - 1].Position, frames[i].Position);

		return lengths;
	}

	/// <summary>
	/// Coalesces short consecutive sections instead of narrowing the surface. Each retained quad is checked using the
	/// actual left/right longitudinal edge lengths at the requested surface width. Cap boundaries remain exact.
	/// </summary>
	internal static WaymakerFrame[] MergeShortEdges(
		WaymakerFrame[] frames,
		float halfWidth,
		float minimumEdgeLength,
		float capStartDistance = -1.0f,
		float capEndDistance = -1.0f)
	{
		if (frames.Length < 3 || minimumEdgeLength <= 0.0f)
			return frames;

		var merged = new List<WaymakerFrame> { frames[0] };
		int spanStart = 0;

		for (int i = 1; i < frames.Length; i++)
		{
			bool isEnd = i == frames.Length - 1;
			bool isCapBoundary = IsAtDistance(frames[i].SplineDistance, capStartDistance)
				|| IsAtDistance(frames[i].SplineDistance, capEndDistance);
			if (!isEnd && !isCapBoundary)
				continue;

			AppendSpan(merged, frames, spanStart, i, halfWidth, minimumEdgeLength);
			spanStart = i;
		}

		return merged.ToArray();
	}

	private static void AppendSpan(
		List<WaymakerFrame> merged,
		WaymakerFrame[] frames,
		int spanStart,
		int spanEnd,
		float halfWidth,
		float minimumEdgeLength)
	{
		int currentStart = spanStart;

		for (int candidate = spanStart + 1; candidate < spanEnd; candidate++)
		{
			if (!HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[candidate], halfWidth, minimumEdgeLength))
				continue;

			// Avoid accepting a short quad at the tail when the same span can be represented by one valid longer quad.
			bool leavesShortTail = !HasMinimumLongitudinalEdgeLength(frames[candidate], frames[spanEnd], halfWidth, minimumEdgeLength);
			bool fullSpanFits = HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[spanEnd], halfWidth, minimumEdgeLength);
			if (leavesShortTail && fullSpanFits)
				continue;

			merged.Add(frames[candidate]);
			currentStart = candidate;
		}

		if (spanEnd > currentStart)
			merged.Add(frames[spanEnd]);
	}

	internal static bool HasMinimumLongitudinalEdgeLength(WaymakerFrame start, WaymakerFrame end, float halfWidth, float minimumEdgeLength)
	{
		Vector3 centerDelta = end.Position - start.Position;
		if (centerDelta.Length <= Epsilon)
			return false;

		Vector3 centerDirection = centerDelta.Normal;
		Vector3 leftEdge = (end.Position - end.Right * halfWidth) - (start.Position - start.Right * halfWidth);
		Vector3 rightEdge = (end.Position + end.Right * halfWidth) - (start.Position + start.Right * halfWidth);

		return IsLongitudinalEdgeLongEnough(leftEdge, centerDirection, minimumEdgeLength)
			&& IsLongitudinalEdgeLongEnough(rightEdge, centerDirection, minimumEdgeLength);
	}

	private static bool IsLongitudinalEdgeLongEnough(Vector3 edge, Vector3 centerDirection, float minimumEdgeLength)
	{
		return edge.Length >= minimumEdgeLength && Vector3.Dot(edge, centerDirection) > 0.0f;
	}

	private static bool IsAtDistance(float actual, float expected)
	{
		return expected >= 0.0f && MathF.Abs(actual - expected) <= Epsilon;
	}
}