Unreal Engine의 숨겨진 최적화: Roughness 1.0이 만드는 마법

Mazeline·2025년 10월 27일
0

TA 가이드

목록 보기
7/9
post-thumbnail

바퀴를 다시 만들 뻔한 이야기

오늘은 제가 언리얼 엔진 소스를 파헤치다가 발견한 흥미로운 최적화 기능과, 그것을 몰라서 같은 기능을 직접 구현하려 했던 뻘짓(?)에 대한 이야기를 들려드리려 합니다.

사실 며칠 전까지만 해도 저는 모바일 플랫폼에서 TwoSided Foliage 셰이더의 성능을 개선하기 위해 고민하고 있었습니다. "모바일에서는 어차피 반사가 별로 안 보이니까, 강제로 Fully Rough 처리를 하면 어떨까?"라는 생각으로 직접 코드를 수정하려고 MaterialHLSLEmitter.cpp를 열었죠.

제가 원했던 것은 머티리얼 인스펙터의 Fully Rough 토글을 사용하지 않고, PC와 모바일 플랫폼 간의 자동 처리를 구현하는 것이었습니다.

그런데...

bool bIsFullyRough,  // 317번 줄에서 만난 운명적인 파라미터

이 한 줄이 제 눈에 들어왔습니다. "어? 이게 뭐지?"
사실은 동료 TA 인 이성학 사원이 예전에 말 했던 기억이 얼핏 있었습니다. 뭐 망각의 동물 아니겠습니까? ㅎㅎ

발견의 순간

호기심에 코드를 더 파보니, 언리얼 엔진이 이미 제가 하려던 일을 자동으로 처리하고 있었습니다:

// MaterialHLSLEmitter.cpp (915-931번 줄)
bIsFullyRough = bIsFullyRough || EmitContext.Material->IsFullyRough();
if (!bIsFullyRough)
{
    // Roughness 필드를 찾아서...
    const FStructField* RoughnessField = 
        CachedTree->GetMaterialAttributesType()->FindFieldByName(
            *FMaterialAttributeDefinitionMap::GetAttributeName(MP_Roughness));
    
    // 상수값인지 확인하고...
    if (Evaluation == EExpressionEvaluation::Constant)
    {
        // 값이 1.0이면 Fully Rough로 처리!
        bIsFullyRough = ConstantValue.Component[RoughnessField->ComponentIndex].Float == 1.f;
    }
}

네, 맞습니다. 머티리얼 에디터에서 Roughness 포트에 상수 1.0을 연결하면, 엔진이 자동으로 이를 감지해서 최적화를 수행하고 있었던 것입니다!

Roughness 1.0이 뭐가 특별한가요?

PBR(Physically Based Rendering)에서 Roughness는 표면의 거칠기를 나타냅니다:

  • 0.0: 완벽한 거울 (perfect mirror)
  • 1.0: 완전히 거친 표면 (fully rough)

Roughness가 1.0이라는 것은 표면이 너무 거칠어서 빛이 모든 방향으로 균등하게 산란된다는 의미입니다. 즉, 반사 계산이 필요 없다는 뜻이죠!

엔진이 해주는 최적화

이 최적화가 활성화되면:

1. 셰이더 컴파일 시점에 매크로 정의

// MaterialHLSLEmitter.cpp (486번 줄)
OutEnvironment.SetDefine(TEXT("MATERIAL_FULLY_ROUGH"), bIsFullyRough);

2. 픽셀 셰이더에서 불필요한 계산 스킵

// BasePassPixelShader.usf
#define FORCE_FULLY_ROUGH (MATERIAL_FULLY_ROUGH)

#if !FORCE_FULLY_ROUGH
    // 반사 캡처 관련 복잡한 계산들...
    // 이 부분이 통째로 컴파일에서 제외됩니다!
#endif

3. 모바일에서는 더 많은 최적화

// MobileBasePassPixelShader.usf
#define FULLY_ROUGH (MATERIAL_FULLY_ROUGH || MOBILE_QL_FORCE_FULLY_ROUGH)

// SSR도 자동으로 비활성화
#if MOBILE_SSR_ENABLED && !(MOBILE_QL_FORCE_FULLY_ROUGH || MATERIAL_FULLY_ROUGH)
    // Screen Space Reflection 계산
#endif

잠깐, Specular 0과 Roughness 1.0은 뭐가 다른가요?

테스트하면서 흥미로운 의문이 생겼습니다. "Specular 포트에 0을 넣어도 반사가 없어서 Fully Rough처럼 보이는데, 똑같은 최적화가 되는 걸까?"

답은 "아니오"입니다!

Specular 0의 진실

코드를 분석해보니 흥미로운 사실을 발견했습니다:

// MaterialHLSLEmitter.cpp에서는 오직 Roughness만 체크합니다
if (Evaluation == EExpressionEvaluation::Constant)
{
    bIsFullyRough = ConstantValue.Float == 1.f;  // Roughness가 1.0인지만 확인
}

// Specular에 대한 유사한 최적화는 없습니다!

렌더링 시점의 차이

더 중요한 차이는 셰이더에서 나타납니다:

// FORCE_FULLY_ROUGH가 활성화되면 (Roughness = 1.0)
#if !FORCE_FULLY_ROUGH
    // 이 전체 블록이 컴파일에서 제외됩니다!
    // 반사 관련 복잡한 계산들...
#endif

// 하지만 Specular = 0일 때는
float3 SpecularColor = lerp(0.08f * [Specular.xxx](http://Specular.xxx), BaseColor, Metallic);
// SpecularColor가 0이 되지만, 여전히 모든 BRDF 계산을 수행합니다!

EnvBRDFApproxFullyRough의 비밀

Roughness가 1.0일 때만 특별한 함수가 호출됩니다:

// BRDF.ush
void EnvBRDFApproxFullyRough(inout half3 DiffuseColor, inout half3 SpecularColor)
{
    // Factors derived from EnvBRDFApprox( SpecularColor, 1, 1 ) == SpecularColor * 0.4524 - 0.0024
    DiffuseColor += SpecularColor * 0.45;  // 에너지 보존을 위해 Diffuse에 추가
    SpecularColor = 0;                      // Specular 제거
}

이 함수는:

  1. 에너지 보존: 반사되지 않은 빛을 Diffuse로 전환
  2. 물리적 정확성: Roughness 1.0에서의 실제 BRDF 근사값 사용

성능 차이 요약

설정컴파일 타임 최적화런타임 계산에너지 보존
Roughness = 1.0BRDF 코드 제거계산 스킵자동 처리
Specular = 0없음모든 계산 수행수동 필요

결론: Platform Switch가 답이었다!

그래서 제가 결국 깨달은 것은, 코드를 수정할 필요가 전혀 없었다는 것입니다!

머티리얼 에디터에서 Platform Switch 활용하기

![Platform Switch 노드 예시]

[Platform Switch]
├─ Default → Texture Sample (기존 Roughness 텍스처)
├─ Mobile → Constant 1.0
└─ Output → Roughness

이렇게 간단하게 설정하면:

  • PC: 원래 의도한 Roughness 텍스처 사용
  • 모바일: 자동으로 1.0 상수값 → Fully Rough 최적화 발동!

특정 셰이더(예: TwoSided Foliage)에만 이런 설정을 명시적으로 해주면, 플랫폼별로 자동으로 최적화가 적용됩니다.

실제로 얼마나 효과가 있나요?

제가 테스트해본 결과:

  • PC: 원본 퀄리티 유지
  • 모바일:
    • 픽셀당 약 10-15개의 ALU 명령어 감소
    • SSR 비활성화로 추가 성능 향상
    • 특히 foliage처럼 오버드로우가 심한 경우 눈에 띄는 개선

실무 활용 팁

1. Platform Switch 활용 예시

// Foliage 머티리얼
[Platform Switch: Roughness]
├─ Default → RoughnessTexture.Sample
├─ Mobile → 1.0
└─ Output → Roughness

// 지면 머티리얼
[Platform Switch: Roughness]  
├─ Default → Lerp(0.3, 0.8, DirtMask)
├─ Mobile → 0.95  // 거의 Rough하지만 완전히는 아닌
└─ Output → Roughness

2. 다른 최적화 조합

// 모바일 최적화 콤보
[Platform Switch: Metallic] → Mobile: 0.0
[Platform Switch: Specular] → Mobile: 0.0  
[Platform Switch: Roughness] → Mobile: 1.0

3. Quality Switch와의 조합

[Quality Switch]
├─ Low → 1.0 (모든 플랫폼에서 최적화)
├─ Medium → Platform Switch (플랫폼별 분기)
├─ High → Original Texture
└─ Output → Roughness

4. 실무 팁

// 최적화를 원한다면
[Platform Switch: Roughness] → Mobile: 1.0  (추천)

// 이것만으로는 부족합니다
[Platform Switch: Specular] → Mobile: 0.0   (비추천)

// 최상의 조합 (필요시)
[Platform Switch: Roughness] → Mobile: 1.0
[Platform Switch: Metallic] → Mobile: 0.0
// Specular는 건드리지 마세요!

엔진을 믿고, 엔진을 알자

이번 경험을 통해 얻은 교훈:

  1. 엔진이 제공하는 도구를 먼저 찾아보자: Platform Switch 같은 기본 도구로도 충분했습니다
  2. 소스 코드를 읽으면 원리를 이해할 수 있다: 왜 1.0이 특별한지, 왜 Specular 0이 다른지 알게 되었습니다
  3. 복잡한 해결책보다 단순한 해결책을: 코드 수정보다 노드 하나가 더 우아했습니다
  4. 비슷해 보여도 다를 수 있다: Specular 0과 Roughness 1.0의 차이처럼

저처럼 "모바일에서 TwoSided Foliage를 강제로 Fully Rough 처리하는" 코드를 짜려다가, Platform Switch 노드 하나로 해결할 수 있다는 걸 뒤늦게 깨달은 분들이 있으실 겁니다.

언리얼 엔진은 우리가 생각하는 것보다 훨씬 똑똑하고, 이미 우리가 필요한 도구들을 제공하고 있습니다. 때로는 코드를 수정하기 전에, 에디터에서 제공하는 기능들을 다시 한 번 살펴보는 것도 좋은 것 같습니다.

다음에는 또 어떤 숨겨진 최적화를 발견하게 될까요?


P.S. 혹시 Platform Switch로 해결할 수 있는 걸 코드로 해결하려 했던 경험이 있으신 분들은 댓글로 공유해주세요! 함께 바퀴를 재발명하지 않는 개발자가 됩시다.

profile
테크아트 컨설팅 전문 회사 "메이즈라인" 입니다.

0개의 댓글