→ 이 글은 「이득우의 게임 수학」을 바탕으로 작성했습니다.
■ 투영 벡터
벡터의 투영은 벡터 $\vec v$를 다른 벡터 $\vec w$의 방향으로 내려놓는 것을 의미한다.
따라서 투영 벡터의 방향은 $\vec w$와 같고, 그 크기는 $\vec v$와 $\vec w$의 내적(각도와 두 벡터의 길이에 따라 결정)에 의해 달라진다.
투영할 벡터를 $\vec v$, 투영의 기준이 되는 벡터를 $\vec w$라 해보자.
벡터 $\vec v$를 $\vec w$의 방향으로 수직으로 내리면, $\vec w$가 만드는 직선 위에 수선의 발이 생긴다. 이 점을 원점과 연결한 벡터가 바로 투영 벡터$(\vec t)$이다.
투영한 벡터의 크기$(|\vec t|)$를 알고 있다면, 우리는 기준 벡터 $(|\vec w|)$를 정규화시킨 벡터$(\hat w = \frac{\vec w}{|\vec w|})$를 곱해 투영된 벡터를 얻어낼 수 있다.
$$ \vec t = |\vec t| \cdot \hat w $$
위 수식에서 내적을 활용해 $\vec v$로부터 $\vec t$를 구하도록 수식을 전개해보자.
1. 단위 벡터 구하기
$$ |\vec t| \cdot \frac{\vec w}{|\vec w|} $$
$\vec w$의 단위 벡터를 구하기 위해 벡터를 크기로 나누어 정규화한다.
2. $\vec t$의 크기 구하기
$$ |\vec v|\cdot \cos\theta \cdot \frac{\vec w}{|\vec w|} $$
$\vec v$에서 수선의 발을 내려 직각삼각형을 만들면, 원점에서 수선의 발까지의 거리는 $|\vec v|\cos\theta$로 표현할 수 있다.
3. 내적 공식 활용
$$ \vec v \cdot \vec w = |\vec v||\vec w|\cos\theta $$
앞서 우리가 배웠던 내적 공식에서 $\cos\theta$를 구하기 위해 양 변을 $|\vec v||\vec w|$로 나누면,
$$ \cos\theta = \frac{\vec v\cdot \vec w}{|\vec v||\vec w|} $$
위와 같이 정리가 된다. 이 식을 2의 수식에 대입하면 아래와 같이 정리가 가능하다.
$$ |\vec v| \cdot \frac{\vec v\cdot \vec w}{|\vec v||\vec w|} \cdot \frac{\vec w}{|\vec w|} $$
4. 최종 정리
$$ \vec t = \frac{\vec v\cdot \vec w}{(\vec w\cdot \vec w)}\cdot \vec w $$
분자와 분모를 소거하고 정리한 뒤, 벡터 크기의 제곱을 내적으로 표현하면 위와 같이 정리된다. 여기서 투영할 기준이 되는 벡터$\vec w$의 크기가 1이라면 아래와 같이 단순하게 정리된다.
$$ \vec t = (\vec v\cdot \vec w)\cdot \vec w $$
■ 투영 벡터의 응용
$$ \vec u = \vec v - \vec t $$
우리가 구한 투영 벡터$(\vec t)$에서 투영할 벡터$(\vec v)$의 성분을 제외하면, 기준이 되는 벡터$(\vec w)$와 수직인 벡터가 생성된다.
- $\vec w$와 $\vec t$의 내적 결과 역시 0이 되므로 수직이 되는 것을 알 수 있다.
일반적인 벡터 $\vec v - \vec t$의 차는 단순히 “$\vec v$에서 $\vec t$로 가는 벡터”지만, $\vec t$가 $\vec v$를 어떤 방향으로 투영한 결과라면 얘기가 달라진다.
즉,
$$ \vec t = \mathrm{proj}_{\vec w}(\vec v) $$
이라면, $\vec v - \vec t$는 $\vec v$에서 $\vec w$방향 성분$\vec t$를 제외한 나머지, 곧 $\vec w$에 수직인 성분이 된다.
투영 벡터를 사용하면 “경사로에서의 플레이어의 이동 방향”을 계산할 수 있다.
- 투영할 벡터 : 플레이어의 정면
- 기준 벡터 : 평면의 노말
- 이동 방향 : 플레이어의 정면 벡터 - 투영된 벡터
public class ProjectionPlayer : MonoBehaviour
{
[SerializeField] private float speed = 20f;
[SerializeField] private Transform rayTransform;
[SerializeField] private LayerMask groundLayer;
private void Update()
{
var ray = new Ray(rayTransform.position, Vector3.down);
if (Physics.Raycast(ray, out var hit, 100f, groundLayer) == false)
{
return;
}
// 투영할 벡터(플레이어의 정면)
var forward = transform.forward.normalized;
// 기준 벡터(평면의 normal)
var normal = hit.normal;
// 투영된 벡터
var projection = Vector3.Dot(forward, normal) * normal;
// 평면의 normal과 수직인 벡터
var dir = forward - projection;
var input = Input.GetAxis("Horizontal");
transform.Translate(dir * speed * input * Time.deltaTime, Space.World);
// debug
Debug.DrawRay(rayTransform.position, projection, Color.red);
Debug.DrawRay(rayTransform.position, dir, Color.green);
}
}
앞서 배웠던 $\vec p = (\vec f\cdot \vec n)\vec n$ 공식을 사용하여 투영 벡터를 구한다.
이후, 원래의 벡터($\vec f :$ 플레이어 정면)에서 투영된 벡터의 성분을 빼면, 평면의 노말$\vec n$과 직교하는 벡터 $(\vec d = \vec f - \vec p)$를 얻을 수 있다.
// 투영할 벡터(플레이어의 정면)
var forward = transform.forward.normalized;
// 기준 벡터(평면의 normal)
var normal = hit.normal;
// 함수 사용
var dir = Vector3.ProjectOnPlane(forward, normal);
Unity에서는 Vector3.ProjectOnPlane(Vector3 vector, Vector3 planeNormal) 함수를 제공한다.
이 함수는 지정한 벡터에서 특정 평면의 노말 방향 성분을 제거하고, 평면 위에 남는 성분을 반환한다.
따라서 transform.forward를 평면 위로 투영하면, 플레이어가 경사면 위에서 실제로 이동할 수 있는 방향을 손쉽게 구할 수 있다.
'게임수학' 카테고리의 다른 글
[게임수학] 6-1. 내적(Dot product) (0) | 2025.09.12 |
---|---|
[게임수학] 5.아핀공간 (1) | 2025.09.08 |
[게임수학] 4. 행렬 (1) | 2025.09.05 |
[게임수학] 3. 삼각함수 (2) | 2025.08.31 |
[게임수학] 2. 벡터(Vector) (2) | 2025.08.27 |