언리얼 엔진 5

[UE5] UPrimitiveComponent 의 바운딩 박스

언린이 2026. 3. 18. 22:33
반응형

1. 바운딩 박스가 필요한 이유

게임에서 두 오브젝트가 겹치는지 판단하려면, 오브젝트를 구성하는 수천 개의 폴리곤을 전부 비교하는 것은 현실적으로 불가능합니다. 대신, 오브젝트를 단순한 기하도형으로 근사하여 먼저 빠르게 걸러내고, 그 이후에만 정밀 검사를 수행합니다. 이 근사 도형 중 가장 널리 쓰이는 것이 바로 바운딩 박스(Bounding Box) 입니다.

언리얼 엔진에서는 모든 UPrimitiveComponent가 바운딩 박스를 내부적으로 유지합니다. 이 바운딩 박스는 렌더링 컬링(Frustum Culling), 물리 브로드페이즈(Broad Phase), 네비게이션 메시 생성, 레벨 스트리밍 등 엔진 전반에서 광범위하게 사용됩니다. 게임플레이 코드에서도 바운딩 박스를 직접 조회하면 값비싼 트레이스(Trace)를 줄이거나, 캐릭터 간 근접 판정, 카메라 범위 계산, AI 시야 계산 등에 활용할 수 있습니다.

바운딩 박스는 크게 두 가지로 나뉩니다. 월드 좌표축과 항상 정렬되어 있는 AABB(Axis-Aligned Bounding Box) 와, 오브젝트의 회전을 따라가는 OBB(Oriented Bounding Box) 입니다. 두 방식은 정밀도와 연산 비용 사이에서 서로 다른 트레이드오프를 가지며, 용도에 따라 적절히 선택해야 합니다.

2. AABB — 축 정렬 바운딩 박스

2-1. AABB의 개념

AABB는 월드 좌표계의 X, Y, Z 축과 항상 평행한 면으로 구성된 직육면체입니다. 오브젝트가 회전하더라도 AABB 자체는 회전하지 않으며, 오브젝트를 완전히 포함하는 최소 크기의 축 정렬 박스로 자동 재계산됩니다.

언리얼 엔진에서 AABB는 FBox 구조체로 표현되며, 박스를 구성하는 두 꼭짓점인 MinMaxFVector로 저장합니다. 이 두 벡터만 있으면 박스의 모든 정보를 복원할 수 있습니다.

FBox
├── Min (FVector) — 박스의 최솟값 꼭짓점 (월드 공간)
└── Max (FVector) — 박스의 최댓값 꼭짓점 (월드 공간)

2-2. UPrimitiveComponent에서 AABB 조회하기

UPrimitiveComponent는 현재 바운드를 Bounds 멤버 변수로 보유합니다. 이 변수의 타입은 FBoxSphereBounds이며, AABB와 바운딩 스피어(Bounding Sphere)를 하나의 구조체에 묶어 관리합니다.

FBoxSphereBounds
├── Origin    (FVector) — 박스와 스피어의 공통 중심점 (월드 공간)
├── BoxExtent (FVector) — 중심에서 각 축 방향의 절반 크기 (Half-Extent)
└── SphereRadius (float) — 바운딩 스피어의 반지름

아래는 UPrimitiveComponent에서 AABB를 직접 조회하는 방법입니다.

// StaticMeshComponent에서 FBoxSphereBounds를 조회하고 FBox로 변환하는 예시
void AMyActor::PrintBoundsInfo()
{
    UStaticMeshComponent* MeshComp = GetComponentByClass<UStaticMeshComponent>();
    if (!MeshComp)
    {
        return;
    }

    // Bounds는 UPrimitiveComponent의 멤버 변수로, 월드 공간 바운드를 반환합니다.
    FBoxSphereBounds WorldBounds = MeshComp->Bounds;

    // GetBox()를 호출하면 FBoxSphereBounds로부터 FBox를 얻습니다.
    FBox WorldAABB = WorldBounds.GetBox();

    FVector Center = WorldAABB.GetCenter();         // 박스 중심점
    FVector Extent = WorldAABB.GetExtent();         // 중심에서 각 면까지의 거리 (Half-Size)
    FVector Size   = WorldAABB.GetSize();           // 박스의 전체 크기 (Extent * 2)

    UE_LOG(LogTemp, Log, TEXT("Center: %s"), *Center.ToString());
    UE_LOG(LogTemp, Log, TEXT("Extent: %s"), *Extent.ToString());
    UE_LOG(LogTemp, Log, TEXT("Size:   %s"), *Size.ToString());
}

위 코드에서 MeshComp->Bounds는 언리얼 엔진이 매 프레임 갱신하는 월드 공간의 바운드이며, GetBox()를 통해 FBox로 변환하면 Min, Max, GetCenter(), GetExtent() 등 다양한 유틸리티 함수를 사용할 수 있습니다. Extent는 중심에서 각 면까지의 절반 크기임에 주의하셔야 합니다.

2-3. 로컬 공간 AABB 조회하기

Bounds는 항상 월드 공간 기준입니다. 오브젝트가 회전해 있을 경우, 월드 공간 AABB는 실제 메시보다 훨씬 크게 계산될 수 있습니다. 메시의 원래 모양에 맞는 바운드를 얻으려면 로컬 공간 AABB를 사용해야 합니다.

// UStaticMeshComponent에서 로컬 공간 AABB를 조회하는 예시
void AMyActor::PrintLocalBounds()
{
    UStaticMeshComponent* MeshComp = GetComponentByClass<UStaticMeshComponent>();
    if (!MeshComp)
    {
        return;
    }

    FVector LocalMin, LocalMax;

    // GetLocalBounds는 컴포넌트 로컬 공간(메시 원점 기준)의 Min/Max를 반환합니다.
    MeshComp->GetLocalBounds(LocalMin, LocalMax);

    FVector LocalCenter = (LocalMin + LocalMax) * 0.5f;
    FVector LocalExtent = (LocalMax - LocalMin) * 0.5f;

    UE_LOG(LogTemp, Log, TEXT("Local Center: %s"), *LocalCenter.ToString());
    UE_LOG(LogTemp, Log, TEXT("Local Extent: %s"), *LocalExtent.ToString());
}

GetLocalBoundsUStaticMeshComponent에서 재정의된 함수로, 메시 에셋이 임포트될 당시 원점 기준으로 계산된 바운드를 반환합니다. 이 값은 오브젝트의 현재 트랜스폼(위치, 회전, 스케일)과 무관하며, 항상 일정한 값을 가집니다.

2-4. AABB의 활용 — 범위 내 오브젝트 필터링

AABB는 연산이 단순하기 때문에 수많은 오브젝트를 빠르게 필터링하는 브로드페이즈에 적합합니다. 두 AABB가 겹치는지 확인하는 연산은 각 축의 범위가 겹치는지 세 번 비교하는 것으로 충분합니다.

아래는 특정 영역에 들어온 액터 목록을 AABB 기반으로 걸러내는 예시입니다.

// 주어진 FBox 범위 안에 있는 액터들을 AABB 방식으로 필터링하는 예시
TArray<AActor*> AMyGameMode::GetActorsInsideRegion(const FBox& RegionBox)
{
    TArray<AActor*> Result;
    TArray<AActor*> AllActors;

    // 월드의 모든 액터를 순회합니다.
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActor::StaticClass(), AllActors);

    for (AActor* Actor : AllActors)
    {
        if (!Actor)
        {
            continue;
        }

        // GetActorBounds는 내부적으로 루트 컴포넌트의 Bounds를 사용합니다.
        FVector ActorOrigin, ActorExtent;
        Actor->GetActorBounds(false, ActorOrigin, ActorExtent);

        // 액터의 AABB를 FBox로 생성합니다.
        FBox ActorAABB(ActorOrigin - ActorExtent, ActorOrigin + ActorExtent);

        // FBox::Intersect — 두 AABB가 겹치는지 확인합니다.
        if (RegionBox.Intersect(ActorAABB))
        {
            Result.Add(Actor);
        }
    }

    return Result;
}

위 코드처럼 FBox::Intersect는 두 박스가 겹치는지를 매우 빠르게 판별합니다. 실제로 엔진 내 Overlap 이벤트의 브로드페이즈 검사도 이와 유사한 AABB 교차 판정을 먼저 수행합니다.

2-5. AABB의 활용 — 카메라 자동 프레이밍

에디터 뷰포트나 인게임 컷신에서 오브젝트를 자동으로 화면에 담아야 할 때, AABB의 크기와 중심을 사용하면 카메라 위치와 시야각을 계산하는 데 활용할 수 있습니다.

// 대상 액터의 AABB를 기준으로 카메라를 자동 배치하는 예시
void AMyGameMode::FrameActorInCamera(AActor* TargetActor, ACameraActor* Camera)
{
    if (!TargetActor || !Camera)
    {
        return;
    }

    FVector Origin, Extent;

    // bOnlyCollidingComponents = false : 모든 컴포넌트의 바운드를 합산합니다.
    TargetActor->GetActorBounds(false, Origin, Extent);

    // AABB의 절반 대각선 길이를 반지름으로 사용합니다.
    float BoundRadius = Extent.Size();

    // 카메라의 수직 시야각(VFoV)에서 대상이 화면에 꽉 차도록 거리를 계산합니다.
    float VFoVRad = FMath::DegreesToRadians(Camera->GetCameraComponent()->FieldOfView * 0.5f);
    float RequiredDistance = BoundRadius / FMath::Tan(VFoVRad);

    // 카메라를 대상 정면에서 계산된 거리만큼 뒤에 배치합니다.
    FVector CameraLocation = Origin - Camera->GetActorForwardVector() * RequiredDistance;
    Camera->SetActorLocation(CameraLocation);
    Camera->SetActorRotation((Origin - CameraLocation).Rotation());
}

위 코드처럼 AABB의 Extent로부터 바운딩 반지름을 계산하고, 카메라 시야각을 고려해 거리를 역산하면 오브젝트 크기에 관계없이 화면에 자동으로 맞출 수 있습니다.

3. OBB — 방향이 있는 바운딩 박스

3-1. OBB의 개념

OBB는 오브젝트의 로컬 좌표축 방향으로 정렬된 바운딩 박스입니다. AABB와 달리, 오브젝트가 회전해도 박스가 함께 회전하므로 오브젝트를 훨씬 더 타이트하게 감쌀 수 있습니다. 예를 들어 45도 기울어진 긴 칼날은 AABB로 표현하면 불필요하게 큰 박스가 만들어지지만, OBB는 칼날에 딱 맞는 박스를 유지합니다.

언리얼 엔진에서 OBB는 FOrientedBox 구조체로 표현됩니다.

FOrientedBox
├── Center      (FVector)  — 박스의 중심점 (월드 공간)
├── AxisX       (FVector)  — 로컬 X축 방향 단위 벡터 (월드 공간에서의 방향)
├── AxisY       (FVector)  — 로컬 Y축 방향 단위 벡터
├── AxisZ       (FVector)  — 로컬 Z축 방향 단위 벡터
├── ExtentX     (float)    — AxisX 방향 절반 크기
├── ExtentY     (float)    — AxisY 방향 절반 크기
└── ExtentZ     (float)    — AxisZ 방향 절반 크기

AxisX, AxisY, AxisZ는 각각 액터의 Forward, Right, Up 벡터에 해당하며, 이 세 축이 OBB의 방향성을 결정합니다.

3-2. UPrimitiveComponent에서 OBB 구성하기

UPrimitiveComponent는 OBB를 직접 반환하는 단일 API를 제공하지 않습니다. 대신, 로컬 공간 AABB와 액터의 트랜스폼을 조합하여 OBB를 직접 구성해야 합니다. 이것이 OBB를 AABB와 구분해서 이해해야 하는 핵심입니다.

아래는 UStaticMeshComponent의 로컬 바운드와 컴포넌트 트랜스폼을 결합하여 FOrientedBox를 생성하는 예시입니다.

// UStaticMeshComponent로부터 FOrientedBox를 구성하는 헬퍼 함수
FOrientedBox BuildOrientedBox(const UStaticMeshComponent* MeshComp)
{
    FOrientedBox OBB;

    if (!MeshComp)
    {
        return OBB;
    }

    // 로컬 공간 바운드의 Min/Max를 가져옵니다.
    FVector LocalMin, LocalMax;
    MeshComp->GetLocalBounds(LocalMin, LocalMax);

    // 로컬 공간에서의 중심과 절반 크기를 계산합니다.
    FVector LocalCenter = (LocalMin + LocalMax) * 0.5f;
    FVector LocalExtent = (LocalMax - LocalMin) * 0.5f;

    // 컴포넌트의 월드 트랜스폼을 가져옵니다.
    FTransform WorldTransform = MeshComp->GetComponentTransform();

    // 로컬 중심을 월드 공간으로 변환합니다.
    OBB.Center = WorldTransform.TransformPosition(LocalCenter);

    // 로컬 축을 월드 공간으로 변환합니다 (방향 벡터이므로 TransformVector 사용).
    FVector WorldAxisX = WorldTransform.TransformVector(FVector::ForwardVector);
    FVector WorldAxisY = WorldTransform.TransformVector(FVector::RightVector);
    FVector WorldAxisZ = WorldTransform.TransformVector(FVector::UpVector);

    OBB.AxisX = WorldAxisX.GetSafeNormal();
    OBB.AxisY = WorldAxisY.GetSafeNormal();
    OBB.AxisZ = WorldAxisZ.GetSafeNormal();

    // 스케일을 고려하여 절반 크기를 계산합니다.
    FVector Scale3D = WorldTransform.GetScale3D();
    OBB.ExtentX = LocalExtent.X * Scale3D.X;
    OBB.ExtentY = LocalExtent.Y * Scale3D.Y;
    OBB.ExtentZ = LocalExtent.Z * Scale3D.Z;

    return OBB;
}

위 코드에서 핵심은 TransformPositionTransformVector의 구분입니다. 중심점처럼 위치를 변환할 때는 이동(Translation)까지 반영하는 TransformPosition을 사용하고, 축 방향처럼 방향 벡터를 변환할 때는 이동을 제외한 TransformVector를 사용합니다.

3-3. OBB의 활용 — SAT를 이용한 정밀 충돌 판정

두 OBB가 겹치는지 판단하는 알고리즘으로 분리 축 정리(Separating Axis Theorem, SAT) 가 사용됩니다. 두 볼록 도형은 분리하는 축(Separating Axis)이 하나라도 존재하면 충돌하지 않습니다. 3D OBB 간 충돌 판정에서는 두 박스의 3개 축 조합인 총 15개 후보 축을 검사합니다.

아래는 두 FOrientedBox 간 SAT 충돌 판정을 구현한 예시입니다.

// 두 FOrientedBox가 겹치는지 SAT로 판정하는 함수
bool AreOBBsOverlapping(const FOrientedBox& OBB_A, const FOrientedBox& OBB_B)
{
    // 두 박스의 로컬 축을 배열로 묶어 처리합니다.
    FVector AxesA[3] = { OBB_A.AxisX, OBB_A.AxisY, OBB_A.AxisZ };
    FVector AxesB[3] = { OBB_B.AxisX, OBB_B.AxisY, OBB_B.AxisZ };
    float   ExtentsA[3] = { OBB_A.ExtentX, OBB_A.ExtentY, OBB_A.ExtentZ };
    float   ExtentsB[3] = { OBB_B.ExtentX, OBB_B.ExtentY, OBB_B.ExtentZ };

    FVector D = OBB_B.Center - OBB_A.Center; // 두 중심 간의 벡터

    // 두 OBB 각각의 축 3개 + 두 OBB 축의 외적 9개 = 총 15개 분리 축을 검사합니다.
    TArray<FVector> SeparatingAxes;

    for (int32 i = 0; i < 3; ++i)
    {
        SeparatingAxes.Add(AxesA[i]);
        SeparatingAxes.Add(AxesB[i]);
    }
    for (int32 i = 0; i < 3; ++i)
    {
        for (int32 j = 0; j < 3; ++j)
        {
            FVector CrossAxis = FVector::CrossProduct(AxesA[i], AxesB[j]);
            if (!CrossAxis.IsNearlyZero()) // 평행한 축의 외적은 무시합니다.
            {
                SeparatingAxes.Add(CrossAxis.GetSafeNormal());
            }
        }
    }

    for (const FVector& Axis : SeparatingAxes)
    {
        // 중심 간 거리를 해당 축에 투영합니다.
        float CenterProjection = FMath::Abs(FVector::DotProduct(D, Axis));

        // 각 박스의 절반 크기를 해당 축에 투영합니다.
        float ProjectionA = 0.0f;
        float ProjectionB = 0.0f;

        for (int32 i = 0; i < 3; ++i)
        {
            ProjectionA += FMath::Abs(FVector::DotProduct(AxesA[i] * ExtentsA[i], Axis));
            ProjectionB += FMath::Abs(FVector::DotProduct(AxesB[i] * ExtentsB[i], Axis));
        }

        // 분리 축을 발견하면 두 박스는 겹치지 않습니다.
        if (CenterProjection > ProjectionA + ProjectionB)
        {
            return false;
        }
    }

    // 모든 축에서 분리 축을 찾지 못했으므로 두 박스는 겹쳐 있습니다.
    return true;
}

위 코드처럼 15개의 모든 후보 축에서 분리 축이 발견되지 않으면, 두 OBB는 겹쳐 있다고 판단합니다. 이 방식은 AABB 단순 교차 비교보다 연산이 무겁지만, 회전된 오브젝트 간의 정확한 겹침 여부를 판단할 수 있습니다.

3-4. OBB의 활용 — 소울라이크 전투의 공격 범위 판정

소울라이크 장르처럼 무기의 휘두름 방향과 각도가 중요한 전투 시스템에서는 AABB만으로는 판정이 부정확합니다. 예를 들어 검이 대각선으로 내려치는 모션을 취하고 있다면, AABB는 공격 방향과 관계없이 항상 축 정렬된 박스로 근사하기 때문에 빗나가야 할 공격이 맞거나, 맞아야 할 공격이 빗나가는 문제가 발생합니다.

아래는 무기 소켓의 트랜스폼으로 OBB를 구성하고 피격 판정에 사용하는 예시입니다.

// 무기 메시의 현재 회전을 반영한 OBB로 히트박스 판정을 수행하는 예시
void AMyWeapon::PerformAttackCollision()
{
    USkeletalMeshComponent* WeaponMesh = GetComponentByClass<USkeletalMeshComponent>();
    if (!WeaponMesh)
    {
        return;
    }

    // 무기 블레이드의 소켓 트랜스폼을 월드 기준으로 가져옵니다.
    FTransform BladeTransform = WeaponMesh->GetSocketTransform(TEXT("BladeSocket"), RTS_World);

    // 무기 메시의 로컬 바운드(Min/Max)를 가져옵니다.
    FVector LocalMin, LocalMax;
    WeaponMesh->GetLocalBounds(LocalMin, LocalMax);

    FVector LocalCenter = (LocalMin + LocalMax) * 0.5f;
    FVector LocalExtent = (LocalMax - LocalMin) * 0.5f;

    // OBB를 소켓 트랜스폼 기준으로 구성합니다.
    FOrientedBox WeaponOBB;
    WeaponOBB.Center  = BladeTransform.TransformPosition(LocalCenter);
    WeaponOBB.AxisX   = BladeTransform.GetRotation().GetForwardVector();
    WeaponOBB.AxisY   = BladeTransform.GetRotation().GetRightVector();
    WeaponOBB.AxisZ   = BladeTransform.GetRotation().GetUpVector();

    FVector Scale3D   = BladeTransform.GetScale3D();
    WeaponOBB.ExtentX = LocalExtent.X * Scale3D.X;
    WeaponOBB.ExtentY = LocalExtent.Y * Scale3D.Y;
    WeaponOBB.ExtentZ = LocalExtent.Z * Scale3D.Z;

    // 공격 범위 내 모든 적 캐릭터를 대상으로 OBB 교차 판정을 수행합니다.
    TArray<AActor*> EnemyActors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyEnemy::StaticClass(), EnemyActors);

    for (AActor* Enemy : EnemyActors)
    {
        if (!Enemy || Enemy == GetOwner())
        {
            continue;
        }

        UStaticMeshComponent* EnemyMesh = Enemy->GetComponentByClass<UStaticMeshComponent>();
        if (!EnemyMesh)
        {
            continue;
        }

        // 적 캐릭터의 OBB도 동일하게 구성합니다.
        FOrientedBox EnemyOBB = BuildOrientedBox(EnemyMesh);

        if (AreOBBsOverlapping(WeaponOBB, EnemyOBB))
        {
            // OBB 판정을 통과한 경우에만 정밀 히트박스 처리를 수행합니다.
            ApplyDamageToEnemy(Enemy);
        }
    }
}

위 코드처럼 무기 소켓의 트랜스폼을 기준으로 OBB를 구성하면, 무기가 어떤 각도로 회전하더라도 실제 공격 방향에 맞는 정확한 히트박스가 만들어집니다. 이 방식은 정밀도가 중요한 액션 게임에서 AABB보다 훨씬 신뢰할 수 있는 판정을 제공합니다.

3-5. OBB의 활용 — 디버그 시각화

디버그 빌드에서 OBB를 화면에 그려 확인하면 판정 구현 중 발생하는 문제를 빠르게 파악할 수 있습니다. 언리얼 엔진의 DrawDebugBox는 AABB만 지원하므로, OBB의 여덟 꼭짓점을 직접 계산하여 모서리를 그려야 합니다.

// FOrientedBox를 화면에 시각화하는 디버그 함수
void DrawDebugOrientedBox(UWorld* World, const FOrientedBox& OBB, const FColor& Color, float Duration)
{
    if (!World)
    {
        return;
    }

    // OBB의 세 축 방향으로 절반 크기만큼 이동한 벡터를 계산합니다.
    FVector X = OBB.AxisX * OBB.ExtentX;
    FVector Y = OBB.AxisY * OBB.ExtentY;
    FVector Z = OBB.AxisZ * OBB.ExtentZ;

    // 중심점에서 여덟 꼭짓점의 위치를 계산합니다.
    TArray<FVector> Corners = {
        OBB.Center + X + Y + Z,
        OBB.Center + X + Y - Z,
        OBB.Center + X - Y + Z,
        OBB.Center + X - Y - Z,
        OBB.Center - X + Y + Z,
        OBB.Center - X + Y - Z,
        OBB.Center - X - Y + Z,
        OBB.Center - X - Y - Z,
    };

    // 12개의 모서리를 선분으로 연결합니다.
    DrawDebugLine(World, Corners[0], Corners[1], Color, false, Duration);
    DrawDebugLine(World, Corners[0], Corners[2], Color, false, Duration);
    DrawDebugLine(World, Corners[0], Corners[4], Color, false, Duration);
    DrawDebugLine(World, Corners[1], Corners[3], Color, false, Duration);
    DrawDebugLine(World, Corners[1], Corners[5], Color, false, Duration);
    DrawDebugLine(World, Corners[2], Corners[3], Color, false, Duration);
    DrawDebugLine(World, Corners[2], Corners[6], Color, false, Duration);
    DrawDebugLine(World, Corners[3], Corners[7], Color, false, Duration);
    DrawDebugLine(World, Corners[4], Corners[5], Color, false, Duration);
    DrawDebugLine(World, Corners[4], Corners[6], Color, false, Duration);
    DrawDebugLine(World, Corners[5], Corners[7], Color, false, Duration);
    DrawDebugLine(World, Corners[6], Corners[7], Color, false, Duration);
}

위 코드처럼 세 축 벡터의 부호 조합으로 여덟 꼭짓점을 구하고 12개 모서리를 선으로 연결하면 OBB를 화면에 정확하게 시각화할 수 있습니다. 판정 디버깅 시 DrawDebugOrientedBoxTick에서 호출하면 OBB가 오브젝트의 회전을 실시간으로 따라가는 것을 확인할 수 있습니다.

4. AABB와 OBB의 비교

항목 AABB OBB
표현 구조체 FBox, FBoxSphereBounds FOrientedBox
회전 추적 여부 추적하지 않음 오브젝트와 함께 회전
타이트한 근사 회전 시 헐겁게 됨 항상 타이트
교차 판정 비용 매우 낮음 (축 비교 3회) 높음 (SAT, 최대 15축)
주요 용도 브로드페이즈, 컬링, 스트리밍 정밀 전투 판정, 물리 내로우페이즈
UE5 직접 API Bounds.GetBox(), GetLocalBounds() 직접 구성 필요 (FOrientedBox)

일반적인 전략은 두 방식을 계층적으로 조합하는 것입니다. 먼저 AABB로 넓은 범위의 후보를 빠르게 걸러내고, 통과한 후보에 대해서만 OBB로 정밀 검사를 수행하면 연산량을 크게 줄일 수 있습니다.

5. 일반 C++과의 비교

일반 C++에서도 AABB는 최솟값·최댓값 FVector(또는 glm::vec3) 한 쌍으로 표현하며, 두 박스의 교차 여부는 각 축의 범위 비교만으로 판별합니다. 이 기본 알고리즘은 언리얼의 FBox::Intersect와 동일한 원리입니다.

그러나 언리얼 엔진은 여기에 다음과 같은 확장을 더합니다.

첫째, FBox+ 연산자로 박스를 합산하고, Contains, ExpandBy, TransformBy 등 다양한 유틸리티 함수를 내장합니다. 순수 C++ 구현에서는 이를 모두 직접 작성해야 합니다.

둘째, FBoxSphereBounds는 AABB와 바운딩 스피어를 동시에 관리하여 렌더러와 물리 시스템이 단일 구조체로 두 가지 컬링 방식을 모두 활용할 수 있게 합니다. 일반 C++에서는 이를 별도로 관리해야 합니다.

셋째, UPrimitiveComponentBounds는 엔진이 트랜스폼 변경 시 자동으로 갱신하지만, 일반 C++에서는 직접 갱신 시점을 관리해야 합니다.

// 일반 C++ 방식 — 직접 구현한 AABB 교차 판정
struct SimpleAABB
{
    glm::vec3 Min, Max;

    bool Intersects(const SimpleAABB& Other) const
    {
        // 각 축에서 범위가 겹치는지 확인합니다.
        return (Min.x <= Other.Max.x && Max.x >= Other.Min.x) &&
               (Min.y <= Other.Max.y && Max.y >= Other.Min.y) &&
               (Min.z <= Other.Max.z && Max.z >= Other.Min.z);
    }
};

// UE5 방식 — FBox가 동일한 로직을 내장합니다.
// FBox BoxA, BoxB;
// bool bOverlap = BoxA.Intersect(BoxB); // 내부적으로 동일한 판정을 수행합니다.

위 코드처럼 판정 로직 자체는 같지만, 언리얼 엔진은 이를 엔진 라이프사이클과 통합하여 훨씬 편리하게 사용할 수 있도록 제공합니다.

6. 유니티 엔진과의 비교

유니티에서는 Renderer.bounds로 월드 공간 AABB를 나타내는 Bounds 구조체를 얻을 수 있습니다. 이 구조체는 center, extents, size, min, max 프로퍼티를 가지며 언리얼의 FBoxSphereBounds와 유사합니다. 로컬 공간 바운드는 Mesh.bounds로 얻을 수 있으며, 이는 언리얼의 GetLocalBounds에 해당합니다.

그러나 유니티에는 엔진 빌트인 OBB 타입이 없습니다. Physics.OverlapBox 함수가 내부적으로 OBB 겹침 판정을 수행하지만, 개발자가 직접 FOrientedBox에 해당하는 구조체를 얻거나 조작할 수는 없습니다. OBB가 필요한 경우 유니티에서는 직접 구현하거나 서드파티 라이브러리를 사용해야 합니다.

반면 언리얼 엔진은 FOrientedBox라는 명시적 구조체를 제공하며, FTransformGetLocalBounds를 결합하면 정형화된 방식으로 OBB를 구성할 수 있습니다. 또한 FBoxSphereBounds가 바운딩 박스와 스피어를 함께 관리하므로, 렌더러가 상황에 따라 두 방식 중 더 효율적인 컬링을 자동으로 선택할 수 있다는 점도 언리얼만의 강점입니다.

7. 주의사항

7-1. Bounds는 월드 공간, GetLocalBounds는 로컬 공간입니다.

UPrimitiveComponent::Bounds는 항상 월드 공간 기준이며, 오브젝트의 트랜스폼(위치, 회전, 스케일)이 모두 반영되어 있습니다. 회전이 적용된 오브젝트의 Bounds를 OBB 구성에 그대로 사용하면, 이미 월드 축으로 늘어난 AABB를 기반으로 OBB를 만드는 오류가 발생합니다. OBB를 구성할 때는 반드시 GetLocalBounds로 로컬 공간의 원래 크기를 가져온 뒤 트랜스폼을 적용해야 합니다.

7-2. Extent는 절반 크기입니다.

FBoxSphereBounds::BoxExtentFBox::GetExtent()는 모두 중심에서 면까지의 절반 크기(Half-Extent) 를 반환합니다. 전체 크기가 필요하다면 두 배를 해야 하며, 이를 혼동하면 바운딩 박스가 실제의 절반이나 두 배 크기로 계산되는 오류가 발생합니다.

7-3. 비균등 스케일(Non-uniform Scale)은 OBB의 직교성을 깨뜨릴 수 있습니다.

액터에 비균등 스케일이 적용된 경우 (Scale = (2, 1, 0.5) 등), 로컬 축을 단순히 월드로 변환한 뒤 정규화하면 축이 서로 직교하지 않는 문제가 발생할 수 있습니다. 이 경우 스케일을 축 벡터가 아닌 ExtentX/Y/Z에만 반영하고, 축 벡터 자체는 회전 성분으로만 구해야 합니다. FQuat::RotateVector를 활용하거나 FTransform::GetRotation()에서 축을 구하는 것이 안전합니다.

7-4. OBB 판정은 브로드페이즈에 사용하지 않는 것이 좋습니다.

SAT 기반 OBB 교차 판정은 AABB 비교보다 연산 비용이 현저히 높습니다. 수천 개의 오브젝트를 대상으로 OBB 판정을 매 프레임 수행하면 성능에 심각한 영향을 줄 수 있습니다. 실제로 사용해보면 AABB 브로드페이즈로 후보를 수십 개 이하로 줄인 뒤, 남은 후보에 대해서만 OBB 판정을 수행하는 계층적 구조가 중요합니다.

7-5. CalcBounds 오버라이드 시 부모를 반드시 고려하세요.

커스텀 UPrimitiveComponent를 제작하고 CalcBounds를 오버라이드하여 바운드를 직접 계산할 경우, 반환되는 FBoxSphereBounds의 SphereRadius를 BoxExtent의 크기에 맞게 함께 갱신해야 합니다. SphereRadius가 올바르지 않으면 렌더링 컬링 판정에서 오브젝트가 부정확하게 제거될 수 있습니다.

 

 

UPrimitiveComponent의 바운딩 박스는 AABB와 OBB라는 두 개념을 이해하고, 각각의 API(Bounds, GetLocalBounds, FOrientedBox)를 상황에 맞게 조합하는 것이 핵심입니다. 연산 비용이 낮은 AABB로 넓은 범위를 빠르게 걸러내고, 정밀도가 필요한 경우에만 OBB를 적용하는 계층적 전략을 따르면, 복잡한 전투 판정이나 공간 최적화를 성능 손실 없이 구현할 수 있습니다.

반응형