---
title: "로컬 좌표와 월드 좌표"
description: "같은 (2, 1) 도 월드 기준이면 한 자리, 캐릭터 기준이면 다른 자리입니다. 좌표계와 기저가 무엇인지부터, TransformPoint 가 축을 섞어 월드 자리로 옮기고 InverseTransformPoint 가 내적으로 되돌리는 과정을 작은 숫자로 따라갑니다."
author: "유니얼"
pubDate: 2026-11-05
category: "game-dev/math"
tags: ["Unity", "기초"]
canonical: https://blog.unialgames.com/posts/math-local-world/
---
캐릭터 오른쪽 앞에 무기를 붙이려고 `weapon.position = new Vector3(1, 0, 1)` 이라고 썼더니, 무기가 캐릭터 옆이 아니라 맵 원점 근처에 생깁니다. 원점은 좌표를 재기 시작하는 자리로, 월드에서는 (0, 0, 0) 입니다.

**어느 축으로 잰 숫자인가**의 문제입니다. 축은 숫자를 재는 방향으로, 오른쪽 · 위 · 앞을 가리키는 길이 1 인 화살표 세 개입니다. 월드 원점과 월드 축으로 잰 숫자가 **월드 좌표**이고,
오브젝트의 위치를 원점으로, 오브젝트의 오른쪽 · 위 · 앞을 축으로 잰 숫자가 **로컬 좌표**입니다. 같은 (1, 0, 1) 이라도 읽는 기준에 따라 다른 자리입니다.

> **로컬 (a, b) 의 월드 자리는 `오브젝트 위치 + a × 오른쪽 축 + b × 위 축` 입니다. `TransformPoint` 가 이 계산입니다.
> 거꾸로 월드 자리의 로컬 숫자는, 위치를 뺀 뒤 각 축과 내적해서 구합니다. `InverseTransformPoint` 가 이 계산입니다.**

## 좌표 숫자는 축을 몇 개씩 섞을지다

**벡터** (2, 1) 은 "x 축 방향으로 2, y 축 방향으로 1" 입니다. 곧 `2 × x 축 + 1 × y 축` 입니다.
월드 축은 `Vector3.right` (1, 0, 0), `Vector3.up` (0, 1, 0), `Vector3.forward` (0, 0, 1) 로 늘 같습니다.

오브젝트도 자기 축을 들고 있습니다. `transform.right`, `transform.up`, `transform.forward` 입니다.
오브젝트가 돌면 이 세 축도 같이 돕니다. 셋 다 길이가 1 이고 서로 수직, 곧 직각(90°)으로 만납니다. 원점 하나와 이렇게 만나는 축 세트를 묶은 것을 좌표계, 그 축 세트를 **기저**라고 부릅니다. 이 세 축이 오브젝트의 기저입니다.

| | 원점 | 축 | 오브젝트가 돌면 |
| --- | --- | --- | --- |
| 월드 | (0, 0, 0) | `Vector3.right · up · forward` | 그대로 |
| 로컬 | 오브젝트의 위치 | `transform.right · up · forward` | 같이 돈다 |

## 로컬에서 월드로: 축을 섞고 위치를 더한다

2D 로 줄인 예입니다. 오브젝트가 (3, 1) 에 있고, 조금 돌아 서 있어서
오른쪽 축이 (0.8, 0.6), 위 축이 (−0.6, 0.8) 입니다.

길이는 가로와 세로를 각각 제곱해 더한 뒤 √ 를 씌워 잽니다(피타고라스). (0.8, 0.6) 은 √(0.64 + 0.36) = √1 = 1 이라 길이 1 이고, (−0.6, 0.8) 도 같습니다.
두 축이 수직인지는 **내적**, 곧 x 끼리, y 끼리 곱해서 더한 숫자로 확인합니다. 수직인 두 방향은 내적이 0 입니다. 0.8 × (−0.6) + 0.6 × 0.8 = 0 이라 서로 수직입니다.

이 오브젝트 기준의 로컬 점 (2, 1) 은 월드의 어디일까요.

| 단계 | 계산 | 결과 |
| --- | --- | --- |
| 1. 오른쪽 축 2 개 | 2 × (0.8, 0.6) | (1.6, 1.2) |
| 2. 위 축 1 개 | 1 × (−0.6, 0.8) | (−0.6, 0.8) |
| 3. 둘을 더함 | (1.6 − 0.6, 1.2 + 0.8) | (1.0, 2.0) |
| 4. 위치를 더함 | (3 + 1.0, 1 + 2.0) | **(4, 3)** |

<x-vec>
<script type="application/json">
{
  "label": "로컬 (2, 1) 을 월드 자리로",
  "view": [-1, -1, 6, 5],
  "steps": [
    {
      "caption": "오브젝트는 (3, 1) 에 있고, 오른쪽 축 (0.8, 0.6) 과 위 축 (−0.6, 0.8) 을 들고 있습니다. 월드 축에서 조금 돌아 있습니다.",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [3.8, 1.6], "color": "accent2", "label": "right", "labelAt": [4.4, 1.5] },
        { "id": "u", "from": [3, 1], "to": [2.4, 1.8], "color": "warn", "label": "up", "labelAt": [2.1, 1.6] }
      ]
    },
    {
      "caption": "로컬 x 가 2 이니 오른쪽 축을 두 번 갑니다. (1.6, 1.2) 만큼.",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [4.6, 2.2], "color": "accent2", "label": "2 × right", "labelAt": [5.2, 1.9] },
        { "id": "u", "from": [3, 1], "to": [2.4, 1.8], "color": "warn", "label": "up", "labelAt": [2.1, 1.6] }
      ]
    },
    {
      "caption": "그 끝에서 로컬 y 1 만큼 위 축을 이어 갑니다. (−0.6, 0.8) 만큼.",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [4.6, 2.2], "color": "accent2", "label": "2 × right", "labelAt": [5.2, 1.9] },
        { "id": "u", "from": [4.6, 2.2], "to": [4, 3], "color": "warn", "label": "1 × up", "labelAt": [4.9, 3.1] }
      ]
    },
    {
      "caption": "도착한 자리가 월드 (4, 3) 입니다. 월드 원점에서 보면 (4, 3), 오브젝트에서 보면 (2, 1). 같은 자리를 두 기준으로 잰 것입니다.",
      "code": "transform.TransformPoint(new Vector3(2, 1, 0))   // (4, 3, 0)",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [4.6, 2.2], "color": "accent2" },
        { "id": "u", "from": [4.6, 2.2], "to": [4, 3], "color": "warn" },
        { "id": "w", "from": [0, 0], "to": [4, 3], "color": "muted", "dash": true, "label": "월드 (4, 3)", "labelAt": [1.4, 2.0] },
        { "id": "p", "type": "point", "at": [4, 3], "color": "accent", "label": "로컬 (2, 1)", "labelAt": [4.3, 3.6] }
      ]
    }
  ]
}
</script>
</x-vec>

이 계산은 **로컬 숫자만큼 오브젝트의 축을 섞고, 오브젝트 위치를 더한 것**입니다.
`transform.TransformPoint(local)` 이 그대로 이 일을 합니다. 오브젝트의 Scale 이 1 이 아니면 로컬 숫자에 Scale 을 먼저 곱합니다. Scale 이 (2, 2) 면 로컬 (2, 1) 은 오른쪽 축 4 칸, 위 축 2 칸입니다.

무기를 캐릭터 오른쪽 앞에 붙이려던 코드는 이렇게 고칩니다.

```csharp
// 캐릭터 기준 오른쪽 1, 앞 1
Vector3 local = new Vector3(1f, 0f, 1f);

weapon.position = character.TransformPoint(local);
```

## 월드에서 로컬로: 위치를 빼고 축과 내적한다

반대 방향도 자주 필요합니다. "이 적이 내 기준으로 앞에 있나, 오른쪽에 있나" 를 숫자로 알고 싶을 때입니다.
앞의 월드 (4, 3) 을 오브젝트 기준 숫자로 되돌리면 다시 (2, 1) 이 나와야 합니다.

길이 1 인 축과의 내적은 그 축 위에 내린 그림자의 길이입니다.
축이 서로 수직이라서, 각 축과의 내적이 곧 그 축으로 몇 칸인지가 됩니다.
(1, 2) 는 2 × right + 1 × up 입니다. 이것을 right 와 내적하면 2 × (right · right) + 1 × (up · right) = 2 × 1 + 1 × 0 = 2 입니다. 길이 1 인 축은 자기와의 내적이 1 이고, 서로 수직인 축끼리의 내적은 0 이라 **다른 축의 몫이 지워지고** 그 축의 몫만 남습니다.

| 단계 | 계산 | 결과 |
| --- | --- | --- |
| 1. 위치를 뺌 | (4, 3) − (3, 1) | (1, 2) |
| 2. 오른쪽 축과 내적 | 1 × 0.8 + 2 × 0.6 | **2** |
| 3. 위 축과 내적 | 1 × (−0.6) + 2 × 0.8 | **1** |

<x-vec>
<script type="application/json">
{
  "label": "월드 (4, 3) 을 오브젝트 기준으로 되돌리기",
  "view": [-1, -1, 6, 5],
  "steps": [
    {
      "caption": "월드 (4, 3) 에서 오브젝트 위치 (3, 1) 을 빼면 (1, 2). 오브젝트에서 그 점으로 가는 화살표입니다.",
      "code": "world - transform.position   // (1, 2)",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [3.8, 1.6], "color": "accent2", "label": "right", "labelAt": [4.4, 1.5] },
        { "id": "u", "from": [3, 1], "to": [2.4, 1.8], "color": "warn", "label": "up", "labelAt": [2.1, 1.6] },
        { "id": "d", "from": [3, 1], "to": [4, 3], "color": "accent", "label": "(1, 2)", "labelAt": [3.3, 2.6] }
      ]
    },
    {
      "caption": "(1, 2) 를 오른쪽 축 위로 그림자를 내립니다. Dot = 1 × 0.8 + 2 × 0.6 = 2. 그림자는 오른쪽 축 2 칸 자리에 떨어집니다.",
      "code": "Vector3.Dot(d, transform.right)   // 2",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [4.6, 2.2], "color": "accent2", "label": "2 칸", "labelAt": [5.0, 1.9] },
        { "id": "u", "from": [3, 1], "to": [2.4, 1.8], "color": "warn", "label": "up", "labelAt": [2.1, 1.6] },
        { "id": "d", "from": [3, 1], "to": [4, 3], "color": "accent", "label": "(1, 2)", "labelAt": [3.3, 2.6] },
        { "id": "s1", "type": "line", "from": [4, 3], "to": [4.6, 2.2], "color": "accent2" }
      ]
    },
    {
      "caption": "위 축에도 그림자를 내립니다. Dot = 1 × (−0.6) + 2 × 0.8 = 1. 위 축 1 칸. 로컬 (2, 1) 로 정확히 돌아왔습니다.",
      "code": "Vector3.Dot(d, transform.up)   // 1",
      "items": [
        { "id": "o", "type": "point", "at": [3, 1], "color": "muted", "label": "위치 (3, 1)", "labelAt": [3.6, 0.5] },
        { "id": "r", "from": [3, 1], "to": [4.6, 2.2], "color": "accent2", "label": "2 칸", "labelAt": [5.0, 1.9] },
        { "id": "u", "from": [3, 1], "to": [2.4, 1.8], "color": "warn", "label": "1 칸", "labelAt": [2.1, 1.6] },
        { "id": "d", "from": [3, 1], "to": [4, 3], "color": "accent", "label": "로컬 (2, 1)", "labelAt": [3.1, 3.3] },
        { "id": "s1", "type": "line", "from": [4, 3], "to": [4.6, 2.2], "color": "accent2" },
        { "id": "s2", "type": "line", "from": [4, 3], "to": [2.4, 1.8], "color": "warn" }
      ]
    }
  ]
}
</script>
</x-vec>

`transform.InverseTransformPoint(world)` 가 이 계산입니다. Scale 이 1 이 아니면 내적한 값을 마지막에 Scale 로 나눕니다.

```csharp
Vector3 local = transform.InverseTransformPoint(enemy.position);

// local.x > 0 이면 내 오른쪽, local.z > 0 이면 내 앞
bool isFrontRight = local.x > 0f && local.z > 0f;
```

로컬 좌표로 바꾸면 앞뒤(z), 좌우(x), 위아래(y)가 부호로 바로 보입니다. 축 하나하나와 따로 내적해서 판정하던 것을 한 번에 하는 셈입니다.

## 점 · 방향 · 벡터를 옮기는 함수가 셋인 이유

로컬에서 월드로 바꾸는 함수는 셋입니다. **위치를 더하는지**, **크기를 곱하는지**가 다릅니다.

| 함수 | 위치를 더함 | 크기를 곱함 | 넣는 것 |
| --- | --- | --- | --- |
| `TransformPoint` | 예 | 예 | 자리. 무기를 붙일 곳 |
| `TransformVector` | 아니요 | 예 | 크기를 따라야 하는 길이 |
| `TransformDirection` | 아니요 | 아니요 | 방향. 총알이 날아갈 쪽 |

**방향**에는 시작점이 없으니 위치를 더하지 않고, 길이 1 인 방향이 커지면 안 되니 크기도 곱하지 않습니다. 셋 다 회전은 적용합니다.
(1, 1) 에 있고 돌지 않았으며 Scale 이 2 인 오브젝트에 로컬 (1, 1) 을 넣으면 셋의 결과가 다릅니다. `TransformPoint` 는 (3, 3), `TransformVector` 는 (2, 2), `TransformDirection` 은 (1, 1) 입니다.

<x-vec>
<script type="application/json">
{
  "label": "(1, 1) 에 있는 Scale 2 오브젝트에 로컬 (1, 1)",
  "view": [-1, -1, 5, 4],
  "cases": [
    {
      "name": "TransformPoint",
      "steps": [
        {
          "caption": "오브젝트는 (1, 1) 에 있고 Scale 이 2 입니다. 로컬 (1, 1) 을 넣습니다.",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] }
          ]
        },
        {
          "caption": "Scale 2 를 곱해 (2, 2), 위치 (1, 1) 을 더해 월드 (3, 3). 자리라서 위치와 크기를 모두 반영합니다.",
          "code": "TransformPoint(1, 1)   // (3, 3)",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] },
            { "id": "v", "from": [1, 1], "to": [3, 3], "color": "muted" },
            { "id": "p", "type": "point", "at": [3, 3], "color": "accent", "label": "(3, 3)", "labelAt": [3.2, 3.5] }
          ]
        }
      ]
    },
    {
      "name": "TransformVector",
      "steps": [
        {
          "caption": "같은 오브젝트, 같은 로컬 (1, 1). 이번에는 자리가 아닌 벡터로 넣습니다.",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] }
          ]
        },
        {
          "caption": "Scale 2 만 곱해 (2, 2). 위치는 더하지 않아 원점에서 그렸습니다. 길이도 두 배가 됐습니다.",
          "code": "TransformVector(1, 1)   // (2, 2)",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] },
            { "id": "v", "from": [0, 0], "to": [2, 2], "color": "accent2", "label": "(2, 2)", "labelAt": [2.2, 2.5] }
          ]
        }
      ]
    },
    {
      "name": "TransformDirection",
      "steps": [
        {
          "caption": "같은 오브젝트, 같은 로컬 (1, 1). 이번에는 방향으로 넣습니다.",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] }
          ]
        },
        {
          "caption": "위치도 크기도 반영하지 않아 (1, 1) 그대로입니다. 오브젝트가 돌아 있었다면 회전만 적용됩니다. 넣은 방향의 길이가 그대로 지켜집니다.",
          "code": "TransformDirection(1, 1)   // (1, 1)",
          "items": [
            { "id": "o", "type": "point", "at": [1, 1], "color": "muted", "label": "위치 (1, 1)", "labelAt": [1.2, 0.6] },
            { "id": "v", "from": [0, 0], "to": [1, 1], "color": "warn", "label": "(1, 1)", "labelAt": [0.4, 1.3] }
          ]
        }
      ]
    }
  ]
}
</script>
</x-vec>

되돌리는 쪽도 `InverseTransformPoint`, `InverseTransformVector`, `InverseTransformDirection` 셋이 짝을 이룹니다.

## position 과 localPosition, Translate 와 +=

같은 생각이 Transform 의 다른 곳에도 그대로 있습니다. 아래에 나오는 부모와 자식은 Hierarchy 관계입니다. 다른 오브젝트 아래에 놓인 오브젝트가 자식, 그 위의 것이 부모이고, 부모가 움직이면 자식이 따라 움직입니다.

| 코드 | 기준 |
| --- | --- |
| `transform.position` | 월드 |
| `transform.localPosition` | **부모** 기준 로컬. 부모가 없으면 position 과 같음 |
| `transform.forward` | 월드로 나타낸 내 앞 방향 |
| `Vector3.forward` | 월드의 앞 (0, 0, 1). 내가 어디를 보든 같음 |
| `transform.Translate(v)` | 기본이 **나** 기준(`Space.Self`). v 를 내 축으로 섞어서 감 |
| `transform.Translate(v, Space.World)` | 월드 기준 |
| `transform.position += v` | 월드 기준 |

그래서 `transform.Translate(Vector3.forward)` 는 내가 보는 쪽으로 갑니다.
`transform.position += Vector3.forward` 는 내가 어디를 보든 늘 월드 앞쪽으로 갑니다.
`transform.position += transform.forward` 라고 쓰면 Translate 와 같은 결과입니다. **`transform.forward` 가 이미 내 앞을 월드 숫자로 바꿔 둔 값**이기 때문입니다.

`localPosition` 의 기준은 **자기 자신이 아니라 부모**라는 점도 헷갈리기 쉽습니다.
자식의 `localPosition` (1, 0, 0) 은 "부모의 오른쪽 축으로 1" 이고, 부모의 Scale 이 2 면 월드로는 2 칸 떨어집니다.

## 정리

- 좌표 숫자는 축을 몇 개씩 섞을지입니다. 월드는 월드 축으로, 로컬은 오브젝트의 축(기저)으로 잽니다
- 로컬 → 월드는 `위치 + x × right + y × up + z × forward` 입니다(크기가 있으면 먼저 곱함). `TransformPoint`
- 월드 → 로컬은 위치를 빼고 각 축과 내적합니다. 축이 서로 수직이고 길이 1 이라 내적이 곧 칸 수입니다. `InverseTransformPoint`
- 로컬 좌표로 바꾸면 앞뒤 · 좌우 · 위아래가 z · x · y 의 부호로 보입니다
- 점은 `TransformPoint`, 크기를 따르는 길이는 `TransformVector`, 방향은 `TransformDirection` 으로 옮깁니다
- `Translate` 는 기본이 나 기준, `position +=` 는 월드 기준입니다. `localPosition` 은 부모 기준입니다