Skip to main content

quatLerp: Spherical Quaternion Interpolation

Spherical Linear Interpolation Algorithm

The quatLerp function implements spherical linear interpolation (SLERP) between two quaternions. This algorithm provides smooth rotation blending by interpolating along the shortest path on the 4D unit sphere, maintaining constant angular velocity.

Mathematical Definition: For quaternions qaq_a and qbq_b with interpolation parameter t[0,1]t \in [0,1]:

slerp(qa,qb,t)=sin((1t)θ)sinθqa+sin(tθ)sinθqb\text{slerp}(q_a, q_b, t) = \frac{\sin((1-t)\theta)}{\sin\theta} q_a + \frac{\sin(t\theta)}{\sin\theta} q_b

Where θ=arccos(qaqb)\theta = \arccos(|q_a \cdot q_b|) represents the angular separation between quaternions.

Algorithm Steps:

  1. Calculate dot product to determine angular separation
  2. Handle quaternion sign to choose shortest path
  3. Apply trigonometric ratios for spherical interpolation
  4. Normalize result to maintain unit quaternion properties

This function ensures smooth rotation transitions without gimbal lock or unwanted rotation artifacts.

Live Editor
const fragment = () => {
      const t = iTime.mul(0.5)
      const phase = t.mod(2)
      const lerpT = phase.select(float(2).sub(phase), phase.lessThan(1))

      const qa = vec4(0, 0, 0, 1)
      const qb = vec4(float(1.57).sin(), 0, 0, float(1.57).cos()).normalize()

      const interpolated = quatLerp(qa, qb, lerpT)
      const rotMatrix = quat2mat3(interpolated)

      const coords = uv.sub(0.5).mul(4)
      const rotated = rotMatrix.mul(vec3(coords, 0))

      const pattern = rotated.x.mul(rotated.y).mul(6).sin().abs()
      const color = vec3(pattern.mul(0.8), pattern, pattern.mul(0.6))

      return vec4(color, 1)

}

Multi-Point Rotation Sequence

This example demonstrates quaternion interpolation across multiple rotation targets, creating smooth animation sequences through spherical linear interpolation chains.

Live Editor
const fragment = () => {
      const cellPos = uv.mul(6)
      const cell = cellPos.floor()
      const local = cellPos.fract().sub(0.5)

      const hash = cell.x.mul(127.1).add(cell.y.mul(311.7)).sin().mul(43758.5).fract()
      const timeOffset = hash.mul(6.28)

      const phase = iTime.add(timeOffset).mul(0.3).mod(4)
      const segmentT = phase.fract()

      const q1 = quatIdentity()
      const q2 = vec4(hash.mul(3.14).sin(), 0, 0, hash.mul(3.14).cos()).normalize()
      const q3 = vec4(0, hash.mul(1.57).sin(), 0, hash.mul(1.57).cos()).normalize()
      const q4 = vec4(0, 0, hash.mul(2.35).sin(), hash.mul(2.35).cos()).normalize()

      const segment = phase.floor()
      const qa = q1.select(q2.select(q3.select(q4, segment.lessThan(3)), segment.lessThan(2)), segment.lessThan(1))
      const qb = q2.select(q3.select(q4.select(q1, segment.lessThan(3)), segment.lessThan(2)), segment.lessThan(1))

      const blended = quatLerp(qa, qb, segmentT)
      const transform = quat2mat3(blended).mul(vec3(local, hash))

      const field = transform.length().mul(8).sin().abs()
      const color = vec3(field.mul(0.9), field.mul(0.7), field)

      return vec4(color, 1)

}