TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역][따로 정리추가] 모바일에서의 언리얼 엔진 "Panner" 노드 정확도 및 퍼포먼스 문제 분석

jplee 2025. 6. 12. 23:48

Panner 노드는 주로 머티리얼의 텍스처에 부드러운 스크롤 효과를 구현하는 데 사용되며, 물결 효과 등에 적용할 수 있습니다. 최근 모바일 환경에서 시간이 지날수록 이 노드를 사용한 머티리얼의 스크롤이 점점 느려지고 버벅이는 현상이 발견되었는데, 이는 정밀도 문제로 추정됩니다.

Panner 노드의 구현

Panner 노드의 구현은 UMaterialExpressionPanner::Compile에 위치해 있으며, 특별한 HLSL 코드가 있는 것이 아니라 기본 연산자들의 조합으로 이루어져 있습니다.

bFractionalPart가 체크된 경우:

  • UV = UV + frac(Time * Speed)

bFractionalPart가 체크되지 않은 경우:

  • UV = UV + Time * Speed

문제의 원인

가장 단순한 머티리얼도 DirectX Mobile에서 변환된 HLSL을 살펴보면:

half Local1 = (View_GameTime * 0.94999999);
	half Local2 = frac(Local1);

	half2 Local3 = Parameters.TexCoords[0].xy;
	half2 Local4 = half2(  Local2 ,frac(0.00000000));
	half2 Local5 = (  Local4  +   Local3 );
	half Local6 =  1.0f;
	half4 Local7 = ProcessMaterialColorTextureLookup(Texture2DSample(Material_Texture2D_0,Material_Texture2D_0Sampler,  Local5 ));
	half Local8 =  1.0f;

문제는 Panner 노드의 변환 과정에 있습니다. frac(GameTime * Speed) 표현식을 인라인 구문으로 변환하지 않고, 중간 변수에 먼저 저장한 후 frac 연산을 수행합니다. 이로 인해 half Local1 = (View_GameTime * 0.94999999); 구문에서 float32를 half 변수에 저장하면서 정밀도가 손실되고, 이후 frac 연산은 이미 정밀도가 손실된 변수에 대해 수행됩니다.

 

해결 방안

  1. 엔진을 수정하지 않으려면, 모바일에서 Panner 노드를 비활성화하고(SceneColor 노드가 ES31에서 차단되는 것처럼), 프로젝트 팀이 UV 계산을 처리하는 Custom 노드를 작성할 수 있습니다.
  2. 엔진을 수정하는 경우에는 다음과 같은 방법들이 있습니다:

체계적인 방안:

가장 체계적인 방안은 Float Scope를 구현하는 것입니다. 이를 통해 머티리얼의 일부 연결된 노드들을 float32 계산으로 승격시켜 여러 문제를 한번에 해결할 수 있습니다. Dynamic Branch를 구현하면서 Float Scope도 함께 구현했는데, 이는 아래 참고 문서를 기반으로 머티리얼 에디터를 수정한 것입니다.

참고: UE4 동적 분기 및 관련 머티리얼 노드 컴파일 원리 - 지후

전반적인 접근 방식은 동일합니다. 컴파일 시 특정 Scope 내에 있는 노드들을 추적하고, 해당 Scope 내의 노드들에 대해 HLSL 생성 시 추가 처리를 수행해야 합니다.

머티리얼에서 컴파일된 데이터 타입은 모두 MaterialFloat이며, 플랫폼별 매크로에 따라 이 타입이 half 또는 float로 변환됩니다. FloatScope의 시작과 끝에서 MaterialFloat 타입을 재정의하면 특정 머티리얼 노드 구간의 계산 정밀도를 자유롭게 제어할 수 있습니다.

수정된 Panner 노드가 생성하는 HLSL 코드는 아래와 같으며, UV 계산 시 float 계산으로 전환된 것을 확인할 수 있습니다.

#undef MaterialFloat #undef MaterialFloat2 #undef MaterialFloat3 #undef MaterialFloat4#undef MaterialFloat3x3 #undef MaterialFloat4x4 #undef MaterialFloat4x3

                    #define MaterialFloat float#define MaterialFloat2 float2#define MaterialFloat3 float3#define MaterialFloat4 float4#define MaterialFloat3x3 float3x3#define MaterialFloat4x4 float4x4 #define MaterialFloat4x3 float4x3
    MaterialFloat Local1 = (View.GameTime * 0.94999999);
    FloatDeriv Local2 = FracDeriv(ConstructConstantFloatDeriv(Local1));

                    #undef MaterialFloat #undef MaterialFloat2 #undef MaterialFloat3 #undef MaterialFloat4 #undef MaterialFloat3x3 #undef MaterialFloat4x4 #undef MaterialFloat4x3 #if PIXELSHADER && !FORCE_MATERIAL_FLOAT_FULL_PRECISION#define MaterialFloat half#define MaterialFloat2 half2#define MaterialFloat3 half3#define MaterialFloat4 half4#define MaterialFloat3x3 half3x3#define MaterialFloat4x4 half4x4 #define MaterialFloat4x3 half4x3 #else// Material translated vertex shader code always uses floats,// Because it's used for things like world position and UVs#define MaterialFloat float#define MaterialFloat2 float2#define MaterialFloat3 float3#define MaterialFloat4 float4#define MaterialFloat3x3 float3x3#define MaterialFloat4x4 float4x4 #define MaterialFloat4x3 float4x3 #endif

    FloatDeriv2 Local3 = ConstructFloatDeriv2(Parameters.TexCoords[0].xy,Parameters.TexCoords_DDX[0].xy,Parameters.TexCoords_DDY[0].xy);
    FloatDeriv2 Local4 = ConstructFloatDeriv2(MaterialFloat2(DERIV_BASE_VALUE(Local2),frac(0.00000000)),MaterialFloat2(Local2.Ddx, 0.0f),MaterialFloat2(Local2.Ddy, 0.0f));
    FloatDeriv2 Local5 = AddDeriv(Local4,Local3);

간단한 해결 방안: (시도해보지는 않았지만, 가능할 것으로 보임)

여기서 발생하는 버그의 근원이 중간 변수가 생성되는 것임을 알 수 있습니다. 코드를 살펴보면 다음 부분에서 확인할 수 있습니다:

Arg1 = Compiler->PeriodicHint(Compiler->Frac(Compiler->Mul(TimeArg, SpeedXArg)));
Arg2 = Compiler->PeriodicHint(Compiler->Frac(Compiler->Mul(TimeArg, SpeedYArg)));

Compiler->Mul의 구현을 살펴보면, Uniform/Constant에 관한 여러 특수 최적화 분기를 제외하고, 최종적으로 다음 코드에 도달합니다:

 return AddCodeChunk(GetArithmeticResultType(A,B),TEXT("(%s * %s)"),*GetParameterCode(A),*GetParameterCode(B));

여기서 `AddCodeChunk`는 표현식의 결과를 저장하기 위한 중간 변수를 생성합니다.

`Compiler->inlineMul` 함수를 추가하고 `AddInlinedCodeChunk` 변형을 호출하도록 하면, `frac(Time * Speed)`와 같은 코드를 직접 생성할 수 있을 것입니다.


별도 정리.

본문은 언리얼 엔진 머티리얼 에디터의 Panner 노드가 모바일 환경(특히 OpenGL ES 3.1과 같은 모바일 그래픽 API)에서 정밀도 손실로 인한 스크롤 버벅임과 성능 저하 문제를 상세히 분석하고, 문제의 원인을 탐구하며, 업계와 커뮤니티에서 제시한 수정 및 최적화 방안을 정리했습니다.

1. Panner 노드의 용도와 구현

Panner 노드는 머티리얼에서 UV 텍스처의 부드러운 스크롤 효과를 구현하는 데 널리 사용되며, 수면이나 에너지 흐름 등의 동적 효과에 주로 적용됩니다. 핵심 구현 로직은 시간 파라미터에 속도를 곱한 후 원래 UV와 합산하여 텍스처의 지속적인 이동을 구현합니다. 이 노드는 소수 부분 사용 여부(Fractional Part)를 지원하며, 해당하는 HLSL 의사 코드는 다음과 같습니다:

  • bFractionalPart 활성화: UV = UV + frac(Time * Speed)
  • bFractionalPart 비활성화: UV = UV + Time * Speed

2. 모바일에서의 정밀도 손실과 버벅임의 근본 원인

모바일 환경에서는 Panner 노드가 컴파일하여 생성하는 HLSL 코드가 일반적으로 시간과 속도의 곱셈 결과를 half(16비트 부동소수점) 변수에 저장합니다. 예를 들면:

half Local1 = (View_GameTime * 0.94999999);
half Local2 = frac(Local1);
...

여기서 핵심 문제는 GameTime이 원래 float32(32비트 부동소수점)이지만 중간 변수 Local1이 half로 되면서 정밀도가 급격히 떨어진다는 점입니다. 특히 시간 값이 커질수록 half 정밀도로는 모든 소수점 변화를 정확하게 표현할 수 없어 frac 연산 후 UV 증분에 "점프"가 발생합니다. 시간이 지날수록 스크롤이 점점 느려지거나 버벅이게 되어 사용자 경험이 현저히 저하됩니다[1].

3. 문제 발생의 코드 메커니즘 분석

UE4/UE5의 머티리얼 컴파일러는 Panner 노드 코드를 생성할 때, 일반적으로 Time * Speed의 결과를 로컬 변수(AddCodeChunk)에 먼저 할당한 후 해당 변수에 대해 frac 연산을 수행합니다. 플랫폼 매크로에서 MaterialFloat 타입이 모바일에서 일반적으로 half로 매핑되기 때문에, 이 과정에서 정밀도 손실이 직접적으로 발생합니다. 예를 들면:

half Local1 = (View_GameTime * Speed);
half Local2 = frac(Local1);

직접 인라인으로 생성하는 대신:

frac(View_GameTime * Speed)

이러한 중간 변수의 도입이 정밀도 문제의 근본적인 원인입니다[1].

4. 기존 해결 방안

4.1 엔진 수정 없이 해결하는 방법

모바일에서 기본 Panner 노드를 비활성화하고(ES31에서의 SceneColor 노드 차단과 유사), 프로젝트 팀이 Custom 노드를 직접 정의하여 UV 스크롤 HLSL 코드를 작성하고 float32 계산을 강제함으로써 half 정밀도 문제를 회피할 수 있습니다[1].

4.2 엔진 수정을 통한 시스템적 해결 방안: Float Scope

더 체계적인 접근 방식은 "Float Scope" 메커니즘을 도입하는 것입니다. 이는 머티리얼 노드 컴파일 단계에서 특정 핵심 노드(Panner 등)와 관련 연결 경로를 추적하여, 이러한 노드들과 그 상하위 MaterialFloat 타입을 플랫폼 기본값인 half 대신 float32로 강제하는 원리입니다. 구현 시에는 HLSL 코드 생성 전후에 타입 매크로 재정의를 삽입할 수 있습니다:

#undef MaterialFloat
#define MaterialFloat float

#undef MaterialFloat
#define MaterialFloat half // 기본값 복원

이러한 방식으로 Panner 노드의 모든 중간 표현식이 float32 정밀도로 계산되어 정밀도 손실 문제를 완전히 해결할 수 있습니다. 관련 접근 방식은 지후 문서 'UE4 동적 분기 및 관련 머티리얼 노드 컴파일 원리' 를 참고할 수 있습니다[1].

4.3 간소화된 엔진 수정

Panner 노드의 코드 생성 로직을 직접 최적화하여 불필요한 중간 변수를 피합니다. 즉, 노드 컴파일 시 Mul과 Frac 연산에 인라인 옵션을 추가하여 frac(Time * Speed)의 인라인 표현식을 생성하고, 중간 변수에 저장한 후 frac을 수행하지 않습니다. 이렇게 하면 MaterialFloat가 half여도 정밀도 손실을 최소화할 수 있습니다.

5. 관련 커뮤니티 및 공식 진행 상황

현재까지 Epic Games 공식적으로는 메인 브랜치에 시스템적 수정사항을 반영하지 않았으며, 커뮤니티는 주로 커스텀 노드나 엔진 소스 코드 수정을 통해 문제를 우회하고 있습니다. 일부 개발자들이 GitHub, 지후 등의 플랫폼에서 자신들의 수정 패치와 접근 방식을 공유했으며, 주요 방향은 위에서 언급한 두 가지입니다: 커스텀 노드로 float32를 강제하거나 엔진의 MaterialFloat 매크로 정의 범위 또는 노드 코드 생성 전략을 수정하는 것입니다.

6. 요약 및 제안

모바일에서 Panner 노드의 half 정밀도 부족으로 인한 버벅임 문제는 본질적으로 머티리얼 컴파일 중간 변수의 정밀도 설정이 부적절한 것이 원인입니다. 단기적으로는 커스텀 노드를 통해 회피할 수 있으며, 장기적으로는 엔진 차원에서 Float Scope를 도입하거나 노드 코드 생성 로직을 최적화하여 중요 노드 및 표현식 체인의 float32 정밀도를 보장하고, 모바일 머티리얼 애니메이션의 부드러움과 안정성을 향상시키는 것이 좋습니다.

Float Scope와 동적 분기 구현 원리에 대해 더 자세히 알아보려면 지후와 Unreal Engine 커뮤니티의 관련 기술 문서를 참고하시기 바랍니다.