---
title: "리지드바디가 떨려 보이는 이유"
description: "물리는 1초에 50번 움직이고 화면은 그보다 자주 그리니, Rigidbody 는 몇 프레임 제자리에 있다가 한 번에 뜁니다. deltaTime 으로는 안 고쳐지고, Interpolate 가 직전 두 물리 위치 사이를 메워 그려서 고칩니다."
author: "유니얼"
pubDate: 2026-11-05
category: "game-dev/math"
tags: ["Unity", "기초"]
canonical: https://blog.unialgames.com/posts/math-rigidbody-interpolation/
---
Rigidbody(물리로 움직이게 하는 컴포넌트)로 움직이는 캐릭터를 카메라로 따라가 보면, 일정한 속도인데도 화면에서 미세하게 덜덜 떨립니다.
`Time.deltaTime`(화면 한 장, 곧 한 프레임을 그리는 데 걸린 시간)을 곱해 봐도, 카메라 코드를 바꿔 봐도 그대로입니다.

**물리와 화면이 다른 박자로 돌기 때문입니다.** 물리는 1초에 50번(0.02초마다)만 위치를 바꾸고, 화면은 그보다 훨씬 자주 그립니다.
물리가 안 돈 프레임에는 화면에 그려지는 위치(Transform 에 담긴 값)가 그대로라서, 몇 장 동안 제자리에 있다가 한 번에 뛰는 것을 반복합니다. 그게 떨림입니다.

> **`Rigidbody.interpolation` 을 `Interpolate` 로 켜면, 화면을 그릴 때마다 직전 물리 위치와 최신 물리 위치 사이를 지난 시간의 비율만큼 메워서 Transform 에 넣습니다.
> 물리 계산은 그대로인데 떨림이 사라집니다. 대신 화면은 늘 물리 한 걸음(0.02초) 전의 자리를 보여 줍니다.**

## 물리 한 걸음 사이에 화면은 여러 장을 그린다

유니티에서 중력 · 속도 · 충돌을 계산해 물체를 옮겨 주는 부분을 **물리 엔진**이라고 부릅니다. "속도 5 로 가라" 고만 하면 위치는 물리 엔진이 정합니다.

물리는 게임 시간 0.02초마다 한 걸음씩 계산합니다. 1초에 50걸음입니다. 이 한 걸음의 길이가 `Time.fixedDeltaTime` 이고, 걸음마다 `FixedUpdate` 함수가 불립니다.
fps 는 화면이 1초에 그리는 장 수입니다. 화면이 120fps 면 한 장이 1 ÷ 120 = 0.0083초라, 물리 한 걸음 사이에 화면은 0.02 ÷ 0.0083 = 2.4장을 그립니다.

속도 5(1초에 5m)로 가는 물체라면 물리 한 걸음에 5 × 0.02 = **0.1** 씩 갑니다.

| 시각 (초) | 물리가 돈 횟수 | 물리 위치 | 화면에 그린 위치 (Interpolate 끔) |
| --- | --- | --- | --- |
| 0.0083 | 0 | 0 | 0 |
| 0.0167 | 0 | 0 | 0 (그대로) |
| 0.0250 | 1 | 0.1 | 0.1 (한 번에 뜀) |
| 0.0333 | 1 | 0.1 | 0.1 (그대로) |
| 0.0417 | 2 | 0.2 | 0.2 (한 번에 뜀) |

**화면의 위치가 시간에 고르게 늘지 않고 계단처럼 늡니다.** 눈은 "멈췄다, 뛰었다" 를 아주 잘 알아챕니다.
아래 그림의 위 줄이 물리가 정한 위치, 아래 줄이 화면에 그린 위치이고, 경고색 눈금이 바로 앞 장과 같은 자리를 다시 그린 프레임입니다. `Interpolate` 쪽은 같은 시간 동안 그 설정을 켠 모습입니다.

<x-dt>
<script type="application/json">
{
  "label": "물리 50Hz, 화면 120fps",
  "kind": "fixed",
  "hz": 50,
  "speed": 5,
  "lanes": [{ "fps": 120 }],
  "seconds": 0.2,
  "stepSec": 0.02,
  "slow": 15,
  "length": 1.1,
  "note": "물리 한 걸음(게임 시간 0.02초) 동안 화면은 2장이나 3장을 그립니다."
}
</script>
</x-dt>

## deltaTime 을 곱해도 안 고쳐지는 이유

`Time.deltaTime` 을 곱하는 것은 **내가 `Update` 에서 직접 위치를 옮길 때** 1초에 가는 거리를 fps 와 상관없게 만드는 방법입니다.
Rigidbody 는 내가 옮기는 것이 아니라 물리 엔진이 0.02초마다 옮깁니다. 속도에 무엇을 곱하든 **위치가 바뀌는 순간은 물리가 도는 순간뿐**이라, 그 사이의 프레임에는 위치가 그대로입니다.
문제는 "얼마나 가나" 가 아니라 **"언제 바뀌나"** 입니다.

`fixedDeltaTime` 을 줄여 물리를 더 자주 돌리는 방법도 있지만, 모든 물체의 물리 계산이 그만큼 늘고(0.01 이면 두 배), 화면 fps 와 딱 맞지 않는 한 계단은 남습니다.
물리는 그대로 두고 **화면이 물리 사이를 메워 그리는 것**이 보간입니다. 보간은 알고 있는 두 값 사이를 비율로 채워 넣는 일로, 0 과 10 사이의 30% 자리는 3 입니다.

## Interpolate 는 지나온 두 위치 사이를 메운다

`Interpolate` 를 켜면 유니티는 화면을 그리기 전마다, 물리가 계산해 둔 **직전 위치와 최신 위치** 사이를 마지막 물리 걸음 뒤 지난 시간의 비율만큼 섞어 Transform 에 넣습니다.

```text
비율 = (지금 시각 − 마지막으로 물리가 돈 시각) ÷ 0.02
그릴 위치 = Lerp(직전 물리 위치, 최신 물리 위치, 비율)
```

`Lerp(a, b, t)` 는 보간을 하는 함수입니다. a + (b − a) × t 를 돌려주니 `Lerp(0, 10, 0.3)` = 3 입니다.

위 표와 같은 프레임에 이 계산을 넣으면 이렇습니다. 물리는 0.02초에 0.1, 0.04초에 0.2 를 정했습니다.

| 시각 | 마지막 물리 | 비율 | 사이 | 그린 위치 | 앞 장보다 |
| --- | --- | --- | --- | --- | --- |
| 0.0083 | 아직 없음 | | | 0 | |
| 0.0167 | 아직 없음 | | | 0 | |
| 0.0250 | 0.02 | 0.005 ÷ 0.02 = 0.25 | 0 과 0.1 | 0.025 | +0.025 |
| 0.0333 | 0.02 | 0.0133 ÷ 0.02 = 0.67 | 0 과 0.1 | 0.067 | +0.042 |
| 0.0417 | 0.04 | 0.0017 ÷ 0.02 = 0.08 | 0.1 과 0.2 | 0.108 | +0.042 |
| 0.0500 | 0.04 | 0.01 ÷ 0.02 = 0.5 | 0.1 과 0.2 | 0.15 | +0.042 |

물리가 한 번이라도 돈 뒤로는 **같은 자리를 두 번 그리는 일이 없고, 매 장 0.042 씩 고르게 갑니다.** 120fps 에서 한 장에 가야 할 거리 5 × 0.0083 = 0.042 그대로입니다.
물리 걸음은 여전히 0.02초마다 한 번이고, 보간은 Transform 에 넣는 값만 바꿉니다. 충돌이나 속도 계산에는 아무 영향이 없습니다.

대신 그린 위치를 보면 시각 0.05 에 0.15 입니다. 속도 5 라면 지금 있어야 할 자리는 0.25 인데, 그 0.02초 전의 자리를 그리고 있습니다.
**직전과 최신 사이만 메울 수 있으니, 화면은 늘 물리 한 걸음 전을 보여 줍니다.** 유니티 매뉴얼도 Interpolate 를 "물리 한 번만큼의 시간 지연이 있다" 고 설명합니다. 위 그림의 `Interpolate` 쪽에서 화면 줄이 물리 줄보다 0.1 뒤에서 끝나는 이유입니다.
0.02초는 사람이 느끼기 어려운 지연이라 대부분의 게임에서 문제가 되지 않습니다.

### 세 가지 설정

| 설정 | 무엇을 그리나 | 틀릴 수 있나 | 지연 |
| --- | --- | --- | --- |
| `None` (기본) | 최신 물리 위치 그대로 | 없음 | 없음. 대신 계단처럼 떨림 |
| **`Interpolate`** | 지나온 두 물리 위치 사이 | 없음 | 물리 한 걸음 |
| `Extrapolate` | 최신 위치에서 속도로 **예측한** 앞 | 있음 | 없음. 오히려 앞서 감 |

`Extrapolate` 는 "이 속도면 다음 걸음엔 저기" 라고 앞을 예측해 그려서 지연이 없습니다. 하지만 예측이라 틀립니다.
벽 바로 앞에서 벽 안쪽을 그렸다가, 다음 물리 걸음이 막혔다고 알려 주면 되돌아옵니다. **예측이 틀리는 순간이 하필 부딪히거나 방향을 바꾸는 순간**이라 가장 눈에 띄는 곳에서 깨집니다.
그래서 속도가 자주 바뀌거나 다른 물체와 부딪히는 것에는 `Interpolate` 를, 아무와도 부딪히지 않고 일정한 속도로만 가는 것에만 `Extrapolate` 를 고려합니다.

```csharp
// 보통은 Inspector 의 Interpolate 칸에서 켠다
rb.interpolation = RigidbodyInterpolation.Interpolate;
```

2D 는 `Rigidbody2D.interpolation` 에 `RigidbodyInterpolation2D.Interpolate` 를 넣습니다.

**모든 Rigidbody 에 켜지는 않습니다.** 보간은 매 프레임 그 물체의 Transform 을 다시 써 넣는 일이라 물체 수만큼 비용이 듭니다.
떨림은 카메라가 따라가는 물체, 곧 플레이어 캐릭터에서 가장 잘 보이니, 유니티도 그 물체에만 켜고 나머지는 `None` 으로 두기를 권합니다.

## 위치를 읽는 곳이 둘로 나뉜다

보간을 켜면 같은 물체의 위치가 두 군데에 다르게 들어 있습니다. 위 표의 시각 0.05 라면 이렇습니다.

| 값 | 들어 있는 것 | 시각 0.05 에 |
| --- | --- | --- |
| `rb.position` | 물리가 정한 최신 위치. 보간과 상관없이 계단 | 0.2 |
| `transform.position` | 보간이 메워 넣은, 화면에 그려지는 위치 | 0.15 |

물리 계산(힘, 속도, 충돌 판정)에는 `rb.position` 이, 화면에 보이는 것을 따라가는 코드에는 `transform.position` 이 맞습니다.

보간을 켠 Rigidbody 는 **물리 엔진이 Transform 을 쥐고 있습니다.** 코드에서 `transform.position` 에 직접 값을 넣으면, `Physics.SyncTransforms` 를 따로 부르지 않는 한 유니티가 그 값을 무시하고 보간한 값으로 덮어씁니다.
그래서 Rigidbody 를 코드로 옮길 때는 목적에 맞는 함수를 씁니다.

| 하고 싶은 것 | 쓰는 것 | 보간 |
| --- | --- | --- |
| 코드로 정한 길을 매끈하게 (Kinematic, 곧 힘을 안 받고 코드로만 움직이는 Rigidbody) | `FixedUpdate` 에서 `rb.MovePosition` | 따름. 걸음 사이를 메워 그림 |
| 한 번에 다른 곳으로 순간이동 | `rb.position = ...` | 메우지 않고 바로 그 자리 |

## 캐릭터는 매끈한데 카메라가 떤다면

보간을 켜도 화면 전체가 떤다면 **카메라가 언제 움직이는지**를 봅니다.
카메라를 `FixedUpdate` 에서 옮기면, 캐릭터가 아니라 **카메라 자신이 1초에 50번만 움직입니다.** 캐릭터는 매 장 고르게 그려지는데 카메라가 2장이나 3장마다 한 번씩 뛰니, 카메라에 비친 캐릭터와 배경이 함께 떨립니다.

| 카메라가 대상을 따라가는 곳 | 카메라가 움직이는 박자 | 결과 |
| --- | --- | --- |
| `FixedUpdate` | 물리와 같은 1초에 50번 | 떨림 |
| `LateUpdate` | 화면과 같은 매 프레임. 모든 `Update` 가 끝난 뒤라 그 프레임의 최종 위치를 읽음 | 매끈 |

카메라 코드는 `LateUpdate` 에 둡니다.
Cinemachine 을 쓴다면 Cinemachine Brain 의 Update Method 가 같은 역할입니다. 권장값은 Smart Update(따라가는 대상이 움직이는 방식에 맞춰 알아서 고름)이고, `Fixed Update` 로 되어 있다면 Smart Update 나 Late Update 로 바꿉니다.

## 직접 확인하기

등속으로 미는 물체의 화면상 속도를 매 프레임 찍어 보면, 보간을 켜고 끈 차이가 숫자로 보입니다.

```csharp
Rigidbody rb;
float lastX;

void Start()
{
    rb = GetComponent<Rigidbody>();
    lastX = transform.position.x;
}

void FixedUpdate()
{
    // 1초에 5. Unity 6 이전 이름은 velocity
    rb.linearVelocity = new Vector3(5f, 0f, 0f);
}

void LateUpdate()
{
    // 이번 프레임에 화면에서 움직인 거리
    float x = transform.position.x;
    float moved = x - lastX;
    lastX = x;

    // 움직인 거리 ÷ 이번 프레임의 시간 = 화면상 속도
    float speed = moved / Time.deltaTime;
    Debug.Log($"화면상 속도 {speed:F3}");
}
```

| 설정 | 찍히는 값 |
| --- | --- |
| `None` | 0 이 여러 번 이어지다가 큰 값이 한 번. 화면 fps 가 높을수록 0 이 많음 |
| `Interpolate` | 거의 매번 5 근처 |

## 정리

- 물리는 0.02초마다 위치를 바꾸고 화면은 그보다 자주 그려서, Rigidbody 는 몇 프레임 제자리에 있다가 한 번에 뜁니다. 그것이 떨림입니다
- 내가 옮기는 것이 아니라 물리가 옮기는 것이라 deltaTime 을 곱해도 안 고쳐집니다. 문제는 거리가 아니라 바뀌는 순간입니다
- `Interpolate` 는 직전과 최신 물리 위치 사이를 시간 비율만큼 `Lerp` 해 그립니다. 물리 계산은 그대로이고, 화면이 늘 한 걸음 늦습니다
- `Extrapolate` 는 속도로 앞을 예측해 지연이 없지만, 부딪히거나 방향을 바꾸는 순간에 틀립니다
- 보간은 카메라가 따라가는 물체에만 켭니다
- 보간을 켜면 `rb.position` 은 물리 위치, `transform.position` 은 그린 위치입니다. 옮길 때는 `MovePosition`, 순간이동은 `rb.position` 입니다
- 카메라는 `LateUpdate` 에서 따라갑니다. Cinemachine 이면 Brain 의 Update Method 를 확인합니다