언리얼 벡터로 도로 이탈 거리 구하기, Cross-Track Error 계산 과정
스플라인 경로를 따라가는 차량 조향을 구현하다 보면 "차가 도로 중심선에서 얼마나, 어느 쪽으로 벗어났는가"를 알아야 하는 순간이 온다. 거리만으로는 부족하다. 어느 쪽으로 벗어났는지 부호까지 있어야 핸들을 어느 방향으로 꺾을지 결정할 수 있다.
이 글은 그 값을 외적과 내적 두 번으로 구하는 과정을 담는다. 자율주행뿐 아니라 점이 경로의 왼쪽인지 오른쪽인지 판별해야 하는 모든 상황(NPC가 경로를 이탈했는지 감지, 카메라 트랙에서 피사체 위치 추적 등)에 같은 방식이 적용된다.
필요한 값
다음 두 가지는 USplineComponent에서 바로 얻을 수 있다고 가정한다.
- RoadPos: 도로 위에서 차에 가장 가까운 점의 월드 좌표
- RoadDir: 그 지점에서 도로의 진행 방향 단위벡터
const FVector RoadPos = Spline->GetLocationAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::World);
const FVector RoadDir = Spline->GetDirectionAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::World);
const FVector VehicleLocation = OwnerPawn->GetActorLocation();
1단계. 도로 위 점에서 차까지의 벡터
차에서 도로까지의 화살표를 일단 그린다.
const FVector Offset = VehicleLocation - RoadPos;
이 Offset 벡터에는 두 가지가 섞여 있다. 도로 진행 방향으로의 앞뒤 차이, 그리고 도로에 수직인 좌우 차이. 필요한 건 좌우 차이 쪽뿐이다.
2단계. 앞뒤 성분을 빼서 좌우 성분만 남기기
Offset에서 "도로 진행 방향 성분"을 빼면 도로에 수직인 성분만 남는다. 벡터를 어떤 방향으로 투영한 길이는 내적으로 구할 수 있다.
const FVector PerpendicularOffset = Offset - RoadDir * FVector::DotProduct(Offset, RoadDir);
풀어서 보면 이렇다.
FVector::DotProduct(Offset, RoadDir): Offset을 RoadDir 방향에 투영한 길이(스칼라). RoadDir이 단위벡터라 별도 정규화 없이 그대로 쓸 수 있다.RoadDir * (그 길이): 그 길이를 다시 RoadDir 방향의 벡터로 복원한 것. 즉 "Offset의 도로 방향 성분"이다.Offset - 그 벡터: 앞뒤 성분을 빼고 좌우 성분만 남긴 벡터.
이 PerpendicularOffset 벡터의 길이가 이탈 거리다. 다만 아직 부호가 없다. 길이 정보만으로는 왼쪽으로 벗어났는지 오른쪽으로 벗어났는지 모른다.
3단계. 도로의 오른쪽 방향 벡터 만들기
부호를 부여하려면 기준이 되는 "오른쪽 방향"이 필요하다. 도로 진행 방향과 위 방향(월드 Up)을 외적하면 오른쪽 방향이 나온다.
const FVector RoadRight = FVector::CrossProduct(FVector::UpVector, RoadDir);
여기서 두 가지 짚어둘 점이 있다.
도로에 수직인 방향은 두 개다(왼쪽, 오른쪽). 그냥 "수직"으로는 어느 쪽인지 정해지지 않는다. 외적은 위 방향까지 함께 고려하면서 한 방향을 정확히 골라준다.
외적은 순서를 바꾸면 방향이 반대가 된다. 언리얼은 왼손 좌표계에서 Up × Forward = Right 규칙을 쓰므로 CrossProduct(UpVector, RoadDir) 순서로 두면 오른쪽이 나온다. 순서를 반대로 두면 왼쪽이 나오니 부호가 뒤집힌다.
4단계. 부호 있는 숫자로 변환
PerpendicularOffset(좌우 차이 벡터)을 RoadRight(오른쪽 방향 단위벡터)에 내적하면 부호 있는 스칼라가 나온다.
const float CrossTrackError = FVector::DotProduct(PerpendicularOffset, RoadRight);
- 양수: 차가 도로 오른쪽으로 벗어남
- 음수: 차가 도로 왼쪽으로 벗어남
- 0에 가까움: 차가 도로 중심선 위에 있음
내적 한 번으로 길이와 방향이 동시에 담긴 숫자가 나온다는 점이 핵심이다. 절댓값이 이탈 거리(cm), 부호가 이탈 방향이다.
전체 코드
const FVector RoadPos = Spline->GetLocationAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::World);
const FVector RoadDir = Spline->GetDirectionAtDistanceAlongSpline(CurrentDistance, ESplineCoordinateSpace::World);
const FVector VehicleLocation = OwnerPawn->GetActorLocation();
// 1. 도로 위 점에서 차까지의 벡터
const FVector Offset = VehicleLocation - RoadPos;
// 2. 앞뒤 성분을 빼서 좌우 성분만 남김
const FVector PerpendicularOffset = Offset - RoadDir * FVector::DotProduct(Offset, RoadDir);
// 3. 도로의 오른쪽 방향 벡터
const FVector RoadRight = FVector::CrossProduct(FVector::UpVector, RoadDir);
// 4. 부호 있는 이탈 거리
const float CrossTrackError = FVector::DotProduct(PerpendicularOffset, RoadRight);
핸들 보정에 적용할 때 부호 처리
조향 식에서 이 값을 빼는 형태로 적용하면 부호 처리가 자동으로 맞는다.
Steering = clamp(YawCmd / MaxYawDelta - CrossTrackError * CrossTrackGain, -1, 1);
- 오른쪽 이탈(CrossTrackError > 0)이면 Steering에서 양수가 빠져 작아짐 → 왼쪽으로 꺾임
- 왼쪽 이탈(CrossTrackError < 0)이면 Steering에서 음수가 빠져(즉 더해져) 커짐 → 오른쪽으로 꺾임
별도 if 분기 없이 한 줄로 처리된다.
정리
점이 경로에서 얼마나, 어느 쪽으로 벗어났는지 구할 때 한 줄 한 줄이 명확한 의미를 가진다.
- 내적 한 번으로 한 방향 성분의 길이를 분리
- 외적 한 번으로 기준이 되는 수직 방향을 생성
- 내적 한 번으로 부호 있는 스칼라로 변환
이 패턴은 도로 이탈 거리 외에도 "어떤 점이 직선의 왼쪽인지 오른쪽인지", "NPC가 순찰 경로에서 얼마나 벗어났는지" 등 여러 상황에 그대로 응용된다. 좌표계가 다른 엔진(언리얼, 유니티, 일반 3D 수학)에서는 외적 순서나 Up 방향 정의가 달라지므로, 부호가 뒤집혀 나오면 외적 순서를 반대로 두면 된다.
'etc' 카테고리의 다른 글
| 언리얼 자율주행 차량, 곡률 반경으로 안전 속도 계산하기 (V = √(μgR)) (0) | 2026.05.13 |
|---|---|
| 자율주행 차량 조향 가중치를 곡률에 따라 동적 조정하기 (Lerp 활용) (0) | 2026.05.13 |
| 언리얼 스플라인 경로 추종 차량 조향, Pure Pursuit + Heading + Cross-Track 블렌딩으로 정리한 과정 (0) | 2026.05.13 |
| 언리얼 USplineComponent 따라가기, 위치를 누적 거리(float) 하나로 추적하기 (0) | 2026.05.13 |
| 언리얼 스플라인 경로 추종 차량 구조 설계 (Actor, Pawn, Component 분리) (0) | 2026.05.13 |
