---
title: "매 프레임 Lerp(a, b, 0.1f) 를 쓰면 생기는 일"
description: "카메라를 부드럽게 따라가게 하려고 매 프레임 Lerp 에 0.1 을 넣으면, fps 가 높은 기계에서 더 빨리 따라갑니다. deltaTime 을 곱해도 딱 맞지 않습니다. 남은 거리가 곱셈으로 줄어들기 때문이고, 비율을 1 − Exp(−k × deltaTime) 로 주면 fps 와 상관없어집니다. 거듭제곱부터 숫자로 따라갑니다."
author: "유니얼"
pubDate: 2026-11-05
category: "game-dev/math"
tags: ["Unity", "기초"]
canonical: https://blog.unialgames.com/posts/math-lerp/
---
카메라가 플레이어를 부드럽게 따라가게 하려고 매 프레임 `Vector3.Lerp(카메라 위치, 플레이어 위치, 0.1f)` 를 넣는 코드는 아주 흔합니다.
그런데 144fps PC 에서는 카메라가 딱 붙어 따라오고, 30fps 휴대폰에서는 늘어지게 따라옵니다.
이 코드는 **매 프레임 남은 거리의 10% 를 가고**, 1초에 몇 번 하느냐는 fps 가 정하기 때문입니다. "0.1 대신 속도 × deltaTime" 도 딱 맞지 않습니다. 남은 거리는 **곱해져서** 줄어드는데 그 고침은 더하기에 맞춘 것이라서입니다.

> **`x = Lerp(x, 목표, 0.1f)` 는 남은 거리를 프레임마다 0.9 배로 만듭니다. 1초에 0.9 를 몇 번 곱하느냐가 fps 라서 빠르기가 달라집니다.
> "1초에 남는 비율" 을 정하고, 한 프레임에는 그 비율을 deltaTime 초 치만큼만 적용하면 맞습니다.
> 코드로는 비율을 `1 − Mathf.Exp(−k × Time.deltaTime)` 로 줍니다.**

## Lerp 는 두 점 사이의 비율 지점이다

Lerp 는 **L**inear int**erp**olation 의 줄임말입니다. 보간(interpolation)은 두 값 사이의 중간값을 구하는 것이고, linear 는 곧은 선이라는 뜻입니다.
`Mathf.Lerp(a, b, t)` 는 a 에서 b 로 가는 길의 **t 비율 지점**입니다. 비율은 전체의 몇 배인가로, 0.1 은 10%, 0.25 는 4분의 1, 1 은 전부입니다. 그래서 t 가 0 이면 a, 1 이면 b, 0.5 면 한가운데입니다.
a 에서 b 까지 거리(b − a)에 t 를 곱한 만큼 a 에서 간 자리라서 식은 이렇습니다.

```text
Lerp(a, b, t) = a + (b − a) × t
```

| 식 | 계산 | 결과 |
| --- | --- | --- |
| `Lerp(0, 10, 0)` | 0 + 10 × 0 | 0 |
| `Lerp(0, 10, 0.25)` | 0 + 10 × 0.25 | 2.5 |
| `Lerp(0, 10, 1)` | 0 + 10 × 1 | 10 |
| `Lerp(4, 10, 0.5)` | 4 + 6 × 0.5 | 7 |

`Vector3.Lerp` 는 x, y, z 에 따로 같은 계산을 합니다. 그래서 결과는 a 와 b 를 잇는 곧은 선 위의 점입니다.

<x-vec>
<script type="application/json">
{
  "label": "Vector3.Lerp(a, b, t) 의 t 를 늘리면",
  "view": [-0.5, -0.8, 4.8, 2.8],
  "dwell": 1.8,
  "steps": [
    { "caption": "t = 0. a 그대로입니다.", "items": [
      { "id": "l", "type": "line", "from": [0, 0], "to": [4, 2], "color": "muted" },
      { "id": "a", "type": "point", "at": [0, 0], "color": "muted", "label": "a (0, 0)", "labelAt": [0, -0.5] },
      { "id": "b", "type": "point", "at": [4, 2], "color": "muted", "label": "b (4, 2)", "labelAt": [4, 2.45] },
      { "id": "p", "type": "point", "at": [0, 0], "color": "accent", "label": "t = 0", "labelAt": [0.4, 0.5] } ] },
    { "caption": "t = 0.25. 가는 길의 4분의 1. (0, 0) + (4, 2) × 0.25 = (1, 0.5).", "items": [
      { "id": "l", "type": "line", "from": [0, 0], "to": [4, 2], "color": "muted" },
      { "id": "a", "type": "point", "at": [0, 0], "color": "muted", "label": "a (0, 0)", "labelAt": [0, -0.5] },
      { "id": "b", "type": "point", "at": [4, 2], "color": "muted", "label": "b (4, 2)", "labelAt": [4, 2.45] },
      { "id": "p", "type": "point", "at": [1, 0.5], "color": "accent", "label": "t = 0.25", "labelAt": [1.1, 1.0] } ] },
    { "caption": "t = 0.5. 한가운데 (2, 1).", "items": [
      { "id": "l", "type": "line", "from": [0, 0], "to": [4, 2], "color": "muted" },
      { "id": "a", "type": "point", "at": [0, 0], "color": "muted", "label": "a (0, 0)", "labelAt": [0, -0.5] },
      { "id": "b", "type": "point", "at": [4, 2], "color": "muted", "label": "b (4, 2)", "labelAt": [4, 2.45] },
      { "id": "p", "type": "point", "at": [2, 1], "color": "accent", "label": "t = 0.5", "labelAt": [2.1, 1.5] } ] },
    { "caption": "t = 1. b 에 닿았습니다. t 를 일정하게 늘렸더니 점도 일정한 간격으로 갔습니다.", "items": [
      { "id": "l", "type": "line", "from": [0, 0], "to": [4, 2], "color": "muted" },
      { "id": "a", "type": "point", "at": [0, 0], "color": "muted", "label": "a (0, 0)", "labelAt": [0, -0.5] },
      { "id": "b", "type": "point", "at": [4, 2], "color": "muted", "label": "b (4, 2)", "labelAt": [4, 2.45] },
      { "id": "p", "type": "point", "at": [4, 2], "color": "accent", "label": "t = 1", "labelAt": [3.6, 1.4] } ] }
  ]
}
</script>
</x-vec>

이렇게 **a 와 b 를 고정해 두고 t 만 0 에서 1 로 늘리는 것**이 Lerp 의 원래 쓰임입니다. 문을 1초 동안 여는 연출이라면 t = 지난 시간 ÷ 1초 로 넣습니다. 0.5초가 지나면 t = 0.5 라서 반쯤 열려 있습니다.
`Lerp` 는 t 를 0 ~ 1 로 잘라 쓰므로 1 을 넘겨도 b 를 지나치지 않습니다. 자르지 않으려면 `LerpUnclamped` 를 씁니다.

## 매 프레임 지금 자리를 a 로 넣으면

화면에 그리는 그림 한 장이 **프레임**이고, **fps** 는 1초에 그리는 프레임 수입니다. 60fps 면 1초에 `Update` 가 60번 불립니다.
카메라 코드는 a 를 고정하지 않고 **매 프레임 지금 자리**를 넣습니다. 그러면 뜻이 "남은 거리의 10% 를 간다" 로 바뀝니다.

```csharp
void LateUpdate()
{
    // LateUpdate 는 모든 Update 가 끝난 뒤 불린다. 플레이어가 움직인 뒤에 따라가려고 여기에 둔다
    // 지금 자리에서 목표까지 남은 거리의 10% 만큼 다가간다
    transform.position = Vector3.Lerp(transform.position, target.position, 0.1f);
}
```

처음이 0, 목표가 10 이면 이렇게 됩니다.

| 프레임 | 남은 거리 | 이번에 간 양 (남은 거리 × 0.1) | 간 뒤 위치 | 간 뒤 남은 거리 |
| --- | --- | --- | --- | --- |
| 1 | 10 | 1 | 1 | 9 |
| 2 | 9 | 0.9 | 1.9 | 8.1 |
| 3 | 8.1 | 0.81 | 2.71 | 7.29 |

10% 를 가면 90% 가 남습니다. 그래서 **남은 거리는 프레임마다 0.9 배**가 됩니다. 10 → 9 → 8.1 → 7.29 입니다.
처음엔 많이 가다가 가까워질수록 조금씩 가는, 부드럽게 다가가는 움직임이 이것입니다.

### 같은 수를 여러 번 곱하는 것: 거듭제곱

0.9 를 세 번 곱한 0.9 × 0.9 × 0.9 를 짧게 **0.9³** 이라고 쓰고 "0.9 의 세제곱" 이라 읽습니다. 오른쪽 위의 작은 숫자가 **몇 번 곱했나**이고, 이 숫자를 **지수**라고 부릅니다. 위에 쓰기 어려운 곳에서는 `0.9^3` 처럼 `^` 뒤에 적습니다. 같은 수를 거듭 곱했다는 뜻으로 **거듭제곱**이라 합니다.

| 쓰는 법 | 뜻 | 값 |
| --- | --- | --- |
| 0.9¹ | 0.9 | 0.9 |
| 0.9² | 0.9 × 0.9 | 0.81 |
| 0.9³ | 0.9 × 0.9 × 0.9 | 0.729 |
| 0.9¹⁵ | 0.9 를 15번 | 약 0.21 |
| 0.9⁶⁰ | 0.9 를 60번 | 약 0.0018 |

n 프레임 뒤에 남은 거리는 10 × 0.9ⁿ 입니다. 3 프레임 뒤면 10 × 0.729 = 7.29 로 위 표와 같습니다.
0.9 를 아무리 많이 곱해도 0 은 되지 않으니 **정확히 닿지는 않습니다.** 실제로는 남은 거리가 float 로 나타낼 수 있는 가장 작은 차이 근처까지 줄면, 더해 줄 10% 가 너무 작아 반올림으로 사라지고 목표 바로 앞에서 멈춰 버립니다.

### n 이 프레임 수라서 fps 가 빠르기를 정한다

문제는 지수 n 이 **시간이 아니라 프레임 수**라는 것입니다. 같은 0.5초 동안 기계마다 곱하는 횟수가 다릅니다.

| 기계 | 0.5초 동안 프레임 | 남은 거리 |
| --- | --- | --- |
| 30fps | 15번 | 10 × 0.9¹⁵ ≈ **2.1** |
| 120fps | 60번 | 10 × 0.9⁶⁰ ≈ **0.02** |

0.5초 뒤에 30fps 카메라는 아직 2.1 이나 뒤처져 있고, 120fps 카메라는 이미 붙어 있습니다.

## deltaTime 을 곱해도 딱 맞지는 않는다

흔히 하는 고침은 0.1 대신 `속도 * Time.deltaTime` 을 넣는 것입니다. `Time.deltaTime` 은 지난 프레임이 걸린 초로, 60fps 면 1/60 ≈ 0.0167초, 30fps 면 1/30 ≈ 0.033초입니다. 프레임이 짧으면 조금, 길면 많이 가게 하려는 것입니다.
많이 나아지지만 아직 어긋납니다. 눈으로 보기 쉽게 프레임을 아주 크게 잡은 예입니다. 속도는 1, 목표까지 10 입니다.

| 0.5초를 | 한 프레임 비율 (1 × deltaTime) | 남는 비율 | 0.5초 뒤 남은 거리 |
| --- | --- | --- | --- |
| 0.5초 한 프레임으로 | 0.5 | 0.5 | 10 × 0.5 = **5** |
| 0.25초 두 프레임으로 | 0.25 | 0.75, 두 번 | 10 × 0.75 × 0.75 = **5.625** |

같은 0.5초인데 남은 거리가 5 와 5.625 로 다릅니다.

deltaTime 을 곱하면 **가는 비율의 합**은 0.25 + 0.25 = 0.5 로 맞춰집니다. 그런데 남은 거리는 비율을 더해서가 아니라 **남는 비율을 곱해서** 줄어듭니다. 1 − 0.5 = 0.5 와 (1 − 0.25) × (1 − 0.25) = 0.5625 는 다른 수입니다.

실제 fps 에서도 같습니다. 속도 6 이면 60fps 한 프레임은 1 − 0.1 = 0.9 가 남고, 120fps 두 프레임은 0.95 × 0.95 = 0.9025 가 남습니다. 차이가 작을 뿐 남아 있습니다.

## 1초에 남는 비율을 정하고, deltaTime 만큼 제곱한다

곱셈으로 줄어드는 것은 처음부터 **"1초 동안 남는 비율"** 로 정하면 됩니다. 예를 들어 1초에 남은 거리가 4분의 1 로 줄게 하려면 이 비율을 r = 0.25 로 둡니다.

- 2초 뒤에는 0.25 를 두 번 곱해 0.25² = 0.0625 가 남습니다
- 그럼 **0.5초** 뒤에는 무엇이 남아야 할까요. 0.5초를 두 번 하면 1초이니, 두 번 곱해서 0.25 가 되는 수여야 합니다. 0.5 × 0.5 = 0.25 이니 **0.5** 입니다
- 이것을 지수에 0.5 를 넣어 **0.25^0.5 = 0.5** 라고 씁니다(위첨자를 쓰기 어려워 `^` 뒤에 지수를 적었습니다). 지수가 꼭 1, 2, 3 같은 횟수일 필요는 없고 "몇 초 치" 를 넣어도 됩니다
- 0.25초 뒤라면 네 번 곱해서 0.25 가 되는 수, 0.25^0.25 ≈ 0.707 입니다. 0.707 × 0.707 = 0.5 이고, 그것을 또 두 번 곱하면 0.5 × 0.5 = 0.25 입니다

정리하면 **dt 초 동안 남는 비율 = r 의 dt 제곱(r^dt)** 입니다. 이렇게 정하면 0.5초를 어떻게 나눠도 결과가 같습니다.

| 0.5초를 | 한 프레임 동안 남는 비율 | 0.5초 뒤 남는 비율 |
| --- | --- | --- |
| 한 프레임으로 | 0.25^0.5 = 0.5 | **0.5** |
| 두 프레임으로 | 0.25^0.25 ≈ 0.707 | 0.707 × 0.707 = **0.5** |
| 네 프레임으로 | 0.25^0.125 ≈ 0.841 | 0.841 × 0.841 × 0.841 × 0.841 = **0.5** |

거듭제곱은 **곱하면 지수끼리 더해집니다.** 0.9² × 0.9³ 은 0.9 를 2번, 3번 곱한 것을 이어 곱했으니 0.9⁵ 입니다. 그래서 r^0.25 × r^0.25 = r^0.5 가 늘 성립합니다.
앞 절의 deltaTime 곱하기는 "가는 비율을 더한다" 에 맞췄다면, 이 방법은 **"남는 비율을 곱하면 시간이 더해진다"** 에 맞춘 것입니다. 남은 거리가 실제로 곱셈으로 줄어드니 이쪽이 맞습니다.

`Mathf.Pow(a, b)` 가 a 의 b 제곱입니다. 코드로는 이렇습니다.

```csharp
// 1초 동안 남은 거리의 몇 배가 남나. 0.25 면 1초 뒤에 4분의 1만 남는다
public float remainPerSecond = 0.25f;

void LateUpdate()
{
    // 이번 프레임(deltaTime 초) 동안 남는 비율
    float remain = Mathf.Pow(remainPerSecond, Time.deltaTime);

    // 가는 비율은 1 에서 남는 비율을 뺀 것
    float t = 1f - remain;

    transform.position = Vector3.Lerp(transform.position, target.position, t);
}
```

## 1 − Exp(−k × deltaTime) 은 같은 식이다

실제 코드와 자료에서는 위 식을 거의 `1f - Mathf.Exp(-k * Time.deltaTime)` 모양으로 씁니다. 같은 식을 다른 손잡이로 쓴 것입니다.

- `Mathf.Exp(x)` 는 **e 의 x 제곱**입니다. e 는 2.718… 로 정해진 수이고, 컴퓨터가 거듭제곱을 계산할 때 기본으로 쓰는 밑(거듭 곱하는 수)이라 함수가 따로 있습니다
- e^(−k × dt) 는 e^(−k) 를 dt 제곱한 것과 같습니다. 거듭제곱을 또 거듭제곱하면 지수끼리 곱해지기 때문입니다. (0.9²)³ 은 0.9² 를 세 번 곱한 것이라 0.9 를 6번 곱한 0.9⁶ 입니다. 곧 **r = e^(−k) 로 둔 r^dt** 입니다
- 그래서 k 는 "e 를 몇 제곱해야 1초에 남는 비율 r 이 되나" 의 부호를 뒤집은 수입니다. e^(−1.39) ≈ 0.25 라서 r = 0.25 면 k = 1.39 입니다. 코드로는 `k = -Mathf.Log(r)` 로 구합니다(`Mathf.Log` 는 Exp 를 거꾸로 푸는 함수)
- **k 가 클수록 남는 비율이 작아져 빨리 따라갑니다**

| k | 1초에 남는 비율 e^(−k) | 남은 거리가 절반이 되는 시간 (0.693 ÷ k) |
| --- | --- | --- |
| 1.39 | 0.25 | 0.5초 |
| 6.32 | 0.0018 | 0.11초 |
| 13.9 | 0.000001 | 0.05초 |

표의 0.693 은 e^(−0.693) = 0.5 라서 나온 수입니다. k × 시간 이 0.693 이 되는 때가 절반이 남는 때입니다.
0.25 보다 "1.39" 가 덜 직관적으로 보이지만, k 는 0 에서 끝없이 키울 수 있고 클수록 빠르다는 방향이 하나라 조절하기 편합니다. "절반이 되는 시간" 으로 감을 잡아도 됩니다.
k = 6.32 는 처음 코드의 0.1 과 맞춘 값입니다. 60fps 한 프레임에 e^(−6.32 ÷ 60) ≈ 0.9 가 남아 10% 를 갑니다. 1초에 남는 0.0018 도 0.9⁶⁰ 과 같습니다.

```csharp
// 클수록 빨리 따라간다. 6.32 면 60fps 에서 한 프레임에 10%
public float sharpness = 6.32f;

void LateUpdate()
{
    float t = 1f - Mathf.Exp(-sharpness * Time.deltaTime);
    transform.position = Vector3.Lerp(transform.position, target.position, t);
}
```

지난 시간을 지수에 넣어 줄여 가는 이 방식을 **지수 감쇠**(exponential decay)라고 부릅니다.
아래는 세 방법을 30 · 60 · 120fps 에 같이 건 것입니다. 셋 다 60fps 에서는 한 프레임에 남은 거리의 10% 를 가도록 맞췄습니다.

<x-dt>
<script type="application/json">
{
  "label": "목표 10 을 따라가는 세 가지 방법",
  "kind": "chase",
  "per": 0.1,
  "speed": 6,
  "target": 10,
  "seconds": 0.5,
  "slow": 10,
  "stepSec": 0.05,
  "modes": ["lerp", "lerpdt", "damp"],
  "note": "고정 비율은 줄마다 크게 다르고, 비율에 deltaTime 은 조금 다르고, 지수 감쇠는 세 줄이 같은 자리에 있습니다."
}
</script>
</x-dt>

## 정해진 속도로 가려면 MoveTowards, 스프링처럼 서려면 SmoothDamp

Lerp 로 따라가면 멀수록 빠르고 가까울수록 느려서 끝에 닿지 않습니다.
**일정한 속도로 가서 정확히 멈추게** 하고 싶으면 `MoveTowards` 를 씁니다.

```csharp
public float speed = 5f;   // 1초에 5

void Update()
{
    // 목표 쪽으로 최대 speed × deltaTime 만큼. 목표를 지나치지 않고 그 자리에서 멈춘다
    transform.position = Vector3.MoveTowards(transform.position, target.position, speed * Time.deltaTime);
}
```

목표까지 10 이면 정확히 2초 뒤에 닿고 멈춥니다. 한 번에 갈 거리를 "남은 거리의 몇 %" 가 아니라 "최대 몇 m" 로 주기 때문에, 곱셈이 아니라 더하기로 쌓입니다. 그래서 deltaTime 을 곱하는 것만으로 fps 와 상관없어집니다.

**스프링처럼 부드럽게 출발하고 서고 싶으면 `SmoothDamp`** 입니다. 속도를 따로 들고 있다가, 멀면 속도를 올리고 가까워지면 줄여서 미끄러지듯 멈춥니다. Lerp 따라가기는 출발하는 순간 속도가 가장 빠르지만 SmoothDamp 는 속도가 서서히 붙습니다.

```csharp
Vector3 velocity;                 // SmoothDamp 가 매 프레임 고쳐 쓰는 속도. 필드로 둔다
public float smoothTime = 0.3f;   // 대략 이 시간 정도에 걸쳐 따라붙는다

void LateUpdate()
{
    transform.position = Vector3.SmoothDamp(transform.position, target.position, ref velocity, smoothTime);
}
```

`velocity` 를 지역 변수로 두면 매 프레임 0 으로 돌아가 속도가 쌓이지 않습니다. `SmoothDamp` 는 안에서 `Time.deltaTime` 을 쓰므로 따로 곱하지 않습니다.
회전도 같습니다. `Quaternion.Slerp` 는 두 회전 사이를 t 비율만큼 돌린 회전을 주는 회전용 Lerp 입니다. `Quaternion.Slerp(지금 회전, 목표, 0.1f)` 를 매 프레임 부르면 같은 문제가 생기니 비율을 지수 감쇠로 주고, 일정한 각속도로 돌리려면 `Quaternion.RotateTowards` 를 씁니다.

## 무엇을 쓸까

| 하고 싶은 것 | 쓸 것 | 끝에 닿나 |
| --- | --- | --- |
| 정해진 시간 동안 A 에서 B 로 | `Lerp(a, b, 지난 시간 ÷ 걸릴 시간)` | 닿음 |
| 일정한 속도로 가서 멈추기 | `MoveTowards` | 닿음 |
| 멀면 빨리, 가까우면 천천히 | `Lerp` + `1 − Exp(−k × dt)` | 거의 |
| 서서히 출발하고 서서히 서기 | `SmoothDamp` | 거의 |
| (피할 것) | 매 프레임 `Lerp(x, 목표, 0.1f)` | 거의, fps 따라 빠르기 다름 |

"거의" 인 둘은 남은 거리가 아주 작아지면 목표 자리를 그대로 넣어 끝내도 됩니다.

## 정리

- `Lerp(a, b, t)` 는 a 와 b 사이를 t 비율만큼 간 자리, `a + (b − a) × t` 입니다. a 와 b 를 고정하고 t 를 0 에서 1 로 늘리는 것이 원래 쓰임입니다
- 매 프레임 지금 자리를 a 로 넣고 0.1 을 주면 남은 거리가 프레임마다 0.9 배가 됩니다. n 프레임 뒤엔 0.9ⁿ 이라, 1초에 몇 번 곱하느냐(fps)가 빠르기를 정합니다
- deltaTime 을 곱하면 가는 비율의 **합**만 맞습니다. 남은 거리는 남는 비율의 **곱**으로 줄어서 여전히 조금 어긋납니다
- 1초에 남는 비율 r 을 정하고 한 프레임에 r^deltaTime 이 남게 하면, 시간을 어떻게 나눠도 같습니다. 거듭제곱은 곱하면 지수가 더해지기 때문입니다
- 코드로는 비율 `1 − Mathf.Exp(−k × Time.deltaTime)`. k 가 클수록 빨리 따라갑니다
- 일정한 속도로 정확히 멈추려면 `MoveTowards`, 서서히 붙고 서려면 `SmoothDamp` 입니다