
오늘은 제가 언리얼 엔진 소스를 파헤치다가 발견한 흥미로운 최적화 기능과, 그것을 몰라서 같은 기능을 직접 구현하려 했던 뻘짓(?)에 대한 이야기를 들려드리려 합니다.
사실 며칠 전까지만 해도 저는 모바일 플랫폼에서 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을 연결하면, 엔진이 자동으로 이를 감지해서 최적화를 수행하고 있었던 것입니다!
PBR(Physically Based Rendering)에서 Roughness는 표면의 거칠기를 나타냅니다:
Roughness가 1.0이라는 것은 표면이 너무 거칠어서 빛이 모든 방향으로 균등하게 산란된다는 의미입니다. 즉, 반사 계산이 필요 없다는 뜻이죠!
이 최적화가 활성화되면:
// MaterialHLSLEmitter.cpp (486번 줄)
OutEnvironment.SetDefine(TEXT("MATERIAL_FULLY_ROUGH"), bIsFullyRough);
// BasePassPixelShader.usf
#define FORCE_FULLY_ROUGH (MATERIAL_FULLY_ROUGH)
#if !FORCE_FULLY_ROUGH
// 반사 캡처 관련 복잡한 계산들...
// 이 부분이 통째로 컴파일에서 제외됩니다!
#endif
// 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을 넣어도 반사가 없어서 Fully Rough처럼 보이는데, 똑같은 최적화가 되는 걸까?"
답은 "아니오"입니다!
코드를 분석해보니 흥미로운 사실을 발견했습니다:
// 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 계산을 수행합니다!
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 제거
}
이 함수는:
| 설정 | 컴파일 타임 최적화 | 런타임 계산 | 에너지 보존 |
|---|---|---|---|
| Roughness = 1.0 | BRDF 코드 제거 | 계산 스킵 | 자동 처리 |
| Specular = 0 | 없음 | 모든 계산 수행 | 수동 필요 |
그래서 제가 결국 깨달은 것은, 코드를 수정할 필요가 전혀 없었다는 것입니다!
![Platform Switch 노드 예시]
[Platform Switch]
├─ Default → Texture Sample (기존 Roughness 텍스처)
├─ Mobile → Constant 1.0
└─ Output → Roughness
이렇게 간단하게 설정하면:
특정 셰이더(예: TwoSided Foliage)에만 이런 설정을 명시적으로 해주면, 플랫폼별로 자동으로 최적화가 적용됩니다.
제가 테스트해본 결과:
// 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
// 모바일 최적화 콤보
[Platform Switch: Metallic] → Mobile: 0.0
[Platform Switch: Specular] → Mobile: 0.0
[Platform Switch: Roughness] → Mobile: 1.0
[Quality Switch]
├─ Low → 1.0 (모든 플랫폼에서 최적화)
├─ Medium → Platform Switch (플랫폼별 분기)
├─ High → Original Texture
└─ Output → Roughness
// 최적화를 원한다면
[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는 건드리지 마세요!
이번 경험을 통해 얻은 교훈:
저처럼 "모바일에서 TwoSided Foliage를 강제로 Fully Rough 처리하는" 코드를 짜려다가, Platform Switch 노드 하나로 해결할 수 있다는 걸 뒤늦게 깨달은 분들이 있으실 겁니다.
언리얼 엔진은 우리가 생각하는 것보다 훨씬 똑똑하고, 이미 우리가 필요한 도구들을 제공하고 있습니다. 때로는 코드를 수정하기 전에, 에디터에서 제공하는 기능들을 다시 한 번 살펴보는 것도 좋은 것 같습니다.
다음에는 또 어떤 숨겨진 최적화를 발견하게 될까요?
P.S. 혹시 Platform Switch로 해결할 수 있는 걸 코드로 해결하려 했던 경험이 있으신 분들은 댓글로 공유해주세요! 함께 바퀴를 재발명하지 않는 개발자가 됩시다.