게임 수학 시리즈 – 월드 행렬(World Matrix)

이전 글에서는 스케일 변환 행렬(Scale Matrix)이 왜 중요한지를 살펴보았습니다.

스케일은 객체의 방향은 유지하면서 크기를 변경하는 공간 변환이며, 게임 엔진에서는 이를 이용해 동일한 모델을 다양한 크기로 배치할 수 있다는 점도 함께 알아보았습니다.

이번에는 이동 행렬과 회전 행렬, 스케일 행렬이 하나로 결합된 형태인 월드 행렬(World Matrix)을 살펴보려고 합니다.

게임 엔진을 공부하다 보면 World Matrix라는 용어가 매우 자주 등장합니다.

렌더링 파이프라인을 설명할 때도 등장하고, 셰이더 코드를 작성할 때도 등장하며, Unity나 Unreal Engine의 Transform 시스템을 이해할 때도 계속 접하게 됩니다.

그리고 지금까지 살펴본 이동과 회전, 스케일 변환은 모두 최종적으로 월드 행렬 안으로 모입니다.


객체는 위치와 회전, 크기를 동시에 가진다

게임 안의 모든 객체는 단순히 위치 정보만 가지는 것이 아닙니다.

객체는 현재 어디에 존재하는지 나타내는 위치(Position) 정보를 가지고 있으며, 어느 방향을 바라보는지 나타내는 회전(Rotation) 정보도 함께 가집니다.

또한 얼마나 크게 보일 것인지를 결정하는 스케일(Scale) 정보 역시 관리합니다.

예를 들어 자동차 모델이 있다고 가정해보겠습니다.

자동차를 월드 좌표 기준 (100, 0, 50) 위치에 배치할 수 있고, 진행 방향을 바라보도록 회전시킬 수도 있습니다.

또한 원본 모델보다 두 배 크게 표시할 수도 있습니다.

즉, 하나의 객체를 화면에 배치하기 위해서는 위치와 회전, 스케일이라는 세 가지 변환을 모두 적용해야 합니다.


변환을 하나의 행렬로 합친다는 것

이론적으로는 이동과 회전, 스케일을 각각 따로 계산할 수 있습니다.

예를 들어 어떤 정점(Vertex)에 대해 먼저 스케일을 적용하고, 이후 회전을 적용한 다음 마지막으로 이동을 적용할 수 있습니다.

하지만 실제 게임 엔진에서는 수많은 정점을 매 프레임 처리해야 합니다.

만약 정점마다 스케일 행렬, 회전 행렬, 이동 행렬을 계속 따로 곱한다면 계산 과정이 복잡해질 수 있습니다.

그래서 게임 엔진은 이동 행렬과 회전 행렬, 스케일 행렬을 미리 하나의 행렬로 결합해 사용합니다.

다만 여기서 중요한 점은 월드 행렬을 사용하는 이유가 단순히 계산 횟수를 줄이기 위해서만은 아니라는 것입니다.

행렬은 여러 개의 공간 변환을 하나의 변환으로 합성(Composition)할 수 있습니다.

따라서 게임 엔진은 위치와 회전, 스케일을 각각의 값으로 관리하면서도, 렌더링 직전에는 이를 하나의 월드 행렬로 결합할 수 있습니다.

이렇게 만들어진 최종 행렬을 월드 행렬(World Matrix)이라고 부릅니다.

그리고 렌더링 시스템은 이 월드 행렬 하나만 사용해 모델 공간(Model Space)의 모든 정점을 월드 공간(World Space)으로 변환할 수 있습니다.


월드 행렬은 세 가지 변환을 하나로 합친 결과

DirectX의 행 벡터(Row Vector) 기준으로 생각하면 월드 행렬은 보통 아래와 같은 형태로 계산합니다.

World = Scale * Rotation * Translation;

이 과정에서 스케일 정보와 회전 정보, 이동 정보가 하나의 행렬 안에 저장됩니다.

조금 더 풀어서 생각하면 아래와 같은 흐름입니다.

Scale Matrix
     ↓
Rotation Matrix
     ↓
Translation Matrix
     ↓
World Matrix

객체의 정점은 이 월드 행렬을 한 번만 곱해도 최종 월드 공간(World Space) 위치를 계산할 수 있습니다.

즉, 월드 행렬은 여러 공간 변환을 하나로 통합한 결과라고 볼 수 있습니다.


월드 행렬 안에는 무엇이 들어 있을까

월드 행렬을 처음 접하면 이름은 자주 보이지만, 실제로 그 안에 무엇이 들어 있는지 감이 잘 오지 않을 수 있습니다.

DirectX의 행 벡터(Row Vector) 기준으로 단순화해서 생각하면 월드 행렬 안에는 객체의 축 방향 정보와 위치 정보가 함께 들어 있습니다.

| Right.x    Right.y    Right.z    0 |
| Up.x       Up.y       Up.z       0 |
| Forward.x  Forward.y  Forward.z  0 |
| Pos.x      Pos.y      Pos.z      1 |

여기서 Right, Up, Forward는 객체가 현재 월드 공간에서 어느 방향을 기준 축으로 삼고 있는지를 나타냅니다.

그리고 Pos는 객체가 월드 공간에서 어디에 배치되어 있는지를 나타냅니다.

물론 실제 행렬 값은 스케일과 회전이 함께 반영되기 때문에 항상 이렇게 단순하게만 보이지는 않습니다.

하지만 큰 관점에서 보면 월드 행렬은 “객체의 방향 축과 위치를 담고 있는 행렬”이라고 이해할 수 있습니다.

그래서 월드 행렬을 보면 이 객체가 어디에 있고, 어떤 방향을 바라보고 있으며, 어떤 크기로 배치되어 있는지를 계산할 수 있습니다.


왜 곱셈 순서가 중요할까

행렬은 일반 숫자 곱셈과 다르게 순서를 바꾸면 결과가 달라집니다.

그래서 아래 두 계산은 서로 다른 결과를 만들 수 있습니다.

Scale * Rotation

Rotation * Scale

예를 들어 먼저 스케일을 적용한 뒤 회전하는 경우와, 먼저 회전한 뒤 스케일을 적용하는 경우는 서로 다른 결과를 만들 수 있습니다.

특히 비균일 스케일(Non-uniform Scale)을 사용하는 경우에는 차이가 더욱 크게 나타납니다.

예를 들어 가로로 긴 직사각형을 생각해보겠습니다.

먼저 X축 방향으로 두 배 늘린 뒤 회전하면, 길어진 형태 자체가 회전합니다.

반대로 먼저 회전한 뒤 X축 방향으로 두 배 늘리면, 월드 좌표계의 X축 방향을 기준으로 늘어나는 결과가 될 수 있습니다.

즉, 같은 스케일 값과 같은 회전 값을 사용하더라도 어떤 순서로 곱하느냐에 따라 최종 모양과 방향이 달라질 수 있습니다.

그래서 게임 엔진은 변환 순서를 매우 중요하게 관리합니다.

DirectX 행 벡터(Row Vector) 기준으로 Scale과 Rotation의 곱셈 순서에 따라 결과가 달라지는 과정을 설명하는 게임 수학 이미지
행렬 곱셈은 일반 숫자 곱셈과 달리 순서를 바꾸면 결과가 달라집니다. DirectX의 행 벡터(Row Vector) 기준으로 Scale→Rotation과 Rotation→Scale의 차이를 비교한 예시입니다.

월드 행렬은 모델 공간을 월드 공간으로 변환한다

3D 모델은 보통 자신의 로컬 좌표계를 기준으로 제작됩니다.

예를 들어 자동차 모델은 원점(0, 0, 0)을 중심으로 만들어질 수 있습니다.

이 좌표는 아직 게임 세계 안의 위치가 아닙니다.

모델 파일 안에서 정의된 자기 자신의 좌표일 뿐입니다.

하지만 실제 게임 안에서는 같은 자동차 모델을 여러 위치에 배치할 수 있어야 합니다.

어떤 자동차는 도로 위에 놓일 수 있고, 어떤 자동차는 주차장에 놓일 수 있으며, 또 다른 자동차는 사고 장면에서 기울어진 상태로 배치될 수도 있습니다.

이때 월드 행렬을 적용하면 모델 공간(Model Space)에 존재하던 정점들이 월드 공간(World Space)으로 변환됩니다.

즉, 월드 행렬은 “이 모델을 게임 세계 어디에, 어떤 방향으로, 얼마나 크게 배치할 것인가”를 결정하는 역할을 담당합니다.


게임 엔진에서는 항상 월드 행렬을 계산한다

Unity의 Transform 컴포넌트 역시 내부적으로는 월드 행렬을 계산합니다.

Unreal Engine 역시 Actor와 Component의 Transform 정보를 이용해 월드 행렬을 생성합니다.

다만 일반적인 게임 개발 과정에서 개발자가 직접 월드 행렬을 계산하는 경우는 많지 않습니다.

대부분 Position, Rotation, Scale 값을 수정하면 엔진이 내부적으로 새로운 월드 행렬을 계산합니다.

캐릭터가 움직이면 위치 정보가 바뀌고, 카메라가 회전하면 회전 정보가 바뀌며, 애니메이션이 재생되면 일부 Bone의 Transform 역시 변경됩니다.

그리고 이런 변화가 발생할 때마다 엔진은 필요한 월드 행렬을 다시 계산합니다.

즉, 월드 행렬은 게임 엔진 전체에서 가장 자주 사용되는 행렬 중 하나라고 볼 수 있습니다.


월드 행렬과 GPU

월드 행렬은 CPU에서만 의미 있는 데이터가 아닙니다.

렌더링 과정에서는 계산된 월드 행렬이 GPU로 전달됩니다.

보통 셰이더에서는 상수 버퍼(Constant Buffer)나 유니폼(Uniform) 형태로 월드 행렬을 전달받습니다.

그리고 GPU는 모델을 구성하는 모든 정점(Vertex)에 동일한 월드 행렬을 적용합니다.

float4 worldPosition = mul(localPosition, World);

위 코드는 DirectX의 행 벡터(Row Vector) 기준으로 로컬 위치를 월드 행렬과 곱해 월드 공간 위치를 계산하는 예시입니다.

이 과정을 통해 모델 공간에 있던 정점은 월드 공간으로 이동합니다.

그리고 이후에는 View Matrix와 Projection Matrix를 거치면서 카메라 기준 공간과 화면 공간으로 계속 변환됩니다.

즉, 월드 행렬은 렌더링 파이프라인에서 가장 첫 번째로 적용되는 중요한 공간 변환 행렬이라고 볼 수 있습니다.


마무리

월드 행렬(World Matrix)은 이동 행렬과 회전 행렬, 스케일 행렬을 하나로 결합한 결과입니다.

게임 엔진은 이를 이용해 모델 공간의 정점을 월드 공간으로 변환합니다.

지금까지 살펴본 이동과 회전, 스케일은 각각 독립된 개념처럼 보였습니다.

하지만 실제 게임 엔진에서는 이 세 가지가 항상 하나의 월드 행렬로 결합되어 처리됩니다.

즉, 월드 행렬은 게임 엔진에서 객체 하나를 표현하는 가장 중요한 공간 정보라고 볼 수 있습니다.

그리고 렌더링 시스템은 이렇게 계산된 월드 공간 데이터를 바탕으로 카메라와 화면 변환을 계속 수행하게 됩니다.

다음 글에서는 월드 공간에 배치된 객체를 카메라 기준 공간으로 변환하는 View Matrix가 왜 필요한지, 그리고 카메라는 실제로 어떤 방식으로 공간을 바라보는지를 이어서 살펴보겠습니다.


👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다