How to Create a Curved Trapezoid with a Straight End Segment Using an Arc in Three.js?

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:

  1. Calculating the end points from the first edge and end edge (these edges are not at the same Z level).
  2. 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;

}

You can extrude a trapezoid by drawing a perpendicular line along a curve!

image

The following jsFiddle creates geometry by extruding a line along a CatmullRomCurve3. It also uses scale and rotation interpolation objects to adjust the line transformations along the curve.

It’s somewhat similar to Blender’s extrude shape to curve function.

// Create your curve
const curvePoints = [
  new THREE.Vector3(-10, 0, 0),
  new THREE.Vector3(-5, 5, 5),
  new THREE.Vector3(0, 0, 10),
  new THREE.Vector3(5, -5, 5),
  new THREE.Vector3(10, 0, 0),
];
const curve = new THREE.CatmullRomCurve3(curvePoints);

// Create the trapezoid geometry along the curve
const width = 1;
const segments = 50;

// Set a scale and rotation interpolation objects
const scale = { 0: 0.1, 0.5: 2, 1: 1 };
const rotation = { 0: 0, 0.5: Math.PI, 1: Math.PI * 2 };

const geometry = createTrapezoidGeometryAlongCurve(
  curve,
  distance,
  segments,
  scale,
  rotation
);
1 Like

If you want to have 90° angles at the ends, you’ll need two concentrical arcs within the same plane as “rail” lines (blue). Subdivide both arcs by an equal number of subdivisions (red), then connect the resulting points with a triangle mesh (green).

1 Like

Perhaps you could use something like this?

CarRacingQuaternionSimple
Create a curved plane surface which dynamically changes its size
CarRacingQuaternion

1 Like

I’ve made some updates to the example, now it can extrude any shape. Here, you can see it extruding a star along a curve with smooth scaling and rotation interpolation in between.

image

3 Likes

Thank you @Fennec :raised_hands:

1 Like

thank you so much @vielzutun.ch :raised_hands: