Hi everyone,
I’m working on a Three.js geometry where I trying to generate a curved trapezoid dynamically. I’ve successfully created this by:
- Calculating the end points from the first edge and end edge (these edges are not at the same Z level).
- Connecting these points with an arc controlled by a radius slider.
However, I’m facing an issue with triangulation when computing the arc geometry:
-
The last triangle at the end of the arc appears skewed.
-
I need the arc to end at a 90° angle (perpendicular) to the end edge, but I can’t seem to adjust the triangulation to achieve this.
-
I’m unable to tilt the last triangle properly to align the arc’s endpoint with the end edge.
-
How can I adjust the triangulation so that the arc terminates at a 90° angle with the end edge?
-
Is there a way to manually control the orientation of the last triangle to prevent skewing?
-
Would a different approach (such as modifying the arc sampling or using a different geometry method) help maintain a smooth and perpendicular transition?
Any suggestions, insights, or sample implementations would be greatly appreciated! Thanks in advance.
function createTrapozoidModel() {
if (!scene) {
scene = new THREE.Scene();
} else {
// Clear the existing scene
while (scene.children.length > 0) {
const child = scene.children[0];
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
scene.remove(child);
}
}
const shapeType = document.querySelector('input[name="shapeType"]:checked').value;
const curveDirection = document.querySelector('input[name="curveDirection"]:checked').value;
const directionMultiplier = curveDirection === 'left' ? 1 : -1;
const edgeLength = parseFloat(document.getElementById('edgeLengthSlider').value);
const parallelEdge = parseFloat(document.getElementById('parallelEdgeSlider').value);
const depth = parseFloat(document.getElementById('depthSlider').value);
const rotation = parseFloat(document.getElementById('rotationSlider').value) * (Math.PI / 180);
const surfaceColor = document.getElementById('surfaceColorPicker').value;
const triangleColor = document.getElementById('triangleColorPicker').value;
const transparency = parseFloat(document.getElementById('transparencySlider').value) / 100;
const height = parseFloat(document.getElementById('heightSlider').value);
const baseHeight = parseFloat(document.getElementById('baseHeightSlider').value);
const middleEdgeLength = parseFloat(document.getElementById('middleEdgeLengthSlider').value);
const middleEdgeDistance = parseFloat(document.getElementById('middleEdgeDistanceSlider').value);
const triangleHeight = parseFloat(document.getElementById('triangleHeightSlider').value);
const triangleRotation = parseFloat(document.getElementById('triangleRotationSlider').value) * (Math.PI / 180);
const radius = parseFloat(document.getElementById('radiusSlider').value);
let geometry;
if (shapeType === 'straight') {
// Create straight trapezoid with middle edge
const vertices = new Float32Array([
// Start edge
-edgeLength / 2, 0, baseHeight, // Start edge left (vertex 0)
edgeLength / 2, 0, baseHeight, // Start edge right (vertex 1)
// Middle edge
-middleEdgeLength / 2, middleEdgeDistance, baseHeight + (height - baseHeight) * (middleEdgeDistance / depth), // Middle edge left (vertex 2)
middleEdgeLength / 2, middleEdgeDistance, baseHeight + (height - baseHeight) * (middleEdgeDistance / depth), // Middle edge right (vertex 3)
// End edge (initially placed vertically at depth, height)
-parallelEdge / 2, depth, height, // End edge left (vertex 4)
parallelEdge / 2, depth, height // End edge right (vertex 5)
]);
// Apply rotation to the end edge vertices based on edgeCurveOrientation
// const rotationMatrix = new THREE.Matrix4().makeRotationY(edgeCurveOrientation);
const endEdgeLeft = new THREE.Vector3(-parallelEdge / 2, depth, height);
const endEdgeRight = new THREE.Vector3(parallelEdge / 2, depth, height);
const axesHelper = new THREE.AxesHelper(500); // 500 is the size of the axes
scene.add(axesHelper);
// Update the end edge vertices with the rotated positions
vertices[12] = endEdgeLeft.x;
vertices[13] = endEdgeLeft.y;
vertices[14] = endEdgeLeft.z;
vertices[15] = endEdgeRight.x;
vertices[16] = endEdgeRight.y;
vertices[17] = endEdgeRight.z;
// Indices for triangulation between Start Edge and Middle Edge
const indices = new Uint16Array([
0, 1, 2, // Triangle 1 (start left, start right, middle left)
1, 3, 2, // Triangle 2 (start right, middle right, middle left)
]);
geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// Create materials for the main surface and triangles
const surfaceMaterial = new THREE.MeshPhongMaterial({
color: new THREE.Color(surfaceColor),
opacity: transparency,
transparent: transparency < 1,
depthWrite: true,
depthTest: true,
side: THREE.DoubleSide,
shininess: 0
});
const triangleMaterial = new THREE.MeshPhongMaterial({
color: new THREE.Color(triangleColor),
opacity: transparency,
transparent: transparency < 1,
depthWrite: true,
depthTest: true,
side: THREE.DoubleSide,
shininess: 0
});
// Create meshes
const path= new THREE.Mesh(geometry, surfaceMaterial);
path.rotation.z = rotation;
scene.add(path);
// Calculate the middle edge's endpoint position
const middleEdgeHeight = baseHeight + (height - baseHeight) * (middleEdgeDistance / depth);
const middleEdgeLeft = new THREE.Vector3(-middleEdgeLength / 2, middleEdgeDistance, middleEdgeHeight);
const middleEdgeRight = new THREE.Vector3(middleEdgeLength / 2, middleEdgeDistance, middleEdgeHeight);
// Calculate apex position as an offset from the middle edge's endpoint
const apexOffset = new THREE.Vector3(0, 0, triangleHeight); // Offset along the Z-axis (height)
// Apply rotation to the apex offset for the right triangle (positive rotation)
const rightRotationMatrix = new THREE.Matrix4().makeRotationY(triangleRotation); // Rotate around the Y-axis
const rightApexOffset = apexOffset.clone().applyMatrix4(rightRotationMatrix);
// Apply opposite rotation to the apex offset for the left triangle (negative rotation)
const leftRotationMatrix = new THREE.Matrix4().makeRotationY(-triangleRotation); // Rotate around the Y-axis in the opposite direction
const leftApexOffset = apexOffset.clone().applyMatrix4(leftRotationMatrix);
// Calculate the final apex positions for the left and right triangles
const rightApex = new THREE.Vector3().copy(middleEdgeRight).add(rightApexOffset);
const leftApex = new THREE.Vector3().copy(middleEdgeLeft).add(leftApexOffset);
// Create separate geometries for the left and right triangles
const createTriangleGeometry = (baseLeft, baseRight, apex) => {
const vertices = new Float32Array([
baseLeft.x, baseLeft.y, baseLeft.z, // Base left
baseRight.x, baseRight.y, baseRight.z, // Base right
apex.x, apex.y, apex.z // Apex
]);
const indices = new Uint16Array([0, 1, 2]);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
return geometry;
};
// Create right triangle
const rightTriangleGeometry = createTriangleGeometry(
middleEdgeRight, // Base right
new THREE.Vector3(edgeLength / 2, 0, baseHeight), // Base left
rightApex // Apex
);
const rightTriangle = new THREE.Mesh(rightTriangleGeometry, triangleMaterial);
rightTriangle.rotation.z = rotation;
rightTriangle.position.z += 0.01; // Offset to avoid z-fighting
scene.add(rightTriangle);
// Create left triangle
const leftTriangleGeometry = createTriangleGeometry(
middleEdgeLeft, // Base left
new THREE.Vector3(-edgeLength / 2, 0, baseHeight), // Base right
leftApex // Apex
);
const leftTriangle = new THREE.Mesh(leftTriangleGeometry, triangleMaterial);
leftTriangle.rotation.z = rotation;
leftTriangle.position.z += 0.01; // Offset to avoid z-fighting
scene.add(leftTriangle);
// Create arc between middle and end edge
const arcGeometry = new THREE.BufferGeometry();
const arcVertices = [];
const arcIndices = [];
const segments = 32;
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const angle = (Math.PI / 2) * t; // 90-degree rotation
const xOffset = directionMultiplier * radius * (1 - Math.cos(angle));
const zOffset = radius * Math.sin(angle);
// Calculate the current width for this segment
const currentWidth = middleEdgeLength + (parallelEdge - middleEdgeLength) * t;
// Calculate the current height for this segment
const currentHeight = middleEdgeHeight + (height - middleEdgeHeight) * t;
// Ensure the end edge is horizontal by fixing the Z-coordinate for the end edge
const zPosition = (i === segments) ? height : currentHeight;
// Calculate the X positions for the left and right points
const leftPointX = -currentWidth / 2; // Left point X-coordinate
const rightPointX = currentWidth / 2; // Right point X-coordinate
// Calculate the Y position for this segment
const yPosition = middleEdgeDistance + (depth - middleEdgeDistance) * t;
// Add the vertices for the left and right points
arcVertices.push(
leftPointX + xOffset, yPosition, zPosition, // Left point
rightPointX + xOffset, yPosition, zPosition // Right point
);
// Add indices for the triangles
if (i < segments) {
const baseIndex = i * 2;
arcIndices.push(
baseIndex, baseIndex + 1, baseIndex + 2, // First triangle
baseIndex + 1, baseIndex + 3, baseIndex + 2 // Second triangle
);
}
}
arcGeometry.setAttribute('position', new THREE.Float32BufferAttribute(arcVertices, 3));
arcGeometry.setIndex(arcIndices);
const arcMaterial = new THREE.MeshPhongMaterial({
color: new THREE.Color(surfaceColor),
opacity: transparency,
transparent: transparency < 1,
depthWrite: true,
depthTest: true,
side: THREE.DoubleSide,
shininess: 0,
wireframe: true
});
const arcMesh = new THREE.Mesh(arcGeometry, arcMaterial);
arcMesh.rotation.z = rotation;
scene.add(arcMesh);
} else {
//nothing here
}
geometry.computeVertexNormals();
// Add lighting to the scene
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(0, 70, 100).normalize();
scene.add(directionalLight);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
return scene;
}