[GAS] 나나이트(Nanite) 메시의 거리 컬링 최적화

2026. 1. 7. 20:47·Dev./UE 언리얼 엔진

1. 문제 정의

1.1 발생한 구체적인 문제점

프로젝트에서 몬스터 캐릭터는 설정된 거리에서 정상적으로 화면에서 사라졌지만,

함정(Trap), 방(Room), 문(Door) 오브젝트들은 아무리 멀리 떨어져도 계속 렌더링되는 현상이 발생했다.


1.2 예상과 다른 동작

// 기존에 작성한 컬링 코드 (작동하지 않음)
void AGS_TrapBase::ApplyDistanceCulling()
{
    float CullDistance = 3000.0f; // 30미터
    
    for (UPrimitiveComponent* PrimComp : GetComponents<UPrimitiveComponent>())
    {
        PrimComp->SetCullDistance(CullDistance);
        PrimComp->SetCachedMaxDrawDistance(CullDistance);
    }
}

위 코드를 실행하고 디버그 로그로 확인하면 분명히 CullDistance: 3000.0이 정상적으로 설정되었다고 출력되지만,

실제 게임 화면에서는 30미터를 훨씬 넘어가도 함정이 그대로 보였다.


1.3 문제가 발생한 조건

오브젝트 메시 타입 컬링 동작
함정 Static Mesh (Nanite) 작동 안 함
방 모듈 Static Mesh (Nanite) 작동 안 함
문 Static Mesh (Nanite) 작동 안 함
몬스터 Skeletal Mesh 정상 작동

2. 문제 원인 분석

2.1 기술적 원인: 나나이트의 독자적인 렌더링 파이프라인

나나이트(Nanite)는 언리얼 엔진 5에서 도입된 가상화 지오메트리 시스템이다.

수백만 개의 폴리곤을 실시간으로 스트리밍하며, 화면에 보이는 픽셀 단위로 메시를 최적화한다.

문제는 나나이트가 기존의 MaxDrawDistance 기반 컬링 시스템을 완전히 우회한다는 점이다.

[일반 Static Mesh의 렌더링 흐름]
메시 데이터 → MaxDrawDistance 체크 → 거리 초과 시 렌더링 스킵

[나나이트 메시의 렌더링 흐름]
메시 데이터 → 나나이트 스트리밍 엔진 → 스크린 픽셀 기반 LOD → 1픽셀이 될 때까지 렌더링
                    ↑
            MaxDrawDistance 체크를 거치지 않음

2.2 잘못 이해했던 개념

처음에는 "모든 렌더링 가능한 컴포넌트는 SetCullDistance()를 호출하면 해당 거리에서 사라진다"고 생각했다.

하지만 나나이트 메시는 자체적인 스크린 사이즈 기반 컬링을 사용하기 때문에,

아무리 SetCullDistance()를 호출해도 엔진이 이를 무시한다.


2.3 추가로 발견된 문제: 라이팅 과다 노출

최적화 과정에서 성능을 높이기 위해 라이트의 물리 기반 감쇠를 비활성화했더니, 예상치 못한 부작용이 발생했다.

// 문제를 일으킨 코드
PointLight->SetUseInverseSquaredFalloff(false);  // 역제곱 감쇠 비활성화
PointLight->SetIntensityUnits(ELightUnits::Unitless);  // 물리 단위 해제

역제곱 감쇠(Inverse Squared Falloff)는 현실 세계의 빛처럼 거리의 제곱에 반비례하여 밝기가 줄어드는 물리 법칙이다. 이를 끄면 빛이 거리와 관계없이 일정한 밝기로 전달되어, 원래 넓은 공간을 밝히기 위해 높게 설정된 광도(Intensity)가 가까운 물체에도 그대로 적용되면서 과도하게 하얗게 되버리는 현상이 발생했다.


3. 해결 과정

3.1 핵심 해결책: 수동 가시성 제어 (Hard Culling)

엔진의 자동 컬링에 의존하지 않고, 직접 거리를 계산하여 SetVisibility()를 호출하는 방식으로 전환했다.

// GS_TrapBase.cpp - 수정된 컬링 로직

void AGS_TrapBase::UpdateCulling()
{
    // 1. 플레이어 카메라 위치 가져오기
    APlayerCameraManager* CamManager = UGameplayStatics::GetPlayerCameraManager(this, 0);
    if (!CamManager) return;
    
    FVector CameraLocation = CamManager->GetCameraLocation();
    float DistanceToCamera = FVector::Dist(GetActorLocation(), CameraLocation);
    
    // 2. 팝핑 방지를 위한 여유치(Hysteresis) 적용
    // - 사라질 때는 설정 거리의 100%에서
    // - 나타날 때는 설정 거리의 90%에서
    // 이렇게 하면 경계선에서 깜빡거리는 현상을 방지할 수 있다
    float CullDistance = GetTrapCullDistance();
    float ShowDistance = CullDistance * 0.9f;  // 10% 여유치
    
    bool bShouldBeVisible;
    if (bCurrentlyVisible)
    {
        // 현재 보이는 상태면, 컬링 거리를 넘어야 숨김
        bShouldBeVisible = (DistanceToCamera < CullDistance);
    }
    else
    {
        // 현재 숨겨진 상태면, 더 가까이 와야 보임
        bShouldBeVisible = (DistanceToCamera < ShowDistance);
    }
    
    // 3. 가시성 상태가 변경될 때만 처리 (불필요한 호출 방지)
    if (bShouldBeVisible != bCurrentlyVisible)
    {
        bCurrentlyVisible = bShouldBeVisible;
        
        // 모든 시각적 컴포넌트의 가시성을 직접 제어
        for (UActorComponent* Comp : GetComponents())
        {
            // 메시 컴포넌트
            if (UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Comp))
            {
                // 트리거나 콜리전 전용 컴포넌트는 제외
                if (!PrimComp->IsVisible() && !bShouldBeVisible) continue;
                PrimComp->SetVisibility(bShouldBeVisible, true);
            }
            
            // 라이트 컴포넌트
            if (ULightComponent* LightComp = Cast<ULightComponent>(Comp))
            {
                LightComp->SetVisibility(bShouldBeVisible);
            }
            
            // 나이아가라(VFX) 컴포넌트
            if (UNiagaraComponent* NiagaraComp = Cast<UNiagaraComponent>(Comp))
            {
                NiagaraComp->SetVisibility(bShouldBeVisible);
                if (bShouldBeVisible)
                    NiagaraComp->Activate();
                else
                    NiagaraComp->Deactivate();
            }
        }
    }
}

3.2 왜 이 방법이 효과적인가

SetVisibility(false)는 SetCullDistance()와 완전히 다른 레벨에서 작동한다.

SetCullDistance() → 렌더링 파이프라인에 "이 거리에서 그리지 마"라고 힌트 제공
                    → 나나이트는 자체 시스템을 쓰므로 이 힌트를 무시

SetVisibility(false) → 컴포넌트 자체를 렌더링 대상 목록에서 제외
                       → 나나이트든 뭐든 목록에 없으면 그릴 수 없음

3.3 성능을 위한 타이머 기반 업데이트

매 프레임마다 거리를 계산하면 CPU 부하가 커지므로, 타이머를 사용하여 주기적으로만 체크한다.

void AGS_TrapBase::BeginPlay()
{
    Super::BeginPlay();
    
    // 0.5초마다 컬링 상태 업데이트
    // 랜덤 오프셋을 추가하여 모든 함정이 동시에 체크하는 것을 방지
    float RandomOffset = FMath::FRandRange(0.0f, 0.5f);
    
    GetWorldTimerManager().SetTimer(
        CullingTimerHandle,
        this,
        &AGS_TrapBase::UpdateCulling,
        0.5f,           // 주기
        true,           // 반복
        RandomOffset    // 최초 지연 (분산 효과)
    );
}

3.4 라이팅 문제 해결: 최소 침습적 최적화

라이트의 물리적 특성은 건드리지 않고, 성능에만 영향을 주는 옵션만 조절했다.

// 수정된 라이트 최적화 (비주얼 영향 없음)
void AGS_RoomBase::OptimizeLights()
{
    for (ULightComponent* LightComp : GetComponents<ULightComponent>())
    {
        // ❌ 하면 안 되는 것 (비주얼 파괴)
        // LightComp->SetUseInverseSquaredFalloff(false);
        // LightComp->SetIntensityUnits(ELightUnits::Unitless);
        
        // ✅ 해도 되는 것 (성능만 개선)
        LightComp->SetCastShadows(false);           // 그림자 비활성화
        LightComp->SetAffectReflection(false);      // 반사 영향 제거
        LightComp->SetAffectGlobalIllumination(false);  // GI 영향 제거
        LightComp->bUseRayTracedDistanceFieldShadows = false;
    }
}

3.5 재사용 가능한 베이스 클래스 생성

같은 로직을 여러 클래스에 복사하는 대신, 공통 부모 클래스를 만들어 상속받도록 했다.

// GS_EnvironmentProp.h - 범용 환경 프랍 베이스 클래스

UCLASS()
class AGS_EnvironmentProp : public AActor
{
    GENERATED_BODY()
    
public:
    // 에디터에서 개별 조절 가능한 컬링 거리
    UPROPERTY(EditAnywhere, Category = "Optimization")
    float BaseCullDistance = 5000.0f;  // 기본 50미터
    
protected:
    virtual void BeginPlay() override;
    
private:
    void UpdateCulling();
    
    FTimerHandle CullingTimerHandle;
    bool bCurrentlyVisible = true;
};

이제 새로운 환경 오브젝트를 만들 때 이 클래스를 상속받기만 하면 자동으로 최적화가 적용된다.


4. 핵심 포인트

4.1 핵심 개념: 렌더링 파이프라인의 계층 구조

[가시성 제어의 우선순위]

1. SetActorHiddenInGame(true)    → 액터 전체를 숨김 (최상위)
2. SetVisibility(false)          → 컴포넌트 단위로 숨김
3. Cull Distance Volume          → 레벨에 배치된 볼륨으로 컬링
4. SetCullDistance()             → 컴포넌트별 거리 기반 컬링
5. 나나이트 자체 LOD             → 스크린 픽셀 기반 컬링 (최하위)

→ 상위 단계에서 숨기면 하위 단계는 실행조차 되지 않음
→ 나나이트 메시는 4, 5단계를 무시하므로 2단계(SetVisibility)로 제어해야 함

4.2 비슷한 문제 예방을 위한 체크리스트

새로운 최적화 코드를 작성할 때 확인해야 할 사항들이다.

  1. 대상 메시가 나나이트인가?
    • 나나이트 메시는 SetCullDistance()가 작동하지 않음
    • SetVisibility() 사용 필요
  2. 물리 기반 렌더링 설정을 건드리는가?
    • 라이트의 감쇠, 단위 등을 변경하면 비주얼이 깨질 수 있음
    • 그림자, 반사, GI만 끄는 것이 안전
  3. 그림자를 끄면 빛 누수가 발생하는가?
    • 근거리에서는 그림자를 켜서 물리적 차단 유지
    • 거리 기반 동적 제어로 해결
  4. 매 프레임 실행되는 코드인가?
    • 거리 계산 등은 타이머로 분산 처리
    • 랜덤 오프셋으로 동시 실행 방지
  5. 재사용 가능한 구조인가?
    • 같은 로직이 여러 곳에 필요하면 베이스 클래스로 분리

요약

이번 트러블슈팅의 핵심은 "엔진의 기본 기능이 항상 기대대로 작동하지는 않는다"는 점이다.
나나이트처럼 새로운 기술을 사용하면 기존 방법이 통하지 않을 수 있으며, 이럴 때는 더 상위 레벨에서 직접 제어하는 방식으로 우회해야 한다.


또한 성능 최적화는 항상 비주얼 품질과의 트레이드오프를 수반한다.
무작정 모든 옵션을 끄기보다는, 어떤 설정이 어떤 영향을 주는지 이해한 후 최소한의 변경만 적용하는 것이 바람직하다.
특히 그림자는 성능 비용이 크지만, 빛 누수 방지를 위해 근거리에서는 반드시 활성화해야 한다는 점을 기억해야 한다.

Draws : 3000~3500 ➡️ 1500~1700

저는 AI를 적극적으로 활용하는 개발자입니다. 코드 구현은 AI 도구와 협업하고, 저는 문제 분석, 기술 설계, 트러블슈팅, 최종 검증에 집중합니다. 모든 기술적 의사결정과 트러블슈팅은 제가 직접 수행한 것이며, AI는 그 과정을 가속화하는 도구였습니다. 이 블로그는 그 판단과 사고의 기록입니다. "어떤 도구를 쓰느냐"보다 "어떤 문제를 해결하느냐"가 진짜 개발자의 가치라고 믿습니다.

I believe a developer's value lies in "what problems they solve," not "what tools they use."

'Dev. > UE 언리얼 엔진' 카테고리의 다른 글

[GAS] 언리얼 엔진 피격 인디케이터 개발  (0) 2026.01.13
[GAS] Wwise 오디오 오클루전 시스템 구축  (0) 2026.01.09
[GAS] LOD 설정 실수로 인한 빌드 크래시, 원인 분석과 해결까지  (0) 2026.01.06
[GAS] 렌더링 최적화 : 30 FPS에서 60 FPS까지 (2)  (2) 2026.01.05
[GAS] 이펙트가 프레임을 떨어뜨리는 이유, 오버드로 | 컷아웃 기법  (1) 2026.01.03
'Dev./UE 언리얼 엔진' 카테고리의 다른 글
  • [GAS] 언리얼 엔진 피격 인디케이터 개발
  • [GAS] Wwise 오디오 오클루전 시스템 구축
  • [GAS] LOD 설정 실수로 인한 빌드 크래시, 원인 분석과 해결까지
  • [GAS] 렌더링 최적화 : 30 FPS에서 60 FPS까지 (2)
raindrovvv
raindrovvv
raindrovvv 님의 블로그 입니다.
  • raindrovvv
    raindrovvv 님의 블로그
    raindrovvv
  • 전체
    오늘
    어제
    • 분류 전체보기 (170) N
      • Dev. (163) N
        • AI 인공지능 (27)
        • UE 언리얼 엔진 (81) N
        • Unity 유니티 (0)
        • Wwise 와이즈 (7)
        • 게임 네트워크 (8)
        • 그래픽스 Graphics (22)
        • 프로젝트 (8)
        • 기타 개발 관련 (10)
      • Computer Science (0)
        • 하드웨어 HW (0)
        • 소프트웨어 SW (0)
        • 통신 (0)
        • 데이터 (0)
      • 블로그 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    언리얼
    에이전트
    바이브코딩
    Wwise
    unrealengine
    게임개발
    devlog
    네트워크
    dev
    그래픽스
    TA
    Unreal
    UE
    인디게임
    Git
    AI
    생산성
    언리얼엔진
    깃
    트러블슈팅
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
raindrovvv
[GAS] 나나이트(Nanite) 메시의 거리 컬링 최적화
상단으로

티스토리툴바