프로젝트/자동차 자율주행 시뮬레이션

Spline 기반 3요소 조향 알고리즘

loooong 2026. 8. 29. 01:21

자동차가 Spline 위의 한 점만 바라보게 하면 경로를 따라 움직일 수는 있다. 하지만 속도가 높아지거나 커브가 급해지면 차량이 도로 중심을 크게 벗어나거나 조향이 늦게 시작되는 문제가 나타난다.

이 문제를 줄이기 위해 전방 목표 위치, Spline의 진행 방향, 도로 중심선 이탈 거리를 함께 사용하는 조향 로직을 구현했다.

 

이 글에서는 USplineFollowerComponent에서 사용한 Position Delta, Heading Delta, Crosstrack Error의 계산 과정과 각 요소를 결합한 이유를 정리한다.

 

Position, Heading, Crosstrack과 속도·곡률 적응형 Look Ahead를 모두 적용한 정상 주행 모습

 

위 GIF는 이 글에서 설명할 세 가지 조향 요소를 모두 적용한 최종 상태다. 차량은 전방 목표점을 따라가면서 Spline의 진행 방향을 미리 반영하고, 중심선에서 이탈하면 Crosstrack 보정으로 복귀한다.

 


GIF의 디버그 표시 읽는 법

  • 파란 구는 차량의 현재 진행 거리에 해당하는 Spline 기준 위치다.
  • 빨간 구는 조향 계산에 실제로 사용하는 Look Ahead 목표점이다.
  • 노란 선은 차량에서 빨간 목표점으로 향하는 방향이다. Position Delta의 기준이 된다.
  • 초록 화살표는 빨간 목표점에서 Spline이 향하는 접선 방향이다. Heading Delta의 기준이 된다.

 


조향 계산의 전체 흐름

조향값은 다음 순서로 계산한다.

차량 위치와 속도 조회
      ↓
속도와 곡률로 Look Ahead 거리 계산   
      ↓
Spline의 전방 위치와 접선 방향 조회
      ↓
Position Delta 계산
Heading Delta 계산
Crosstrack Error 계산
      ↓
Position과 Heading 가중 결합
      ↓
Crosstrack 보정 추가
      ↓
-1부터 1 사이로 Clamp
      ↓
Chaos Vehicle Steering Input

Position Delta와 Heading Delta는 각도 오차다. 두 값을 가중 결합해 기본 조향 방향을 구한다.

Crosstrack Error는 각도가 아니라 도로 중심선에서 벗어난 거리다. 따라서 두 각도와 동일한 가중평균에 넣지 않고 별도의 중심선 복귀 보정으로 사용했다.

 


고정 Look Ahead의 한계

Look Ahead는 차량이 현재 위치보다 얼마나 앞의 경로(빨간 구)를 바라볼 것인지 결정하는 거리다. 

고정된 Look Ahead를 사용하면 모든 상황에 같은 거리를 적용하게 된다.

(좌) Fixed Long Look Ahead (우) Fixed Short Look Ahead

좌측은 Look Ahead를 길게 고정한 결과다. 빨간 구가 멀리 있어 직선에서는 조향이 완만하지만 급커브에서는 차량이 현재 도로의 굴곡보다 먼 목표점을 먼저 바라본다. 그 결과 노란 선이 커브 안쪽을 가로지르고 차량도 중심선을 크게 잘라 주행할 수 있다.

 

우측은 Look Ahead를 매우 짧게 설정한 극단적인 사례다. 빨간 구가 차량 가까이에 붙기 때문에 작은 위치 변화도 목표 방향을 크게 바꾸고 조향이 민감해진다. 

 

GIF 속 커브길에 적절한 Look Ahead 값을 찾았다 해도, 차량의 속도과 커브의 정도에 따라 주행 결과가 또 어긋난다.

  • 저속에서 너무 멀리 보면 가까운 경로 변화에 둔감해진다.
  • 고속에서 너무 가까이 보면 작은 오차에도 조향이 크게 바뀐다.
  • 급커브에서 너무 멀리 보면 커브 안쪽을 가로지르기 쉽다.

이는 빨간 구가 동일한 Spline 진행 거리 기준 항상 동일한 거리 앞에 놓이게 되면 모든 차량 속도와 도로 형상을 동시에 만족시키기 어렵다는 것을 보여준다.

 


속도와 곡률에 따라 Look Ahead 조절하기

위와 같이 다양한 상황에 대응하기 위해, 차량 속도가 높을수록 Look Ahead를 늘리고, 현재 곡률이 커질수록 다시 줄였다.

CurvatureScale = Lerp(
    1.0,
    0.5,
    Clamp(CurrentCurvature × 3.0, 0.0, 1.0)
)

LookAheadDistance =
    (LookAheadBase + Speed × LookAheadSpeedFactor)
    × CurvatureScale

 

직선에서는 CurvatureScale이 1에 가까워진다. 차량 속도에 따라 더 먼 지점을 바라보며 조향 변화가 완만해진다.

커브가 급해질수록 Scale은 0.5에 가까워진다. 목표점을 차량과 가까운 위치로 당겨 경로의 굴곡을 더 적극적으로 따라가게 한다.

Speed + Curvature Adaptive Look Ahead

 

 

그럼에도 조금은 어색한 부분이 있다.

차량 앞 방향과 노란선이 벌어지는 양상을 보면, 차량이 Spline이 꺾이는 방향을 늦게 반영하고 있는 것을 볼 수 있다. 그래서 중심선을 비스듬하게 슬라이딩하는 모습까지 보이게 된다.

 

이것이 바로 목표점 하나만 추종했을 때의 문제이다.

 


Position Delta / Heading Delta / Crosstrack Error

*현재까지는 이 Position Delta만 적용되고 있다.

 

Spline 추종을 구현하는 가장 단순한 방법은 차량 앞의 목표점을 구하고 그 방향으로 조향하는 것이다.

차량 현재 위치
    ↓
Spline 위 전방 목표점 (빨간 구)
    ↓
목표점 방향과 차량 방향의 각도 차이
    ↓
Steering Input

 

 

Position Delta는 차량이 전방 목표 위치를 바라보기 위해 필요한 Yaw 각도 차이다.

전방 목표 위치로 가려면
현재 방향에서 얼마나 회전해야 하는가?

 

먼저 현재 진행 거리에 Look Ahead를 더해 전방 목표 위치를 구한다.

// 열린 Spline에서는 목표 거리가 전체 길이를 넘지 않도록 제한한다. 
// 닫힌 Spline에서는 전체 길이를 기준으로 Wrap한다.

LookAheadProgress = ProgressDistance + LookAheadDistance

LookAheadPosition =
    Spline.GetLocationAtDistanceAlongSpline(
        LookAheadProgress,
        WorldSpace
    )

 

차량 위치에서 목표 위치로 향하는 방향은 다음과 같이 구한다.

DirectionToTarget =
    Normalize(LookAheadPosition - VehicleLocation)

TargetYaw = atan2(
    DirectionToTarget.Y,
    DirectionToTarget.X
)

 

마지막으로 차량 Yaw와 목표 방향의 가장 짧은 각도 차이를 계산한다.

PositionDelta =
    FindDeltaAngleDegrees(VehicleYaw, TargetYaw)

 


Position Delta / Heading Delta / Crosstrack Error

 

Position Delta는 목표점의 위치를 보지만 그 지점에서 도로가 어느 방향으로 이어지는지는 알지 못한다.

Heading Delta는 Look Ahead 지점의 Spline 접선 방향(초록 화살표)과 차량 방향의 차이다.

전방의 도로는 어느 방향으로 이어지며
차량은 그 방향과 얼마나 어긋나 있는가?
PathDirection =
    Spline.GetDirectionAtDistanceAlongSpline(
        LookAheadProgress,
        WorldSpace
    )

PathYaw = atan2(
    PathDirection.Y,
    PathDirection.X
)

HeadingDelta =
    FindDeltaAngleDegrees(VehicleYaw, PathYaw)

 

Position Delta가 목표 위치를 향하게 만든다면 Heading Delta는 전방 경로의 방향을 미리 조향에 반영한다.

두 값은 비슷해 보이지만 역할이 다르다.

  • Position Delta는 목표점으로 수렴하게 한다.
  • Heading Delta는 Spline의 진행 방향에 차량의 자세를 맞춘다.

 

Position + Heading

 

Position에 Heading을 추가한 결과다. 빨간 구의 위치는 같지만 초록 화살표가 나타내는 Spline 접선 방향을 실제 조향에 함께 사용한다. 따라서 단순히 목표점을 향하는 것을 넘어 전방에서 도로가 꺾이는 방향으로 차량 자세를 미리 맞춘다.

 

차량 자세를 맞추면서 중앙선에서 벗어나는 현상은 다음으로 나오는 Crosstrack Error로 해결할 수 있다.

 


Position Delta / Heading Delta / Crosstrack Error

Position과 Heading을 함께 사용해도 차량이 도로 중심에서 벗어난 상태로 경로와 평행하게 달릴 수 있다.

중심선에서 약간 이탈시켜 출발한 차량, Position + Heading

 

Crosstrack Error는 차량과 Spline 중심선 사이의 좌우 이탈 거리를 나타낸다.

먼저 현재 진행 거리에서 Spline의 위치와 접선 방향을 구한다.

NearestOnPath =
    Spline.GetLocationAtDistanceAlongSpline(
        ProgressDistance,
        WorldSpace
    )

PathTangent = Normalize(NearestOnPath)

그다음 차량 위치와 중심선 위치의 Offset을 계산한다.

Offset = VehicleLocation - NearestOnPath

Offset에는 Spline의 진행 방향 성분도 포함될 수 있다. 좌우 이탈만 얻기 위해 접선 방향 성분을 제거한다.

// Offset에서 접선 방향 성분을 제거해 수직 이탈을 구하는 과정
// 주황 화살표 - 초록 화살표

PerpendicularOffset = Offset - PathTangent × Dot(Offset, PathTangent)

 

 


월드 UpVector만 사용하지 않은 이유

평평한 도로라면 월드 UpVector와 Path Tangent의 Cross Product로 도로 오른쪽 방향을 구할 수 있다. (왼손 좌표계)

하지만 경사로나 높낮이가 있는 도로에서는 월드 수평면만 기준으로 계산하면 좌우 방향에 불필요한 오차가 생길 수 있다.

이를 줄이기 위해 월드 UpVector를 Path Tangent에 수직인 평면으로 투영해 도로 기준의 법선 방향을 만들었다.

ProjectedUp =
    WorldUp
    - PathTangent × Dot(WorldUp, PathTangent)

RoadNormal = Normalize(ProjectedUp)

PathRight = Cross(RoadNormal, PathTangent)

마지막으로 수직 Offset을 Path Right에 투영한다.

CrosstrackError =
    Dot(PerpendicularOffset, PathRight)

 

경사로와 평지를 달리는 차량 우측 모습

 

부호는 차량이 중심선의 어느 쪽에 있는지를 나타낸다.

  • 양수면 도로 오른쪽으로 이탈한 상태다.
  • 음수면 도로 왼쪽으로 이탈한 상태다.

오른쪽으로 이탈했다면 왼쪽으로 조향하고, 왼쪽으로 이탈했다면 오른쪽으로 조향해야 한다.

 


Crosstrack 보정 후

 

Position + Heading + Crosstrack 차량이 중심선으로 돌아온다

 

동일하게 중심선에서 이탈한 상태로 시작해도, Crosstrack Error의 부호와 크기가 최종 Steering에 추가되면서 차량이 파란 구가 이동하는 중심선 방향으로 더 명시적으로 복귀한다.

 


세 요소 결합하기

Position Delta와 Heading Delta는 HeadingWeight로 결합한다.

YawCommand =
    PositionDelta × (1 - HeadingWeight)
    + HeadingDelta × HeadingWeight

기본값에서는 Heading의 비중을 높여 전방 경로 방향을 미리 반영한다.

 

다만 커브가 급할 때는 Position의 비중을 높인다.

목표 위치로의 수렴을 강화해 커브에서 경로를 크게 벗어나는 것을 줄이기 위해서다.

DynamicHeadingWeight = Lerp(
    HeadingWeight,
    0.3,
    Clamp(CurrentCurvature × 3.0, 0.0, 1.0)
)

 

최종 Steering은 Yaw 명령을 정규화하고 Crosstrack 보정을 빼서 계산한다.

Steering = Clamp(
    YawCommand / MaxYawDelta
    - CrosstrackError × CrosstrackGain,
    -1.0,
    1.0
)

오른쪽 이탈은 양수이므로 Steering에서 양수 보정값을 뺀다. 결과적으로 차량은 왼쪽으로 조향한다.

반대로 왼쪽 이탈은 음수다. 음수를 빼면 Steering 값이 증가해 오른쪽 복귀 조향이 적용된다.

 


마무리

세 요소를 결합하면서 각 요소가 서로 다른 문제를 담당하게 됐다. 목표 위치만 추종할 때보다 커브 방향을 먼저 반영할 수 있었고, 중심선에서 벗어난 뒤에도 복귀 방향을 명시적으로 계산할 수 있었다.

Position Delta

  • 전방 목표 위치를 향하게 한다.
  • 차량을 경로 위의 목표점으로 수렴시킨다.

Heading Delta

  • 전방 경로의 진행 방향을 미리 반영한다.
  • 차량 자세를 Spline 접선 방향에 맞춘다.

Crosstrack Error

  • 도로 중심선에서 벗어난 거리를 측정한다.
  • 좌우 이탈을 별도의 복귀 조향으로 보정한다.

 


계산한 값을 Chaos Vehicle에 전달하기

USplineFollowerComponent는 소유 Pawn의 공개 인터페이스를 호출하여 최종 Steering 값을 넘긴다.

OwnerPawn->DoSteering(Steering);

 

ACityVehiclePawn은 전달받은 값을 Chaos Vehicle Movement Component에 적용한다.

void ACityVehiclePawn::DoSteering(float SteeringValue)
{
    ChaosVehicleMovement->SetSteeringInput(
        SteeringValue
    );
}

이 구조를 사용하면 SplineFollowerComponent는 차량 물리 컴포넌트의 세부사항보다 조향 계산에 집중할 수 있다.

 


기술 기록 시리즈

  1. Unreal Engine 5로 만든 자동차 자율주행 시뮬레이션
  2. DataTable과 SceneCapture2D로 구현한 인터랙티브 차량 선택 UI
  3. UI 인터랙션을 위한 세 가지 Material 기록
  4. Spline 기반 3요소 조향 알고리즘 (현재 게시글)
  5. 곡률 기반 안전속도와 계층형 속도 제한
  6. Blackboard와 Behavior Tree를 이용한 환경 대응
  7. 구현 과정에서 발견한 문제와 개선 방향