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 pathStep 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);