언리얼 자율주행 차량, 곡률 반경으로 안전 속도 계산하기 (V = √(μgR))
자율주행 차량을 구현하다 커브에서 어떻게 감속할지 정해야 하는 순간이 온다. "휘어 있으면 느리게, 직선이면 빠르게" 같은 if/else로 시작했다가 곧 한계에 부딪힌다. 어떤 커브는 너무 천천히 들어가고, 어떤 커브는 너무 빠르게 들어가 미끄러진다. 곡률 값(라디안) 자체를 임의의 상수로 속도에 매핑하려고 하면 그 상수를 어떤 기준으로 정해야 할지 감이 안 잡힌다.
물리 공식을 한 번 쓰면 이 문제가 깔끔하게 풀린다. 차량이 원형 곡선 위에서 미끄러지지 않을 조건은 원심력 ≤ 마찰력이고, 이걸 정리하면 한계 속도는 다음과 같다.
V = √(μ × g × R)
- μ (마찰 계수): 노면 상태 (일반 도로 0.7~0.8, 빙판 0.1)
- g (중력 가속도): 9.8 m/s²
- R (곡률 반경): 그 지점에서 도로가 그리는 가상의 원의 반지름
이 글은 스플라인에서 측정한 곡률(라디안)에서 R을 뽑아내는 과정과, 그 결과를 한계 속도로 변환하는 과정을 담는다.
곡률(라디안)에서 곡률 반경(cm)으로
스플라인에서 곡률을 측정하는 방식은 두 지점에서의 도로 방향 사이 각도 차이를 보는 것이다(이 부분은 다른 글에서 따로 다뤘다).
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));
}
여기서 반환되는 값은 두 지점 사이 도로 진행 방향의 각도 차이(라디안)다. 그리고 두 지점은 스플라인 위에서 50cm 떨어져 있다.
이 두 값을 호의 길이 공식에 대입하면 곡률 반경이 나온다.
호의 길이 = 반지름 × 각도
L = R × θ
스플라인의 한 구간을 원의 일부로 근사하면 다음 대응이 성립한다.
- 호의 길이 L: 두 측정 지점 사이 도로 거리 = 50cm
- 각도 θ: 두 지점에서 도로 방향의 각도 차이 = 측정한 곡률(라디안)
- 반지름 R: 그 원의 반지름 = 구하려는 값
식을 R에 대해 풀면 이렇게 된다.
R = L / θ = 50cm / Curvature
곡률(각도)이 작을수록(직선에 가까울수록) R이 커지고, 곡률이 클수록(급커브일수록) R이 작아진다. 직관과도 일치한다.
공식을 코드로
float ComputeCurveSpeedLimit(float Curvature) const
{
if (Curvature <= KINDA_SMALL_NUMBER)
return MaxSpeed;
const float Radius = CurvatureSampleSpan / Curvature;
return FMath::Sqrt(LateralFriction * 980.f * Radius);
}
세 부분으로 나뉜다.
직선 처리. Curvature가 거의 0이면 곡률 반경이 무한대에 수렴하고, 그대로 나누면 0으로 나누기 사고가 난다. KINDA_SMALL_NUMBER(0.0001f) 이하면 직선으로 간주하고 최대 속도를 반환한다. 부동소수점 비교에서 == 0을 쓰지 않는 이유는 오차 때문에 정확히 0이 되지 않을 수 있어서다.
곡률 반경 계산. 호의 길이 공식 그대로다. CurvatureSampleSpan은 곡률을 측정할 때 쓴 두 지점 사이 도로 거리(50cm)다. 이 값은 EstimateCurvature 함수와 한 쌍으로 맞춰야 한다. 측정 간격을 바꿨는데 여기서 안 바꾸면 결과가 어긋난다.
한계 속도 계산. V = √(μ × g × R) 그대로다.
단위가 중요하다 (g = 980)
이 부분에서 한 번 실수했다. 언리얼은 기본 단위가 cm다. Radius도 cm로 구해진다. 그런데 공식의 g를 9.8(m/s²)로 두면 단위가 어긋나서 결과가 실제보다 작게 나온다. 한계 속도가 비정상적으로 낮게 잡혀서 차가 직선에서도 굼떴다.
언리얼 좌표계에서는 g = 980 cm/s²로 쓰는 것이 맞다.
return FMath::Sqrt(LateralFriction * 980.f * Radius);
결과로 나오는 한계 속도도 자동으로 cm/s 단위가 된다. 이걸 km/h로 표시하고 싶다면 × 0.036을 곱하면 된다(예: 1000 cm/s = 36 km/h).
최소/최대 속도 클램프
이 공식만으로 두면 두 가지 문제가 남는다. 곡률이 거의 없는 도로에서 한계 속도가 비현실적으로 높게 나오고, 급커브에서 너무 낮게 나와 차가 멈출 정도로 느려진다. 양쪽을 클램프로 막는다.
return FMath::Clamp(
FMath::Sqrt(LateralFriction * 980.f * Radius),
MinSpeed,
MaxSpeed
);
MinSpeed는 14km/h 정도(400 cm/s)에서 시작했다. 너무 낮으면 차가 거의 정지해서 시뮬레이션이 답답해진다. MaxSpeed는 차량 성능과 도로 종류에 맞춰 잡는다. 둘 다 UPROPERTY로 빼두면 도로마다 다르게 설정할 수 있다.
마찰 계수 튜닝
LateralFriction은 노면 상태에 대응하는 값이다. 실제 물리값과 정확히 일치시킬 필요는 없고, 시뮬레이션상 차량이 자연스럽게 보이는 값을 튜닝으로 찾는 편이 빠르다. 이 프로젝트에서는 0.6 정도에서 시작했다.
이 값을 도로 메타데이터로 두면 "빙판 도로", "젖은 도로" 같은 시나리오에서 같은 공식 그대로 한계 속도가 자동으로 낮아진다. 별도 조건 분기를 추가할 필요가 없다.
const float Friction = TargetRoad->SurfaceFriction;
return FMath::Sqrt(Friction * 980.f * Radius);
전방 곡률로 미리 감속
현재 위치의 곡률만 보면 커브 진입 직전에 급제동을 하게 된다. 전방 N미터 앞의 곡률도 함께 측정하고, 둘 중 더 제한적인 속도를 채택하면 미리 감속이 자연스러워진다.
const float CurvHere = EstimateCurvature(0.f);
const float CurvAhead = EstimateCurvature(BrakePreviewDist);
const float SpeedLimit = FMath::Min(
ComputeCurveSpeedLimit(CurvHere),
ComputeCurveSpeedLimit(CurvAhead)
);
FMath::Min으로 더 작은 쪽을 고르는 패턴이라 전방에 급커브가 있으면 지금부터 감속이 시작된다. BrakePreviewDist는 차량 속도에 비례해 조절하면 더 자연스럽다(빠를수록 멀리 미리 봐야 한다).
정리
곡률 값(라디안)을 임의의 상수로 속도에 매핑하려 하지 말고, 물리 공식 한 줄을 쓰면 튜닝할 값이 자연스럽게 정리된다.
- 호의 길이 공식
L = R × θ로 곡률(라디안)을 곡률 반경(cm)으로 변환 V = √(μ × g × R)로 한계 속도 계산- 언리얼에서는 g = 980 (cm/s²)
- 0으로 나누기 방지를 위해 KINDA_SMALL_NUMBER 비교
- 최소/최대 속도는 Clamp로 보장
- 전방 곡률과 현재 곡률 둘 중 더 제한적인 쪽을 FMath::Min으로 채택
이 패턴은 자율주행 시뮬레이션뿐 아니라 레이싱 게임 AI, 비행체 곡선 비행, 카메라 트랙 속도 조절 등 "곡선 위에서 안전한 속도를 정해야 하는" 모든 상황에 그대로 응용된다.
'etc' 카테고리의 다른 글
| 언리얼 런타임 SplineMeshComponent 동적 생성: 호출 순서·탄젠트 정규화·핫리로드 함정 (0) | 2026.05.16 |
|---|---|
| 언리얼 스플라인 경로 추종 차량이 도로 밖으로 튕길 때, 평면 조향과 이탈 페일세이프 분리 (0) | 2026.05.16 |
| 자율주행 차량 조향 가중치를 곡률에 따라 동적 조정하기 (Lerp 활용) (0) | 2026.05.13 |
| 언리얼 벡터로 도로 이탈 거리 구하기, Cross-Track Error 계산 과정 (0) | 2026.05.13 |
| 언리얼 스플라인 경로 추종 차량 조향, Pure Pursuit + Heading + Cross-Track 블렌딩으로 정리한 과정 (0) | 2026.05.13 |
