외적: 왼쪽인가 오른쪽인가
포탑이 적을 향해 천천히 돌게 만들려면, 매 프레임 왼쪽으로 돌지 오른쪽으로 돌지를 정해야 합니다. 적이 앞에 있는지는 내적으로 알 수 있습니다. 그런데 내적은 왼쪽 45 도와 오른쪽 45 도에 똑같은 값을 줍니다. 그대로 쓰면 포탑이 반대쪽으로 한 바퀴를 돌아서 갑니다.
이때 쓰는 것이 외적입니다. 외적은 벡터, 곧 방향과 길이를 가진 화살표 두 개를 받아 두 벡터에 모두 수직인 벡터를 돌려줍니다. 수직은 바닥과 기둥처럼 직각(90°)으로 만나는 것입니다. 오른쪽과 위는 수직입니다. 바닥 위의 두 방향을 외적하면 결과는 위나 아래를 가리키고, 그 부호가 왼쪽과 오른쪽을 가릅니다.
Vector3.Cross(앞, 적 방향)의 y 가 0 보다 크면 적은 오른쪽, 작으면 왼쪽, 0 이면 정면이나 정 뒤입니다. 외적의 길이는 두 벡터가 평행할수록, 곧 한 직선 위에 나란히 놓일수록 0 에 가까워지고, 결과의 방향은 넣는 순서를 바꾸면 뒤집힙니다.
내적만으로는 왼쪽과 오른쪽을 못 가린다
내적은 x 끼리, y 끼리, z 끼리 곱해서 더한 숫자이고, 길이 1 인 두 방향이면 사이각의 cos 입니다. cos 는 각도를 넣으면 숫자를 돌려주는 함수로, 직각삼각형에서 밑변 ÷ 빗변 입니다. 0° 면 1, 90° 면 0 입니다. 그런데 cos 는 각의 크기만 보기 때문에 왼쪽으로 45 도든 오른쪽으로 45 도든 같은 값이 나옵니다.
3D 벡터는 숫자 셋 (x, y, z) 로 적고, 유니티에서는 x 가 오른쪽, y 가 위, z 가 앞입니다. 그래서 오른쪽으로 1 은 (1, 0, 0), 앞으로 1 은 (0, 0, 1) 입니다. 앞 (0, 0, 1) 을 보고 있을 때입니다.
| 적 방향 | Dot(앞, 적) |
실제 위치 |
|---|---|---|
| (0.71, 0, 0.71) | 0.71 | 오른쪽 앞 |
| (−0.71, 0, 0.71) | 0.71 | 왼쪽 앞 |
내적은 “앞에서 45 도 벗어났다” 까지만 압니다. 어느 쪽으로 벗어났는지는 다른 계산이 필요합니다.
외적은 두 벡터에 수직인 벡터다
외적은 3D 벡터 둘로 정의됩니다. 식은 이렇습니다.
a × b = ( a.y × b.z − a.z × b.y,
a.z × b.x − a.x × b.z,
a.x × b.y − a.y × b.x )
읽는 요령은 각 자리를 자기를 뺀 나머지 두 자리로 만든다는 것입니다. x 자리는 y 와 z 로, y 자리는 z 와 x 로, z 자리는 x 와 y 로 만듭니다.
직접 계산할 일은 거의 없고 Vector3.Cross(a, b) 가 해 줍니다. 중요한 것은 결과가 a 에도 수직이고 b 에도 수직이라는 성질입니다.
앞 a = (0, 0, 1) 과 오른쪽 b = (1, 0, 0) 의 외적입니다.
| 자리 | 계산 | 값 |
|---|---|---|
| x | 0 × 0 − 1 × 0 | 0 |
| y | 1 × 1 − 0 × 0 | 1 |
| z | 0 × 0 − 0 × 1 | 0 |
결과는 (0, 1, 0), 위입니다. 앞과도 수직이고 오른쪽과도 수직입니다.
축이 아닌 벡터도 마찬가지입니다. 오른쪽 앞 a = (1, 0, 1) 과 위 b = (0, 1, 0) 의 외적입니다.
| 자리 | 계산 | 값 |
|---|---|---|
| x | 0 × 0 − 1 × 1 | −1 |
| y | 1 × 0 − 1 × 0 | 0 |
| z | 1 × 1 − 0 × 0 | 1 |
결과 (−1, 0, 1) 이 정말 수직인지는 내적이 0 인지로 확인합니다. a 와는 (−1) × 1 + 0 × 0 + 1 × 1 = 0, b 와는 0 + 0 + 0 = 0 입니다. 둘 다 수직입니다.
바닥에 놓인 두 방향은 y 가 0 입니다. 식에 a.y = 0, b.y = 0 을 넣으면 x 자리는 0 × b.z − a.z × 0 = 0, z 자리는 a.x × 0 − 0 × b.x = 0 입니다. 그래서 바닥 위 두 방향의 외적은 y 만 남습니다. 바닥에 수직인 방향은 위 아니면 아래뿐이라는 말과 같습니다.
y 의 부호가 왼쪽과 오른쪽이다
앞 a = (0, 0, 1) 과 바닥 위의 적 방향 b = (b.x, 0, b.z) 를 식에 넣으면, y 자리는 1 × b.x − 0 × b.z = b.x 입니다.
적이 오른쪽(x 가 양수)에 있으면 y 가 양수, 왼쪽에 있으면 음수입니다.
포탑이 다른 쪽을 봐도 같습니다. 오른쪽 (1, 0, 0) 을 보는 포탑에게 오른손 쪽은 뒤 (0, 0, −1) 입니다. 이 둘의 외적 y 는 0 × 0 − 1 × (−1) = 1, 양수라서 오른쪽입니다. 이렇게 y 자리 하나만 떼어 쓰는 계산을 2D 외적이라고 부르기도 합니다.
적이 내 주위를 도는 동안 내적과 외적의 y 는 이렇게 바뀝니다. 위에서 내려다본 그림이라 가로가 x, 세로가 z 이고, 외적은 화면 밖으로 나오거나 들어가는 방향이라 숫자로만 적었습니다.
포탑 코드는 이렇게 됩니다.
Vector3 toEnemy = enemy.position - turret.position; // 도착 − 출발
toEnemy.y = 0f; // 바닥 위에서만 본다
float side = Vector3.Cross(turret.forward, toEnemy).y;
// 양수면 오른쪽, 음수면 왼쪽으로 돈다. Sign 은 양수면 1, 음수면 -1 을 돌려준다
// Time.deltaTime 은 지난 프레임이 걸린 초. 곱하면 1초에 turnSpeed 도씩 돈다
float turn = Mathf.Sign(side) * turnSpeed * Time.deltaTime;
turret.Rotate(0f, turn, 0f);
Rotate 의 y 에 양수를 넣으면 위에서 볼 때 시계 방향, 곧 오른쪽으로 돕니다. 외적 y 의 부호와 방향이 맞습니다.
Mathf.Sign 은 0 에도 1 을 돌려주므로, 정면을 맞춘 뒤에도 조금씩 오른쪽으로 돌며 떨 수 있습니다. 실제로는 side 가 아주 작으면 돌지 않게 하거나 남은 각도만큼만 돌게 막습니다.
부호만 쓰니까 toEnemy 를 정규화(길이를 1 로 맞추기)할 필요는 없습니다. 외적의 길이는 두 벡터의 길이를 곱한 만큼 커지지만, 부호는 바뀌지 않습니다.
순서를 바꾸면 방향이 뒤집힌다
외적은 넣는 순서가 결과를 바꿉니다. a × b 와 b × a 는 길이는 같고 방향이 정반대입니다. 식에서 각 자리가 “앞 × 뒤 − 뒤 × 앞” 꼴이라, a 와 b 를 맞바꾸면 빼는 순서가 바뀌어 부호만 뒤집힙니다. 보통 곱셈은 2 × 3 = 3 × 2 이지만 외적은 그렇지 않습니다.
다른 축끼리도 같은 규칙입니다.
| 식 | 결과 | 순서를 바꾸면 |
|---|---|---|
Cross(위, 앞) |
(1, 0, 0) 오른쪽 | (−1, 0, 0) 왼쪽 |
Cross(오른쪽, 위) |
(0, 0, 1) 앞 | (0, 0, −1) 뒤 |
어느 쪽이 나오는지 손으로 확인하는 방법이 왼손 법칙입니다. 유니티가 왼손 좌표계를 쓰기 때문입니다. 좌표계는 숫자 셋을 어느 쪽으로 읽을지 정한 약속입니다. 왼손 엄지를 오른쪽(x), 검지를 위(y)로 세우면 중지가 앞(z), 곧 화면 안쪽을 가리키는데, 유니티 축이 이 배치입니다. 오른손으로 같은 자세를 하면 중지가 나를 향하고, 그쪽을 z 로 삼는 것이 오른손 좌표계입니다. 왼손을 펴서 손가락들이 a 를 가리키게 하고, 손가락을 b 쪽으로 감아쥐면 엄지가 가리키는 쪽이 a × b 입니다.
식 자체는 어느 엔진이나 수학책이나 같습니다. Cross((1, 0, 0), (0, 1, 0)) 은 어디서나 (0, 0, 1) 입니다.
달라지는 것은 그 (0, 0, 1) 이 화면 안쪽인지(왼손 좌표계) 나를 향하는지(오른손 좌표계)입니다.
그래서 오른손 좌표계로 그린 자료에서 “외적이 화면 밖으로 나온다” 고 설명한 그림은, 유니티에서는 반대 방향으로 읽어야 맞습니다.
길이는 평행할수록 0 이다
외적의 길이는 a 의 길이 × b 의 길이 × sin(사이각) 입니다.
sin 은 cos 의 짝으로, 직각삼각형에서 높이 ÷ 빗변 입니다. 반지름 1 인 원에서 각 θ 만큼 돈 점의 세로 위치와 같습니다. 그래서 0 도와 180 도(눕혀서 높이가 없음)에서 0, 90 도(똑바로 섬)에서 1 입니다.
위 그림에서 적이 정면이나 정 뒤일 때 외적이 0, 바로 옆일 때 가장 컸던 이유입니다.
| 두 벡터 | 사이각 | 외적의 길이 |
|---|---|---|
| 수직 | 90° | 길이 × 길이 (가장 큼) |
| 비스듬히 | 45° | 길이 × 길이 × 0.71 |
| 평행 | 0° 또는 180° | 0 |
이 길이는 두 벡터를 두 변으로 하는 평행사변형(마주 보는 변끼리 평행한 사각형. 직사각형도 그중 하나)의 넓이와 같습니다. (2, 0, 0) 과 (0, 0, 3) 을 외적하면 (0, −6, 0), 길이 6 이고, 가로 2 세로 3 인 직사각형의 넓이도 6 입니다. 삼각형은 그 절반이라, 메시 삼각형의 넓이를 구할 때 이 길이 ÷ 2 를 씁니다.
외적은 두 방향이 평행한지 확인하는 데도 씁니다. 결과가 영벡터, 곧 길이가 0 이라 방향이 없는 (0, 0, 0) 에 가까우면 두 방향은 같은 직선 위에 있습니다. 평행하면 둘에 수직인 방향이 하나로 정해지지 않아서 외적이 0 이 되는 것입니다. 카메라가 바로 아래를 내려다보면 카메라의 앞과 월드의 위가 평행해져, 외적으로 오른쪽 축을 만드는 코드가 영벡터를 받게 됩니다. 이런 자리는 평행인지 먼저 확인합니다.
면의 방향을 구할 때
외적의 원래 쓰임은 면의 법선을 구하는 것입니다. 법선은 면에 수직으로 선 방향으로, 바닥이면 위 (0, 1, 0) 입니다. 면이 어느 쪽을 보는지 알아야 빛을 받는 밝기와 앞뒷면을 정할 수 있습니다. 삼각형 세 꼭짓점 A, B, C 가 있으면 두 변을 벡터로 만들어 외적합니다.
예를 들어 바닥에 누운 삼각형 A (0, 0, 0), B (0, 0, 1), C (1, 0, 0) 이 있습니다. 위에서 보면 A 에서 앞으로 B, 오른쪽으로 C 입니다.
| 단계 | 계산 | 결과 |
|---|---|---|
| 변 1 | B − A | (0, 0, 1) |
| 변 2 | C − A | (1, 0, 0) |
| 법선 | Cross(B − A, C − A) |
(0, 1, 0) 위 |
꼭짓점을 적는 순서를 바꾸면 법선이 뒤집힙니다.
꼭짓점을 적는 순서를 와인딩 순서라고 부릅니다. 유니티는 카메라에서 볼 때 시계 방향으로 이어지는 면을 앞면으로 보고, 기본으로 뒷면은 그리지 않습니다.
메시를 코드로 만들 때 꼭짓점 순서를 반대로 넣으면 면이 통째로 안 보이는 것이 이 때문입니다.
Mesh.RecalculateNormals() 도 삼각형마다 이 외적으로 면의 방향을 구하고, 꼭짓점을 같이 쓰는 면들의 방향을 모아 꼭짓점의 법선을 만듭니다.
정리
- 내적은 왼쪽 45 도와 오른쪽 45 도에 같은 값을 줘서 좌우를 못 가립니다
- 외적
Vector3.Cross(a, b)는 a 와 b 에 모두 수직인 벡터입니다. 수직인지는 내적이 0 인지로 확인합니다 - 바닥 위 두 방향의 외적은 y 만 남고,
Cross(앞, 적).y가 양수면 오른쪽, 음수면 왼쪽입니다 - 내적의 부호(앞뒤)와 외적 y 의 부호(좌우)를 같이 보면 네 방향이 다 갈립니다
- 순서를 바꾸면 방향이 뒤집힙니다. 유니티는 왼손 좌표계라 왼손 법칙으로 확인합니다
- 외적의 길이는 sin(사이각) 에 비례해서, 평행하면 0 입니다. 길이는 평행사변형의 넓이이고, 평행 확인에도 씁니다
- 삼각형 두 변의 외적이 면의 법선이고, 꼭짓점 순서(와인딩)가 앞면을 정합니다. 유니티는 시계 방향이 앞면입니다