Hi everyone, I’m working on a custom 3D geometry layer in a Mapbox + Three.js environment. I’m generating a road-like surface with left/right side surfaces by interpolating points along a CatmullRom curve. Geometry is constructed via cross-section points per segment.
The problem is that the faces appear distorted/Fuzzy or crumpled, even though the logic seems correct. The distortion gets worse when I use smaller values (due to precision issues), and sometimes face connectivity looks incorrect even after converting width units from meters to Mercator units.
- I previously used a small
scale = 0.0000001
to shrink geometry, but it led to face artifacts. - I switched to a meters-to-Mercator conversion function, but geometry was way off in the way it was created
- I suspect the issue may lie in how I’m connecting faces across segments, or possibly in the way I compute left/right offset vectors per cross-section?
Code snippet of my createRoadGeometry() is attached below. Can someone check if the face-building logic is flawed?
Thanks in advance!
Code:
metersToMercatorUnits()
function
function metersToMercatorUnits(meters, latitude) {
const earthCircumference = 40075016.686;
return meters / (earthCircumference * Math.cos(latitude * Math.PI / 180));
}
Cross-section vertex building
const baseCenterLat = currentPoints[i]?.lngLat?.lat || 0;
const widthInMercator = metersToMercatorUnits(params.width, baseCenterLat);
const height = params.height;
const heightParallel = params.heightParallel;
const baseCenter = new THREE.Vector3().copy(point);
const mainLeft = baseCenter.clone()
.add(right.clone().multiplyScalar(-widthInMercator / 2))
.add(worldUp.clone().multiplyScalar(height));
const mainRight = baseCenter.clone()
.add(right.clone().multiplyScalar(widthInMercator / 2))
.add(worldUp.clone().multiplyScalar(height));
const leftOuter = baseCenter.clone()
.add(right.clone().multiplyScalar(-widthInMercator * 1.5))
.add(worldUp.clone().multiplyScalar(heightParallel));
const rightOuter = baseCenter.clone()
.add(right.clone().multiplyScalar(widthInMercator * 1.5))
.add(worldUp.clone().multiplyScalar(heightParallel));
mainVertices.push(mainLeft, mainRight);
leftVertices.push(mainLeft, leftOuter);
rightVertices.push(mainRight, rightOuter);
Face index pushing logic
if (i < segmentCount) {
const baseMain = i * 2;
const baseLeft = i * 2;
const baseRight = i * 2;
mainFaces.push(baseMain, baseMain + 1, baseMain + 3,
baseMain, baseMain + 3, baseMain + 2);
leftFaces.push(baseLeft, baseLeft + 1, baseLeft + 3,
baseLeft, baseLeft + 3, baseLeft + 2);
rightFaces.push(baseRight, baseRight + 1, baseRight + 3,
baseRight, baseRight + 3, baseRight + 2);
}
BufferGeometry
building section
const mainGeometry = new THREE.BufferGeometry();
const vertices = new Float32Array(mainVertices.length * 3);
mainVertices.forEach((v, i) => {
vertices[i * 3] = v.x;
vertices[i * 3 + 1] = v.y;
vertices[i * 3 + 2] = v.z;
});
mainGeometry.setAttribute(‘position’, new THREE.BufferAttribute(vertices, 3));
mainGeometry.setIndex(mainFaces);
mainGeometry.computeVertexNormals();
Road Function:
createRoadGeometry: function(pathCurve) {
if (!pathCurve || currentPoints.length < 2) return;
// Define how many points to sample along the curve
const segmentCount = Math.max((currentPoints.length - 1) * 15, 30);
const wireframeInterval = Math.max(Math.ceil(segmentCount / ((currentPoints.length - 1) * 2)), 1);
// Arrays for vertices and faces
const mainVertices = [];
const mainFaces = [];
const leftVertices = [];
const leftFaces = [];
const rightVertices = [];
const rightFaces = [];
// Store wireframe points for each cross-section
const wireframePoints = [];
// Sample points along the curve
for (let i = 0; i <= segmentCount; i++) {
const t = i / segmentCount;
const point = pathCurve.getPoint(t);
const params = this.getInterpolatedParams(t, currentPoints);
// Calculate the forward direction (tangent along the curve)
const tangent = pathCurve.getTangent(t).normalize();
// Up vector for Mapbox GL JS coordinate system
const worldUp = new THREE.Vector3(0, 0, 1);
// Calculate the right vector
const right = new THREE.Vector3().crossVectors(tangent, worldUp).normalize();
const scale = 0.0000001;
// Scale factors for width and height
const width = params.width * scale;
const height = params.height * scale;
const heightParallel = params.heightParallel * scale;
// Center point at base level
const baseCenter = new THREE.Vector3().copy(point);
// Calculate cross-section points
// Main road surface (gray middle)
const mainLeft = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(-width / 2))
.add(new THREE.Vector3().copy(worldUp).multiplyScalar(height));
const mainRight = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(width / 2))
.add(new THREE.Vector3().copy(worldUp).multiplyScalar(height));
// Left side surface (yellow)
const leftOuter = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(-width * 1.5))
.add(new THREE.Vector3().copy(worldUp).multiplyScalar(heightParallel));
// Right side surface (yellow)
const rightOuter = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(width * 1.5))
.add(new THREE.Vector3().copy(worldUp).multiplyScalar(heightParallel));
// Bottom points for wireframe
const bottomMainLeft = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(-width / 2));
const bottomMainRight = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(width / 2));
const bottomLeftOuter = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(-width * 1.5));
const bottomRightOuter = new THREE.Vector3().copy(baseCenter)
.add(new THREE.Vector3().copy(right).multiplyScalar(width * 1.5));
// Store vertices for each surface
mainVertices.push(mainLeft, mainRight);
leftVertices.push(mainLeft, leftOuter);
rightVertices.push(mainRight, rightOuter);
// Create faces (triangles) connecting this cross-section to the next
if (i < segmentCount) {
const base = i * 2;
// Main surface faces
mainFaces.push(base, base + 1, base + 3, base, base + 3, base + 2);
// Left side faces
leftFaces.push(base, base + 1, base + 3, base, base + 3, base + 2);
// Right side faces
rightFaces.push(base, base + 1, base + 3, base, base + 3, base + 2);
}
// Collect ALL possible wireframe points for this cross-section
const crossSectionWireframe = [
// Top lines
mainLeft, mainRight, // Top middle line
mainLeft, leftOuter, // Top left line to outer left
mainRight, rightOuter, // Top right line to outer right
// Vertical connections
bottomMainLeft, mainLeft, // Bottom to top left
bottomMainRight, mainRight, // Bottom to top right
bottomLeftOuter, leftOuter, // Bottom to top outer left
bottomRightOuter, rightOuter, // Bottom to top outer right
// Bottom lines
bottomMainLeft, bottomMainRight, // Bottom middle line
bottomMainLeft, bottomLeftOuter, // Bottom middle to left outer
bottomMainRight, bottomRightOuter, // Bottom middle to right outer
bottomLeftOuter, bottomRightOuter // Bottom outer line
];
// Only store wireframe points for selected intervals
if (settings.wireframeMode && (i % wireframeInterval === 0 || i === 0 || i === segmentCount)) {
wireframePoints.push(crossSectionWireframe);
}
}
// Create wireframe lines
if (settings.wireframeMode) {
for (let j = 0; j < wireframePoints.length; j++) {
const points = wireframePoints[j];
// Draw all lines for the current cross-section
for (let k = 0; k < points.length; k += 2) {
this.createWireframeLine(points[k], points[k + 1]);
}
// Connect adjacent cross-sections
if (j > 0) {
const prevPoints = wireframePoints[j - 1];
const currentPoints = wireframePoints[j];
// Systematically connect corresponding points between cross-sections
const connectPairs = [
[0, 0], // Top middle left
[1, 1], // Top middle right
[2, 2], // Top left outer
[3, 3], // Top right outer
[4, 4], // Bottom middle left
[5, 5], // Bottom middle right
[6, 6], // Bottom left outer
[7, 7], // Bottom outer line left
[8, 8], // Bottom middle left
[9, 9], // Bottom middle right
[10, 10], // Bottom left outer
[11, 11], // Bottom right outer
];
connectPairs.forEach(([prevIndex, currIndex]) => {
this.createWireframeLine(
prevPoints[prevIndex],
currentPoints[currIndex]
);
});
}
}
}
return {
mainVertices, mainFaces,
leftVertices, leftFaces,
rightVertices, rightFaces
};
},