- 지수 평활화는 토글 버튼, 카메라, UI 요소, 오디오 볼륨처럼 목표값을 향해 부드럽게 따라가야 하는 애니메이션에 넓게 쓸 수 있는 단순한 기법임
- 핵심 식은
position += (target - position) * (1 - exp(- speed * dt))이며, 현재 위치와 목표 위치만으로 목표 변경에 자연스럽게 반응함
- 선형 이동과 일반적인 이징(easing)은 중간 클릭, 빠른 카메라 입력, 큰
dt에서 점프·떨림·입력 큐 같은 처리가 필요하지만, 지수 평활화는 같은 구조로 복잡도를 줄임
1 - exp(- speed * dt)는 비례 갱신식이 푸는 미분방정식의 해에서 나오며, 작은 dt에서는 기존 식과 거의 같고 큰 dt에서도 오버슈트를 피함
- 목표값에 수학적으로 정확히 도달하지는 않지만, 부동소수점 정밀도와 사용자가 감지할 수 있는 변화량 한계 때문에 실제 애니메이션에서는 충분히 완료된 것처럼 동작함
토글 버튼에서 드러나는 문제
- 토글 버튼의 스위치 위치는
turned_on ? max_x : min_x처럼 단순히 계산할 수 있지만, 상태가 바뀔 때 위치가 즉시 순간이동해 생동감이 부족함
- 선형 애니메이션은 현재 위치를 일정 속도로 갱신하고 범위를 제한하는 방식으로 구현됨
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
- 일정 속도 이동은 위치가 시간에 대해 선형 함수가 되어 움직임이 둔하게 보일 수 있음
- 이징 함수를 얹으면 움직임을 더 부드럽게 만들 수 있음
- 고전적인 cubic smoothstep:
3t² - 2t³
- 제곱근 이징:
sqrt(t)
smoothstep은 1 - f(t) = f(1 - t) 대칭성이 있어 앞뒤 애니메이션에 같은 코드를 쓸 수 있음
sqrt는 방향에 따라 다른 식이 필요함
- 켜질 때:
sqrt(t)
- 꺼질 때:
1 - sqrt(1 - t)
sqrt는 시작 시 빠르게 움직이고 목표에 가까워질수록 부드럽게 느려지지만, 단순한 2상태 토글에서도 t, 방향, 이징 계산 같은 상태 관리가 필요함
- 사용자가 애니메이션 중간에 다시 클릭하면 기존 이징 방식은 위치가 갑자기 점프하는 불연속성을 만들 수 있음
지수 평활화 식
- 지수 평활화는 목표 위치를 정한 뒤 현재 위치를 목표 쪽으로 조금씩 당기는 방식임
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
- 갱신량은 현재 위치와 목표 위치의 차이인
target - position.x를 기준으로 정해짐
1 - exp(- dt * speed)는 프레임마다 목표 쪽으로 얼마나 이동할지 결정하는 보간 계수임
- 중간에 목표가 바뀌어도 별도의 진행 시간이나 애니메이션 방향 상태 없이 새 목표를 향해 바로 이어서 움직임
- 토글 버튼 예시에서는
sqrt 이징처럼 빠르게 시작하고 목표 근처에서 느려지면서도, 중간 클릭 시 점프 문제가 줄어듦
카메라 이동에서 더 커지는 장점
- 맵 위를 이동하는 카메라에도 같은 문제가 나타남
- 일정 속도 보간을 단순하게 구현하면 축별로 목표 방향의 부호를 사용함
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
- 애니메이션 완료 지점에서
target - position의 부호가 양수와 음수 사이를 오가며 떨림이 생길 수 있음
- 이를 막으려면
delta를 max_delta 범위로 제한하는 별도 갱신 함수가 필요함
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
- cubic 이징을 카메라 이동에 적용하려면 요청된 이동 이벤트를 큐에 넣고 하나씩 처리해야 해 구조가 복잡해짐
- 애니메이션 중 사용자 입력을 무시하는 방식은 사용자 입장에서 매우 답답하게 느껴짐
- 지수 평활화를 쓰면 토글 버튼과 거의 같은 코드로 카메라 이동을 처리할 수 있음
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
- 사용자가 빠르게 클릭하면 목표와 현재 위치의 차이가 커지므로 카메라가 자연스럽게 더 빨라짐
왜 1 - exp(- speed * dt)인가
position += (target - position) * speed * dt;
- 이 식은 목표와 현재 위치의 차이가 클수록 빠르게 움직이고, 현재 위치와 목표 위치 외에는 추가 상태가 필요 없음
lerp로 쓰면 같은 식이 됨
position = lerp(position, target, speed * dt);
speed * dt가 0에 가까우면 느리게 움직이고, 1에 가까우면 빠르게 목표에 접근함
speed * dt가 1보다 크면 보간이 목표를 지나치는 오버슈트가 발생함
- 예시에서
speed = 220, dt = 1 / 125일 때 speed * dt가 1보다 커져 떨림이 나타남
speed * dt < 2이면 절대적인 차이는 줄어들 수 있지만, speed * dt > 2이면 유용한 동작을 하지 못함
- 보간 계수를
min(1, speed * dt)로 제한할 수는 있지만, dt가 커지는 상황까지 매끄럽게 처리하는 해법은 아님
- 코드가 느려 프레임률이 떨어질 수 있음
- 사용자가 다른 탭이나 창으로 이동해 코드가 멈췄다가 여러 초의
dt로 깨어날 수 있음
- 물리 시뮬레이션에서는 큰
dt를 제한하거나 여러 업데이트로 나눌 수 있지만, 애니메이션에서는 카메라와 버튼이 큰 dt에서도 자연스럽게 동작하는 편이 사용자 경험에 좋음
미분방정식으로 본 해법
A += B * dt 형태는 일반적으로 dA/dt = B라는 미분방정식의 수치적 풀이에 해당함
- 단순 갱신식은 다음 방정식을 푸는 형태임
d(position) / dt = (target - position) * speed
- 변수를
x = position, a = target, c = speed로 두면 다음과 같음
dx / dt = (a - x) * c
x = x0 + (a - x0) * (1 - exp(-c * t))
- 따라서 작은
dt에서 맞는 식이 position += (target - position) * speed * dt라면, 임의의 dt에서 쓸 수 있는 식은 다음과 같음
position += (target - position) * (1 - exp(- speed * dt));
- 테일러 전개에서
exp(x) ≈ 1 + x이므로, 작은 dt에서는 1 - exp(-speed * dt) ≈ speed * dt가 되어 기존 단순식과 같아짐
speed * dt가 매우 커도 exp(-speed * dt)는 0에 가까워지고 1 - exp(...)는 1에 가까워져, 목표에 가까운 값으로 안정적으로 이동함
- 같은 식은
lerp로도 쓸 수 있음
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));
속도 값 고르기
- 일반적인 애니메이션은 “0.125초 안에 이동”처럼 지속 시간으로 생각하기 쉽지만, 지수 평활화는 기술적으로 무한한 시간이 지나야 목표에 정확히 도달함
exp(- speed * time)은 시간이 지날수록 작아지지만 0이 되지는 않으므로, 시작값과 목표값이 다르면 position도 수학적으로는 target과 완전히 같아지지 않음
- 실제로는 부동소수점 정밀도 한계에 도달하거나, 카메라 위치 차이가 사용자가 볼 수 없을 만큼 작아져 애니메이션이 끝난 것처럼 보임
speed의 의미는 1 / speed 시간 동안 position이 target에 정확히 e = 2.71828...배 더 가까워진다는 것임
- 실사용에서는 보통
speed를 5..50 범위로 두며, 같은 느낌의 선형·cubic 애니메이션 속도에 대해서는 지수 평활화의 2 * speed 정도가 적절하게 느껴질 수 있음
신호 처리와의 연결
- “exponential smoothing” 또는 “exponential moving average”로 검색하면 Wikipedia의 Exponential smoothing 문서를 찾을 수 있음
- 고정된
dt에서 target이 매 반복마다 바뀌는 경우, 값은 반복 인덱스 기준으로 다음과 비슷하게 갱신됨
factor = 1 - exp(- speed * dt)
- 보통은
factor를 0과 1 사이 값으로 직접 정함
- 이산적인 지수 평활화는 애니메이션에서 쓰는 방식의 이산 아날로그임
- 신호 처리에서도 이전 값 목록이나 복잡한 상태를 유지하지 않고 현재 평균값만으로 동작한다는 이유로 사용됨
- 디지털 오디오에서는 보통
dt가 샘플링 주파수의 역수인 1 / freq로 고정됨