Create Parametric Surfaces with Three.js

This article provides a step-by-step guide on how to generate procedurally defined 3D parametric surfaces in Three.js using ParametricGeometry. You will learn how to write a parametric mathematical function, import the necessary geometry module, instantiate the surface, and render it inside a Three.js scene.

What is a Parametric Surface?

A parametric surface is a 3D shape defined by a mathematical function that maps 2D coordinates (usually represented as \(u\) and \(v\)) into 3D space coordinates (\(x, y, z\)). In Three.js, both \(u\) and \(v\) range from 0.0 to 1.0. By changing these input values sequentially, Three.js coordinates a grid of vertices to build a custom 3D mesh.

Step 1: Import ParametricGeometry

In modern versions of Three.js, ParametricGeometry is not included in the core namespace. You must import it from the addons folder.

import * as THREE from 'three';
import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';

Step 2: Define the Parametric Function

The core of a parametric surface is the generator function. This function must accept three arguments: 1. u (number from 0 to 1) 2. v (number from 0 to 1) 3. target (a THREE.Vector3 object where the calculated \(x, y, z\) coordinates are stored)

Here is a function that generates a waving, sinusoidal ripple surface:

const radialWave = (u, v, target) => {
    // Scale u and v to map to a 10x10 area centered at the origin
    const x = (u - 0.5) * 10;
    const z = (v - 0.5) * 10;
    
    // Calculate the distance from the center to create a radial ripple effect
    const distance = Math.sqrt(x * x + z * z);
    const y = Math.sin(distance);

    // Set the coordinates in the target Vector3
    target.set(x, y, z);
};

Step 3: Instantiate the Geometry

To construct the geometry, pass your custom function to the ParametricGeometry constructor. You must also specify the number of subdivisions (slices and stacks) to define the resolution of the grid.

// Define the resolution of the surface grid
const slices = 40;
const stacks = 40;

// Create the geometry
const geometry = new ParametricGeometry(radialWave, slices, stacks);

Note: Higher values for slices and stacks result in a smoother surface but require more processing power.

Step 4: Create the Mesh and Add to Scene

Once the geometry is defined, associate it with a material and add it to your scene as a standard mesh. To see both the top and bottom of the generated surface, set the material’s side property to THREE.DoubleSide.

// Create a material that responds to light
const material = new THREE.MeshStandardMaterial({
    color: 0x00ffcc,
    side: THREE.DoubleSide,
    wireframe: false
});

// Create the mesh and add it to the scene
const surfaceMesh = new THREE.Mesh(geometry, material);
scene.add(surfaceMesh);

Complete Implementation Example

Below is a complete script demonstrating how to set up the scene, light, camera, and the parametric surface:

import * as THREE from 'three';
import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';

// 1. Setup Scene, Camera, and Renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 10);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 2. Add Lighting
const light = new THREE.DirectionalLight(0xffffff, 1.5);
light.position.set(5, 10, 7);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));

// 3. Define the Parametric Equation (Saddle Shape)
const saddleFunction = (u, v, target) => {
    const x = (u - 0.5) * 6;
    const z = (v - 0.5) * 6;
    const y = (x * x - z * z) * 0.25; // Hyperbolic paraboloid equation
    target.set(x, y, z);
};

// 4. Create Parametric Geometry
const geometry = new ParametricGeometry(saddleFunction, 30, 30);
const material = new THREE.MeshStandardMaterial({ color: 0x3a86ff, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 5. Animation Loop
function animate() {
    requestAnimationFrame(animate);
    mesh.rotation.y += 0.01; // Slowly rotate the surface
    renderer.render(scene, camera);
}
animate();