RoadComponent partial class methods that build a "Country Side" sidewalk geometry. It computes a noisy sloped verge and flat shoulder, creates two polygon meshes (shoulder and verge), assigns materials and UVs, splits quads into triangles and instantiates child GameObjects with MeshComponent.
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Utility;
namespace RedSnail.RoadTool;
public partial class RoadComponent
{
/// <summary>
/// Builds the "Country Side" sidewalk: a flat road-material shoulder that steps down and falls away on an
/// undulating, organically irregular slope (sidewalk material) which merges into the terrain. The shoulder and the
/// verge live in separate meshes because each carries a different material (matching the one-material-per-mesh flow
/// the rest of the tool uses). Vertices are shared within each mesh so the rolling slope shades smoothly while the
/// near-vertical drop and the road/verge material seam stay crisp.
/// </summary>
private void BuildCountrySideSidewalk(Transform[] _Frames, List<int> _SegmentsToKeep)
{
var roadMaterial = RoadMaterial ?? Material.Load("materials/dev/reflectivity_30.vmat");
var sidewalkMaterial = SidewalkMaterial ?? Material.Load("materials/dev/reflectivity_70.vmat");
var shoulderMesh = new PolygonMesh(); // ROAD material: the flat shoulder + the vertical drop edge (the road slab's thickness)
var vergeMesh = new PolygonMesh(); // SIDEWALK material: the undulating slope down to the terrain
int count = _SegmentsToKeep.Count;
float halfRoad = RoadWidth * 0.5f;
int slopeSegments = Math.Max(1, VergeSlopeSegments);
int nodeCount = 2 + slopeSegments; // 0 = drop top (road level), 1 = drop bottom, 2.. = slope down to the outer edge
int vergeNodeCount = nodeCount - 1; // the slope reuses the drop-bottom node as its top
// Along-road distance drives the V coordinate and the noise sampling, so the wobble is stable frame to frame.
var dist = new float[count];
for (int i = 1; i < count; i++)
dist[i] = dist[i - 1] + Vector3.DistanceBetween(_Frames[_SegmentsToKeep[i - 1]].Position, _Frames[_SegmentsToKeep[i]].Position);
// shoulder verts per frame = [roadEdge, dropTop (node0), dropBottom (node1)]; verge verts = [node1 .. node(nodeCount-1)]
var shoulderVerts = new HalfEdgeMesh.VertexHandle[2][][];
var shoulderTopUV = new Vector2[2][][];
var vergeVerts = new HalfEdgeMesh.VertexHandle[2][][];
var vergeU = new float[2][][];
// side 0 = right (+1), side 1 = left (-1)
for (int s = 0; s < 2; s++)
{
float sideSign = s == 0 ? 1.0f : -1.0f;
shoulderVerts[s] = new HalfEdgeMesh.VertexHandle[count][];
shoulderTopUV[s] = new Vector2[count][];
vergeVerts[s] = new HalfEdgeMesh.VertexHandle[count][];
vergeU[s] = new float[count][];
for (int i = 0; i < count; i++)
{
var frameNodes = new Vector3[nodeCount];
Vector3 roadEdge = ComputeCountrySideProfile(_Frames[_SegmentsToKeep[i]], dist[i], sideSign, halfRoad, slopeSegments, frameNodes);
shoulderVerts[s][i] = new[]
{
shoulderMesh.AddVertices(roadEdge)[0],
shoulderMesh.AddVertices(frameNodes[0])[0],
shoulderMesh.AddVertices(frameNodes[1])[0],
};
shoulderTopUV[s][i] = new[] { PlanarRoadUV(roadEdge), PlanarRoadUV(frameNodes[0]) };
// The slope shares the drop-bottom node (node1) as its top, so it starts at index 1.
var slopeNodes = new Vector3[vergeNodeCount];
Array.Copy(frameNodes, 1, slopeNodes, 0, vergeNodeCount);
vergeVerts[s][i] = vergeMesh.AddVertices(slopeNodes);
var u = new float[vergeNodeCount];
for (int n = 1; n < vergeNodeCount; n++)
u[n] = u[n - 1] + Vector3.DistanceBetween(slopeNodes[n - 1], slopeNodes[n]) / SidewalkTextureRepeat;
vergeU[s][i] = u;
}
}
float roadRepeat = RoadTextureInchesPerRepeat;
float dropU = CountrySideDrop / roadRepeat;
for (int s = 0; s < 2; s++)
{
bool leftSide = s == 1;
for (int i = 0; i < count - 1; i++)
{
// Flat road-material shoulder (road edge -> shoulder outer edge), planar UVs so it tiles with the road.
AddCountrySideQuad(shoulderMesh, roadMaterial, leftSide,
shoulderVerts[s][i][0], shoulderVerts[s][i][1], shoulderVerts[s][i + 1][0], shoulderVerts[s][i + 1][1],
shoulderTopUV[s][i][0], shoulderTopUV[s][i][1], shoulderTopUV[s][i + 1][0], shoulderTopUV[s][i + 1][1]);
// Vertical drop = the exposed thickness of the road slab, so it keeps the ROAD material. Wrapped UVs
// (U down the drop, V along the road) because a planar projection would smear on a vertical face.
float dropV0 = dist[i] / roadRepeat;
float dropV1 = dist[i + 1] / roadRepeat;
AddCountrySideQuad(shoulderMesh, roadMaterial, leftSide,
shoulderVerts[s][i][1], shoulderVerts[s][i][2], shoulderVerts[s][i + 1][1], shoulderVerts[s][i + 1][2],
new Vector2(0.0f, dropV0), new Vector2(dropU, dropV0), new Vector2(0.0f, dropV1), new Vector2(dropU, dropV1));
// Undulating slope (sidewalk material).
float v0 = dist[i] / SidewalkTextureRepeat;
float v1 = dist[i + 1] / SidewalkTextureRepeat;
for (int n = 0; n < vergeNodeCount - 1; n++)
{
AddCountrySideQuad(vergeMesh, sidewalkMaterial, leftSide,
vergeVerts[s][i][n], vergeVerts[s][i][n + 1], vergeVerts[s][i + 1][n], vergeVerts[s][i + 1][n + 1],
new Vector2(vergeU[s][i][n], v0), new Vector2(vergeU[s][i][n + 1], v0),
new Vector2(vergeU[s][i + 1][n], v1), new Vector2(vergeU[s][i + 1][n + 1], v1));
}
}
}
CreateCountrySideMeshChild("Sidewalk Shoulder", shoulderMesh);
CreateCountrySideMeshChild("Sidewalk", vergeMesh);
}
/// <summary>
/// Fills <paramref name="_Nodes"/> with the verge cross-section (node 0 = top of the drop at road level, node 1 =
/// bottom of the drop, the rest stepping down the slope to the outer edge) and returns the road-edge point where the
/// flat shoulder begins. Perlin/FBM noise meanders the shoulder and outer edges and rolls the slope depth so the
/// verge reads like uneven natural ground; a per-side seed keeps the two sides from mirroring each other.
/// </summary>
private Vector3 ComputeCountrySideProfile(Transform _Frame, float _Dist, float _SideSign, float _HalfRoad, int _SlopeSegments, Vector3[] _Nodes)
{
Vector3 p = _Frame.Position;
Vector3 outward = _Frame.Rotation.Right * _SideSign; // points away from the road centre for this side
Vector3 up = _Frame.Rotation.Up;
float scale = VergeChaosScale;
float amp = VergeChaosAmount;
float seed = _SideSign > 0.0f ? 0.0f : 1337.0f;
// Noise is 0..1; centre it to roughly ±amp. Separate Y bands keep the three offsets from correlating.
float meanderShoulder = (Noise.Perlin(_Dist * scale + seed, 11.0f) - 0.5f) * 2.0f * amp;
float meanderOuter = (Noise.Perlin(_Dist * scale + seed, 71.0f) - 0.5f) * 2.0f * amp;
// Floor at a small positive width so a strong inward meander can never collapse two nodes onto each other
// (a zero-width strip would be a degenerate, dropped face).
float shoulderWidth = MathF.Max(1.0f, SidewalkWidth + meanderShoulder);
float vergeSpan = MathF.Max(1.0f, CountrySideVergeWidth + meanderOuter);
float latShoulder = _HalfRoad + shoulderWidth;
_Nodes[0] = p + outward * latShoulder; // drop top (road level)
_Nodes[1] = p + outward * latShoulder - up * CountrySideDrop; // drop bottom
for (int k = 1; k <= _SlopeSegments; k++)
{
float f = (float)k / _SlopeSegments;
// Roll varies along the road AND across the slope (Y = nominal cross position) so the surface undulates in 3D.
float roll = (Noise.Fbm(3, _Dist * scale + seed, 200.0f + f * CountrySideVergeWidth * scale) - 0.5f) * 2.0f * amp;
float lat = latShoulder + f * vergeSpan;
float h = -CountrySideDrop - f * CountrySideVergeDepth + roll;
_Nodes[1 + k] = p + outward * lat + up * h;
}
return p + outward * _HalfRoad;
}
private Vector2 PlanarRoadUV(Vector3 _WorldPos)
{
return new Vector2(_WorldPos.x, _WorldPos.y) / RoadTextureInchesPerRepeat;
}
/// <summary>
/// Emits one up/outward-facing quad of a verge strip. The left side is the mirror of the right, which flips the
/// winding, so the vertex order is chosen per side to keep every face front-facing.
/// </summary>
private static void AddCountrySideQuad(PolygonMesh _Mesh, Material _Material, bool _LeftSide,
HalfEdgeMesh.VertexHandle _Inner0, HalfEdgeMesh.VertexHandle _Outer0,
HalfEdgeMesh.VertexHandle _Inner1, HalfEdgeMesh.VertexHandle _Outer1,
Vector2 _UvInner0, Vector2 _UvOuter0, Vector2 _UvInner1, Vector2 _UvOuter1)
{
// Emit two triangles rather than a quad. The rolling slope and lateral meander make many of these faces
// non-planar (and, at high chaos scale, slightly folded), which AddFace rejects outright — that is the holes
// that appear when the chaos is pushed up. The two triangles a quad splits into are always planar, so they hold.
if (!_LeftSide)
{
// quad winding: Inner0 -> Outer0 -> Outer1 -> Inner1
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Inner0, _Outer0, _Outer1, _UvInner0, _UvOuter0, _UvOuter1);
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Inner0, _Outer1, _Inner1, _UvInner0, _UvOuter1, _UvInner1);
}
else
{
// quad winding: Inner0 -> Inner1 -> Outer1 -> Outer0
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Inner0, _Inner1, _Outer1, _UvInner0, _UvInner1, _UvOuter1);
MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Inner0, _Outer1, _Outer0, _UvInner0, _UvOuter1, _UvOuter0);
}
}
private void CreateCountrySideMeshChild(string _Name, PolygonMesh _PolygonMesh)
{
var child = new GameObject(GameObject, true, _Name);
child.Tags.Add(RoadMeshTag);
child.Tags.Add(SidewalkSurfaceTag);
var meshComponent = child.AddComponent<MeshComponent>();
meshComponent.Mesh = _PolygonMesh;
meshComponent.SmoothingAngle = 40.0f;
}
}