The RoadArmMapping class manages texture coordinates for road segments that extend past their defined meshes. It implements logic to map a world point to UV coordinates on the road's texture based on its curve and width.
using System;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// A road's own texture mapping carried on past the row its mesh starts with, in to the node it ends at: U across
/// the road and V along it, measured from the road's own centreline (the curve itself, carried on straight past
/// either end), exactly as the road's chunks lay them (see <see cref="RoadSurfaceBuilder.Build"/>). The road's texture
/// runs on into a junction or a lot without a seam and follows the road wherever it curves on its way in; carried on
/// straight from the row instead, it would drift off a curving road and be stretched to meet its kerb.
/// </summary>
internal sealed class RoadArmMapping : IFanMapping
{
private readonly RoadCurveTable m_Table;
private readonly RoadWidth m_Widths;
private readonly float m_Low;
private readonly float m_High;
private readonly float m_Repeat;
private readonly bool m_FitWidth;
private readonly bool m_HoldCentre;
/// <summary>
/// <paramref name="_Trim"/> is how far from the node, along the road, its row is. <paramref name="_HoldCentre"/>
/// keeps a centre that lies off to the side of the road's lane (a parking lot's middle, the node a ramp's offset
/// lane ends beside) inside the road's texture, so the road converges on it rather than running its edges on
/// across the surface.
/// </summary>
public RoadArmMapping(RoadCurveTable _Table, bool _AtStart, float _Trim, in RoadWidth _Widths, float _Repeat, bool _FitWidth, bool _HoldCentre = false)
{
m_Table = _Table;
m_Widths = _Widths;
m_Repeat = MathF.Max(1.0f, _Repeat);
m_FitWidth = _FitWidth;
m_HoldCentre = _HoldCentre;
float node = _AtStart ? 0.0f : _Table.Length;
float row = _AtStart ? _Trim : _Table.Length - _Trim;
m_Low = MathF.Min(node, row);
m_High = MathF.Max(node, row);
}
public Vector2 Uv(Vector3 _Point)
{
Locate(_Point, out float distance, out float right);
return new Vector2(Across(distance, right), distance / m_Repeat);
}
/// <summary>
/// The mapping at a point of the outline, which is kerb all round: U held at the road's edge on that side, where
/// the texture has its edge, so whatever runs along the edge of the road in the texture follows the kerb round a
/// corner however it curves, instead of running on and missing it. V as everywhere.
/// </summary>
public Vector2 KerbUv(Vector3 _Point)
{
Locate(_Point, out float distance, out float right);
float along = Math.Clamp(distance, 0.0f, m_Table.Length);
float edge = right < 0.0f ? -m_Widths.LeftAt(along) : m_Widths.RightAt(along);
return new Vector2(Across(distance, edge), distance / m_Repeat);
}
/// <summary>
/// The mapping at the fan's centre: as everywhere, or, held, with U kept to the middle half of a texture fitted to
/// the road's width, so the texture between the road and a centre off to its side never reaches its edges.
/// </summary>
public Vector2 CentreUv(Vector3 _Centre)
{
Vector2 uv = Uv(_Centre);
if (m_HoldCentre && m_FitWidth)
uv.x = Math.Clamp(uv.x, 0.25f, 0.75f);
return uv;
}
/// <summary>
/// Where a point sits on the road, in world units: how far to the right of the centreline, and how far along the
/// curve.
/// </summary>
public Vector2 Measure(Vector3 _Point)
{
Locate(_Point, out float distance, out float right);
return new Vector2(right, distance);
}
/// <summary>
/// The road's lane where a point is along it: U at its left and right edges there, and its width.
/// </summary>
public void Lane(Vector3 _Point, out float _LeftU, out float _RightU, out float _Width)
{
Locate(_Point, out float distance, out _);
float along = Math.Clamp(distance, 0.0f, m_Table.Length);
float left = m_Widths.LeftAt(along);
float right = m_Widths.RightAt(along);
_LeftU = Across(distance, -left);
_RightU = Across(distance, right);
_Width = left + right;
}
/// <summary>
/// U at a point an offset to the right of the centreline, a distance along it: at the texture's own size out from
/// the centre line, or fitted between the road's two edges there.
/// </summary>
private float Across(float _Distance, float _Right)
{
if (!m_FitWidth)
return _Right / m_Repeat;
float along = Math.Clamp(_Distance, 0.0f, m_Table.Length);
float left = m_Widths.LeftAt(along);
float width = left + m_Widths.RightAt(along);
return width > 0.001f ? (_Right + left) / width : 0.5f;
}
/// <summary>
/// Where a point sits in the road's own terms: the curve distance of the foot of its perpendicular on the
/// centreline between the node and the row, and how far to the road's right of that foot it lies. A point behind
/// the node or past the row is measured from the centreline carried on straight there.
/// </summary>
private void Locate(Vector3 _Point, out float _Distance, out float _Right)
{
var point = new Vector2(_Point.x, _Point.y);
float low = m_Low;
float high = m_High;
if (Ahead(point, low) <= 0.0f)
{
Measure(point, low, out _Distance, out _Right);
return;
}
if (Ahead(point, high) >= 0.0f)
{
Measure(point, high, out _Distance, out _Right);
return;
}
// The foot is somewhere between: halve the stretch it is in until it is pinned down.
for (int i = 0; i < 40 && high - low > 0.001f; i++)
{
float middle = (low + high) * 0.5f;
if (Ahead(point, middle) > 0.0f)
low = middle;
else
high = middle;
}
Measure(point, (low + high) * 0.5f, out _Distance, out _Right);
}
/// <summary>
/// How far ahead of the centreline's point at a distance a point lies, the way the road runs there.
/// </summary>
private float Ahead(Vector2 _Point, float _Distance)
{
Frame(_Distance, out Vector2 position, out Vector2 along);
return Vector2.Dot(_Point - position, along);
}
/// <summary>
/// A point's distance and right offset measured from the centreline's point at a distance and the way the road
/// runs there: exact at the foot of the point's perpendicular, and the centreline carried on straight elsewhere.
/// </summary>
private void Measure(Vector2 _Point, float _From, out float _Distance, out float _Right)
{
Frame(_From, out Vector2 position, out Vector2 along);
Vector2 offset = _Point - position;
_Distance = _From + Vector2.Dot(offset, along);
_Right = offset.x * along.y - offset.y * along.x;
}
private void Frame(float _Distance, out Vector2 _Position, out Vector2 _Along)
{
m_Table.Frame(_Distance, out Vector3 position, out Vector3 tangent);
// The road's right as its chunks take it: flat and square to the way it runs, which is a quarter turn back
// from it.
Vector3 right = RoadCurve.RightOf(tangent);
_Position = new Vector2(position.x, position.y);
_Along = new Vector2(-right.y, right.x);
}
}