Draw Dynamic 2D Shapes with Three.js ShapeGeometry

Drawing dynamic 2D shapes in Three.js is an efficient way to create custom flat geometry that can be updated programmatically. By utilizing the THREE.Shape class to define vector paths and passing them into THREE.ShapeGeometry, developers can generate complex, custom 2D meshes. This article covers how to define a 2D shape, convert it into geometry, render it in a Three.js scene, and dynamically update its structure during runtime.

Step 1: Create the Shape Path

The first step is to define the 2D path of your shape using THREE.Shape. This class provides methods similar to the HTML5 Canvas 2D context API, such as moveTo, lineTo, and quadraticCurveTo.

const shape = new THREE.Shape();

// Start the path
shape.moveTo(0, 0);

// Draw a dynamic triangle or custom polygon
shape.lineTo(1, 2);
shape.lineTo(2, 0);
shape.lineTo(0, 0); // Close the path

Step 2: Generate the ShapeGeometry

Once the path is defined, pass the shape object into THREE.ShapeGeometry. This automatically triangulates the 2D path into a 3D-renderable surface.

const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

Step 3: Making the Shape Dynamic

Because shape triangulation is computationally expensive, updating a ShapeGeometry dynamically depends on how the shape is changing. There are two primary methods to update your shapes:

Method A: Rebuilding the Geometry (For Structural Changes)

If you are changing the number of control points, curves, or the overall structure of the shape, you must generate a new geometry and swap it in your mesh. To avoid memory leaks, always dispose of the old geometry.

function updateDynamicShape(mesh, newPoints) {
    // 1. Create a new shape path
    const newShape = new THREE.Shape();
    newShape.moveTo(newPoints[0].x, newPoints[0].y);
    
    for (let i = 1; i < newPoints.length; i++) {
        newShape.lineTo(newPoints[i].x, newPoints[i].y);
    }
    newShape.closePath();

    // 2. Dispose of the old geometry to free GPU memory
    mesh.geometry.dispose();

    // 3. Assign the new geometry
    mesh.geometry = new THREE.ShapeGeometry(newShape);
}

Method B: Modifying Existing Vertices (For Deformations)

If the number of vertices remains constant and you only want to shift, stretch, or morph the shape, you can manipulate the underlying position attributes directly without recreating the geometry.

function morphShape(geometry, time) {
    const positionAttribute = geometry.attributes.position;
    
    for (let i = 0; i < positionAttribute.count; i++) {
        // Get current vertex coordinates
        let x = positionAttribute.getX(i);
        let y = positionAttribute.getY(i);
        
        // Apply a dynamic wave displacement
        const wave = Math.sin(time + x) * 0.1;
        positionAttribute.setY(i, y + wave);
    }
    
    // Tell Three.js to update the GPU buffer
    positionAttribute.needsUpdate = true;
}

Step 4: Animation Loop Integration

To see the dynamic changes in action, call your update function inside the standard Three.js requestAnimationFrame loop.

function animate(time) {
    requestAnimationFrame(animate);

    // Convert time to seconds
    const elapsedSeconds = time * 0.001; 
    
    // Apply dynamic vertex morphing
    morphShape(mesh.geometry, elapsedSeconds);

    renderer.render(scene, camera);
}

animate(0);