뒤로가기

선형 보간법(Linear Interpolation): 애니메이션과 그래픽스의 핵심 수학

math

선형 보간법이란?

선형 보간(Linear Interpolation, LERP)은 두 값 사이의 중간값을 선형적으로 계산하는 방법입니다. 컴퓨터 그래픽스, 애니메이션, 게임 개발에서 가장 기본적이면서도 강력한 도구로, 부드러운 전환 효과를 만드는 핵심 수학 개념입니다.

일상 속 선형 보간

예: 온도계
오전 10시: 15°C
오후 2시: 25°C

정오(12시)의 온도는?
→ 선형 보간 사용: (15 + 25) / 2 = 20°C

이처럼 선형 보간은 두 알려진 값 사이의 미지값을 추정하는 데 사용됩니다.

수학적 정의와 증명

1차원 선형 보간

두 값 aabb 사이에서 매개변수 tt (0 ≤ t ≤ 1)에 따른 보간값:

lerp(a,b,t)=a+t(ba)=(1t)a+tb\text{lerp}(a, b, t) = a + t \cdot (b - a) = (1 - t) \cdot a + t \cdot b

동일 공식의 두 가지 형태:

  1. 가산 형태: a+t(ba)a + t \cdot (b - a)

    • 시작점 aa에서 차이값 (ba)(b - a)tt배만큼 이동
    • 컴퓨터 연산에 효율적 (곱셈 1회, 덧셈 1회, 뺄셈 1회)
  2. 가중 평균 형태: (1t)a+tb(1 - t) \cdot a + t \cdot b

    • aabb의 가중 평균
    • 수학적으로 직관적

증명: 극한값 검증

경계 조건:

t=0lerp(a,b,0)=a+0(ba)=at=1lerp(a,b,1)=a+1(ba)=bt=0.5lerp(a,b,0.5)=a+0.5(ba)=a+b2\begin{align*} t = 0 &\Rightarrow \text{lerp}(a, b, 0) = a + 0 \cdot (b - a) = a \\ t = 1 &\Rightarrow \text{lerp}(a, b, 1) = a + 1 \cdot (b - a) = b \\ t = 0.5 &\Rightarrow \text{lerp}(a, b, 0.5) = a + 0.5 \cdot (b - a) = \frac{a + b}{2} \end{align*}

선형성 증명:

t1<t2t_1 < t_2일 때, lerp(a,b,t1)<lerp(a,b,t2)\text{lerp}(a, b, t_1) < \text{lerp}(a, b, t_2) (단, a<ba < b):

lerp(a,b,t2)lerp(a,b,t1)=(t2t1)(ba)>0(∵ t2>t1b>a)\begin{align*} \text{lerp}(a, b, t_2) - \text{lerp}(a, b, t_1) &= (t_2 - t_1) \cdot (b - a) \\ &> 0 \quad \text{(∵ } t_2 > t_1 \text{, } b > a\text{)} \end{align*}

2차원 선형 보간

두 점 P0(x0,y0)P_0(x_0, y_0)P1(x1,y1)P_1(x_1, y_1) 사이의 보간:

P(t)=(1t)P0+tP1=((1t)x0+tx1(1t)y0+ty1)P(t) = (1 - t) \cdot P_0 + t \cdot P_1 = \begin{pmatrix} (1-t) \cdot x_0 + t \cdot x_1 \\ (1-t) \cdot y_0 + t \cdot y_1 \end{pmatrix}

예제: P0(1,2)P_0(1, 2)P1(3,4)P_1(3, 4) 사이에서 t=0.5t = 0.5:

x(0.5)=0.51+0.53=2y(0.5)=0.52+0.54=3P(0.5)=(2,3)\begin{align*} x(0.5) &= 0.5 \cdot 1 + 0.5 \cdot 3 = 2 \\ y(0.5) &= 0.5 \cdot 2 + 0.5 \cdot 4 = 3 \\ P(0.5) &= (2, 3) \end{align*}

JavaScript 구현

기본 LERP 함수

/**
 * 선형 보간 함수
 * @param {number} a - 시작값
 * @param {number} b - 끝값
 * @param {number} t - 보간 매개변수 (0 ≤ t ≤ 1)
 * @returns {number} 보간된 값
 */
function lerp(a, b, t) {
  return a + t * (b - a);
}
 
// 사용 예
console.log(lerp(0, 100, 0));    // 0
console.log(lerp(0, 100, 0.5));  // 50
console.log(lerp(0, 100, 1));    // 100
console.log(lerp(10, 20, 0.3));  // 13

2D/3D 벡터 보간

// 2D 벡터
function lerp2D(p0, p1, t) {
  return {
    x: lerp(p0.x, p1.x, t),
    y: lerp(p0.y, p1.y, t),
  };
}
 
// 3D 벡터
function lerp3D(p0, p1, t) {
  return {
    x: lerp(p0.x, p1.x, t),
    y: lerp(p0.y, p1.y, t),
    z: lerp(p0.z, p1.z, t),
  };
}
 
// 사용 예: 카메라 위치 이동
const cameraStart = { x: 0, y: 0, z: 10 };
const cameraEnd = { x: 5, y: 3, z: 15 };
 
for (let t = 0; t <= 1; t += 0.1) {
  const cameraPos = lerp3D(cameraStart, cameraEnd, t);
  console.log(`t=${t.toFixed(1)}:`, cameraPos);
}

색상 보간

// RGB 색상 보간
function lerpColor(color1, color2, t) {
  return {
    r: Math.round(lerp(color1.r, color2.r, t)),
    g: Math.round(lerp(color1.g, color2.g, t)),
    b: Math.round(lerp(color1.b, color2.b, t)),
  };
}
 
// 사용 예: 빨강에서 파랑으로 전환
const red = { r: 255, g: 0, b: 0 };
const blue = { r: 0, g: 0, b: 255 };
 
for (let t = 0; t <= 1; t += 0.25) {
  const color = lerpColor(red, blue, t);
  console.log(`t=${t}:`, `rgb(${color.r}, ${color.g}, ${color.b})`);
}
// 출력:
// t=0: rgb(255, 0, 0)     // 빨강
// t=0.25: rgb(191, 0, 64)
// t=0.5: rgb(128, 0, 128) // 보라
// t=0.75: rgb(64, 0, 191)
// t=1: rgb(0, 0, 255)     // 파랑

애니메이션 활용

CSS 트랜지션과의 관계

CSS transition도 내부적으로 LERP를 사용합니다:

.box {
  width: 100px;
  transition: width 1s linear;
}
 
.box:hover {
  width: 200px;
}

위 CSS는 다음 JavaScript와 동일합니다:

const box = document.querySelector('.box');
const duration = 1000; // 1초
const startWidth = 100;
const endWidth = 200;
let startTime;
 
function animate(currentTime) {
  if (!startTime) startTime = currentTime;
  const elapsed = currentTime - startTime;
  const t = Math.min(elapsed / duration, 1); // 0 ~ 1로 정규화
 
  const width = lerp(startWidth, endWidth, t);
  box.style.width = `${width}px`;
 
  if (t < 1) {
    requestAnimationFrame(animate);
  }
}
 
box.addEventListener('mouseenter', () => {
  startTime = null;
  requestAnimationFrame(animate);
});

Easing 함수와의 조합

LERP는 선형이므로 등속 운동을 만듭니다. 자연스러운 애니메이션을 위해 easing 함수를 조합합니다:

// Easing 함수들
const easing = {
  linear: t => t,
  easeInQuad: t => t * t,
  easeOutQuad: t => t * (2 - t),
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  easeInCubic: t => t * t * t,
  easeOutCubic: t => (--t) * t * t + 1,
};
 
// LERP + Easing
function lerpEased(a, b, t, easingFunc = easing.linear) {
  const easedT = easingFunc(t);
  return lerp(a, b, easedT);
}
 
// 사용 예: 부드러운 감속
for (let t = 0; t <= 1; t += 0.2) {
  const linearPos = lerp(0, 100, t);
  const easedPos = lerpEased(0, 100, t, easing.easeOutQuad);
  console.log(`t=${t}: linear=${linearPos}, eased=${easedPos.toFixed(1)}`);
}
// 출력:
// t=0: linear=0, eased=0.0
// t=0.2: linear=20, eased=36.0
// t=0.4: linear=40, eased=64.0
// t=0.6: linear=60, eased=84.0
// t=0.8: linear=80, eased=96.0
// t=1: linear=100, eased=100.0

실전: 스크롤 애니메이션

class SmoothScroll {
  constructor() {
    this.current = window.scrollY;
    this.target = window.scrollY;
    this.ease = 0.1; // 보간 속도
 
    this.update();
  }
 
  setTarget(target) {
    this.target = target;
  }
 
  update = () => {
    // LERP로 부드러운 스크롤
    this.current = lerp(this.current, this.target, this.ease);
 
    // 거의 도달하면 정확히 맞춤
    if (Math.abs(this.target - this.current) < 0.1) {
      this.current = this.target;
    }
 
    window.scrollTo(0, this.current);
    requestAnimationFrame(this.update);
  };
}
 
const smoothScroll = new SmoothScroll();
 
// 사용
document.querySelector('.link').addEventListener('click', (e) => {
  e.preventDefault();
  const target = document.querySelector(e.target.hash);
  smoothScroll.setTarget(target.offsetTop);
});

게임 개발 활용

캐릭터 이동

class Character {
  constructor(x, y) {
    this.position = { x, y };
    this.targetPosition = { x, y };
    this.speed = 0.1; // LERP 속도
  }
 
  moveTo(x, y) {
    this.targetPosition = { x, y };
  }
 
  update() {
    // LERP로 부드러운 이동
    this.position.x = lerp(this.position.x, this.targetPosition.x, this.speed);
    this.position.y = lerp(this.position.y, this.targetPosition.y, this.speed);
  }
 
  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, 10, 0, Math.PI * 2);
    ctx.fill();
  }
}
 
// 게임 루프
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const character = new Character(100, 100);
 
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  character.moveTo(e.clientX - rect.left, e.clientY - rect.top);
});
 
function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  character.update();
  character.draw(ctx);
  requestAnimationFrame(gameLoop);
}
 
gameLoop();

카메라 추적

class Camera {
  constructor() {
    this.position = { x: 0, y: 0 };
    this.followSpeed = 0.05;
  }
 
  follow(target) {
    // LERP로 부드러운 카메라 추적
    this.position.x = lerp(this.position.x, target.x, this.followSpeed);
    this.position.y = lerp(this.position.y, target.y, this.followSpeed);
  }
 
  apply(ctx) {
    ctx.translate(-this.position.x, -this.position.y);
  }
}

라이브러리 사용 예제

GSAP (GreenSock Animation Platform)

import { gsap } from 'gsap';
 
// GSAP 내부적으로 LERP 사용
gsap.to('.box', {
  x: 200,
  duration: 1,
  ease: 'power2.out', // easing + LERP
});
 
// 커스텀 LERP 애니메이션
const obj = { value: 0 };
gsap.to(obj, {
  value: 100,
  duration: 2,
  onUpdate: () => {
    console.log(obj.value.toFixed(2));
  },
});

Framer Motion (React)

import { motion } from 'framer-motion';
 
function AnimatedComponent() {
  return (
    <motion.div
      initial={{ opacity: 0, x: -100 }}
      animate={{ opacity: 1, x: 0 }}
      transition={{ duration: 0.5, ease: 'easeOut' }}
    >
      내용
    </motion.div>
  );
}

Three.js (3D 그래픽스)

import * as THREE from 'three';
 
// Vector3.lerp() 메서드 제공
const start = new THREE.Vector3(0, 0, 0);
const end = new THREE.Vector3(10, 10, 10);
const result = new THREE.Vector3();
 
result.lerpVectors(start, end, 0.5); // (5, 5, 5)
 
// 애니메이션 루프에서 사용
function animate() {
  camera.position.lerp(targetPosition, 0.1);
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

성능 최적화

1. 반복 계산 피하기

// [주의] 나쁜 예: 매 프레임마다 차이 계산
function badLerp(a, b, t) {
  return a + t * (b - a); // (b - a) 매번 계산
}
 
for (let i = 0; i < 1000; i++) {
  const value = badLerp(0, 100, i / 1000);
}
 
// [권장] 좋은 예: 차이를 미리 계산
const diff = 100 - 0;
for (let i = 0; i < 1000; i++) {
  const value = 0 + (i / 1000) * diff;
}

2. 조기 종료

class SmoothValue {
  constructor(initial) {
    this.current = initial;
    this.target = initial;
  }
 
  update() {
    const diff = Math.abs(this.target - this.current);
 
    // 거의 도달하면 바로 설정 (부동소수점 오차 방지)
    if (diff < 0.001) {
      this.current = this.target;
      return false; // 업데이트 불필요
    }
 
    this.current = lerp(this.current, this.target, 0.1);
    return true; // 계속 업데이트 필요
  }
}

3. 백터화 (SIMD)

// SIMD를 지원하는 환경에서 여러 값을 한 번에 보간
// (WebAssembly 또는 네이티브 코드에서 유용)
function lerpArray(a, b, t, result) {
  const len = a.length;
  for (let i = 0; i < len; i++) {
    result[i] = a[i] + t * (b[i] - a[i]);
  }
}
 
const a = new Float32Array([0, 10, 20, 30]);
const b = new Float32Array([100, 110, 120, 130]);
const result = new Float32Array(4);
 
lerpArray(a, b, 0.5, result); // [50, 60, 70, 80]

고급 응용: Inverse LERP

역 선형 보간은 보간된 값으로부터 매개변수 tt를 역산합니다.

t=valueabat = \frac{\text{value} - a}{b - a}
/**
 * 역 선형 보간
 * @param {number} a - 시작값
 * @param {number} b - 끝값
 * @param {number} value - 보간된 값
 * @returns {number} 매개변수 t
 */
function inverseLerp(a, b, value) {
  return (value - a) / (b - a);
}
 
// 사용 예: 스크롤 진행률 계산
const scrollStart = 0;
const scrollEnd = document.body.scrollHeight - window.innerHeight;
 
window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;
  const progress = inverseLerp(scrollStart, scrollEnd, scrollY);
  console.log(`스크롤 진행률: ${(progress * 100).toFixed(1)}%`);
});

Remap: 범위 변환

Inverse LERP + LERP 조합으로 값의 범위를 변환합니다:

/**
 * 값의 범위를 변환
 * @param {number} value - 입력 값
 * @param {number} inMin - 입력 최소값
 * @param {number} inMax - 입력 최대값
 * @param {number} outMin - 출력 최소값
 * @param {number} outMax - 출력 최대값
 * @returns {number} 변환된 값
 */
function remap(value, inMin, inMax, outMin, outMax) {
  const t = inverseLerp(inMin, inMax, value);
  return lerp(outMin, outMax, t);
}
 
// 예: 온도 (섭씨 → 화씨)
const celsius = 25;
const fahrenheit = remap(celsius, 0, 100, 32, 212);
console.log(`${celsius}°C = ${fahrenheit}°F`); // 77°F
 
// 예: 마우스 X 위치 → 회전 각도
canvas.addEventListener('mousemove', (e) => {
  const mouseX = e.clientX;
  const angle = remap(mouseX, 0, window.innerWidth, -90, 90);
  element.style.transform = `rotate(${angle}deg)`;
});

실전 예제: 진행 표시줄

<!DOCTYPE html>
<html>
<head>
  <style>
    .progress-container {
      width: 300px;
      height: 20px;
      background: #ddd;
      border-radius: 10px;
      overflow: hidden;
    }
    .progress-bar {
      height: 100%;
      background: linear-gradient(to right, #4CAF50, #8BC34A);
      width: 0%;
      transition: width 0.3s ease-out;
    }
  </style>
</head>
<body>
  <div class="progress-container">
    <div class="progress-bar" id="progressBar"></div>
  </div>
  <button onclick="updateProgress()">진행</button>
 
  <script>
    function lerp(a, b, t) {
      return a + t * (b - a);
    }
 
    let currentProgress = 0;
    let targetProgress = 0;
 
    function updateProgress() {
      targetProgress = Math.min(targetProgress + 20, 100);
    }
 
    function animate() {
      // LERP로 부드러운 진행
      currentProgress = lerp(currentProgress, targetProgress, 0.1);
 
      const progressBar = document.getElementById('progressBar');
      progressBar.style.width = `${currentProgress}%`;
 
      requestAnimationFrame(animate);
    }
 
    animate();
  </script>
</body>
</html>

정리

선형 보간(LERP)은 다음과 같은 이유로 애니메이션과 그래픽스의 핵심입니다:

  1. 간단한 수학: a+t(ba)a + t \cdot (b - a) 한 줄로 구현
  2. 성능 우수: 덧셈, 곱셈, 뺄셈만 사용 (3회 연산)
  3. 범용성: 숫자, 벡터, 색상, 회전 등 모든 값에 적용
  4. 조합 가능: Easing, Inverse LERP, Remap 등으로 확장

활용 분야:

  • 웹 애니메이션 (GSAP, Framer Motion)
  • 게임 개발 (Unity, Unreal, Three.js)
  • 데이터 시각화 (D3.js, Chart.js)
  • UI/UX 트랜지션
  • 물리 시뮬레이션

LERP를 마스터하면 부드럽고 자연스러운 사용자 경험을 만드는 강력한 도구를 얻게 됩니다.

관련 아티클