Three.js MathUtils.smoothstep for Non-Linear Transitions

In Three.js, creating natural, organic animations often requires non-linear interpolation rather than rigid linear movement. The MathUtils.smoothstep function is a built-in utility that calculates a smooth, S-shaped transition between two boundary values. This article explains how MathUtils.smoothstep works under the hood, details its syntax, highlights how it differs from linear interpolation, and demonstrates how to implement it for fluid animations in your 3D scenes.

What is MathUtils.smoothstep?

The MathUtils.smoothstep function is the Three.js implementation of the standard smoothstep formula commonly used in computer graphics and GLSL shaders. It performs Hermite interpolation between two limits.

When you pass an input value to smoothstep, the function normalizes the value between a specified minimum and maximum range, clamps it between 0 and 1, and then applies the following cubic polynomial formula:

\[f(t) = 3t^2 - 2t^3\]

This mathematical curve results in a smooth “ease-in, ease-out” behavior. The transition starts slowly, accelerates in the middle, and decelerates smoothly as it approaches the target value.

Syntax and Parameters

The syntax for using smoothstep in Three.js is straightforward:

import * as THREE from 'three';

const result = THREE.MathUtils.smoothstep(value, min, max);

The function accepts three parameters:

If the value is between min and max, the function returns a smoothly interpolated float between 0.0 and 1.0.

Linear Interpolation (lerp) vs. Smoothstep

To understand the visual benefits of smoothstep, it helps to compare it to linear interpolation (lerp):

Implementing smoothstep in Three.js Animations

MathUtils.smoothstep is highly versatile. It is commonly used to animate mesh positions, scale, material opacity, or camera movements.

Here is a practical example of how to use smoothstep to smoothly fade a mesh’s opacity in an active render loop:

import * as THREE from 'three';

// Set up scene, camera, renderer, and a mesh
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ 
    color: 0x00ff00, 
    transparent: true, 
    opacity: 0 
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);

    const elapsedTime = clock.getElapsedTime();

    // Fade in the mesh opacity between 1.0 and 4.0 seconds of elapsed time
    // The opacity will smoothly transition from 0 to 1
    mesh.material.opacity = THREE.MathUtils.smoothstep(elapsedTime, 1.0, 4.0);

    renderer.render(scene, camera);
}

animate();

In this setup, before elapsedTime reaches 1.0, the opacity remains strictly 0. Between 1.0 and 4.0 seconds, the opacity smoothly ramps up to 1. After 4.0 seconds, the opacity stays at 1. The transition avoids any sudden jumps, producing a professional, eased transition.