황현동 블로그 개발, 인생, 유우머

260113 유니티 충돌 감지 용어 정리

Tags:

유니티 충돌 감지 용어 심층 가이드

게임 개발에서 물리 기반 충돌 감지는 필수적인 요소입니다. 유니티(Unity) 게임 엔진은 다양한 충돌 감지 메커니즘을 제공하며, 이를 효과적으로 활용하기 위해서는 핵심 용어들에 대한 깊이 있는 이해가 필요합니다. 이 가이드에서는 유니티 충돌 감지와 관련된 5가지 핵심 개념을 상세히 살펴보겠습니다.


1. Bounding Box (바운딩 박스)

1.1 개념 및 정의

Bounding Box(바운딩 박스)는 3D 객체를 둘러싸는 가상의 직육면체 볼륨입니다. 복잡한 메쉬의 형태를 단순화하여 빠른 충돌 검사를 가능하게 하는 기본적인 충돌 감지 기법입니다.

바운딩 박스는 다음과 같은 특징을 가집니다:

  • 단순화된 볼륨: 복잡한 3D 메쉬를 간단한 박스 형태로 근사화
  • 빠른 계산: 박스 간의 교차 검사는 매우 효율적
  • 근사적 정확도: 정확한 충돌이 아닌 “대략적인” 충돌 감지

1.2 Unity에서의 구현

Unity에서 바운딩 박스는 Bounds 구조체로 표현됩니다:

// Collider의 바운딩 박스 접근
Collider collider = GetComponent<Collider>();
Bounds bounds = collider.bounds;

// 바운딩 박스 정보 확인
Vector3 center = [bounds.center](http://bounds.center);      // 중심점
Vector3 size = bounds.size;          // 크기 (width, height, depth)
Vector3 min = bounds.min;            // 최소 좌표
Vector3 max = bounds.max;            // 최대 좌표
Vector3 extents = bounds.extents;    // 반 크기 (size / 2)

1.3 바운딩 박스 교차 검사

Unity는 Bounds.Intersects() 메서드를 제공하여 두 바운딩 박스의 교차 여부를 확인할 수 있습니다:

public class BoundingBoxChecker : MonoBehaviour
{
    public Collider targetCollider;
    
    void Update()
    {
        // 현재 객체의 바운딩 박스
        Bounds myBounds = GetComponent<Collider>().bounds;
        
        // 대상 객체의 바운딩 박스
        Bounds targetBounds = targetCollider.bounds;
        
        // 교차 검사
        if (myBounds.Intersects(targetBounds))
        {
            Debug.Log("바운딩 박스 충돌 감지!");
        }
    }
}

1.4 사용 시나리오

바운딩 박스는 다음과 같은 상황에서 유용합니다:

  1. 초기 충돌 필터링: 정밀한 충돌 검사 전에 “대략적인” 충돌 가능성을 빠르게 판단
  2. 공간 분할 (Spatial Partitioning): Octree, BSP Tree 등의 공간 분할 알고리즘에서 활용
  3. 컬링 (Culling): 카메라 절두체(Frustum) 안에 있는 객체를 빠르게 판별
  4. 레이캐스트 최적화: 레이캐스트 전에 바운딩 박스로 대상을 필터링

1.5 성능 고려사항

바운딩 박스는 매우 효율적이지만 다음 사항을 주의해야 합니다:

// 좋은 예: 한 번만 계산
void Start()
{
    Bounds bounds = GetComponent<Collider>().bounds;
    // bounds를 캐싱하여 재사용
}

// 나쁜 예: 매 프레임마다 계산
void Update()
{
    // 매번 bounds를 새로 가져오는 것은 비효율적
    if (GetComponent<Collider>().bounds.Intersects(otherBounds))
    {
        // ...
    }
}

최적화 팁:

  • 바운딩 박스 정보는 가능한 캐싱하여 재사용
  • 정적 객체의 경우 시작 시 한 번만 계산
  • 동적 객체는 변환(Transform)이 변경될 때만 재계산

1.6 시각화 (디버깅)

개발 중 바운딩 박스를 시각화하면 디버깅에 유용합니다:

void OnDrawGizmos()
{
    Collider collider = GetComponent<Collider>();
    if (collider != null)
    {
        // 바운딩 박스를 녹색 와이어프레임으로 그리기
        Gizmos.color = [Color.green](http://Color.green);
        Gizmos.DrawWireCube([collider.bounds.center](http://collider.bounds.center), collider.bounds.size);
    }
}

2. Overlap Box (오버랩 박스)

2.1 개념 및 정의

Overlap Box는 특정 위치에 가상의 박스 영역을 생성하고, 그 영역 내에 있는 모든 콜라이더를 탐지하는 유니티의 Physics 쿼리 메서드입니다. 레이캐스트와 달리 “방향”이 없으며, 특정 순간의 “공간적 쿼리”를 수행합니다.

2.2 Unity Physics.OverlapBox API

Unity는 3D와 2D 버전 모두 제공합니다:

// 3D 버전
public static Collider[] OverlapBox(
    Vector3 center,                // 박스의 중심 위치
    Vector3 halfExtents,           // 박스의 반 크기 (size / 2)
    Quaternion orientation = Quaternion.identity,  // 회전
    int layerMask = AllLayers,     // 레이어 마스크
    QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal
);

// 2D 버전
public static Collider2D OverlapBox(
    Vector2 point,
    Vector2 size,
    float angle,
    int layerMask = DefaultRaycastLayers
);

2.3 실전 예제

예제 1: 폭발 범위 내 적 탐지

public class ExplosionDetector : MonoBehaviour
{
    public Vector3 explosionCenter;
    public Vector3 explosionSize = new Vector3(5f, 5f, 5f);
    public LayerMask enemyLayer;
    
    public void DetectEnemiesInExplosion()
    {
        // 폭발 범위 내의 모든 적 탐지
        Collider[] hitColliders = Physics.OverlapBox(
            explosionCenter,
            explosionSize / 2f,  // halfExtents 계산
            Quaternion.identity,
            enemyLayer
        );
        
        // 탐지된 적들에게 피해 적용
        foreach (Collider hitCollider in hitColliders)
        {
            Enemy enemy = hitCollider.GetComponent<Enemy>();
            if (enemy != null)
            {
                float distance = Vector3.Distance(explosionCenter, hitCollider.transform.position);
                float damage = CalculateDamage(distance);
                enemy.TakeDamage(damage);
            }
        }
        
        Debug.Log($"폭발 범위에서 {hitColliders.Length}개의 적 탐지");
    }
    
    float CalculateDamage(float distance)
    {
        // 거리에 따른 데미지 감소
        float maxDistance = explosionSize.magnitude;
        return Mathf.Lerp(100f, 10f, distance / maxDistance);
    }
}

예제 2: 건축 가능 영역 확인

public class BuildingPlacement : MonoBehaviour
{
    public Vector3 buildingSize = new Vector3(3f, 2f, 3f);
    public LayerMask obstructionLayer;
    
    public bool CanPlaceBuilding(Vector3 position)
    {
        // 건물을 배치할 위치에 장애물이 있는지 확인
        Collider[] obstacles = Physics.OverlapBox(
            position + Vector3.up * (buildingSize.y / 2f),  // 중심을 지면에서 올림
            buildingSize / 2f,
            Quaternion.identity,
            obstructionLayer
        );
        
        // 장애물이 없으면 배치 가능
        return obstacles.Length == 0;
    }
    
    void OnDrawGizmos()
    {
        // 건물 배치 영역 시각화
        Vector3 position = transform.position;
        bool canPlace = CanPlaceBuilding(position);
        
        Gizmos.color = canPlace ? [Color.green](http://Color.green) : [Color.red](http://Color.red);
        Gizmos.DrawWireCube(
            position + Vector3.up * (buildingSize.y / 2f),
            buildingSize
        );
    }
}

2.4 OverlapBox vs BoxCast 비교

많은 개발자들이 OverlapBoxBoxCast를 혼동합니다. 두 메서드의 차이점을 명확히 이해하는 것이 중요합니다:

OverlapBox

  • 정적 공간 쿼리: 특정 위치에서 “지금 이 순간” 겹치는 것을 찾음
  • 방향 없음: 이동이나 방향 개념이 없음
  • 모든 겹침 반환: 박스와 겹치는 모든 콜라이더 배열 반환
  • 사용 예: 폭발 범위, 스킬 효과 범위, 건축 가능 영역 체크

BoxCast

  • 동적 스윕 쿼리: 박스를 특정 방향으로 “이동”시키며 충돌 감지
  • 방향 있음: 레이캐스트처럼 방향과 거리가 있음
  • 첫 번째 충돌 반환: 경로상 처음 만나는 충돌 정보 반환
  • 사용 예: 두꺼운 레이캐스트, 이동 예측, 스윕 충돌 감지
public class OverlapVsBoxCast : MonoBehaviour
{
    public Vector3 boxSize = new Vector3(2f, 2f, 2f);
    
    void CompareDetectionMethods()
    {
        Vector3 center = transform.position;
        
        // OverlapBox: "여기에 뭐가 있나요?"
        Collider[] overlaps = Physics.OverlapBox(
            center,
            boxSize / 2f
        );
        Debug.Log($"OverlapBox: {overlaps.Length}개의 콜라이더 발견");
        
        // BoxCast: "이 방향으로 가면 뭔가 부딪히나요?"
        RaycastHit hit;
        bool didHit = Physics.BoxCast(
            center,
            boxSize / 2f,
            Vector3.forward,  // 방향
            out hit,
            Quaternion.identity,
            10f  // 최대 거리
        );
        
        if (didHit)
        {
            Debug.Log($"BoxCast: {hit.distance}m 앞에서 {[hit.collider.name](http://hit.collider.name)} 충돌");
        }
    }
}

2.5 성능 최적화

OverlapBox는 강력하지만 성능 비용이 있습니다:

public class OptimizedOverlap : MonoBehaviour
{
    // 나쁜 예: 매 프레임 실행
    void Update_Bad()
    {
        Collider[] hits = Physics.OverlapBox(
            transform.position,
            [Vector3.one](http://Vector3.one)
        );
        // 매우 비효율적!
    }
    
    // 좋은 예 1: 주기적으로 실행
    private float checkInterval = 0.2f;  // 0.2초마다
    private float nextCheckTime = 0f;
    
    void Update_Good1()
    {
        if (Time.time >= nextCheckTime)
        {
            nextCheckTime = Time.time + checkInterval;
            
            Collider[] hits = Physics.OverlapBox(
                transform.position,
                [Vector3.one](http://Vector3.one)
            );
            // 처리 로직
        }
    }
    
    // 좋은 예 2: 레이어 마스크 활용
    public LayerMask targetLayers;  // Inspector에서 설정
    
    void Update_Good2()
    {
        // 특정 레이어만 검사 (훨씬 빠름)
        Collider[] hits = Physics.OverlapBox(
            transform.position,
            [Vector3.one](http://Vector3.one),
            Quaternion.identity,
            targetLayers  // 레이어 필터링
        );
    }
    
    // 좋은 예 3: NonAlloc 버전 사용
    private Collider[] resultsBuffer = new Collider[10];  // 재사용 가능한 배열
    
    void Update_Good3()
    {
        // 메모리 할당 없이 검사 (GC 압력 감소)
        int count = Physics.OverlapBoxNonAlloc(
            transform.position,
            [Vector3.one](http://Vector3.one),
            resultsBuffer,
            Quaternion.identity,
            targetLayers
        );
        
        // resultsBuffer의 앞 count개만 유효
        for (int i = 0; i < count; i++)
        {
            Debug.Log($"발견: {resultsBuffer[i].name}");
        }
    }
}

최적화 체크리스트:

  1. 매 프레임 호출 피하기 (주기적 체크 또는 이벤트 기반)
  2. 레이어 마스크로 검사 대상 제한
  3. OverlapBoxNonAlloc 사용으로 GC 압력 감소
  4. 박스 크기를 필요한 만큼만 설정
  5. Trigger 콜라이더 쿼리 옵션 적절히 설정

2.6 디버깅 시각화

public class OverlapBoxVisualizer : MonoBehaviour
{
    public Vector3 boxCenter;
    public Vector3 boxSize = [Vector3.one](http://Vector3.one);
    public Quaternion boxRotation = Quaternion.identity;
    
    void OnDrawGizmos()
    {
        // OverlapBox 영역을 시각화
        Gizmos.color = Color.yellow;
        Gizmos.matrix = Matrix4x4.TRS(boxCenter, boxRotation, [Vector3.one](http://Vector3.one));
        Gizmos.DrawWireCube([Vector3.zero](http://Vector3.zero), boxSize);
    }
    
    void OnDrawGizmosSelected()
    {
        // 실제로 감지된 콜라이더들 표시
        Collider[] hits = Physics.OverlapBox(
            boxCenter,
            boxSize / 2f,
            boxRotation
        );
        
        Gizmos.color = [Color.red](http://Color.red);
        foreach (Collider hit in hits)
        {
            Gizmos.DrawLine(boxCenter, hit.transform.position);
            Gizmos.DrawSphere(hit.transform.position, 0.2f);
        }
    }
}

3. Penetration (관통/침투)

3.1 개념 및 정의

Penetration(침투)은 두 콜라이더가 서로 겹쳐 있을 때, 얼마나 깊이 침투했는지를 나타내는 개념입니다. 게임 물리 엔진에서는 콜라이더가 서로 관통하는 것을 방지하기 위해 침투 깊이(Penetration Depth)를 계산하고, 이를 기반으로 객체들을 분리시킵니다.

물리 엔진의 작동 원리:

  1. 충돌 감지: 두 객체가 겹쳤는지 확인
  2. 침투 깊이 계산: 얼마나 겹쳤는지 측정
  3. 분리 벡터 계산: 어느 방향으로 밀어내야 하는지 결정
  4. 충돌 응답: 객체를 분리하고 물리적 반응(반발, 마찰 등) 적용

3.2 Unity Physics.ComputePenetration API

Unity는 두 콜라이더 간의 침투 정보를 수동으로 계산할 수 있는 API를 제공합니다:

public static bool ComputePenetration(
    Collider colliderA,           // 첫 번째 콜라이더
    Vector3 positionA,            // A의 위치
    Quaternion rotationA,         // A의 회전
    Collider colliderB,           // 두 번째 콜라이더
    Vector3 positionB,            // B의 위치
    Quaternion rotationB,         // B의 회전
    out Vector3 direction,        // 분리 방향 (출력)
    out float distance            // 침투 깊이 (출력)
);

반환값:

  • true: 콜라이더가 겹쳐 있음 (침투 발생)
  • false: 콜라이더가 겹쳐 있지 않음

출력 매개변수:

  • direction: colliderA를 이 방향으로 이동시키면 분리됨
  • distance: 최소 분리 거리 (direction * distance만큼 이동하면 분리)

3.3 실전 예제

예제 1: 침투 깊이 계산 및 분리

public class PenetrationResolver : MonoBehaviour
{
    public Collider myCollider;
    public Collider otherCollider;
    
    void FixedUpdate()
    {
        // 침투 계산
        Vector3 direction;
        float distance;
        
        bool isOverlapping = Physics.ComputePenetration(
            myCollider,
            myCollider.transform.position,
            myCollider.transform.rotation,
            otherCollider,
            otherCollider.transform.position,
            otherCollider.transform.rotation,
            out direction,
            out distance
        );
        
        if (isOverlapping)
        {
            Debug.Log($"침투 깊이: {distance}m, 방향: {direction}");
            
            // 침투 해결: 계산된 방향으로 객체 이동
            transform.position += direction * distance;
            
            // 또는 절반씩 이동 (양쪽 객체를 조금씩 밀어냄)
            // transform.position += direction * (distance * 0.5f);
        }
    }
}

예제 2: 캐릭터 밀어내기 (Push System)

public class CharacterPushSystem : MonoBehaviour
{
    private Collider characterCollider;
    private Rigidbody rb;
    
    void Start()
    {
        characterCollider = GetComponent<Collider>();
        rb = GetComponent<Rigidbody>();
    }
    
    void OnCollisionStay(Collision collision)
    {
        // 벽이나 장애물과 겹쳤을 때 부드럽게 밀어냄
        Collider obstacleCollider = collision.collider;
        
        Vector3 pushDirection;
        float penetrationDepth;
        
        bool isPenetrating = Physics.ComputePenetration(
            characterCollider,
            transform.position,
            transform.rotation,
            obstacleCollider,
            obstacleCollider.transform.position,
            obstacleCollider.transform.rotation,
            out pushDirection,
            out penetrationDepth
        );
        
        if (isPenetrating)
        {
            // 침투 깊이에 비례한 힘으로 밀어냄
            float pushForce = penetrationDepth * 100f;  // 강도 조절
            rb.AddForce(pushDirection * pushForce, ForceMode.Force);
            
            Debug.Log($"밀어내기: {penetrationDepth:F3}m 침투, 방향: {pushDirection}");
        }
    }
}

예제 3: 침투 허용 범위 (Tolerance)

실무에서는 작은 침투는 허용하는 경우가 많습니다:

public class PenetrationTolerance : MonoBehaviour
{
    public Collider colliderA;
    public Collider colliderB;
    
    [Range(0f, 0.1f)]
    public float toleranceDistance = 0.01f;  // 1cm까지는 허용
    
    public bool HasSignificantPenetration()
    {
        Vector3 direction;
        float distance;
        
        bool isOverlapping = Physics.ComputePenetration(
            colliderA,
            colliderA.transform.position,
            colliderA.transform.rotation,
            colliderB,
            colliderB.transform.position,
            colliderB.transform.rotation,
            out direction,
            out distance
        );
        
        // 침투가 있고, 허용 범위를 초과하는 경우만 true
        if (isOverlapping && distance > toleranceDistance)
        {
            Debug.LogWarning($"의미있는 침투 감지: {distance:F4}m (허용: {toleranceDistance}m)");
            return true;
        }
        
        return false;
    }
}

3.4 PhysX와 침투 처리

Unity의 물리 엔진인 PhysX는 자동으로 침투를 처리합니다:

시뮬레이션 단계:
1. 충돌 감지 (Broad Phase → Narrow Phase)
2. 침투 깊이 및 방향 계산
3. 제약 조건(Constraint) 생성
4. 반복적 해결 (Iterative Solver)
5. 위치 및 속도 보정

중요한 개념:

  • PhysX는 충돌을 “예방”하지 않고 “감지 후 해결”합니다
  • 빠른 물체는 여러 프레임에 걸쳐 분리될 수 있습니다
  • 이것이 고속 물체에 Continuous Collision Detection이 필요한 이유입니다

3.5 실무 활용 시나리오

1. 건축 시스템 - 간섭 검사

public class BuildingInterferenceChecker : MonoBehaviour
{
    public float allowedPenetration = 0.05f;  // 5cm 허용
    
    public bool CheckBuildingInterference(Collider newBuilding, Collider[] existingBuildings)
    {
        foreach (Collider existing in existingBuildings)
        {
            Vector3 direction;
            float depth;
            
            bool hasInterference = Physics.ComputePenetration(
                newBuilding,
                newBuilding.transform.position,
                newBuilding.transform.rotation,
                existing,
                existing.transform.position,
                existing.transform.rotation,
                out direction,
                out depth
            );
            
            // 허용 범위를 초과하는 간섭이 있으면 건축 불가
            if (hasInterference && depth > allowedPenetration)
            {
                Debug.LogError($"건축 불가: {[existing.name](http://existing.name)}과 {depth:F3}m 간섭 (허용: {allowedPenetration}m)");
                return false;
            }
        }
        
        return true;  // 간섭 없음, 건축 가능
    }
}

2. 캐릭터 컨트롤러 - 경사면 보정

public class SlopeCorrection : MonoBehaviour
{
    private CapsuleCollider capsuleCollider;
    
    void FixedUpdate()
    {
        // 경사면에서 캐릭터가 지형에 "파묻히는" 것을 방지
        RaycastHit hit;
        if (Physics.Raycast(transform.position, Vector3.down, out hit, 2f))
        {
            Vector3 direction;
            float penetration;
            
            bool isPenetrating = Physics.ComputePenetration(
                capsuleCollider,
                transform.position,
                transform.rotation,
                hit.collider,
                hit.collider.transform.position,
                hit.collider.transform.rotation,
                out direction,
                out penetration
            );
            
            if (isPenetrating && penetration > 0.01f)
            {
                // 지형에서 캐릭터를 들어올림
                transform.position += Vector3.up * penetration;
            }
        }
    }
}

3.6 제약 사항 및 주의사항

API 제한 사항:

  • Physics.ComputePenetration은 일부 콜라이더 조합에서 부정확할 수 있습니다
  • 매우 복잡한 메쉬 콜라이더 간의 계산은 비용이 높습니다
  • 회전이 복잡한 경우 정확도가 떨어질 수 있습니다

디버깅 팁:

void OnDrawGizmos()
{
    // 침투 방향 시각화
    Vector3 direction;
    float distance;
    
    if (Physics.ComputePenetration(
        colliderA, colliderA.transform.position, colliderA.transform.rotation,
        colliderB, colliderB.transform.position, colliderB.transform.rotation,
        out direction, out distance))
    {
        // 빨간 화살표로 침투 방향 표시
        Gizmos.color = [Color.red](http://Color.red);
        Vector3 start = colliderA.transform.position;
        Vector3 end = start + direction * distance;
        Gizmos.DrawLine(start, end);
        Gizmos.DrawSphere(end, 0.1f);
        
        // 거리 텍스트 표시 (Scene 뷰에서)
#if UNITY_EDITOR
        UnityEditor.Handles.Label(end, $"Penetration: {distance:F3}m");
#endif
    }
}

4. Convex Collider (볼록 콜라이더)

4.1 수학적 정의

Convex(볼록) 도형의 수학적 정의:

도형 내의 임의의 두 점을 선택했을 때, 그 두 점을 연결하는 선분이 완전히 도형 내부에 있다면 그 도형은 볼록(Convex)하다.

반대로 Concave(오목) 도형은:

도형 내의 어떤 두 점을 연결하는 선분이 도형 외부를 지나가는 경우가 있다면 그 도형은 오목(Concave)하다.

Convex 예시:               Concave 예시:
    ●───●                     ●───●
   /     \                   /     \
  ●       ●                 ●   ○   ●  ← 이 점들을 연결하면
   \     /                   \  ↓  /      선분이 바깥으로 나감
    ●───●                     ●─┼─●
                                 ●

4.2 Unity Mesh Collider의 Convex 옵션

Unity에서 Mesh Collider는 두 가지 모드로 작동합니다:

Convex = False (기본값)

  • 정확한 형태: 메쉬의 모든 세부사항을 그대로 유지
  • 오목한 부분 포함: 구멍, 홈, 들어간 부분 모두 충돌 처리
  • 제한 사항:
    • 다른 Mesh Collider와 충돌 불가
    • Rigidbody와 함께 사용 불가 (정적 객체만 가능)
    • 더 많은 계산 비용

Convex = True

  • 단순화된 형태: Convex Hull(볼록 껍질) 생성
  • 오목한 부분 제거: 모든 “들어간” 부분이 “채워짐”
  • 이점:
    • 다른 모든 콜라이더와 충돌 가능
    • Rigidbody와 함께 사용 가능 (동적 물체)
    • 더 빠른 충돌 계산
  • 제한 사항:
    • 최대 255개 삼각형으로 제한
    • 형태의 정확도 손실

4.3 Convex Hull 알고리즘

Convex Hull(볼록 껍질)은 주어진 점 집합을 모두 포함하는 최소 볼록 다각형입니다. 마치 점들 주위에 고무줄을 씌운 것과 같습니다.

Unity는 내부적으로 Quickhull 알고리즘을 사용하여 Convex Hull을 계산합니다:

Quickhull 알고리즘 개념:
1. 가장 멀리 떨어진 두 점을 찾아 초기 선분 생성
2. 이 선분으로부터 가장 먼 점을 찾아 삼각형 생성
3. 각 면에 대해 가장 먼 외부 점들을 재귀적으로 처리
4. 더 이상 외부 점이 없을 때까지 반복

4.4 실전 예제

예제 1: 런타임에서 Convex 설정

public class ConvexSwitcher : MonoBehaviour
{
    private MeshCollider meshCollider;
    
    void Start()
    {
        meshCollider = GetComponent<MeshCollider>();
        
        // 초기에는 정확한 충돌 (정적 객체)
        meshCollider.convex = false;
    }
    
    public void MakeItFall()
    {
        // 동적 물체로 변경 (낙하시킬 때)
        meshCollider.convex = true;  // Convex로 전환
        
        // 이제 Rigidbody 추가 가능
        Rigidbody rb = gameObject.AddComponent<Rigidbody>();
        rb.mass = 10f;
        
        Debug.Log("객체가 이제 떨어집니다!");
    }
}

예제 2: Convex 품질 검증

public class ConvexQualityChecker : MonoBehaviour
{
    void Start()
    {
        MeshCollider meshCollider = GetComponent<MeshCollider>();
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        
        if (meshCollider.convex)
        {
            int originalTriangles = meshFilter.mesh.triangles.Length / 3;
            
            // Convex Hull의 삼각형 수는 내부적으로 계산됨
            // 원본과 비교하여 단순화 정도 확인
            
            if (originalTriangles > 255)
            {
                Debug.LogWarning(
                    $"{[gameObject.name](http://gameObject.name)}: 원본 메쉬가 {originalTriangles}개의 삼각형을 가지고 있습니다. " +
                    $"Convex 모드에서는 255개로 제한되어 형태 정확도가 크게 손실될 수 있습니다."
                );
            }
            
            // 볼륨 비교 (근사치)
            float originalVolume = CalculateMeshVolume(meshFilter.mesh);
            // Note: Convex hull의 볼륨은 항상 원본보다 크거나 같음
            
            Debug.Log($"Convex Hull 사용 중: 원본 {originalTriangles}개 삼각형");
        }
    }
    
    float CalculateMeshVolume(Mesh mesh)
    {
        // 간단한 볼륨 계산 (근사치)
        float volume = 0f;
        Vector3[] vertices = mesh.vertices;
        int[] triangles = mesh.triangles;
        
        for (int i = 0; i < triangles.Length; i += 3)
        {
            Vector3 p1 = vertices[triangles[i]];
            Vector3 p2 = vertices[triangles[i + 1]];
            Vector3 p3 = vertices[triangles[i + 2]];
            
            volume += SignedVolumeOfTriangle(p1, p2, p3);
        }
        
        return Mathf.Abs(volume);
    }
    
    float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
    {
        return [Vector3.Dot](http://Vector3.Dot)(p1, Vector3.Cross(p2, p3)) / 6.0f;
    }
}

예제 3: 복합 Convex 콜라이더 (Compound Collider)

복잡한 오목한 형태를 여러 Convex 콜라이더로 분해하여 정확도를 높일 수 있습니다:

public class CompoundConvexCollider : MonoBehaviour
{
    public Mesh[] convexParts;  // 분해된 Convex 메쉬들
    
    void Start()
    {
        // 부모 객체 (물리 속성 보유)
        Rigidbody rb = gameObject.AddComponent<Rigidbody>();
        
        // 각 Convex 부분을 자식으로 추가
        for (int i = 0; i < convexParts.Length; i++)
        {
            GameObject part = new GameObject($"ConvexPart_{i}");
            part.transform.SetParent(transform);
            part.transform.localPosition = [Vector3.zero](http://Vector3.zero);
            part.transform.localRotation = Quaternion.identity;
            
            MeshCollider collider = part.AddComponent<MeshCollider>();
            collider.sharedMesh = convexParts[i];
            collider.convex = true;  // 각 부분은 Convex
            
            // 시각화용 (옵션)
            MeshFilter filter = part.AddComponent<MeshFilter>();
            filter.mesh = convexParts[i];
            MeshRenderer renderer = part.AddComponent<MeshRenderer>();
            renderer.material = new Material(Shader.Find("Standard"));
            renderer.material.color = new Color(0, 1, 0, 0.3f);  // 반투명 녹색
        }
        
        Debug.Log($"복합 Convex 콜라이더 생성: {convexParts.Length}개 부분");
    }
}

4.5 성능 및 최적화

성능 비교

public class ConvexPerformanceTest : MonoBehaviour
{
    void CompareColliderPerformance()
    {
        // 성능 순서 (빠름 → 느림)
        // 1. Primitive Colliders (Box, Sphere, Capsule) - 가장 빠름
        // 2. Convex Mesh Collider - 빠름
        // 3. Non-Convex Mesh Collider - 느림
        
        // 예시: 충돌 검사 횟수 측정
        int iterations = 10000;
        
        // Primitive (Box)
        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            Physics.OverlapBox([Vector3.zero](http://Vector3.zero), [Vector3.one](http://Vector3.one));
        }
        sw.Stop();
        Debug.Log($"Box Collider: {sw.ElapsedMilliseconds}ms");
        
        // Convex Mesh
        sw.Restart();
        MeshCollider convexCollider = GetComponent<MeshCollider>();
        convexCollider.convex = true;
        for (int i = 0; i < iterations; i++)
        {
            Physics.OverlapSphere([Vector3.zero](http://Vector3.zero), 1f);
        }
        sw.Stop();
        Debug.Log($"Convex Mesh: {sw.ElapsedMilliseconds}ms");
    }
}

최적화 전략

public class ConvexOptimizationStrategy : MonoBehaviour
{
    [Header("전략 1: Primitive 우선 사용")]
    public bool usePrimitiveWhenPossible = true;
    
    [Header("전략 2: LOD 기반 콜라이더")]
    public Mesh detailedConvexMesh;   // 가까이 있을 때
    public Mesh simplifiedConvexMesh; // 멀리 있을 때
    
    [Header("전략 3: 정적 vs 동적")]
    public bool isStaticObject = true;
    
    void Start()
    {
        OptimizeCollider();
    }
    
    void OptimizeCollider()
    {
        MeshCollider meshCollider = GetComponent<MeshCollider>();
        
        // 전략 1: 가능하면 Primitive 사용
        if (usePrimitiveWhenPossible && CanUsePrimitive())
        {
            // Mesh Collider 대신 Box Collider 사용
            Destroy(meshCollider);
            BoxCollider boxCollider = gameObject.AddComponent<BoxCollider>();
            Debug.Log("Primitive Collider로 최적화됨");
            return;
        }
        
        // 전략 2: LOD 적용
        float distanceToCamera = Vector3.Distance(transform.position, Camera.main.transform.position);
        if (distanceToCamera > 50f)
        {
            meshCollider.sharedMesh = simplifiedConvexMesh;
        }
        else
        {
            meshCollider.sharedMesh = detailedConvexMesh;
        }
        
        // 전략 3: 정적 객체는 Convex 비활성화
        if (isStaticObject)
        {
            meshCollider.convex = false;  // 더 정확한 충돌
            gameObject.isStatic = true;    // Unity의 정적 최적화
        }
        else
        {
            meshCollider.convex = true;    // 동적 물체에 필수
        }
    }
    
    bool CanUsePrimitive()
    {
        // 객체가 대략 박스 형태인지 확인하는 휴리스틱
        Bounds bounds = GetComponent<Renderer>().bounds;
        float volume = bounds.size.x * bounds.size.y * bounds.size.z;
        float meshVolume = CalculateMeshVolume(GetComponent<MeshFilter>().mesh);
        
        // 볼륨 비율이 0.7 이상이면 박스로 근사 가능
        return (meshVolume / volume) > 0.7f;
    }
    
    float CalculateMeshVolume(Mesh mesh)
    {
        // 이전 예제의 CalculateMeshVolume 메서드 사용
        return 0f;  // 실제 구현 필요
    }
}

4.6 실무 활용 가이드

언제 Convex를 사용할까?

시나리오 Convex 사용 이유
낙하하는 물체 ✅ Yes Rigidbody 필수
플레이어가 던지는 물체 ✅ Yes 동적 물체
이동하는 플랫폼 ✅ Yes Kinematic Rigidbody
지형 (Terrain) ❌ No 정적, 복잡한 형태
건물 외벽 ❌ No 정적, 정확한 충돌 필요
복잡한 실내 구조 ❌ No 오목한 부분 많음
간단한 상자/공 ✅ Yes 또는 Primitive 사용

Convex 품질 보장 체크리스트

public class ConvexQualityChecklist : MonoBehaviour
{
    void OnValidate()  // Inspector에서 값 변경 시 자동 실행
    {
        MeshCollider meshCollider = GetComponent<MeshCollider>();
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        
        if (meshCollider == null || meshFilter == null) return;
        
        if (meshCollider.convex)
        {
            List<string> warnings = new List<string>();
            
            // 체크 1: 삼각형 수
            int triangleCount = meshFilter.mesh.triangles.Length / 3;
            if (triangleCount > 255)
            {
                warnings.Add($"삼각형 수 초과: {triangleCount}/255 (단순화 추천)");
            }
            
            // 체크 2: 메쉬 크기
            float maxSize = Mathf.Max(
                meshFilter.mesh.bounds.size.x,
                meshFilter.mesh.bounds.size.y,
                meshFilter.mesh.bounds.size.z
            );
            if (maxSize > 100f)
            {
                warnings.Add($"메쉬가 너무 큼: {maxSize}m (스케일 축소 추천)");
            }
            
            // 체크 3: 오목한 부분
            if (HasConcaveFeatures(meshFilter.mesh))
            {
                warnings.Add("오목한 특징 감지: Convex Hull로 단순화됨");
            }
            
            // 체크 4: Rigidbody 확인
            Rigidbody rb = GetComponent<Rigidbody>();
            if (rb == null)
            {
                warnings.Add("Rigidbody 없음: Convex 불필요할 수 있음");
            }
            
            // 경고 출력
            if (warnings.Count > 0)
            {
                Debug.LogWarning(
                    $"[{[gameObject.name](http://gameObject.name)}] Convex 품질 경고:\n" +
                    string.Join("\n", warnings)
                );
            }
        }
    }
    
    bool HasConcaveFeatures(Mesh mesh)
    {
        // 간단한 오목 감지: 메쉬 볼륨 vs Bounds 볼륨
        float meshVolume = CalculateMeshVolume(mesh);
        float boundsVolume = mesh.bounds.size.x * mesh.bounds.size.y * mesh.bounds.size.z;
        
        // 볼륨 비율이 0.6 이하면 오목한 특징이 많음
        return (meshVolume / boundsVolume) < 0.6f;
    }
    
    float CalculateMeshVolume(Mesh mesh)
    {
        // 구현 (이전 예제 참조)
        return 0f;
    }
}

5. OBB vs AABB (Oriented Bounding Box vs Axis-Aligned Bounding Box)

5.1 개념 및 정의

바운딩 볼륨의 두 가지 주요 유형:

AABB (Axis-Aligned Bounding Box)

  • 축 정렬 바운딩 박스: 월드 좌표계의 X, Y, Z 축과 항상 평행
  • 회전 불가: 객체가 회전해도 AABB는 축과 평행하게 유지
  • 재계산 필요: 객체 회전 시 AABB를 다시 계산해야 함
  • 장점: 매우 빠른 충돌 검사 (6개의 평면만 비교)
  • 단점: 회전된 객체에 대해 낭비되는 공간이 많음

OBB (Oriented Bounding Box)

  • 방향 바운딩 박스: 객체의 로컬 좌표계를 따라 회전
  • 회전 가능: 객체와 함께 회전
  • 재계산 불필요: 객체 회전 시에도 박스 형태 유지
  • 장점: 회전된 객체를 더 타이트하게 감쌈
  • 단점: 더 복잡한 충돌 검사 (SAT 알고리즘 필요)

5.2 시각적 비교

사각형 객체 (45도 회전)

AABB:                      OBB:
  ┌─────────┐               ╱─────────╲
  │    ╱────┤              ╱  ╱────╲  ╲
  │   ╱  ■  │             │  ╱  ■  ╲  │  ← 객체에 딱 맞음
  │  ╱      │              ╲ ╱      ╲ ╱
  └─────────┘               ╲─────────╱
     ↑                            ↑
  낭비 공간 많음              낭비 공간 적음

5.3 Unity에서의 구현

Unity는 주로 AABB를 사용하지만, OBB 개념을 적용할 수 있습니다:

AABB 구현 (Unity 기본)

public class AABBExample : MonoBehaviour
{
    void Start()
    {
        // Unity의 Bounds는 AABB임
        Collider collider = GetComponent<Collider>();
        Bounds aabb = collider.bounds;
        
        Debug.Log($"AABB Center: {[aabb.center](http://aabb.center)}");
        Debug.Log($"AABB Size: {aabb.size}");
        Debug.Log($"AABB Min: {aabb.min}");
        Debug.Log($"AABB Max: {aabb.max}");
    }
    
    void Update()
    {
        // 객체를 회전시켜도
        transform.Rotate(0, 30 * Time.deltaTime, 0);
        
        // AABB는 항상 축과 평행하게 재계산됨
        Bounds aabb = GetComponent<Collider>().bounds;
        Debug.DrawLine(aabb.min, aabb.max, [Color.green](http://Color.green));
    }
    
    // AABB 충돌 검사 (매우 빠름)
    public bool AABBIntersects(Bounds a, Bounds b)
    {
        return (a.min.x <= b.max.x && a.max.x >= b.min.x) &&
               (a.min.y <= b.max.y && a.max.y >= b.min.y) &&
               (a.min.z <= b.max.z && a.max.z >= b.min.z);
    }
}

OBB 구현 (커스텀)

public struct OBB
{
    public Vector3 center;        // 중심점
    public Vector3[] axes;        // 3개의 로컬 축 (right, up, forward)
    public Vector3 extents;       // 각 축 방향의 반 크기
    
    public OBB(Transform transform, Vector3 size)
    {
        center = transform.position;
        axes = new Vector3[3]
        {
            transform.right,
            transform.up,
            transform.forward
        };
        extents = size * 0.5f;
    }
    
    // OBB의 8개 꼭짓점 계산
    public Vector3[] GetCorners()
    {
        Vector3[] corners = new Vector3[8];
        
        for (int i = 0; i < 8; i++)
        {
            Vector3 corner = center;
            corner += axes[0] * extents.x * ((i & 1) == 0 ? -1 : 1);
            corner += axes[1] * extents.y * ((i & 2) == 0 ? -1 : 1);
            corner += axes[2] * extents.z * ((i & 4) == 0 ? -1 : 1);
            corners[i] = corner;
        }
        
        return corners;
    }
}

public class OBBExample : MonoBehaviour
{
    private OBB obb;
    
    void Start()
    {
        // OBB 생성 (로컬 크기 지정)
        Vector3 localSize = new Vector3(2f, 1f, 3f);
        obb = new OBB(transform, localSize);
    }
    
    void Update()
    {
        // 객체 회전
        transform.Rotate(0, 30 * Time.deltaTime, 0);
        
        // OBB 업데이트 (회전 반영)
        Vector3 localSize = new Vector3(2f, 1f, 3f);
        obb = new OBB(transform, localSize);
    }
    
    void OnDrawGizmos()
    {
        // OBB 시각화
        if (obb.axes != null)
        {
            Gizmos.color = [Color.blue](http://Color.blue);
            Vector3[] corners = obb.GetCorners();
            
            // 12개의 엣지 그리기
            DrawOBBEdges(corners);
        }
    }
    
    void DrawOBBEdges(Vector3[] corners)
    {
        // 하단 4개 엣지
        Gizmos.DrawLine(corners[0], corners[1]);
        Gizmos.DrawLine(corners[1], corners[3]);
        Gizmos.DrawLine(corners[3], corners[2]);
        Gizmos.DrawLine(corners[2], corners[0]);
        
        // 상단 4개 엣지
        Gizmos.DrawLine(corners[4], corners[5]);
        Gizmos.DrawLine(corners[5], corners[7]);
        Gizmos.DrawLine(corners[7], corners[6]);
        Gizmos.DrawLine(corners[6], corners[4]);
        
        // 수직 4개 엣지
        Gizmos.DrawLine(corners[0], corners[4]);
        Gizmos.DrawLine(corners[1], corners[5]);
        Gizmos.DrawLine(corners[2], corners[6]);
        Gizmos.DrawLine(corners[3], corners[7]);
    }
}

5.4 Separating Axis Theorem (SAT)

OBB 간의 충돌을 검사하려면 분리축 정리(Separating Axis Theorem)가 필요합니다.

SAT의 핵심 개념

“두 볼록 도형이 충돌하지 않는다면, 반드시 그들을 분리하는 축(선)이 존재한다.”

역으로:

“모든 가능한 분리축에 투영했을 때 겹친다면, 두 도형은 충돌한다.”

SAT 알고리즘 단계

1. 테스트할 축 결정:
   - OBB A의 3개 축 (right, up, forward)
   - OBB B의 3개 축
   - 두 OBB 축들의 외적 9개 (총 15개 축)

2. 각 축에 대해:
   a) OBB A를 축에 투영 → 범위 [minA, maxA]
   b) OBB B를 축에 투영 → 범위 [minB, maxB]
   c) 범위가 겹치는지 확인
      - 겹침: 이 축으로는 분리 안됨
      - 안 겹침: 분리축 발견! → 충돌 없음

3. 모든 축에서 겹침 → 충돌 발생

SAT 구현 예제

public class SATCollisionDetector : MonoBehaviour
{
    // OBB 간 충돌 검사 (SAT 사용)
    public static bool OBBIntersects(OBB a, OBB b)
    {
        // 테스트할 15개의 축
        List<Vector3> axes = new List<Vector3>();
        
        // A의 3개 축
        axes.AddRange(a.axes);
        
        // B의 3개 축
        axes.AddRange(b.axes);
        
        // A와 B 축들의 외적 9개
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                Vector3 axis = Vector3.Cross(a.axes[i], b.axes[j]);
                if (axis.sqrMagnitude > 0.0001f)  // 거의 평행한 축 제외
                {
                    axes.Add(axis.normalized);
                }
            }
        }
        
        // 각 축에 대해 분리 테스트
        foreach (Vector3 axis in axes)
        {
            if (!OverlapOnAxis(a, b, axis))
            {
                // 분리축 발견! 충돌 없음
                return false;
            }
        }
        
        // 모든 축에서 겹침 → 충돌
        return true;
    }
    
    // 특정 축에 투영했을 때 겹치는지 확인
    static bool OverlapOnAxis(OBB a, OBB b, Vector3 axis)
    {
        // OBB A의 투영 범위
        float[] rangeA = ProjectOBB(a, axis);
        
        // OBB B의 투영 범위
        float[] rangeB = ProjectOBB(b, axis);
        
        // 범위 겹침 확인
        return !(rangeA[1] < rangeB[0] || rangeB[1] < rangeA[0]);
    }
    
    // OBB를 축에 투영하여 범위 계산
    static float[] ProjectOBB(OBB obb, Vector3 axis)
    {
        // OBB의 중심 투영
        float centerProjection = [Vector3.Dot](http://Vector3.Dot)([obb.center](http://obb.center), axis);
        
        // OBB의 extents 투영 (각 축 기여도 합산)
        float extentsProjection = 0f;
        for (int i = 0; i < 3; i++)
        {
            extentsProjection += Mathf.Abs([Vector3.Dot](http://Vector3.Dot)(obb.axes[i], axis)) * obb.extents[i];
        }
        
        // [min, max] 범위 반환
        return new float[2]
        {
            centerProjection - extentsProjection,  // min
            centerProjection + extentsProjection   // max
        };
    }
    
    // 테스트 코드
    public Transform obbA;
    public Transform obbB;
    public Vector3 sizeA = [Vector3.one](http://Vector3.one);
    public Vector3 sizeB = [Vector3.one](http://Vector3.one);
    
    void Update()
    {
        OBB a = new OBB(obbA, sizeA);
        OBB b = new OBB(obbB, sizeB);
        
        bool isColliding = OBBIntersects(a, b);
        
        Debug.Log($"OBB 충돌: {isColliding}");
    }
}

5.5 성능 비교 및 분석

계산 복잡도

비교 항목 AABB OBB
교차 검사 O(1) - 6개 비교 O(1) - 15개 축 투영
FLOP 수 (2D) ~10-15 ~40-50
FLOP 수 (3D) ~20-30 ~200
메모리 6 floats (min, max) 15 floats (center, axes, extents)
업데이트 비용 회전 시 재계산 필요 회전 자동 반영

성능 벤치마크 예제

public class BoundingBoxBenchmark : MonoBehaviour
{
    public int iterations = 10000;
    
    void Start()
    {
        BenchmarkAABB();
        BenchmarkOBB();
    }
    
    void BenchmarkAABB()
    {
        // AABB 준비
        Bounds aabbA = new Bounds([Vector3.zero](http://Vector3.zero), [Vector3.one](http://Vector3.one));
        Bounds aabbB = new Bounds(new Vector3(0.5f, 0.5f, 0.5f), [Vector3.one](http://Vector3.one));
        
        // 성능 측정
        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        
        int collisionCount = 0;
        for (int i = 0; i < iterations; i++)
        {
            if (aabbA.Intersects(aabbB))
            {
                collisionCount++;
            }
        }
        
        sw.Stop();
        
        Debug.Log($"AABB: {iterations}회 검사, {sw.ElapsedMilliseconds}ms, " +
                  $"평균 {(float)sw.ElapsedMilliseconds / iterations * 1000f:F3}μs/검사");
    }
    
    void BenchmarkOBB()
    {
        // OBB 준비
        Transform transA = transform;
        Transform transB = new GameObject("TempOBB").transform;
        transB.position = new Vector3(0.5f, 0.5f, 0.5f);
        transB.rotation = Quaternion.Euler(45, 45, 0);
        
        OBB obbA = new OBB(transA, [Vector3.one](http://Vector3.one));
        OBB obbB = new OBB(transB, [Vector3.one](http://Vector3.one));
        
        // 성능 측정
        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        
        int collisionCount = 0;
        for (int i = 0; i < iterations; i++)
        {
            if (SATCollisionDetector.OBBIntersects(obbA, obbB))
            {
                collisionCount++;
            }
        }
        
        sw.Stop();
        
        Debug.Log($"OBB: {iterations}회 검사, {sw.ElapsedMilliseconds}ms, " +
                  $"평균 {(float)sw.ElapsedMilliseconds / iterations * 1000f:F3}μs/검사");
        
        Destroy(transB.gameObject);
    }
}

실제 성능 결과 (예상)

일반적인 PC에서:
AABB: 10000회 검사, 2ms, 평균 0.2μs/검사
OBB:  10000회 검사, 15ms, 평균 1.5μs/검사

→ AABB가 약 7-8배 빠름

5.6 실무 활용 가이드

언제 AABB를 사용할까?

장점 활용 시나리오:

  1. 공간 분할 구조: Quadtree, Octree, BVH
  2. 초기 충돌 필터링: Broad Phase Collision Detection
  3. 정렬된 객체: 회전하지 않는 UI, 타일 기반 게임
  4. 대량의 객체: 수천~수만 개의 충돌 검사
  5. 컬링 시스템: Frustum Culling, Occlusion Culling

언제 OBB를 사용할까?

정확도가 중요한 시나리오:

  1. 회전하는 객체: 비행기, 회전하는 플랫폼
  2. 타이트한 피팅: 메모리 절약이 중요할 때
  3. 정밀 충돌: 레이싱 게임의 차량, 물리 시뮬레이션
  4. 좁은 객체: 긴 막대, 검, 화살 등

하이브리드 접근법

실무에서는 두 방법을 조합하여 사용합니다:

public class HybridCollisionSystem : MonoBehaviour
{
    public List<GameObject> allObjects = new List<GameObject>();
    
    void Update()
    {
        // 2단계 충돌 검사
        DetectCollisionsHybrid();
    }
    
    void DetectCollisionsHybrid()
    {
        // Phase 1: Broad Phase (AABB) - 빠른 필터링
        List<(int, int)> potentialCollisions = BroadPhaseAABB();
        
        Debug.Log($"Broad Phase: {potentialCollisions.Count}개의 잠재적 충돌");
        
        // Phase 2: Narrow Phase (OBB) - 정밀 검사
        List<(int, int)> actualCollisions = NarrowPhaseOBB(potentialCollisions);
        
        Debug.Log($"Narrow Phase: {actualCollisions.Count}개의 실제 충돌");
        
        // Phase 3: 충돌 응답
        foreach (var (i, j) in actualCollisions)
        {
            HandleCollision(allObjects[i], allObjects[j]);
        }
    }
    
    // Broad Phase: AABB로 빠르게 필터링
    List<(int, int)> BroadPhaseAABB()
    {
        List<(int, int)> potentials = new List<(int, int)>();
        
        for (int i = 0; i < allObjects.Count; i++)
        {
            for (int j = i + 1; j < allObjects.Count; j++)
            {
                Collider colliderA = allObjects[i].GetComponent<Collider>();
                Collider colliderB = allObjects[j].GetComponent<Collider>();
                
                // AABB 충돌 검사 (매우 빠름)
                if (colliderA.bounds.Intersects(colliderB.bounds))
                {
                    potentials.Add((i, j));
                }
            }
        }
        
        return potentials;
    }
    
    // Narrow Phase: OBB로 정밀 검사
    List<(int, int)> NarrowPhaseOBB(List<(int, int)> potentials)
    {
        List<(int, int)> actuals = new List<(int, int)>();
        
        foreach (var (i, j) in potentials)
        {
            // OBB 생성
            OBB obbA = CreateOBB(allObjects[i]);
            OBB obbB = CreateOBB(allObjects[j]);
            
            // OBB 충돌 검사 (정확하지만 느림)
            if (SATCollisionDetector.OBBIntersects(obbA, obbB))
            {
                actuals.Add((i, j));
            }
        }
        
        return actuals;
    }
    
    OBB CreateOBB(GameObject obj)
    {
        // 객체의 실제 크기 계산 (Renderer 기준)
        Renderer renderer = obj.GetComponent<Renderer>();
        if (renderer != null)
        {
            Vector3 size = renderer.bounds.size;
            return new OBB(obj.transform, size);
        }
        
        // Fallback: 기본 크기
        return new OBB(obj.transform, [Vector3.one](http://Vector3.one));
    }
    
    void HandleCollision(GameObject objA, GameObject objB)
    {
        Debug.Log($"충돌 발생: {[objA.name](http://objA.name)} <-> {[objB.name](http://objB.name)}");
    }
}

Unity Physics의 내부 동작

Unity의 PhysX 엔진은 실제로 다음과 같은 계층적 접근을 사용합니다:

1. Broad Phase
   └─> AABB 기반 Sweep and Prune
   └─> 잠재적 충돌 쌍 생성

2. Mid Phase (복잡한 메쉬의 경우)
   └─> BVH (Bounding Volume Hierarchy)
   └─> 메쉬를 작은 부분으로 분할

3. Narrow Phase
   └─> 실제 콜라이더 형태로 정밀 검사
   └─> GJK/EPA 알고리즘 (볼록 형태)
   └─> 침투 깊이 및 접촉점 계산

4. Constraint Solver
   └─> 충돌 응답 계산
   └─> 위치 및 속도 보정

5.7 고급 최적화 기법

Spatial Hashing과 AABB

public class SpatialHashingSystem : MonoBehaviour
{
    private Dictionary<Vector3Int, List<GameObject>> spatialHash = new Dictionary<Vector3Int, List<GameObject>>();
    private float cellSize = 5f;  // 그리드 셀 크기
    
    public List<GameObject> allObjects = new List<GameObject>();
    
    void Update()
    {
        // 공간 해시 재구성
        RebuildSpatialHash();
        
        // 충돌 검사 (같은 셀 내의 객체들만)
        DetectCollisionsWithSpatialHash();
    }
    
    void RebuildSpatialHash()
    {
        spatialHash.Clear();
        
        foreach (GameObject obj in allObjects)
        {
            // 객체의 AABB가 걸치는 모든 셀에 등록
            Bounds aabb = obj.GetComponent<Collider>().bounds;
            
            Vector3Int minCell = WorldToCell(aabb.min);
            Vector3Int maxCell = WorldToCell(aabb.max);
            
            for (int x = minCell.x; x <= maxCell.x; x++)
            {
                for (int y = minCell.y; y <= maxCell.y; y++)
                {
                    for (int z = minCell.z; z <= maxCell.z; z++)
                    {
                        Vector3Int cell = new Vector3Int(x, y, z);
                        
                        if (!spatialHash.ContainsKey(cell))
                        {
                            spatialHash[cell] = new List<GameObject>();
                        }
                        
                        spatialHash[cell].Add(obj);
                    }
                }
            }
        }
    }
    
    Vector3Int WorldToCell(Vector3 worldPos)
    {
        return new Vector3Int(
            Mathf.FloorToInt(worldPos.x / cellSize),
            Mathf.FloorToInt(worldPos.y / cellSize),
            Mathf.FloorToInt(worldPos.z / cellSize)
        );
    }
    
    void DetectCollisionsWithSpatialHash()
    {
        int totalChecks = 0;
        int collisions = 0;
        
        // 각 셀마다 내부 객체들 간의 충돌만 검사
        foreach (var cell in spatialHash.Values)
        {
            for (int i = 0; i < cell.Count; i++)
            {
                for (int j = i + 1; j < cell.Count; j++)
                {
                    totalChecks++;
                    
                    Bounds aabbA = cell[i].GetComponent<Collider>().bounds;
                    Bounds aabbB = cell[j].GetComponent<Collider>().bounds;
                    
                    if (aabbA.Intersects(aabbB))
                    {
                        collisions++;
                    }
                }
            }
        }
        
        // Brute Force와 비교
        int bruteForceChecks = (allObjects.Count * (allObjects.Count - 1)) / 2;
        float efficiency = (1f - (float)totalChecks / bruteForceChecks) * 100f;
        
        Debug.Log($"Spatial Hashing: {totalChecks}회 검사 (절약: {efficiency:F1}%), {collisions}개 충돌");
    }
    
    void OnDrawGizmos()
    {
        // 공간 해시 그리드 시각화
        Gizmos.color = Color.cyan;
        foreach (var cellPos in spatialHash.Keys)
        {
            Vector3 worldPos = new Vector3(
                cellPos.x * cellSize + cellSize * 0.5f,
                cellPos.y * cellSize + cellSize * 0.5f,
                cellPos.z * cellSize + cellSize * 0.5f
            );
            Gizmos.DrawWireCube(worldPos, [Vector3.one](http://Vector3.one) * cellSize);
        }
    }
}

6. 종합 활용: 실무 시나리오

6.1 시나리오 1: 액션 게임의 스킬 범위 공격

public class SkillAreaAttack : MonoBehaviour
{
    [Header("스킬 설정")]
    public Vector3 attackBoxSize = new Vector3(5f, 2f, 5f);
    public float attackDuration = 0.5f;
    public LayerMask enemyLayer;
    
    [Header("데미지 설정")]
    public float baseDamage = 50f;
    public AnimationCurve damageFalloff;  // 거리에 따른 데미지 감소
    
    private bool isAttacking = false;
    private float attackTimer = 0f;
    
    void Update()
    {
        if (Input.GetKeyDown([KeyCode.Space](http://KeyCode.Space)) && !isAttacking)
        {
            StartCoroutine(ExecuteSkillAttack());
        }
    }
    
    IEnumerator ExecuteSkillAttack()
    {
        isAttacking = true;
        attackTimer = 0f;
        
        // 공격 범위 계산 (플레이어 앞쪽)
        Vector3 attackCenter = transform.position + transform.forward * 2.5f;
        Quaternion attackRotation = transform.rotation;
        
        // OverlapBox로 적 탐지
        Collider[] hitEnemies = Physics.OverlapBox(
            attackCenter,
            attackBoxSize / 2f,
            attackRotation,
            enemyLayer
        );
        
        Debug.Log($"스킬 적중: {hitEnemies.Length}명의 적");
        
        // 각 적에게 데미지 적용
        foreach (Collider enemyCollider in hitEnemies)
        {
            ApplyDamageToEnemy(enemyCollider, attackCenter);
        }
        
        // 공격 지속 시간
        yield return new WaitForSeconds(attackDuration);
        
        isAttacking = false;
    }
    
    void ApplyDamageToEnemy(Collider enemyCollider, Vector3 attackCenter)
    {
        // 적과의 거리 계산
        float distance = Vector3.Distance(attackCenter, enemyCollider.transform.position);
        float maxDistance = attackBoxSize.magnitude / 2f;
        
        // 거리에 따른 데미지 감소 (0~1)
        float distanceRatio = Mathf.Clamp01(distance / maxDistance);
        float damageMultiplier = damageFalloff.Evaluate(distanceRatio);
        
        float finalDamage = baseDamage * damageMultiplier;
        
        // 데미지 적용
        Enemy enemy = enemyCollider.GetComponent<Enemy>();
        if (enemy != null)
        {
            enemy.TakeDamage(finalDamage);
            Debug.Log($"{[enemy.name](http://enemy.name)}에게 {finalDamage:F1} 데미지 (거리: {distance:F2}m)");
        }
    }
    
    void OnDrawGizmos()
    {
        if (isAttacking)
        {
            // 공격 범위 시각화 (빨간색)
            Gizmos.color = new Color(1f, 0f, 0f, 0.5f);
        }
        else
        {
            // 평상시 (노란색)
            Gizmos.color = new Color(1f, 1f, 0f, 0.3f);
        }
        
        Vector3 attackCenter = transform.position + transform.forward * 2.5f;
        Gizmos.matrix = Matrix4x4.TRS(attackCenter, transform.rotation, [Vector3.one](http://Vector3.one));
        Gizmos.DrawCube([Vector3.zero](http://Vector3.zero), attackBoxSize);
    }
}

6.2 시나리오 2: 건축 시스템의 간섭 검사

public class AdvancedBuildingSystem : MonoBehaviour
{
    [Header("건물 설정")]
    public GameObject buildingPrefab;
    public LayerMask obstructionLayers;
    
    [Header("간섭 허용 범위")]
    public float penetrationTolerance = 0.05f;  // 5cm까지 허용
    
    private GameObject ghostBuilding;  // 미리보기 객체
    private bool canPlace = false;
    
    void Update()
    {
        UpdateGhostBuilding();
        
        if (Input.GetMouseButtonDown(0) && canPlace)
        {
            PlaceBuilding();
        }
    }
    
    void UpdateGhostBuilding()
    {
        // 마우스 위치로 레이캐스트
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        
        if (Physics.Raycast(ray, out hit, 100f))
        {
            Vector3 buildPosition = hit.point;
            
            // 고스트 건물 생성 (처음 한 번만)
            if (ghostBuilding == null)
            {
                ghostBuilding = Instantiate(buildingPrefab, buildPosition, Quaternion.identity);
                // 반투명하게 만들기
                SetGhostMaterial(ghostBuilding, 0.5f);
            }
            else
            {
                ghostBuilding.transform.position = buildPosition;
            }
            
            // 건축 가능 여부 검사
            canPlace = CheckBuildingPlacement(ghostBuilding);
            
            // 색상 변경 (가능: 초록, 불가능: 빨강)
            Color ghostColor = canPlace ? [Color.green](http://Color.green) : [Color.red](http://Color.red);
            SetGhostColor(ghostBuilding, ghostColor);
        }
    }
    
    bool CheckBuildingPlacement(GameObject building)
    {
        MeshCollider buildingCollider = building.GetComponent<MeshCollider>();
        if (buildingCollider == null) return false;
        
        // 1단계: AABB로 빠른 필터링
        Bounds buildingBounds = buildingCollider.bounds;
        Collider[] nearbyObjects = Physics.OverlapBox(
            [buildingBounds.center](http://buildingBounds.center),
            buildingBounds.extents,
            building.transform.rotation,
            obstructionLayers
        );
        
        if (nearbyObjects.Length == 0)
        {
            // 주변에 아무것도 없음 → 건축 가능
            return true;
        }
        
        // 2단계: 각 장애물과 침투 깊이 확인
        foreach (Collider obstacle in nearbyObjects)
        {
            Vector3 direction;
            float penetrationDepth;
            
            bool hasPenetration = Physics.ComputePenetration(
                buildingCollider,
                building.transform.position,
                building.transform.rotation,
                obstacle,
                obstacle.transform.position,
                obstacle.transform.rotation,
                out direction,
                out penetrationDepth
            );
            
            // 허용 범위를 초과하는 침투가 있으면 건축 불가
            if (hasPenetration && penetrationDepth > penetrationTolerance)
            {
                Debug.Log(
                    $"건축 불가: {[obstacle.name](http://obstacle.name)}과 {penetrationDepth:F3}m 간섭 " +
                    $"(허용: {penetrationTolerance}m)"
                );
                return false;
            }
        }
        
        // 모든 검사 통과
        return true;
    }
    
    void PlaceBuilding()
    {
        // 실제 건물 생성
        GameObject realBuilding = Instantiate(
            buildingPrefab,
            ghostBuilding.transform.position,
            ghostBuilding.transform.rotation
        );
        
        Debug.Log($"건물 배치 완료: {[realBuilding.name](http://realBuilding.name)}");
        
        // 고스트 건물 제거
        Destroy(ghostBuilding);
        ghostBuilding = null;
    }
    
    void SetGhostMaterial(GameObject obj, float alpha)
    {
        Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
        foreach (Renderer rend in renderers)
        {
            foreach (Material mat in rend.materials)
            {
                // Transparent 모드로 변경
                mat.SetFloat("_Mode", 3);
                mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                mat.EnableKeyword("_ALPHABLEND_ON");
                mat.renderQueue = 3000;
                
                Color color = mat.color;
                color.a = alpha;
                mat.color = color;
            }
        }
    }
    
    void SetGhostColor(GameObject obj, Color color)
    {
        Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
        foreach (Renderer rend in renderers)
        {
            foreach (Material mat in rend.materials)
            {
                Color currentColor = mat.color;
                color.a = currentColor.a;  // 알파값 유지
                mat.color = color;
            }
        }
    }
}

6.3 시나리오 3: 복잡한 메쉬의 동적 충돌

public class DynamicMeshCollisionSystem : MonoBehaviour
{
    [Header("복잡한 메쉬 객체")]
    public GameObject complexMeshObject;
    
    [Header("Convex 분해 설정")]
    public int maxConvexParts = 10;
    
    void Start()
    {
        // 복잡한 메쉬를 여러 Convex 콜라이더로 분해
        DecomposeToConvexParts();
    }
    
    void DecomposeToConvexParts()
    {
        MeshFilter meshFilter = complexMeshObject.GetComponent<MeshFilter>();
        Mesh originalMesh = meshFilter.mesh;
        
        // 메쉬를 공간적으로 분할
        List<Mesh> convexParts = SpatialDecomposition(originalMesh, maxConvexParts);
        
        Debug.Log($"{[originalMesh.name](http://originalMesh.name)}을 {convexParts.Count}개의 Convex 부분으로 분해");
        
        // 원본 콜라이더 제거
        MeshCollider originalCollider = complexMeshObject.GetComponent<MeshCollider>();
        if (originalCollider != null)
        {
            Destroy(originalCollider);
        }
        
        // 각 Convex 부분을 자식으로 추가
        for (int i = 0; i < convexParts.Count; i++)
        {
            GameObject part = new GameObject($"ConvexPart_{i}");
            part.transform.SetParent(complexMeshObject.transform);
            part.transform.localPosition = [Vector3.zero](http://Vector3.zero);
            part.transform.localRotation = Quaternion.identity;
            part.transform.localScale = [Vector3.one](http://Vector3.one);
            
            // Convex Mesh Collider 추가
            MeshCollider collider = part.AddComponent<MeshCollider>();
            collider.sharedMesh = convexParts[i];
            collider.convex = true;
            
            Debug.Log($"  Part {i}: {convexParts[i].triangles.Length / 3}개 삼각형");
        }
    }
    
    List<Mesh> SpatialDecomposition(Mesh mesh, int targetParts)
    {
        // 간단한 공간 분할 알고리즘 (실제로는 VHACD 같은 알고리즘 사용)
        List<Mesh> parts = new List<Mesh>();
        
        // 메쉬의 바운딩 박스 계산
        Bounds bounds = mesh.bounds;
        
        // 가장 긴 축을 기준으로 분할
        Vector3 size = bounds.size;
        int splitAxis = 0;  // 0: X, 1: Y, 2: Z
        if (size.y > size.x && size.y > size.z) splitAxis = 1;
        else if (size.z > size.x && size.z > size.y) splitAxis = 2;
        
        // 축을 따라 정렬
        Vector3[] vertices = mesh.vertices;
        int[] triangles = mesh.triangles;
        
        // 삼각형을 그룹으로 분할
        int trianglesPerPart = Mathf.CeilToInt(triangles.Length / 3f / targetParts);
        
        for (int i = 0; i < targetParts; i++)
        {
            int startTriangle = i * trianglesPerPart * 3;
            int endTriangle = Mathf.Min((i + 1) * trianglesPerPart * 3, triangles.Length);
            
            if (startTriangle >= triangles.Length) break;
            
            // 이 부분의 메쉬 생성
            Mesh partMesh = CreateSubMesh(mesh, startTriangle, endTriangle);
            
            // 255 삼각형 제한 확인
            if (partMesh.triangles.Length / 3 <= 255)
            {
                parts.Add(partMesh);
            }
            else
            {
                Debug.LogWarning($"Part {i}가 255 삼각형을 초과하여 추가 분할 필요");
                // 추가 분할 로직...
            }
        }
        
        return parts;
    }
    
    Mesh CreateSubMesh(Mesh originalMesh, int startIndex, int endIndex)
    {
        Vector3[] originalVertices = originalMesh.vertices;
        int[] originalTriangles = originalMesh.triangles;
        
        // 이 범위의 삼각형에 사용되는 정점들 추출
        Dictionary<int, int> vertexMap = new Dictionary<int, int>();
        List<Vector3> newVertices = new List<Vector3>();
        List<int> newTriangles = new List<int>();
        
        for (int i = startIndex; i < endIndex; i++)
        {
            int originalIndex = originalTriangles[i];
            
            if (!vertexMap.ContainsKey(originalIndex))
            {
                vertexMap[originalIndex] = newVertices.Count;
                newVertices.Add(originalVertices[originalIndex]);
            }
            
            newTriangles.Add(vertexMap[originalIndex]);
        }
        
        // 새 메쉬 생성
        Mesh subMesh = new Mesh();
        subMesh.vertices = newVertices.ToArray();
        subMesh.triangles = newTriangles.ToArray();
        subMesh.RecalculateNormals();
        subMesh.RecalculateBounds();
        
        return subMesh;
    }
}

7. 성능 최적화 종합 가이드

7.1 레이어 기반 충돌 필터링

public class LayerOptimization : MonoBehaviour
{
    void Start()
    {
        ConfigureCollisionMatrix();
    }
    
    void ConfigureCollisionMatrix()
    {
        // Unity Physics Settings에서 설정 가능하지만, 코드로도 가능
        
        int playerLayer = LayerMask.NameToLayer("Player");
        int enemyLayer = LayerMask.NameToLayer("Enemy");
        int bulletLayer = LayerMask.NameToLayer("Bullet");
        int environmentLayer = LayerMask.NameToLayer("Environment");
        
        // Player vs Enemy: 충돌 O
        Physics.IgnoreLayerCollision(playerLayer, enemyLayer, false);
        
        // Player vs Player: 충돌 X (멀티플레이어에서)
        Physics.IgnoreLayerCollision(playerLayer, playerLayer, true);
        
        // Bullet vs Bullet: 충돌 X
        Physics.IgnoreLayerCollision(bulletLayer, bulletLayer, true);
        
        // Environment vs Environment: 충돌 X (정적 객체끼리)
        Physics.IgnoreLayerCollision(environmentLayer, environmentLayer, true);
        
        Debug.Log("충돌 레이어 매트릭스 최적화 완료");
    }
}

7.2 충돌 검사 주기 조절

public class AdaptiveCollisionCheck : MonoBehaviour
{
    [Header("동적 업데이트 주기")]
    public float nearUpdateInterval = 0.05f;   // 가까이: 50ms
    public float farUpdateInterval = 0.5f;     // 멀리: 500ms
    public float distanceThreshold = 20f;      // 거리 기준
    
    private float nextUpdateTime = 0f;
    private Transform cameraTransform;
    
    void Start()
    {
        cameraTransform = Camera.main.transform;
    }
    
    void Update()
    {
        if (Time.time >= nextUpdateTime)
        {
            // 카메라와의 거리에 따라 업데이트 주기 조절
            float distance = Vector3.Distance(transform.position, cameraTransform.position);
            
            if (distance < distanceThreshold)
            {
                // 가까이 있음: 자주 업데이트
                PerformCollisionCheck();
                nextUpdateTime = Time.time + nearUpdateInterval;
            }
            else
            {
                // 멀리 있음: 가끔 업데이트
                PerformCollisionCheck();
                nextUpdateTime = Time.time + farUpdateInterval;
            }
        }
    }
    
    void PerformCollisionCheck()
    {
        // 실제 충돌 검사 로직
        Debug.Log($"충돌 검사 실행: {Time.time}");
    }
}

7.3 Collision Detection Mode 최적화

public class CollisionModeOptimizer : MonoBehaviour
{
    private Rigidbody rb;
    
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        OptimizeCollisionDetectionMode();
    }
    
    void OptimizeCollisionDetectionMode()
    {
        // 객체의 특성에 따라 최적 모드 선택
        
        float speed = rb.velocity.magnitude;
        float size = GetComponent<Collider>().bounds.size.magnitude;
        
        if (speed > 10f || size < 0.5f)
        {
            // 빠르거나 작은 객체: Continuous
            rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
            Debug.Log("Continuous 모드: 고속/소형 객체");
        }
        else if (speed > 5f)
        {
            // 중간 속도: ContinuousSpeculative
            rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
            Debug.Log("ContinuousSpeculative 모드: 중속 객체");
        }
        else
        {
            // 느린 객체: Discrete (기본값)
            rb.collisionDetectionMode = CollisionDetectionMode.Discrete;
            Debug.Log("Discrete 모드: 저속 객체");
        }
    }
    
    void FixedUpdate()
    {
        // 속도 변화에 따라 동적으로 모드 조절
        if (Time.frameCount % 60 == 0)  // 1초마다
        {
            OptimizeCollisionDetectionMode();
        }
    }
}

8. 디버깅 및 시각화 도구

8.1 종합 충돌 시각화 시스템

public class CollisionDebugVisualizer : MonoBehaviour
{
    [Header("시각화 옵션")]
    public bool showAABB = true;
    public bool showOBB = false;
    public bool showConvexHull = false;
    public bool showPenetration = true;
    public bool showCollisionContacts = true;
    
    [Header("색상 설정")]
    public Color aabbColor = [Color.green](http://Color.green);
    public Color obbColor = [Color.blue](http://Color.blue);
    public Color penetrationColor = [Color.red](http://Color.red);
    public Color contactColor = Color.yellow;
    
    private Collider col;
    private List<ContactPoint> recentContacts = new List<ContactPoint>();
    
    void Start()
    {
        col = GetComponent<Collider>();
    }
    
    void OnCollisionStay(Collision collision)
    {
        if (showCollisionContacts || showPenetration)
        {
            recentContacts.Clear();
            recentContacts.AddRange(collision.contacts);
        }
    }
    
    void OnDrawGizmos()
    {
        if (col == null) col = GetComponent<Collider>();
        if (col == null) return;
        
        // 1. AABB 시각화
        if (showAABB)
        {
            DrawAABB();
        }
        
        // 2. OBB 시각화
        if (showOBB)
        {
            DrawOBB();
        }
        
        // 3. Convex Hull 시각화
        if (showConvexHull)
        {
            DrawConvexHull();
        }
        
        // 4. 침투 벡터 시각화
        if (showPenetration && Application.isPlaying)
        {
            DrawPenetration();
        }
        
        // 5. 접촉점 시각화
        if (showCollisionContacts && recentContacts.Count > 0)
        {
            DrawContactPoints();
        }
    }
    
    void DrawAABB()
    {
        Gizmos.color = aabbColor;
        Bounds bounds = col.bounds;
        Gizmos.DrawWireCube([bounds.center](http://bounds.center), bounds.size);
        
#if UNITY_EDITOR
        // AABB 정보 표시
        UnityEditor.Handles.Label(
            [bounds.center](http://bounds.center) + Vector3.up * bounds.extents.y,
            $"AABB Size: {bounds.size}"
        );
#endif
    }
    
    void DrawOBB()
    {
        Gizmos.color = obbColor;
        
        // OBB는 로컬 바운드를 월드 변환하여 그림
        if (col is BoxCollider)
        {
            BoxCollider boxCol = col as BoxCollider;
            Matrix4x4 matrix = Matrix4x4.TRS(
                transform.TransformPoint([boxCol.center](http://boxCol.center)),
                transform.rotation,
                Vector3.Scale(boxCol.size, transform.lossyScale)
            );
            Gizmos.matrix = matrix;
            Gizmos.DrawWireCube([Vector3.zero](http://Vector3.zero), [Vector3.one](http://Vector3.one));
            Gizmos.matrix = Matrix4x4.identity;
        }
    }
    
    void DrawConvexHull()
    {
        if (col is MeshCollider)
        {
            MeshCollider meshCol = col as MeshCollider;
            if (meshCol.convex)
            {
                Gizmos.color = new Color(0, 1, 0, 0.3f);
                Gizmos.DrawMesh(
                    meshCol.sharedMesh,
                    transform.position,
                    transform.rotation,
                    transform.lossyScale
                );
            }
        }
    }
    
    void DrawPenetration()
    {
        // 주변 콜라이더와의 침투 확인
        Collider[] nearby = Physics.OverlapSphere(transform.position, 5f);
        
        foreach (Collider other in nearby)
        {
            if (other == col) continue;
            
            Vector3 direction;
            float distance;
            
            bool isPenetrating = Physics.ComputePenetration(
                col, transform.position, transform.rotation,
                other, other.transform.position, other.transform.rotation,
                out direction, out distance
            );
            
            if (isPenetrating)
            {
                // 침투 벡터 그리기
                Gizmos.color = penetrationColor;
                Vector3 start = transform.position;
                Vector3 end = start + direction * distance;
                
                Gizmos.DrawLine(start, end);
                Gizmos.DrawSphere(end, 0.1f);
                
#if UNITY_EDITOR
                UnityEditor.Handles.Label(end, $"Penetration: {distance:F3}m");
#endif
            }
        }
    }
    
    void DrawContactPoints()
    {
        Gizmos.color = contactColor;
        
        foreach (ContactPoint contact in recentContacts)
        {
            // 접촉점
            Gizmos.DrawSphere(contact.point, 0.05f);
            
            // 접촉 법선
            Gizmos.DrawLine(contact.point, contact.point + contact.normal * 0.5f);
            
#if UNITY_EDITOR
            UnityEditor.Handles.Label(
                contact.point,
                $"Normal: {contact.normal}\nSeparation: {contact.separation:F4}"
            );
#endif
        }
    }
}

8.2 성능 프로파일러

public class CollisionPerformanceProfiler : MonoBehaviour
{
    private class ProfileData
    {
        public int frameCount;
        public float totalTime;
        public float minTime = float.MaxValue;
        public float maxTime = 0f;
        
        public void AddSample(float time)
        {
            frameCount++;
            totalTime += time;
            minTime = Mathf.Min(minTime, time);
            maxTime = Mathf.Max(maxTime, time);
        }
        
        public float Average => totalTime / frameCount;
    }
    
    private Dictionary<string, ProfileData> profiles = new Dictionary<string, ProfileData>();
    
    public void ProfileMethod(string name, System.Action method)
    {
        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        method();
        sw.Stop();
        
        float timeMs = (float)sw.Elapsed.TotalMilliseconds;
        
        if (!profiles.ContainsKey(name))
        {
            profiles[name] = new ProfileData();
        }
        
        profiles[name].AddSample(timeMs);
    }
    
    public void LogResults()
    {
        Debug.Log("=== Collision Performance Profile ===");
        
        foreach (var kvp in profiles)
        {
            string name = kvp.Key;
            ProfileData data = kvp.Value;
            
            Debug.Log(
                $"{name}:\n" +
                $"  평균: {data.Average:F4}ms\n" +
                $"  최소: {data.minTime:F4}ms\n" +
                $"  최대: {data.maxTime:F4}ms\n" +
                $"  호출 횟수: {data.frameCount}"
            );
        }
    }
    
    void OnDestroy()
    {
        LogResults();
    }
}

9. 참고 자료 및 추가 학습

9.1 Unity 공식 문서

9.2 성능 최적화 가이드

9.3 알고리즘 및 이론

9.4 고급 주제

9.5 실무 커뮤니티


10. 결론 및 요약

10.1 핵심 개념 정리

용어 핵심 특징 주요 사용 사례 성능
Bounding Box 객체를 둘러싸는 단순한 박스 초기 충돌 필터링, 공간 분할 ⚡⚡⚡ 매우 빠름
Overlap Box 특정 영역 내 콜라이더 탐지 스킬 범위, 건축 검사 ⚡⚡ 빠름
Penetration 충돌 시 침투 깊이 측정 충돌 해결, 간섭 검사 ⚡ 보통
Convex Collider 볼록한 형태의 콜라이더 동적 물체, Rigidbody 필수 ⚡⚡ 빠름
AABB 축 정렬 바운딩 박스 Broad Phase, 빠른 필터링 ⚡⚡⚡ 매우 빠름
OBB 회전 가능 바운딩 박스 Narrow Phase, 정밀 검사 ⚡ 느림

10.2 실무 적용 원칙

원칙 1: 계층적 충돌 검사

1단계 (Broad Phase): AABB로 빠르게 필터링
2단계 (Mid Phase): 필요시 공간 분할
3단계 (Narrow Phase): 정밀한 충돌 검사 (OBB, SAT)

원칙 2: 적절한 콜라이더 선택

1순위: Primitive Colliders (Box, Sphere, Capsule)
2순위: Convex Mesh Collider
3순위: Non-Convex Mesh Collider (정적 객체만)

원칙 3: 레이어 기반 최적화

- 충돌이 필요한 레이어 조합만 활성화
- Layer Collision Matrix 적극 활용
- 불필요한 충돌 검사 제거

원칙 4: 동적 품질 조절

- 거리에 따른 업데이트 주기 조절
- LOD 기반 콜라이더 단순화
- 카메라 밖 객체 비활성화

10.3 마무리

유니티의 충돌 감지 시스템은 PhysX 엔진 기반으로 강력하고 효율적입니다. 하지만 효과적으로 활용하기 위해서는:

  1. 기본 개념 이해: 각 용어의 정확한 의미와 차이점
  2. 적절한 도구 선택: 상황에 맞는 콜라이더와 검사 방법
  3. 성능 최적화: 레이어, 주기, 단순화 등의 기법
  4. 디버깅 능력: 시각화와 프로파일링 도구 활용

이 가이드에서 다룬 개념들을 실제 프로젝트에 적용하면서, Unity Physics 시스템의 강력함을 경험하시기 바랍니다.