From 16bff3a42a60e142835e316cfdefc74b442a9b10 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 13 May 2026 18:28:48 +0200 Subject: [PATCH] Refactor spline road mesh generation by simplifying boundary handling and capping logic --- src/geometry/spline-road-mesh.ts | 185 ++++++------------------------- 1 file changed, 36 insertions(+), 149 deletions(-) diff --git a/src/geometry/spline-road-mesh.ts b/src/geometry/spline-road-mesh.ts index 39159d56..fe6a2d9b 100644 --- a/src/geometry/spline-road-mesh.ts +++ b/src/geometry/spline-road-mesh.ts @@ -51,17 +51,11 @@ interface RoadStation { tangent: Vec3; } -interface EdgeRoadStation extends RoadStation { - edgeHeightScale: number; -} - interface EdgeProfilePoint { offset: number; heightOffset: number; } -const DISTANCE_EPSILON = 1e-6; - function cloneVec3(vector: Vec3): Vec3 { return { x: vector.x, @@ -339,134 +333,6 @@ function buildVisibleRoadStationRuns( return runs; } -function getClipBoundaryDistances( - clipIntervals: readonly SplineCorridorPathClipInterval[] -): number[] { - return clipIntervals.flatMap((clipInterval) => [ - clipInterval.startDistance, - clipInterval.endDistance - ]); -} - -function getEdgeJunctionTaperDistance(edge: ScenePathRoadEdgeSettings): number { - return Math.min(1.25, Math.max(0.35, edge.width * 1.5)); -} - -function interpolateStationInRun( - run: readonly RoadStation[], - distance: number -): RoadStation { - const first = run[0]!; - const last = run[run.length - 1]!; - - if (distance <= first.distance + DISTANCE_EPSILON) { - return cloneVec3Station(first); - } - - if (distance >= last.distance - DISTANCE_EPSILON) { - return cloneVec3Station(last); - } - - for (let index = 0; index < run.length - 1; index += 1) { - const start = run[index]!; - const end = run[index + 1]!; - - if ( - distance >= start.distance - DISTANCE_EPSILON && - distance <= end.distance + DISTANCE_EPSILON - ) { - return interpolateStation(start, end, distance); - } - } - - return cloneVec3Station(last); -} - -function cloneVec3Station(station: RoadStation): RoadStation { - return { - center: cloneVec3(station.center), - distance: station.distance, - tangent: cloneVec3(station.tangent) - }; -} - -function getNearestClipBoundaryDistance( - distance: number, - boundaries: readonly number[] -): number | null { - let nearestDistance: number | null = null; - - for (const boundary of boundaries) { - const boundaryDistance = Math.abs(distance - boundary); - - if (nearestDistance === null || boundaryDistance < nearestDistance) { - nearestDistance = boundaryDistance; - } - } - - return nearestDistance; -} - -function getEdgeHeightScale( - distance: number, - boundaries: readonly number[], - taperDistance: number -): number { - const nearestDistance = getNearestClipBoundaryDistance(distance, boundaries); - - if (nearestDistance === null || nearestDistance >= taperDistance) { - return 1; - } - - return Math.min(1, Math.max(0, nearestDistance / taperDistance)); -} - -function pushUniqueDistance(distances: number[], distance: number) { - if ( - Number.isFinite(distance) && - !distances.some( - (candidate) => Math.abs(candidate - distance) <= DISTANCE_EPSILON - ) - ) { - distances.push(distance); - } -} - -function buildEdgeStationRunWithJunctionTapers(options: { - run: readonly RoadStation[]; - clipIntervals: readonly SplineCorridorPathClipInterval[]; - edge: ScenePathRoadEdgeSettings; -}): EdgeRoadStation[] { - const runStart = options.run[0]?.distance ?? 0; - const runEnd = options.run[options.run.length - 1]?.distance ?? 0; - const taperDistance = getEdgeJunctionTaperDistance(options.edge); - const clipBoundaries = getClipBoundaryDistances(options.clipIntervals).filter( - (distance) => - distance >= runStart - DISTANCE_EPSILON && - distance <= runEnd + DISTANCE_EPSILON - ); - const distances = options.run.map((station) => station.distance); - - for (const boundary of clipBoundaries) { - pushUniqueDistance(distances, boundary); - pushUniqueDistance( - distances, - Math.min(runEnd, Math.max(runStart, boundary - taperDistance)) - ); - pushUniqueDistance( - distances, - Math.min(runEnd, Math.max(runStart, boundary + taperDistance)) - ); - } - - return distances - .sort((left, right) => left - right) - .map((distance) => ({ - ...interpolateStationInRun(options.run, distance), - edgeHeightScale: getEdgeHeightScale(distance, clipBoundaries, taperDistance) - })); -} - function createGeometryFromMeshData(meshData: SplineRoadMeshData) { const geometry = new BufferGeometry(); geometry.setAttribute( @@ -527,6 +393,8 @@ function addEdgeProfileCaps(options: { profileVertexCount: number; vertexOffset: number; stationCount: number; + capStart: boolean; + capEnd: boolean; }) { if (options.profileVertexCount < 3 || options.stationCount < 2) { return; @@ -536,16 +404,33 @@ function addEdgeProfileCaps(options: { options.vertexOffset + (options.stationCount - 1) * options.profileVertexCount; for (let index = 1; index < options.profileVertexCount - 1; index += 1) { - // Start cap faces backward along the spline; end cap faces forward. - options.indices.push( - options.vertexOffset, - options.vertexOffset + index + 1, - options.vertexOffset + index - ); - options.indices.push(lastRow, lastRow + index, lastRow + index + 1); + if (options.capStart) { + // Start cap faces backward along the spline. + options.indices.push( + options.vertexOffset, + options.vertexOffset + index + 1, + options.vertexOffset + index + ); + } + + if (options.capEnd) { + // End caps face forward, but clipped junction ends stay open for junction edge handoff. + options.indices.push(lastRow, lastRow + index, lastRow + index + 1); + } } } +function isClipBoundaryDistance( + distance: number, + clipIntervals: readonly SplineCorridorPathClipInterval[] +): boolean { + return clipIntervals.some( + (clipInterval) => + Math.abs(distance - clipInterval.startDistance) <= 1e-6 || + Math.abs(distance - clipInterval.endDistance) <= 1e-6 + ); +} + export function buildSplineRoadMeshData(options: { path: SplineRoadPathLike; terrains?: readonly Terrain[]; @@ -683,12 +568,7 @@ export function buildSplineRoadEdgeMeshData(options: { ); let stationCount = 0; - for (const rawRun of stationRuns) { - const run = buildEdgeStationRunWithJunctionTapers({ - run: rawRun, - clipIntervals: options.clipIntervals ?? [], - edge - }); + for (const run of stationRuns) { const runVertexOffset = positions.length / 3; for (let stationIndex = 0; stationIndex < run.length; stationIndex += 1) { @@ -721,7 +601,7 @@ export function buildSplineRoadEdgeMeshData(options: { positions.push( vertex.x, - vertex.y + profilePoint.heightOffset * station.edgeHeightScale, + vertex.y + profilePoint.heightOffset, vertex.z ); uvs.push(profileIndex / profileLastIndex, station.distance); @@ -743,11 +623,18 @@ export function buildSplineRoadEdgeMeshData(options: { } if (edge.kind !== "softShoulder") { + const clipIntervals = options.clipIntervals ?? []; + addEdgeProfileCaps({ indices, profileVertexCount: profile.length, vertexOffset: runVertexOffset, - stationCount: run.length + stationCount: run.length, + capStart: !isClipBoundaryDistance(run[0]!.distance, clipIntervals), + capEnd: !isClipBoundaryDistance( + run[run.length - 1]!.distance, + clipIntervals + ) }); }