상세 컨텐츠

본문 제목

내적(Dot Product)과 외적(Cross Product)

유니티 학습/C#과 유니티

by 남민우_ 2026. 4. 11. 17:49

본문

내적과 외적의 필요

우리가 게임 개발을 하다 보면 몬스터의 행동 패턴을 설계하는 경우가 있다. 몬스터가 플레이어를 추적하면서 자동으로 쫓아오게 만들어야 한다고 할 때, 어느 방향으로 경로를 설정하여 회전할지 계산해야 할 것이다.

혹은 몬스터와의 전투 과정에서 헤드 어택, 혹은 백어택 등을 성공하면 추가 데미지를 입히는 시스템이 있다고 생각해보자. 이 경우 정확한 판정을 위해서 플레이어의 방향과 몬스터의 방향을 비교하며 계산해야 할 것이다.

 

이러한 상황에서 내적과 외적을 알맞게 사용하여 정확한 판별 로직을 설계할 수 있다는 점에서 내적과 외적이 정확히 어떤 의미인지, 어떤 식으로 계산되는지 혹은 어떻게 활용할 수 있을지 알고 있으면 큰 도움이 될 것이다.

 

정확한 계산 방식은 깊게 다루기에 내용이 깊어져 링크를 첨부한다.

https://en.wikipedia.org/wiki/Dot_product

 

https://en.wikipedia.org/wiki/Cross_product

 

하여 본문에서는 이러한 내적 계산이 게임 개발에서는 어떤 활용으로 이루어지는지에 대해 주로 서술하고자 한다.

 

내적(Dot Product)

.(점)으로 찍어서 계산한다는 뜻에서 Dot 을 상징하는 내적으로, 계산 결과로 하나의 숫자(Scalar)가 도출되는 계산이다.

Unity에서는

float dot = Vector3.Dot(A, B);

다음과 같은 방식으로 사용한다.

내적 결과 = (A의 길이) * (B의 길이) * (두 사이의 각도 : 코사인값) 으로 도출되며, 이 때 A와 B의 길이를 정규화(Normalize)를 통해 1로 만들면 내적 결과는 두 사이의 각도 코사인만 남게 된다.

 

여기서 정규화가 무엇인지 한번 짚고 넘어가보자.

기본적으로 벡터는 힘(길이)와 방향을 둘 다 내포하고 있다.

다만 우리가 방향을 계산하고자 하는 경우 방향과 방향끼리만 계산하면 되는데 여기서 힘에 대한 정보는 필요 없을 것이다.

여기에서 정규화가 사용되는데, 화살표의 길이, 즉 힘의 값을 무조건 1로 만들어 힘에 대한 정보를 빼고 순수한 방향만 남기는 것이다.

 

다시 내적에 대한 이야기로 돌아가서, 내적 결과에 따라 우리는 '두 벡터 사이의 각도나 앞/뒤 방향에 대한 관계'를 판별할 수 있다.

내적 결과가 양수로 도출되면 앞쪽(90도 미만), 음수로 도출되면 뒤쪽(90도 초과), 혹은 결과가 0이라면 완벽한 옆면인 수직(90도)으로 볼 수 있다.

이러한 결과에 따라 로직에서는

if(dot >= 0) { ... }

과 같은 조건문과 부등호를 통해 어떤 식으로 처리할 것인지를 결정할 수 있다.

 

그렇다면 이 '앞/뒤 방향에 대한 관계'가 정확히 무엇일까?

2D로 게임을 만들다 보면 오브젝트가 그려지는 순서인 Layer 에 대해서 알텐데, 이것과는 다른 개념이다.

Layer 는 그리기 순서에 대한 정의로 '누가 위에 덧칠해지냐'를 결정한다면, 내적 결과로 나오는 벡터의 앞/뒤(Relationship)은 내 눈 앞에 있나/등 뒤에 있나 에 관한 '공간적 관계'를 말한다.

 

실무에서 어떻게 활용하는지 그 예시를 살펴보자.

내적값을 이용하여 NPC의 시야 및 기습 판정을 구현한다고 가정한다.

   내적값에 따른 판정 기준(정규화된 벡터 기준)
   1. 1.0 : 나와 완전히 같은 방향(정면)
   2. 0.5 : 나랑 60도 정도 차이남(비스듬한 옆)
   3. 0.0 : 나랑 정확히 90도(완벽한 옆) > 수직
   4. -1.0 : 나와 완전히 반대(정후면)

NPC가 플레이어를 보고 있는가?에 대한 로직

// 1. NPC의 정면 방향 (Forward)
Vector3 npcForward = transform.forward; 

// 2. NPC에서 플레이어로 향하는 방향 (Direction)
Vector3 dirToPlayer = (player.position - npc.position).normalized;

// 3. 내적 계산
float dot = Vector3.Dot(npcForward, dirToPlayer);

// 4. 결과 판정
if (dot > 0.7f) {
    // 약 45도 이내의 좁은 시야에 들어옴!
    Debug.Log("플레이어 발견!");
} else if (dot < -0.5f) {
    // 플레이어가 NPC의 뒤쪽에 있음
    Debug.Log("플레이어가 NPC를 기습할 수 있는 위치임!");

처음 4가지 조건을 통해 내적값에 따라 어떻게 판단할 수 있는지에 대한 기준을 정할 수 있다.

그 기준에 따라 NPC가 플레이어를 보고 있는지 판단하는 로직을 설계하는 과정이다.

 

여기서 dot의 기준을 0.7로 잡은 이유는 코사인의 특징에 근거하는데, 내적 결과값은 각도와 직선으로 비례하지 않는다는 점이다.

Normalize 기준 내적 결과값과 각도의 관계는 다음과 같다.

1. 1.0 : 0도 (완전 정면)
2. 0.707... = 45도(정면과 옆의 중간)
3. 0.5 = 60도
4. 0 = 90도 (완전 옆)

 

또한 90도(dot == 0) 의 의미는 앞서 말햇듯 완벽한 옆면의 뜻으로, 실무에서는 다음과 같은 경우에서 활용할 수 있다.

1. 캐릭터가 벽을 등지고 있는지, 혹은 벽과 평평하게 걷고 있는지 체크

2. 조명 연산에서 빛이 면을 스쳐지나가는 상태인지 > 면은 어둡게 표시

 

또한 이 내적 계산은 Vector3 뿐만 아니라 Vector2에도 존재한다.

판단 방식은 3D와 동일하지만, 차이점으로는 3D 상에서는 상하좌우를 포함한 원뿔형 시야를 체크한다면 2D 상에서는 평면 상의 부채꼴 시야를 체크한다는 점이 있다.

 

처음에 들었던 예시인 백어택/헤드어택 판정을 예시로 들면서 내적 계산을 활용한다면 다음과 같이 사용할 수 있을 것이다.

1. 캐릭터의 공격 방향 벡터
2. 몬스터의 방향 벡터
3. 두 결과를 내적 -> 1 도출 = 같은 방향을 보고 있다
4. 같은 방향을 보고 있는데 적을 때렸다 = 등 뒤를 때렸다 => 백어택 판정
5. 내적 결과가 -1 = 마주보고 있다 => 헤드어택 판정

Vector3 playerAttackVec;
Vector3 monsterVec;
float dot = Vector3.Dot(playerAttackVec, monsterVec);

if(dot == 1)
{
	Debug.Log("백어택!");
}
else if (dot == -1)
{
	Debug.Log("헤드어택!");
}
else
{
	... // 그 외 일반적 공격 처리 혹은 추가 조건문을 통한 좌/우 등 옆면에 대한 공격 처리
}

 

외적(Cross Product)

내적이 .(점) 모양으로 계산하여 Dot 이었다면, 외적은 x 모양으로 계산한다고 하여 Cross 라 한다.

계산 결과는 새로운 방향을 가진 화살표(Vector)가 도출된다.

 

여기서 x 모양으로 계산한다 라는 것은 A와 B 벡터를 서로 엇갈려서 곱하기 때문에 그런데, 계산 공식을 보면 쉽게 이해할 수 있다.

Vector3.Cross(A, B);

A(x1, y1, z1) / B(x2, y2, z2)

Result.y = (z1 * x2) - (x1 * z2)

즉 구하려는 축(y)를 제외한 나머지 축(x, z)를 서로 엇갈려 곱하고 그 결과를 빼는 방식으로 진행되며, 이 결과가 양수냐 혹은 음수냐를 통해 y의 방향이 결정된다.

 

이 외적 계산을 통해서는 A와 B가 만드는 '바닥의 천장 방향'을 알 수 있다.

두 가지 예시를 들어서 설명해보자면, 캐릭터가 경사면 위에 있고, 그 경사면이 어디를 보는지(Normal Vector)를 알고 싶을 때, 외적 결과로 경사면의 수직 방향을 알 수 있다.

또 문을 열거나 바퀴를 돌릴 때, '어떤 축을 중심으로 돌릴 것인가'에 외적 결과값이 중심축으로 활용될 수 있을 것이다.

 

우리가 벡터를 그릴 때 주로 2차원 평면에서 그리면서 계산하기에 Vector2.Cross() 도 있느냐? 라고 생각할 수 있는데, 아쉽게도 외적 계산은 2차원에서는 진행되지 않는다. 

'외적 계산'은 3처원 공간에서 두 화살표에 수직인 '입체적인 기둥'을 세우는 연산이다. Vector2.Cross() 는 없다는 것이다.

하지만 우리는 2D 상에서도 좌/우 판별이 필요한 경우가 있다. 이때는 Vector3로 변환하여 연산하거나, 외적 계산 공식 중 Z값만 추출하여 사용하는 약식 방법 등으로 활용할 수 있다.

 

내적과 외적의 혼합 사용

내적과 외적을 하나씩 보면, 각자 하나의 결과만을 도출하곤 한다. 이 결과값을 통해 분기 처리하여 로직을 수행할 순 있겠으나 더 화려한 활용을 위해서는 내적과 외적을 혼합하여 사용할 수 있어야 한다.

 

예를 들어 적 몬스터의 행동 패턴을 설계한다고 해보자.

내적으로는 '적(몬스터 기준)이 내 앞 45도에 있다' 까지 판별 가능하다. 여기서 그 다음 행동을 처리할 때, 왼쪽으로 꺾어서 움직여야 하나, 오른쪽으로 꺾어서 움직여야 하나는 외적으로 계산해야 한다.

 

그 예시 코드는 다음과 같이 작성할 수 있다.

Vector3 myForward, enemyForward;
Vector3 cross = Vector3.Cross(myForward, enemyForward);

해당 결과로 나온 cross 화살표가 하늘을 향한다면(y > 0) 오른쪽으로 꺾고, 땅을 향한다면(y < 0) 왼쪽으로 꺾는다 등의 판단이 가능하다.

더 자세한 방식은 다음과 같다.

Vector3 forward = transform.forward;
Vector3 dirToTarget = (target.position - transform.position).normalized;

// 외적 실행
Vector3 cross = Vector3.Cross(forward, dirToTarget);

if (cross.y > 0) {
    // 적이 내 오른쪽에 있음 -> 오른쪽으로 회전!
} else if (cross.y < 0) {
    // 적이 내 왼쪽에 있음 -> 왼쪽으로 회전!
} else {
    // 정확히 앞이나 뒤에 있음
}

 

외적 계산은 오른손 법칙을 따른다.

https://ko.wikipedia.org/wiki/%EC%98%A4%EB%A5%B8%EC%86%90_%EB%B2%95%EC%B9%99

또한 내적 계산은 앞/뒤를 알려주는데, 외적 계산은 왼쪽/오른쪽을 알려준다.

이를 오른손 법칙 개념과 혼합하여 활용하면 다음과 같은 계산이 가능하다.

1. 오른손 엄지 : A (내 정면)
2. 오른손 검지 : B (적 방향)
3. 오른손 중지 : 외적 결과 방향
	ex) 중지가 하늘을 향하면 = 적은 내 오른쪽
	ex) 중지가 땅을 향하면 = 적은 내 왼쪽
	=> Vector3.Cross(내 정면, 적방향).y => 양수면 오른쪽, 음수면 왼쪽

 

정리하자면 외적은 '길잡이 화살표'를 만드는 과정으로, 캐릭터의 회전 방향 등의 대한 결정이 가능해진다.

 

Unreal의 FindLookAtRotation 이나, Unity의 Quaternion.LookRotation 또한 이러한 내적/외적 계산이 활용된 메서드라 할 수 있다.

이 두 메서드를 사용하게 되면 내부적으로 몇가지 로직이 실행되는데, 그 과정은 다음과 같다.

1. 방향 백터 구하기
	> 타겟 위치 - 내 위치 => 바라봐야 하는 목표 화살표 도출
2. 옆/위 방향 찾기(외적)
	> 목표 화살표 x Vector3.up(하늘 방향) 을 외적 => 나의 오른쪽 화살표 도출
	> 목표 화살표 x 오른쪽 화살표 외적 => 나의 위 화살표 도출
	=> 3방향 세트 구성
3. 이 3방향 화살표 세트로 Rotation값으로 치환

이 과정을 통해 나온 값으로 SetRotation 을 해주면 우리가 아는 특정 액터를 바라보는 동작이 가능해진다.

여기서 한걸음 더 나아가서, 특정 액터를 바라보도록 할 때 부드럽게 회전하려면 어떻게 해야 할까?

외적은 어디로 돌지 판정이 가능하니, 내적을 회전의 완성도에 활용할 수 있다.

현재 바라보고 있는 정확도를 숫자로 계산하여, 목표치에 도달할 수록 회전 속도를 감속한다던가 보간 처리한다던가 하는 식으로 말이다.

 

관련글 더보기