How to use easing functions to improve lerp/slerp

Hey,

I am animating a gltf skeleton dynamically and interpolating between position, rotation, scale for each bone.

Using lerp and slerp works, but it still looks very sudden and unnatural, so I have been trying to use easing functions to improve the quality of interpolation a bit.

For example I am using .lerpVectors(a, b, t) like so. The value t is calculated using my render loop: I start at 0 and do t += 0.01 until t >= 1.

Now to make use of easing functions I was trying to pass t through https://threejs.org/docs/#api/en/math/MathUtils.smootherstep like so Math.Utils(t, 0, 1), but I am not seeing any difference in the speed of interpolation. I am also unsure whether updating t with t += 0.01 in my render loop is correct. I have read that one should scale this by deltaTime, but then my values go from 0 to 1 too quickly.

Thanks for any advice on how to lerp in a smoother way.

1 Like

The next simplest after lerp is probably a quadratic filter with ease:

f = -ease * t * t + (1 + ease) * t); // ease [-1, 1]

it creates motions like this:

1 Like

Could you explain a bit more the reasoning behind this quadratic filter? What is ‘ease’ in this context?

Most of these functions just map input to output using some non-linear function, ease is just a parameter of the filter function that allows you to create different curves, in this case something that either slows down at the end or at the beginning. With ease = 0 you get a straight line, so it’s lerp.

ease > 0 boosts the input, making something to change faster than lerp, ease < 0 suppresses it

filter

Ok, that clears it up thank you. So this is basically 3 different easing functions in one dependent on the parameter ‘ease’? Wouldn’t passing the ‘alpha’ parameter of lerpVectors through a manually defined easing function e.g. smoothstep, before calling .lerpVectors(a1, a2, smoothstep(t)) achieve similar results?

Yes.

Lerp doesn’t add anything to the result, it maps values one to one, if you already use quadratic filter or smoothstep, then just use that result directly but technically yes.

Added another sphere that uses smoothstep as the input for lerp:

1 Like

Cool thanks, I have a few approaches in mind now.

1 Like

The JSFiddles do not work anymore. :frowning_face:
I wanted to add them to the collection.

@hofk Not sure what happened but I found this one:

Ok. Added this exanple. :slightly_smiling_face:

adding the code the above fiddle here for future:

import * as CONTROLS from 'https://alikim.com/_v1_jsm/controls.js'

import * as THREE from 'https://unpkg.com/three/build/three.module.js'

import * as MathUtils from 'https://unpkg.com/three/src/math/MathUtils.js'

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);


const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const hlp = new THREE.AxesHelper(3);

const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.2, 16, 16),
new THREE.MeshBasicMaterial({color:0x800080}),
);

const sphere1 = sphere.clone();
const sphere2 = sphere.clone();
const sphere3 = sphere.clone();

scene.background = new THREE.Color(0x002200);
scene.add(hlp).add(sphere).add(sphere1).add(sphere2).add(sphere3);

camera.position.set(5, 3, 5);
camera.lookAt(0, 0, 0);

const duration = 3000;
const idur = 1 / duration;

let t0 = performance.now();

const tick = (ease, now) => {
  const dif = now - t0;
  // quadratic filter
  const s = dif ? Math.min(1, dif * idur) : 0;
  const f = dif ? Math.min(1, -ease * s * s + (1 + ease) * s) : 0;
  return f;
};

const v1 = new THREE.Vector3(0, 0, 3);
const v2 = new THREE.Vector3(0, 1, 3);
const smoothstep = now => { 
	const dif = now - t0;
  const t = dif * idur;
	return new THREE.Vector3().lerpVectors(v1, v2, MathUtils.smoothstep(t, 0, 1));
};

const render = (f0, f1, f2, v) => {
  sphere.position.set(0, 2 * f0, 0);
  sphere1.position.set(0, 2 * f1, 1);
  sphere2.position.set(0, 2 * f2, 2);
  sphere3.position.set(v.x, 2 * v.y, v.z);
  renderer.render(scene, camera);
};

const state = {
  play: true
};

const maybeRender = () => {
  if (!state.play) render()
};

const framerate = 100;
const fpsint = 1000 / framerate;
let elapsed, now, then;

let tid = 0;
const animate = newtime => {

  if (!state.play) return;
  requestAnimationFrame(animate);

  if (!newtime) then = newtime = window.performance.now();

  now = newtime;
  elapsed = now - then;

  if (!fpsint || elapsed > fpsint) {

    then = now - (elapsed % fpsint);

    const [fy0, fy1, fy2, fy3] = [
    tick(0, now), 
    tick(-1, now), 
    tick(1, now), 
    smoothstep(now)
    ];
    
    if(fy0 == 1 && fy1 == 1 && fy2 == 1 && fy3.equals(v2) && !tid) { 
      tid = setTimeout(()=>{t0 = now; tid = 0; }, 1000);
    }

    render(fy0, fy1, fy2, fy3);
  }
};

CONTROLS.create({
  cont: renderer.domElement,
  cam: camera,
  type: 'Orbital',
  overlay: true,
  lookAt: [0, 0, 0],
  callback: maybeRender
});

animate();