자율주행 차량 조향 가중치를 곡률에 따라 동적 조정하기 (Lerp 활용)
스플라인 경로 추종 차량의 조향 식을 PositionError + HeadingError + CrossTrackError 형태로 짜고 나면, 다음으로 부딪히는 문제는 가중치 튜닝이다. 직선에서 부드럽게 가게 잡은 HeadingWeight = 0.7이 급커브에서는 차를 도로 안쪽으로 잘라먹게 만든다. 그렇다고 커브 기준으로 낮게 잡으면 직선에서 흔들린다.
해답은 가중치를 고정값으로 두지 않고 도로 형태에 따라 매 프레임 다르게 두는 것이다. 이 글은 그 동적 조정 로직을 정리한다.
고정 가중치의 한계
조향 식은 보통 두 오차의 가중 평균으로 구성된다.
YawCmd = PositionError * (1 - HeadingWeight) + HeadingError * HeadingWeight
이 식에서 HeadingWeight가 높으면 차체 방향을 도로 방향에 맞추는 데 더 비중을 둔다. 직선에서는 이쪽이 안정적이지만, 급커브에서는 차가 커브 진행 방향을 너무 빨리 따라가려고 해서 도로가 휘기 전에 미리 휘는 현상이 생긴다.
커브에서는 반대로 PositionError(어디로 가야 하는가)의 비중을 높여야 한다. 즉 도로 형태에 따라 HeadingWeight가 달라져야 한다는 뜻이다.
곡률을 한 줄로 측정하기
도로 형태를 판단하려면 곡률이 필요하다. 두 지점에서의 도로 진행 방향이 얼마나 다른지를 보면 된다.
float EstimateCurvature(float AheadOffset) const
{
const FVector D1 = Spline->GetDirectionAtDistanceAlongSpline(
CurrentDistance + AheadOffset, ESplineCoordinateSpace::World);
const FVector D2 = Spline->GetDirectionAtDistanceAlongSpline(
CurrentDistance + AheadOffset + 50.f, ESplineCoordinateSpace::World);
return FMath::Acos(FMath::Clamp(FVector::DotProduct(D1, D2), -1.f, 1.f));
}
핵심은 단위벡터 두 개의 내적이 cos(각도)라는 점이다. 거기에 Acos를 씌우면 각도(라디안)가 복원된다.
- 직선이면 두 방향이 거의 같음 → 내적 ≈ 1 → Acos ≈ 0
- 살짝 곡선이면 내적이 약간 작아짐 → Acos가 작은 양수
- 급커브면 내적이 더 작아짐 → Acos가 큰 양수
Clamp를 씌운 이유는 부동소수점 오차다. 두 단위벡터가 거의 같으면 내적이 1을 살짝 넘는 1.0000001 같은 값이 나올 수 있는데, Acos는 이 입력에서 NaN을 반환한다. -1~1로 잘라두면 안전하다.
곡률을 0~1로 정규화
EstimateCurvature가 반환하는 라디안 값은 보통 0.0 ~ 0.3 정도 범위에 들어온다. 이걸 가중치 계산에 바로 쓰기는 불편하니 0~1로 정규화한다.
const float CurvHere = EstimateCurvature(0.f);
const float CurvNorm = FMath::Clamp(CurvHere * 3.f, 0.f, 1.f);
3.f를 곱하는 이유는 0.33 라디안 정도면 충분히 급커브로 보고 CurvNorm을 1로 만들기 위해서다. 이 배율은 도로 곡률 분포에 따라 튜닝이 필요한 부분이다. 직선이 많은 고속도로면 더 크게(예: 5.f), 산악 도로면 더 작게 두는 식이다.
결과적으로:
- 직선 → CurvNorm = 0
- 적당한 곡선 → CurvNorm = 0.5 부근
- 급커브 → CurvNorm = 1
Lerp로 가중치 보간
이제 CurvNorm을 가중치로 변환한다. Lerp 한 줄이면 된다.
const float HdgWeight = FMath::Lerp(0.7f, 0.3f, CurvNorm);
Lerp(A, B, t)는 t=0이면 A, t=1이면 B, 그 사이는 선형 보간이다.
- 직선(CurvNorm = 0): HdgWeight = 0.7 → 방향 맞추기에 비중
- 급커브(CurvNorm = 1): HdgWeight = 0.3 → 위치 맞추기에 비중
- 중간(CurvNorm = 0.5): HdgWeight = 0.5 → 반반
조향 식은 그대로 두고 가중치만 매 프레임 달라진다.
const float HdgWeight = FMath::Lerp(0.7f, 0.3f, CurvNorm);
float YawCmd = PositionError * (1.f - HdgWeight) + HeadingError * HdgWeight;
전방 주시 거리도 같은 패턴으로
전방 주시 거리(Look-Ahead Distance)도 도로 형태에 따라 달라야 한다.
- 직선: 멀리 봐야 안정적. 가까이만 보면 휘청거림.
- 급커브: 가까이 봐야 정확. 멀리 보면 도로 안쪽을 잘라먹음.
같은 Lerp 패턴으로 처리할 수 있다.
const float CurvScale = FMath::Lerp(1.f, 0.5f, CurvNorm);
const float LookAheadDist = (LookAheadBase + Speed * 0.3f) * CurvScale;
LookAheadBase는 기본 거리, Speed * 0.3f는 빠를수록 더 멀리 보는 효과를 주는 항이다. 거기에 CurvScale을 곱해 직선에서는 그대로, 급커브에서는 절반 정도로 줄어들게 했다.
전체 조향 흐름
// 1. 곡률 측정 → 0~1 정규화
const float CurvHere = EstimateCurvature(0.f);
const float CurvNorm = FMath::Clamp(CurvHere * 3.f, 0.f, 1.f);
// 2. 가중치와 전방 주시 거리를 Lerp로 결정
const float HdgWeight = FMath::Lerp(0.7f, 0.3f, CurvNorm);
const float CurvScale = FMath::Lerp(1.f, 0.5f, CurvNorm);
const float LookAheadDist = (LookAheadBase + Speed * 0.3f) * CurvScale;
// 3. 각 오차 계산 (LookAheadDist를 인자로 사용)
// PositionError = ...
// HeadingError = ...
// CrossTrackError = ...
// 4. 블렌딩
float YawCmd = PositionError * (1.f - HdgWeight) + HeadingError * HdgWeight;
float Steering = FMath::Clamp(YawCmd / MaxYawDelta - CrossTrackError * CrossTrackGain, -1.f, 1.f);
조향 식 자체는 글 A에서 정리한 형태 그대로다.
달라진 건 가중치와 전방 주시 거리가 매 프레임 곡률에 따라 다시 계산된다는 점이다.
정리
조향 가중치를 고정값으로 두면 직선과 커브 중 한쪽은 반드시 어색해진다. 곡률을 매 프레임 측정하고, Lerp로 가중치와 전방 주시 거리를 함께 보간하면 한 세트의 파라미터로 양쪽을 다 커버할 수 있다.
핵심 도구는 세 가지다.
- 단위벡터 내적 + Acos: 두 방향 사이 각도 복원 (곡률 근사)
- Clamp로 0~1 정규화: 이후 단계에서 쓰기 좋은 형태로
- Lerp: 직선/커브 양 끝값 사이를 부드럽게 전환
이 패턴은 자율주행 외에도 "환경에 따라 파라미터를 부드럽게 전환해야 하는 모든 상황"(난이도 조절, 카메라 거리, AI 공격성 등)에 그대로 응용된다.
'etc' 카테고리의 다른 글
| 언리얼 스플라인 경로 추종 차량이 도로 밖으로 튕길 때, 평면 조향과 이탈 페일세이프 분리 (0) | 2026.05.16 |
|---|---|
| 언리얼 자율주행 차량, 곡률 반경으로 안전 속도 계산하기 (V = √(μgR)) (0) | 2026.05.13 |
| 언리얼 벡터로 도로 이탈 거리 구하기, Cross-Track Error 계산 과정 (0) | 2026.05.13 |
| 언리얼 스플라인 경로 추종 차량 조향, Pure Pursuit + Heading + Cross-Track 블렌딩으로 정리한 과정 (0) | 2026.05.13 |
| 언리얼 USplineComponent 따라가기, 위치를 누적 거리(float) 하나로 추적하기 (0) | 2026.05.13 |
