Refactor spline road mesh generation by simplifying boundary handling and capping logic
This commit is contained in:
@@ -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
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user