Mesh builder that sweeps a road-border profile (sidewalk or countryside verge) along a path or road segment and emits polygon mesh geometry. It evaluates cross-sections, handles corner arcs and mitres, tapering/blending between segments, UVs, vertex blends for gravel spills, and produces caps and corner patch geometry.
using System;
using System.Collections.Generic;
using System.Text;
using Sandbox;
using Sandbox.Utility;
namespace RedSnail.RoadTool2;
/// <summary>
/// Which ends of a road stretch's borders are open (nothing continues them) and need closing off.
/// </summary>
[Flags]
internal enum BorderCaps
{
None = 0,
StartLeft = 1,
StartRight = 2,
EndLeft = 4,
EndRight = 8
}
/// <summary>
/// Generates road borders (sidewalks, country side verges) by sweeping a profile's cross-section along a path of
/// road-edge points: the sides of a road chunk, or the outline of a junction between two arms.
/// </summary>
internal static class RoadBorderBuilder
{
/// <summary>
/// A point of a border run: the road edge, the direction away from the road, and the along-road coordinate
/// the textures run on.
/// </summary>
public readonly struct PathPoint
{
public readonly Vector3 Position;
public readonly Vector3 Outward;
public readonly float Along;
/// <summary>
/// How far the outer part of the profile is lifted above the road edge here (the kerb stays on the road,
/// the outer edge takes all of it). Along a road it runs linearly from one end's corner height difference
/// to the other's; on a corner's arc (<see cref="IsArc"/>) it is whatever brings the row to the corner's
/// one height, and nodes further out than the fillet radius collapse onto the centre.
/// </summary>
public readonly float Lift;
public readonly RoadJunctionLayout.CornerFold Fold;
public readonly bool IsArc;
public PathPoint(Vector3 _Position, Vector3 _Outward, float _Along, float _Lift = 0.0f, RoadJunctionLayout.CornerFold _Fold = null, bool _IsArc = false)
{
Position = _Position;
Outward = _Outward;
Along = _Along;
Lift = _Lift;
Fold = _Fold;
IsArc = _IsArc;
}
/// <summary>
/// True when a node at this offset sits on the corner's centre rather than at its own offset.
/// </summary>
public bool IsFolded(float _Offset)
{
return IsArc && Fold is not null && _Offset >= Fold.Radius;
}
}
/// <summary>
/// The height the outer edge of one side's border is lifted along a segment: the corner height difference at
/// each end (0 where the end has no corner on that side), interpolated linearly between the two tangent
/// points. Arm chunks and the junction runs evaluate this same line, so the seam between them is exact and
/// nothing depends on how long the road is.
/// </summary>
public readonly record struct SideBlend(bool Active, float StartDistance, float StartDelta, float EndDistance, float EndDelta)
{
public float DeltaAt(float _Distance)
{
if (!Active)
return 0.0f;
float span = EndDistance - StartDistance;
if (span <= 0.001f)
return StartDelta;
return MathX.Lerp(StartDelta, EndDelta, Math.Clamp((_Distance - StartDistance) / span, 0.0f, 1.0f));
}
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(Active ? 1 : 0);
if (!Active)
return;
_Hash.Add(StartDistance);
_Hash.Add(StartDelta);
_Hash.Add(EndDistance);
_Hash.Add(EndDelta);
}
}
/// <summary>
/// The border lifts of a segment's two sides.
/// </summary>
public readonly record struct SegmentBorderBlends(SideBlend Left, SideBlend Right)
{
public void AddToHash(ref StableHash _Hash)
{
Left.AddToHash(ref _Hash);
Right.AddToHash(ref _Hash);
}
}
/// <summary>
/// One surface of the profile, between two of its nodes. U runs across the profile: fixed per node, or
/// accumulated along the cross-section from <see cref="AccumulateFrom"/> when the shape varies (verges, and a
/// pavement whose texture keeps its size whatever its width), at <see cref="AcrossRepeat"/> units a repeat when
/// that is set and at <see cref="Repeat"/> otherwise. V runs along the road at <see cref="Repeat"/>. Planar faces
/// take a top-down projection instead, like the road they extend. A blendable face is the one a neighbouring
/// country road's gravel spills onto (the pavement), painted with blend weights where it does.
/// </summary>
private readonly struct Face
{
public readonly int NodeA;
public readonly int NodeB;
public readonly Material Material;
public readonly float Repeat;
public readonly bool Planar;
public readonly int AccumulateFrom;
public readonly float U0;
public readonly float U1;
public readonly bool Blendable;
public readonly float AcrossRepeat;
public Face(int _NodeA, int _NodeB, Material _Material, float _Repeat, bool _Planar = false, int _AccumulateFrom = -1, float _U0 = 0.0f, float _U1 = 1.0f, bool _Blendable = false, float _AcrossRepeat = 0.0f)
{
NodeA = _NodeA;
NodeB = _NodeB;
Material = _Material;
Repeat = _Repeat;
Planar = _Planar;
AccumulateFrom = _AccumulateFrom;
U0 = _U0;
U1 = _U1;
Blendable = _Blendable;
AcrossRepeat = _AcrossRepeat;
}
}
// Separate regions of the noise field for each thing that wanders, so they don't move in step.
private const float BandShoulder = 0.0f;
private const float BandOuter = 1000.0f;
private const float BandRoll = 2000.0f;
private const float BandSpill = 3000.0f;
/// <summary>
/// Two corners of a face closer than this are one point: the face is a triangle.
/// </summary>
private const float JoinDistance = 1.0f;
/// <summary>
/// How far the gravel's front wanders back and forth along the road, as a fraction of the spill length, and
/// the size of the noise it wanders by (about a wavelength per hundred units).
/// </summary>
private const float SpillWobble = 0.35f;
private const float SpillNoiseScale = 0.01f;
/// <summary>
/// Both borders of a stretch of road, rows at the same distances as the road surface.
/// </summary>
public static PolygonMesh BuildSegment(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, RoadProfile _Profile, float _Spacing, float _Lift, BorderCaps _Caps, in SegmentBorderBlends _Blends, in SegmentBorderTapers _Tapers, BorderDebug _Debug = null)
{
var mesh = new PolygonMesh();
// The same rows the surface is built on, so the border's inner edge sits exactly on the road's.
var rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Profile.Simplification);
var left = new List<PathPoint>(rows.Count);
var right = new List<PathPoint>(rows.Count);
_Debug?.Note($"SEGMENT start={_StartDistance:0.###} end={_EndDistance:0.###} length={_Table.Length:0.###} width={_Width.Base} extras={_Width.LeftStart:0.#}/{_Width.LeftEnd:0.#}/{_Width.RightStart:0.#}/{_Width.RightEnd:0.#} lift={_Lift} spacing={_Spacing} caps={_Caps} profile={_Profile.ResourceName} border={_Profile.Border} borderWidth={_Profile.BorderWidth} curb={_Profile.CurbHeight} bevel={_Profile.CurbBevel}");
_Debug?.Note($"BLEND left {Describe(_Blends.Left)}");
_Debug?.Note($"BLEND right {Describe(_Blends.Right)}");
_Debug?.Note($"TAPER left start={_Tapers.Left.StartLength:0.###}/{_Tapers.Left.StartWidth:0.###} end={_Tapers.Left.EndLength:0.###}/{_Tapers.Left.EndWidth:0.###} right start={_Tapers.Right.StartLength:0.###}/{_Tapers.Right.StartWidth:0.###} end={_Tapers.Right.EndLength:0.###}/{_Tapers.Right.EndWidth:0.###}");
foreach (float distance in rows)
{
Vector3 rightDirection = RoadCurve.RightOf(_Table.TangentAt(distance));
RoadSurfaceBuilder.CrossSection(_Table, distance, _Width, _Lift, out Vector3 leftPos, out Vector3 rightPos);
left.Add(new PathPoint(leftPos, -rightDirection, distance, _Blends.Left.DeltaAt(distance)));
right.Add(new PathPoint(rightPos, rightDirection, distance, _Blends.Right.DeltaAt(distance)));
}
var leftShape = new ProfileShape(_Profile, _Tapers.Left, _Table.Length);
var rightShape = new ProfileShape(_Profile, _Tapers.Right, _Table.Length);
Sweep(mesh, leftShape, left, _Caps.HasFlag(BorderCaps.StartLeft), _Caps.HasFlag(BorderCaps.EndLeft), _Debug, BorderDebug.ArmColor, "arm L");
Sweep(mesh, rightShape, right, _Caps.HasFlag(BorderCaps.StartRight), _Caps.HasFlag(BorderCaps.EndRight), _Debug, BorderDebug.ArmColor, "arm R");
return mesh;
}
/// <summary>
/// A border along a path that is not a road's edge: the rim of a parking lot, where the points and the way out
/// of the surface come from the caller. No taper, no corner lifts; just the profile's cross-section swept along.
/// </summary>
public static void BuildPath(PolygonMesh _Mesh, RoadProfile _Profile, IReadOnlyList<PathPoint> _Path, bool _CapStart, bool _CapEnd, BorderDebug _Debug = null)
{
if (!_Profile.HasBorder || _Path.Count < 2)
return;
Sweep(_Mesh, new ProfileShape(_Profile, default, 1.0f), _Path, _CapStart, _CapEnd, _Debug, BorderDebug.ArmColor, "lot");
}
/// <summary>
/// A profile as swept along one segment: the profile itself, plus the taper at the segment's ends and the
/// segment's length the taper is measured against.
/// </summary>
public readonly struct ProfileShape
{
public readonly RoadProfile Profile;
public readonly RoadBorderTaper Taper;
public readonly float Length;
public ProfileShape(RoadProfile _Profile, in RoadBorderTaper _Taper, float _Length)
{
Profile = _Profile;
Taper = _Taper;
Length = _Length;
}
}
/// <summary>
/// The borders round a junction: each corner path is swept in two halves, one in each neighbouring arm's
/// profile, so a road's border carries on round the corner in its own style. Where two road types meet round a
/// corner, each arm's border has already tapered, by its trim, into the cross-section the two share, and its
/// taper holds that shape on past the trim, so both halves are built in it and meet flush. Where they have not
/// (borders of different kinds, or a road type with no transition) the halves are closed off against each other.
/// The arms' segment blends and tapers are what their chunks were built with, so each half continues its arm's
/// border exactly.
/// </summary>
public static PolygonMesh BuildJunction(RoadJunctionLayout _Layout, Func<RoadSegment, RoadProfile> _ProfileOf, Func<RoadSegment, SegmentBorderBlends> _BlendsOf, Func<RoadSegment, SegmentBorderTapers> _TaperOf, BorderDebug _Debug = null)
{
var mesh = new PolygonMesh();
if (_Debug is not null)
{
_Debug.Note($"JUNCTION node={_Layout.Node.Id} position={BorderDebug.Format(_Layout.Node.Position)} radius={_Layout.Radius} centre={BorderDebug.Format(_Layout.Centre)}");
_Layout.Describe(_Debug);
foreach (var arm in _Layout.Arms)
_Debug.Note($"BLEND arm={arm.Segment.Id} left {Describe(_BlendsOf(arm.Segment).Left)} right {Describe(_BlendsOf(arm.Segment).Right)}");
}
foreach (var path in _Layout.CornerPaths)
{
var profileA = _ProfileOf(path.ArmA.Segment);
var profileB = _ProfileOf(path.ArmB.Segment);
bool differ = !SameProfile(profileA, profileB);
// The first half runs on arm A's left looking out of the node, the second on arm B's right; which side of
// the segment that is depends on which way the segment runs.
var taperA = _TaperOf(path.ArmA.Segment).Side(path.ArmA.AtStart);
var taperB = _TaperOf(path.ArmB.Segment).Side(!path.ArmB.AtStart);
var shapeA = new ProfileShape(profileA, taperA, path.ArmA.Length);
var shapeB = new ProfileShape(profileB, taperB, path.ArmB.Length);
// Two borders that have both tapered into the shape they share meet flush; anything else is two
// different borders, closed off against each other.
bool meet = taperA.TapersAt(path.ArmA.AtStart) && taperB.TapersAt(path.ArmB.AtStart);
bool closeBetween = differ && !meet;
if (profileA.HasBorder)
Sweep(mesh, shapeA, HalfPath(path, true, _BlendsOf(path.ArmA.Segment)), false, closeBetween || !profileB.HasBorder, _Debug, BorderDebug.FirstHalfColor, $"J{_Layout.Node.Id} A{path.ArmA.Segment.Id}");
if (profileB.HasBorder)
Sweep(mesh, shapeB, HalfPath(path, false, _BlendsOf(path.ArmB.Segment)), closeBetween || !profileA.HasBorder, false, _Debug, BorderDebug.SecondHalfColor, $"J{_Layout.Node.Id} B{path.ArmB.Segment.Id}");
}
return mesh;
}
private static string Describe(in SideBlend _Blend)
{
return _Blend.Active
? $"active from={_Blend.StartDistance:0.###} delta={_Blend.StartDelta:0.###} to={_Blend.EndDistance:0.###} delta={_Blend.EndDelta:0.###}"
: "inactive";
}
/// <summary>
/// The same road type, whether or not the two references are the same loaded instance of the asset.
/// </summary>
public static bool SameProfile(RoadProfile _A, RoadProfile _B)
{
if (ReferenceEquals(_A, _B))
return true;
if (_A is null || _B is null)
return false;
return !string.IsNullOrEmpty(_A.ResourcePath) && _A.ResourcePath == _B.ResourcePath;
}
/// <summary>
/// One half of a corner path as a border run. Away-from-road directions come with the path points (the frame
/// of the arm or the arc they are on), so the arm chunk's border meets this one exactly at the row. The along
/// coordinate carries the arm's curve distance on round the corner.
/// </summary>
private static List<PathPoint> HalfPath(RoadJunctionLayout.CornerPath _Path, bool _FirstHalf, in SegmentBorderBlends _Blends)
{
var points = _Path.Points;
int first = _FirstHalf ? 0 : _Path.MidIndex;
int last = _FirstHalf ? _Path.MidIndex : points.Count - 1;
var arm = _FirstHalf ? _Path.ArmA : _Path.ArmB;
int count = last - first + 1;
var result = new List<PathPoint>(count);
if (count < 2)
return result;
// The first half runs on its arm's left (looking out of the node), the second on its arm's right; which
// side of the segment that is depends on which way the segment runs.
bool segmentLeft = _FirstHalf == arm.AtStart;
var side = segmentLeft ? _Blends.Left : _Blends.Right;
// Walking towards the node runs against an arm that starts there, and with one that ends there.
float sign = _FirstHalf ? (arm.AtStart ? -1.0f : 1.0f) : (arm.AtStart ? 1.0f : -1.0f);
var along = new float[count];
if (_FirstHalf)
{
along[0] = arm.CurveDistance(arm.Trim);
for (int k = 1; k < count; k++)
along[k] = along[k - 1] + sign * Vector3.DistanceBetween(points[first + k - 1], points[first + k]);
}
else
{
along[count - 1] = arm.CurveDistance(arm.Trim);
for (int k = count - 1; k > 0; k--)
along[k - 1] = along[k] - sign * Vector3.DistanceBetween(points[first + k - 1], points[first + k]);
}
for (int k = 0; k < count; k++)
{
int index = first + k;
Vector3 outward = _Path.Outwards[index];
// A sharp corner point has no frame of its own: take the right of the way through it.
if (outward.IsNearZeroLength && index > 0 && index < points.Count - 1)
{
Vector3 direction = points[index + 1] - points[index - 1];
outward = new Vector3(direction.y, -direction.x, 0.0f).Normal;
}
var fold = _Path.Fold;
bool onArc = fold is not null && index >= _Path.ArcFirst && index <= _Path.ArcLast;
// Straight stretches take the segment's lift line, exactly like the arm chunk's rows do.
result.Add(new PathPoint(points[index], outward, along[k], side.DeltaAt(along[k]), fold, onArc));
}
return result;
}
/// <summary>
/// Sweeps the profile's cross-section along the path, one row of vertices per point, and closes the open ends.
/// Every face is emitted as two triangles: a verge that rolls with noise is nowhere near planar.
/// </summary>
public static void Sweep(PolygonMesh _Mesh, in ProfileShape _Shape, IReadOnlyList<PathPoint> _Path, bool _CapStart, bool _CapEnd, BorderDebug _Debug = null, Color _DebugColor = default, string _DebugName = null)
{
var profile = _Shape.Profile;
if (!profile.HasBorder)
return;
var path = Compact(_Path);
if (path.Count < 2)
return;
var faces = BuildFaces(profile);
int nodeCount = NodeCount(profile);
var shapes = new Vector2[path.Count][];
var positions = new Vector3[path.Count][];
var across = new float[path.Count][];
var handles = new HalfEdgeMesh.VertexHandle[path.Count][];
var reliefs = new float[path.Count];
var spills = new float[path.Count];
// Gravel spilling over from a country road on the other side of a node is painted onto the pavement as
// vertex blend weights, which need a material that mixes gravel in by them.
var blendMaterial = profile.Border == RoadBorderStyle.Sidewalk ? profile.BlendMaterial : null;
// One winding for the whole run, from which side of its path the border lies on, summed over every step of
// the path: the first and last points alone say nothing on a run that comes back round on itself (the rim
// of a parking lot). Deciding it per quad from the quad's own normal goes wrong on thin or folded quads,
// and a quad wound against its neighbours makes the mesh builder drop whichever face comes second: a hole.
float winding = 0.0f;
for (int i = 0; i < path.Count - 1; i++)
winding += Vector3.Dot(Vector3.Cross(path[i + 1].Position - path[i].Position, path[i].Outward), Vector3.Up);
bool flip = winding > 0.0f;
_Debug?.Note($"RUN name=\"{_DebugName}\" rows={path.Count} (given {_Path.Count}) capStart={(_CapStart ? 1 : 0)} capEnd={(_CapEnd ? 1 : 0)} flip={(flip ? 1 : 0)}");
for (int i = 0; i < path.Count; i++)
{
var shape = new Vector2[nodeCount];
var rows = new Vector3[nodeCount];
reliefs[i] = EvaluateProfile(_Shape, path[i], shape, out spills[i]);
// A pavement takes its lift across the width it has at this row, which a taper may have changed: two
// halves of a corner meeting at a shared width then lift their outer edges alike.
float liftWidth = profile.Border == RoadBorderStyle.Sidewalk ? ShapeWidth(shape) : profile.BorderWidth;
for (int k = 0; k < nodeCount; k++)
rows[k] = NodePosition(path[i], shape[k], liftWidth);
shapes[i] = shape;
positions[i] = rows;
}
// A border tapered to nothing at its end, in height or in width, has no face to close.
_CapStart &= reliefs[0] > 0.01f && ShapeWidth(shapes[0]) > 0.5f;
_CapEnd &= reliefs[^1] > 0.01f && ShapeWidth(shapes[^1]) > 0.5f;
var stalls = RemoveFolds(path, positions, nodeCount);
for (int i = 0; i < path.Count; i++)
{
var rows = positions[i];
var distances = new float[nodeCount];
for (int k = 1; k < nodeCount; k++)
distances[k] = distances[k - 1] + Vector3.DistanceBetween(rows[k - 1], rows[k]);
across[i] = distances;
handles[i] = new HalfEdgeMesh.VertexHandle[nodeCount];
for (int k = 0; k < nodeCount; k++)
{
if (stalls[i][k] >= 0)
continue;
// Two nodes of a row standing in the same place are one vertex. It happens wherever a profile's
// relief has run out — a kerb flattened into a country road's shoulder has its kerb face and its
// outer face squeezed to nothing, and a border grown from nothing at a gore has the whole section
// on one point. Two vertices there would leave the faces between them as zero-area triangles,
// which is degenerate geometry; sharing the vertex makes the face builder drop them instead, and
// the quad on the other side of the seam comes out as the triangle it should be.
if (k > 0 && stalls[i][k - 1] < 0 && Vector3.DistanceBetween(rows[k - 1], rows[k]) < JoinDistance)
{
handles[i][k] = handles[i][k - 1];
continue;
}
handles[i][k] = _Mesh.AddVertices(rows[k])[0];
}
}
// A stalled node shares the vertex it stalled on, so the faces round it come out as triangles instead of
// zero-area quads. Backward stalls point at later rows, hence the second pass.
for (int i = 0; i < path.Count; i++)
{
for (int k = 0; k < nodeCount; k++)
{
int source = i;
while (stalls[source][k] >= 0)
source = stalls[source][k];
handles[i][k] = handles[source][k];
}
}
if (_Debug is not null)
{
for (int i = 0; i < path.Count; i++)
{
var point = path[i];
var rows = positions[i];
_Debug.Line(rows[0], rows[nodeCount - 1], _DebugColor);
if (i == 0 || i == path.Count - 1)
_Debug.Marker(rows[nodeCount - 1], _DebugColor);
var nodes = new StringBuilder();
for (int k = 0; k < nodeCount; k++)
nodes.Append(' ').Append(BorderDebug.Format(rows[k])).Append(point.IsFolded(shapes[i][k].x) ? "F" : "").Append(stalls[i][k] >= 0 ? $"S{stalls[i][k]}" : "");
_Debug.Note($"R {i} pos={BorderDebug.Format(point.Position)} out={BorderDebug.Format(point.Outward)} along={point.Along:0.###} lift={point.Lift:0.###} arc={(point.IsArc ? 1 : 0)} fold={(point.Fold is not null ? 1 : 0)} relief={reliefs[i]:0.###} spill={spills[i]:0.###} |{nodes}");
}
}
if (_Debug is not null && _CapStart)
_Debug.Line(positions[0][0], positions[0][nodeCount - 1], BorderDebug.CapColor);
if (_Debug is not null && _CapEnd)
_Debug.Line(positions[^1][0], positions[^1][nodeCount - 1], BorderDebug.CapColor);
for (int i = 0; i < path.Count - 1; i++)
{
foreach (var face in faces)
{
int a = face.NodeA;
int b = face.NodeB;
// A face whose nodes have both collapsed onto the corner centre has nothing left between them;
// its stretch across the corner is the block corner patch instead.
if (IsFoldedFace(path[i], shapes[i], a, b) || IsFoldedFace(path[i + 1], shapes[i + 1], a, b))
continue;
// Corners of the two rows that sit on one point (the row before a corner's arc reaches the centre
// the arc rows fold onto; every arc row's outer node is the centre) make the face a triangle. Cut
// as a quad it would also give a needle-thin sliver with a random normal, which shows through from
// below and z-fights from above.
bool outerJoined = positions[i][b].Distance(positions[i + 1][b]) < JoinDistance;
bool innerJoined = positions[i][a].Distance(positions[i + 1][a]) < JoinDistance;
if (outerJoined && innerJoined)
continue;
Vector2 uv0 = FaceUv(face, path[i], positions[i][a], across[i], a);
Vector2 uv1 = FaceUv(face, path[i], positions[i][b], across[i], b);
Vector2 uv2 = FaceUv(face, path[i + 1], positions[i + 1][b], across[i + 1], b);
Vector2 uv3 = FaceUv(face, path[i + 1], positions[i + 1][a], across[i + 1], a);
var material = face.Material;
Color32[] blends = null;
// Only the faces the gravel reaches take the blend material; the rest of the pavement stays on the
// plain one, and a weight of nothing on their shared row makes the change invisible.
if (face.Blendable && blendMaterial is not null && (spills[i] > 0.0f || spills[i + 1] > 0.0f))
{
material = blendMaterial;
// One weight per row, the same right across the pavement, so a face's four corners hold a
// gradient that runs one way only. Weighting the kerb side differently from the outer one
// leaves the four corners non-planar, and the two triangles the face is split into then read
// that gradient differently: the split shows up as a kink in the gravel's front, which is a
// far louder thing than the slant it was there to give.
var near = SpillBlend(spills[i]);
var far = SpillBlend(spills[i + 1]);
blends = new[] { near, near, far, far };
}
if (outerJoined)
{
// The folded row's vertex is the exact centre; keep that one.
bool keepNext = path[i + 1].IsFolded(shapes[i + 1][b].x) || !path[i].IsFolded(shapes[i][b].x);
var outer = keepNext ? handles[i + 1][b] : handles[i][b];
Vector2 uvOuter = keepNext ? uv2 : uv1;
AddTriangle(_Mesh, material, flip, handles[i][a], outer, handles[i + 1][a], uv0, uvOuter, uv3,
blends is null ? null : new[] { blends[0], keepNext ? blends[2] : blends[1], blends[3] });
}
else if (innerJoined)
{
AddTriangle(_Mesh, material, flip, handles[i][a], handles[i][b], handles[i + 1][b], uv0, uv1, uv2,
blends is null ? null : new[] { blends[0], blends[1], blends[2] });
}
else
{
AddQuad(_Mesh, material, flip,
handles[i][a], handles[i][b], handles[i + 1][b], handles[i + 1][a],
uv0, uv1, uv2, uv3, blends);
}
}
}
AddCornerPatches(_Mesh, faces, path, shapes, positions, across, _Debug);
var capMaterial = profile.Border == RoadBorderStyle.CountrySide ? profile.ResolvedRoadMaterial : profile.ResolvedBorderMaterial;
float capRepeat = profile.Border == RoadBorderStyle.CountrySide ? profile.RoadTextureRepeat : profile.BorderTextureRepeat;
if (_CapStart)
AddCap(_Mesh, capMaterial, capRepeat, handles[0], positions[0], shapes[0], path[0].Position - path[1].Position);
if (_CapEnd)
AddCap(_Mesh, capMaterial, capRepeat, handles[^1], positions[^1], shapes[^1], path[^1].Position - path[^2].Position);
}
/// <summary>
/// How far out from the road edge a cross-section reaches.
/// </summary>
private static float ShapeWidth(Vector2[] _Shape)
{
float width = 0.0f;
foreach (var node in _Shape)
width = MathF.Max(width, node.x);
return width;
}
private static List<PathPoint> Compact(IReadOnlyList<PathPoint> _Path)
{
var result = new List<PathPoint>(_Path.Count);
foreach (var point in _Path)
{
if (result.Count > 0 && Vector3.DistanceBetween(result[^1].Position, point.Position) < 0.01f)
continue;
result.Add(point);
}
return result;
}
private static int NodeCount(RoadProfile _Profile)
{
return _Profile.Border switch
{
RoadBorderStyle.Sidewalk => 4,
RoadBorderStyle.CountrySide => DropFoot(_Profile) + 1 + _Profile.SlopeSegments,
_ => 0
};
}
/// <summary>
/// Whether a country side border has a shoulder of road material before it drops away. A road type that asks
/// for no width there gets none: the verge starts at the road's edge, rather than a hair out from it behind a
/// strip too thin to see and too thin to be worth a face.
/// </summary>
private static bool HasShoulder(RoadProfile _Profile)
{
return _Profile.BorderWidth > 0.0f;
}
/// <summary>
/// The node at the foot of the drop, which the verge's slope runs from: past the road's edge, and past the
/// shoulder when there is one.
/// </summary>
private static int DropFoot(RoadProfile _Profile)
{
return HasShoulder(_Profile) ? 2 : 1;
}
private static Face[] BuildFaces(RoadProfile _Profile)
{
switch (_Profile.Border)
{
case RoadBorderStyle.Sidewalk:
{
var material = _Profile.ResolvedBorderMaterial;
float repeat = _Profile.BorderTextureRepeat;
// Kerb face, pavement, outer face. The pavement's texture keeps its own size across, measured out
// from the kerb: a narrower pavement shows less of it and a wider one runs on into the next repeat,
// instead of the one texture being squeezed or pulled to fit. The kerb texture follows the slope of a
// bevelled kerb instead of stretching.
float bevelU = new Vector2(_Profile.CurbBevel, _Profile.CurbHeight).Length / repeat;
float heightU = _Profile.CurbHeight / repeat;
return new[]
{
new Face(0, 1, material, repeat, _U0: bevelU, _U1: 0.0f),
new Face(1, 2, material, repeat, _AccumulateFrom: 1, _Blendable: true, _AcrossRepeat: _Profile.BorderTextureWidth),
new Face(2, 3, material, repeat, _U0: 1.0f - heightU, _U1: 1.0f)
};
}
case RoadBorderStyle.CountrySide:
{
var roadMaterial = _Profile.ResolvedRoadMaterial;
var vergeMaterial = _Profile.ResolvedBorderMaterial;
float roadRepeat = _Profile.RoadTextureRepeat;
float vergeRepeat = _Profile.BorderTextureRepeat;
// Shoulder (an extension of the road, so it tiles like one) where the road type has one, the drop
// (the road's exposed thickness), then the slope down to the terrain.
int foot = DropFoot(_Profile);
var faces = new List<Face>();
if (HasShoulder(_Profile))
faces.Add(new Face(0, 1, roadMaterial, roadRepeat, _Planar: true));
faces.Add(new Face(foot - 1, foot, roadMaterial, roadRepeat, _U0: 0.0f, _U1: _Profile.Drop / roadRepeat));
for (int k = 0; k < _Profile.SlopeSegments; k++)
faces.Add(new Face(foot + k, foot + 1 + k, vergeMaterial, vergeRepeat, _AccumulateFrom: foot));
return faces.ToArray();
}
default:
return Array.Empty<Face>();
}
}
/// <summary>
/// The cross-section at a point as (distance from the road edge, height above the road surface) per node.
/// Country side borders read noise at the point's position, so the wander is continuous across chunk and
/// junction seams and differs between the two sides of the road. Near a tapered end the relief (kerb height,
/// drop, verge depth, roll) is scaled down and the width run towards the neighbour's, so the border comes
/// down to a flat strip of the neighbour's width at the node. Returns the relief in force at the point, and
/// how much of a neighbouring country road's gravel has spilled this far (1 at the node, 0 beyond the taper).
/// </summary>
private static float EvaluateProfile(in ProfileShape _Shape, in PathPoint _Point, Vector2[] _Nodes, out float _Spill)
{
var profile = _Shape.Profile;
float relief = _Shape.Taper.ReliefAt(_Point.Along, _Shape.Length, out float towards, out float targetWidth, out bool spill);
bool tapering = towards > 0.0f;
_Spill = spill ? SpillAt(towards, _Point.Position) : 0.0f;
switch (profile.Border)
{
case RoadBorderStyle.Sidewalk:
{
float width = tapering ? MathX.Lerp(profile.BorderWidth, targetWidth, towards) : profile.BorderWidth;
float height = profile.CurbHeight * relief;
_Nodes[0] = new Vector2(0.0f, 0.0f);
_Nodes[1] = new Vector2(MathF.Min(profile.CurbBevel * relief, width), height);
_Nodes[2] = new Vector2(width, height);
_Nodes[3] = new Vector2(width, 0.0f);
break;
}
case RoadBorderStyle.CountrySide:
{
float scale = profile.ChaosScale;
// A relief above 1 raises a verge to meet a deeper one; it does not make it any wilder.
float amplitude = profile.Chaos * MathF.Min(relief, 1.0f);
float x = _Point.Position.x * scale;
float y = _Point.Position.y * scale;
// Noise is 0..1; centre it to roughly plus or minus the chaos amount.
float meanderShoulder = (Noise.Perlin(x + BandShoulder, y) - 0.5f) * 2.0f * amplitude;
float meanderOuter = (Noise.Perlin(x + BandOuter, y) - 0.5f) * 2.0f * amplitude;
// A shoulder that is meant to be there is floored, so a strong inward wander can never fold it back
// onto the road; a road type that asks for none has none at all, and its verge drops away at the
// road's own edge.
bool hasShoulder = HasShoulder(profile);
float shoulder = hasShoulder ? MathF.Max(1.0f, profile.BorderWidth + meanderShoulder) : 0.0f;
float span = MathF.Max(1.0f, profile.VergeWidth + meanderOuter);
// Towards a different road type the whole strip runs to that type's width, keeping its proportions.
if (tapering)
{
float total = shoulder + span;
float ratio = total > 0.001f ? MathX.Lerp(total, targetWidth, towards) / total : 1.0f;
shoulder = hasShoulder ? MathF.Max(0.5f, shoulder * ratio) : 0.0f;
span = MathF.Max(0.5f, span * ratio);
}
int foot = DropFoot(profile);
_Nodes[0] = new Vector2(0.0f, 0.0f);
if (hasShoulder)
_Nodes[1] = new Vector2(shoulder, 0.0f);
_Nodes[foot] = new Vector2(shoulder, -profile.Drop * relief);
for (int k = 1; k <= profile.SlopeSegments; k++)
{
float fraction = (float)k / profile.SlopeSegments;
// The roll varies along the road and across the slope, so the verge undulates in 3D.
float roll = (Noise.Fbm(3, x, y, BandRoll + fraction * profile.VergeWidth * scale) - 0.5f) * 2.0f * amplitude;
_Nodes[foot + k] = new Vector2(shoulder + fraction * span, (-profile.Drop - fraction * profile.VergeDepth) * relief + roll);
}
break;
}
}
return relief;
}
/// <summary>
/// How much gravel has spilled to a point a fraction of the way into a taper (1 at the node): the fraction
/// itself, with its front pushed back and forth by noise read at the point, most in the middle of the run so
/// the node stays fully covered and the far end clean.
/// </summary>
private static float SpillAt(float _Towards, Vector3 _Position)
{
float wobble = (Noise.Perlin(_Position.x * SpillNoiseScale + BandSpill, _Position.y * SpillNoiseScale) - 0.5f) * 2.0f * SpillWobble;
return Math.Clamp(_Towards + wobble * 4.0f * _Towards * (1.0f - _Towards), 0.0f, 1.0f);
}
/// <summary>
/// A spill amount as the vertex blend a blendable material reads: its red channel is how much of the second
/// layer (the gravel) shows.
/// </summary>
private static Color32 SpillBlend(float _Spill)
{
byte weight = (byte)Math.Clamp(MathF.Round(_Spill * 255.0f), 0.0f, 255.0f);
return new Color32(weight, 0, 0, 255);
}
private static Vector2 FaceUv(in Face _Face, in PathPoint _Point, Vector3 _Position, float[] _Across, int _Node)
{
if (_Face.Planar)
return new Vector2(_Position.x, _Position.y) / _Face.Repeat;
float across = _Face.AcrossRepeat > 0.0f ? _Face.AcrossRepeat : _Face.Repeat;
float u = _Face.AccumulateFrom >= 0
? (_Across[_Node] - _Across[_Face.AccumulateFrom]) / across
: (_Node == _Face.NodeA ? _Face.U0 : _Face.U1);
return new Vector2(u, _Point.Along / _Face.Repeat);
}
/// <summary>
/// On the inside of a bend tighter than the road is wide plus the border, the border's outer edge runs
/// backwards: a row's outer node lands behind the previous row's and the face between them folds over itself.
/// The offset curve has a loop there, and the fix is the one for offset curves: cut the loop out. A node whose
/// face would fold instead stays on the previous row's node (it shares that vertex), so the outer edge runs
/// straight from the last good node to the next one. The first and last rows are never moved: they are the
/// rows a run shares with its neighbouring piece, and the seam stays exact; a fold against the last row is
/// taken out by stalling the rows before it onto it instead. Arc rows are left to the corner fold. Returns,
/// per row and node, the row whose node it stalled onto (-1 when it kept its own).
/// </summary>
internal static int[][] RemoveFolds(List<PathPoint> _Path, Vector3[][] _Positions, int _NodeCount)
{
int count = _Path.Count;
var stalls = new int[count][];
for (int i = 0; i < count; i++)
{
stalls[i] = new int[_NodeCount];
Array.Fill(stalls[i], -1);
}
if (count < 3)
return stalls;
for (int k = 1; k < _NodeCount; k++)
{
// Forward: each row against the last row that kept its own node.
for (int i = 1; i < count - 1; i++)
{
if (_Path[i].IsArc || _Path[i - 1].IsArc)
continue;
if (FaceIsSimple(_Positions[i - 1][0], _Positions[i][0], _Positions[i][k], _Positions[i - 1][k]))
continue;
_Positions[i][k] = _Positions[i - 1][k];
stalls[i][k] = i - 1;
}
// Backward: the last row cannot move, so rows folding against it move onto it.
for (int i = count - 2; i >= 1; i--)
{
if (_Path[i].IsArc || _Path[i + 1].IsArc)
continue;
if (FaceIsSimple(_Positions[i][0], _Positions[i + 1][0], _Positions[i + 1][k], _Positions[i][k]))
break;
_Positions[i][k] = _Positions[i + 1][k];
stalls[i][k] = i + 1;
}
}
return stalls;
}
/// <summary>
/// True when the face with these corners (kerb, next kerb, next node, node) does not cross itself in plan.
/// </summary>
private static bool FaceIsSimple(Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D)
{
return !SegmentsCross(Flat(_A), Flat(_B), Flat(_C), Flat(_D)) && !SegmentsCross(Flat(_B), Flat(_C), Flat(_D), Flat(_A));
}
/// <summary>
/// Proper crossing of the segments p-q and r-s; touching at an end does not count.
/// </summary>
private static bool SegmentsCross(Vector2 _P, Vector2 _Q, Vector2 _R, Vector2 _S)
{
Vector2 pq = _Q - _P;
Vector2 rs = _S - _R;
float denominator = pq.x * rs.y - pq.y * rs.x;
if (MathF.Abs(denominator) < 0.000001f)
return false;
Vector2 pr = _R - _P;
float t = (pr.x * rs.y - pr.y * rs.x) / denominator;
float u = (pr.x * pq.y - pr.y * pq.x) / denominator;
return t > 0.001f && t < 0.999f && u > 0.001f && u < 0.999f;
}
/// <summary>
/// Where a profile node lands for a path point. The lift is taken in full by the outer edge of the border and
/// not at all by the kerb, with the nodes in between sharing it by their offset, so a lifted row tilts across
/// its width. On a corner's arc the lift is whatever brings the row to the corner's one height, and a node
/// further out than the fillet radius would pass through the centre and come back folded, so it stops at the
/// centre instead: the rows of the arc make a pie, and the block corner beyond the centre is patched in
/// separately.
/// </summary>
private static Vector3 NodePosition(in PathPoint _Point, Vector2 _Node, float _BorderWidth)
{
if (_Point.IsFolded(_Node.x))
return new Vector3(_Point.Fold.Centre.x, _Point.Fold.Centre.y, _Point.Fold.ApexHeight + _Node.y);
float weight = MathF.Min(_Node.x / MathF.Max(_BorderWidth, 1.0f), 1.0f);
float lift = _Point.IsArc && _Point.Fold is { } fold ? fold.ApexHeight - _Point.Position.z : _Point.Lift;
Vector3 flat = _Point.Position + _Point.Outward * _Node.x;
return new Vector3(flat.x, flat.y, _Point.Position.z + weight * lift + _Node.y);
}
private static bool IsFoldedFace(in PathPoint _Point, Vector2[] _Shape, int _NodeA, int _NodeB)
{
return _Point.IsFolded(_Shape[_NodeA].x) && _Point.IsFolded(_Shape[_NodeB].x);
}
/// <summary>
/// The block corner: what lies beyond the corner centre, out to where the two straight outer edges of the
/// border meet. For a face whose outer node collapsed onto the centre it is the triangle from the centre to
/// that node on the last full row and to the mitre point; for a face whose nodes both collapsed it is the
/// strip between their two mitre points. Both halves of a corner contribute their triangle, together making
/// the square. Fresh vertices: these faces share no edges with the sweep.
/// </summary>
private static void AddCornerPatches(PolygonMesh _Mesh, Face[] _Faces, List<PathPoint> _Path, Vector2[][] _Shapes, Vector3[][] _Positions, float[][] _Across, BorderDebug _Debug)
{
int arcStart = -1;
int arcEnd = -1;
for (int i = 0; i < _Path.Count; i++)
{
if (!_Path[i].IsArc)
continue;
if (arcStart < 0)
arcStart = i;
arcEnd = i;
}
if (arcStart < 0 || _Path[arcStart].Fold is not { } fold)
return;
// The last full row before the arc (first half of a corner) or the first after it (second half).
int t = arcStart > 0 ? arcStart - 1 : (arcEnd < _Path.Count - 1 ? arcEnd + 1 : -1);
if (t < 0)
return;
var row = _Path[t];
var shape = _Shapes[t];
var bisector = new Vector3(fold.Bisector.x, fold.Bisector.y, 0.0f);
// Which way the along-the-road coordinate runs towards the corner, for the faces that keep it.
float towardsCorner = MathF.Sign(_Path[arcStart].Along - row.Along);
_Debug?.Note($"PATCHES arcStart={arcStart} arcEnd={arcEnd} row={t} apex={BorderDebug.Format(fold.Apex)} centre={BorderDebug.Format(fold.Centre)} bisector={BorderDebug.Format(fold.Bisector)} halfSin={fold.HalfAngleSin:0.####} radius={fold.Radius:0.###} apexHeight={fold.ApexHeight:0.###}");
foreach (var face in _Faces)
{
int a = face.NodeA;
int b = face.NodeB;
// Nothing collapsed: the sweep covered this face all the way round.
if (shape[b].x < fold.Radius)
continue;
_Debug?.Note($"PATCH face={a}-{b} offsetA={shape[a].x:0.###} offsetB={shape[b].x:0.###} mitreA={BorderDebug.Format(fold.MitrePoint(shape[a].x))} mitreB={BorderDebug.Format(fold.MitrePoint(shape[b].x))} rowA={BorderDebug.Format(_Positions[t][a])} rowB={BorderDebug.Format(_Positions[t][b])}");
Vector2 edge = shape[b] - shape[a];
Vector3 expected = bisector * -edge.y + Vector3.Up * edge.x;
// The profile's texture runs across the border and along the road; a block corner keeps the row's
// across coordinate and carries the along coordinate on to the mitre.
Vector2 mitreB = fold.MitrePoint(shape[b].x);
var pointB = _Positions[t][b];
var cornerB = new Vector3(mitreB.x, mitreB.y, fold.ApexHeight + shape[b].y);
Vector2 uvB = FaceUv(face, row, pointB, _Across[t], b);
Vector2 uvCornerB = face.Planar ? PlanarUv(cornerB, face.Repeat) : uvB + new Vector2(0.0f, towardsCorner * Flat(pointB).Distance(Flat(cornerB)) / face.Repeat);
if (shape[a].x < fold.Radius)
{
var centre = new Vector3(fold.Centre.x, fold.Centre.y, fold.ApexHeight + shape[b].y);
// A border exactly as wide as the fillet: the block corner is a point.
if (Flat(pointB).Distance(Flat(centre)) < 0.5f || Flat(cornerB).Distance(Flat(centre)) < 0.5f)
continue;
Vector2 uvCentre = face.Planar ? PlanarUv(centre, face.Repeat) : uvB;
AddPatchTriangle(_Mesh, face.Material, expected, centre, pointB, cornerB, uvCentre, uvB, uvCornerB);
if (_Debug is not null)
{
_Debug.Line(centre, pointB, BorderDebug.PatchColor);
_Debug.Line(pointB, cornerB, BorderDebug.PatchColor);
_Debug.Line(cornerB, centre, BorderDebug.PatchColor);
}
continue;
}
Vector2 mitreA = fold.MitrePoint(shape[a].x);
var pointA = _Positions[t][a];
var cornerA = new Vector3(mitreA.x, mitreA.y, fold.ApexHeight + shape[a].y);
if (Flat(pointA).Distance(Flat(cornerA)) < 0.5f && Flat(pointB).Distance(Flat(cornerB)) < 0.5f)
continue;
Vector2 uvA = FaceUv(face, row, pointA, _Across[t], a);
Vector2 uvCornerA = face.Planar ? PlanarUv(cornerA, face.Repeat) : uvA + new Vector2(0.0f, towardsCorner * Flat(pointA).Distance(Flat(cornerA)) / face.Repeat);
AddPatchQuad(_Mesh, face.Material, expected, pointA, cornerA, cornerB, pointB, uvA, uvCornerA, uvCornerB, uvB);
if (_Debug is not null)
{
_Debug.Line(pointA, cornerA, BorderDebug.PatchColor);
_Debug.Line(cornerA, cornerB, BorderDebug.PatchColor);
_Debug.Line(cornerB, pointB, BorderDebug.PatchColor);
}
}
}
private static void AddPatchTriangle(PolygonMesh _Mesh, Material _Material, Vector3 _Expected, Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)
{
var handles = _Mesh.AddVertices(_A, _B, _C);
if (Vector3.Dot(Vector3.Cross(_B - _A, _C - _A), _Expected) >= 0.0f)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[1], handles[2], _UvA, _UvB, _UvC);
else
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[1], _UvA, _UvC, _UvB);
}
private static void AddPatchQuad(PolygonMesh _Mesh, Material _Material, Vector3 _Expected, Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)
{
var handles = _Mesh.AddVertices(_A, _B, _C, _D);
Vector3 normal = Vector3.Cross(_B - _A, _C - _A);
if (normal.IsNearZeroLength)
normal = Vector3.Cross(_C - _A, _D - _A);
// One decision for both triangles: they share an edge and must agree.
if (Vector3.Dot(normal, _Expected) >= 0.0f)
{
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[1], handles[2], _UvA, _UvB, _UvC);
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[3], _UvA, _UvC, _UvD);
}
else
{
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[1], _UvA, _UvC, _UvB);
MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[3], handles[2], _UvA, _UvD, _UvC);
}
}
private static Vector2 PlanarUv(Vector3 _Position, float _Repeat)
{
return new Vector2(_Position.x, _Position.y) / _Repeat;
}
private static Vector2 Flat(Vector3 _Vector)
{
return new Vector2(_Vector.x, _Vector.y);
}
/// <summary>
/// One triangle in the run's winding: the corners in the same order a quad's would be.
/// </summary>
/// <summary>
/// A triangle in the run's winding, with blend weights per corner (in the given, unflipped order) when the
/// face is painted.
/// </summary>
private static void AddTriangle(PolygonMesh _Mesh, Material _Material, bool _Flip,
HalfEdgeMesh.VertexHandle _HA, HalfEdgeMesh.VertexHandle _HB, HalfEdgeMesh.VertexHandle _HC,
Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Color32[] _Blends = null)
{
HalfEdgeMesh.FaceHandle face;
if (!_Flip)
{
face = MeshUtility.AddTexturedTriangle(_Mesh, _Material, _HA, _HB, _HC, _UvA, _UvB, _UvC);
if (_Blends is not null)
MeshUtility.SetFaceBlends(_Mesh, face, _Blends);
}
else
{
face = MeshUtility.AddTexturedTriangle(_Mesh, _Material, _HA, _HC, _HB, _UvA, _UvC, _UvB);
if (_Blends is not null)
MeshUtility.SetFaceBlends(_Mesh, face, new[] { _Blends[0], _Blends[2], _Blends[1] });
}
}
/// <summary>
/// A quad as two triangles, in the run's winding.
/// </summary>
private static void AddQuad(PolygonMesh _Mesh, Material _Material, bool _Flip,
HalfEdgeMesh.VertexHandle _HA, HalfEdgeMesh.VertexHandle _HB, HalfEdgeMesh.VertexHandle _HC, HalfEdgeMesh.VertexHandle _HD,
Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD, Color32[] _Blends = null)
{
AddTriangle(_Mesh, _Material, _Flip, _HA, _HB, _HC, _UvA, _UvB, _UvC, _Blends is null ? null : new[] { _Blends[0], _Blends[1], _Blends[2] });
AddTriangle(_Mesh, _Material, _Flip, _HA, _HC, _HD, _UvA, _UvC, _UvD, _Blends is null ? null : new[] { _Blends[0], _Blends[2], _Blends[3] });
}
/// <summary>
/// Closes a run's end with the cross-section itself, fanned from the road edge and facing along the road.
/// </summary>
private static void AddCap(PolygonMesh _Mesh, Material _Material, float _Repeat, HalfEdgeMesh.VertexHandle[] _Handles, Vector3[] _Positions, Vector2[] _Shape, Vector3 _Facing)
{
for (int k = 1; k < _Positions.Length - 1; k++)
{
Vector3 normal = Vector3.Cross(_Positions[k] - _Positions[0], _Positions[k + 1] - _Positions[0]);
Vector2 uv0 = _Shape[0] / _Repeat;
Vector2 uv1 = _Shape[k] / _Repeat;
Vector2 uv2 = _Shape[k + 1] / _Repeat;
if (Vector3.Dot(normal, _Facing) >= 0.0f)
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Handles[0], _Handles[k], _Handles[k + 1], uv0, uv1, uv2);
else
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Handles[0], _Handles[k + 1], _Handles[k], uv0, uv2, uv1);
}
}
}