Custom normal texture encoding function creation How to.

모바일 게임을 제작 하다 보면 여러가지 문제점을 만나게 된다.

그 중에서 하나 대표적인 것인 CPU 에서 빈번하게 발생 하게 되는 Draw-Call 이외에도 Texture fetching 이 빈번하게 발생 하는 부분이다.

최적화를 하다 보면 경력자 아티스트도 이 부분을 어느정도 들은 적이 있기 때문에 최대한 Sampler 를 많이 만들지 않으려고 노력 한다.

하지만 근본적인 원인은 잘 알고 있지 않다.

아무튼 하드웨어 적인 메카니즘에 대한 이야기를 하려는 것은 아니다.

이런 이야기를 이해 한다는 것은 내가 생각 할 때 굳이 내 글을 볼 필요가 없는 수준에 이미 도달 한 숙련자 일 것이기 때문이다.

어쨌든 Sampler 를 최대한 소량으로 선언 하는 것이 좋다는 것만 일단 알고 있자.

보통 Sampler2D 또는 tex2D 는 자주 봤을 것인데 이것은 CPU 에서 Texture Fetching 을 요구 한다.

결국 Draw-call 처럼 직렬 처리 되는 Texture Fetching 의 빈도가 높다는 것은 그렇게 기쁜 소식은 아닐 것이다.

피부 셰이더를 개발 하거나 기타 조금 복잡한 셰이더를 만들 경우에 한 장 이상의 노말맵을 사용 해야 하는 경우는 꼭 발생한다.

결국 두 장의 노말맵을 어떻게 한 장으로 묶어 줄 수 있는가에 대한 의문을 갖게 될 것이다.

수학적인 처리 방법은 그다지 관심이 없을 듯 하기 때문에 본론으로 바로 들어가서 어떻게 구현 할 수 있는지 함께 살펴보자.

또한 이 내요을 좀 더 쉽게 풀어가기 위해서 나는 Amplify shader Editor 를 사용 했다.

URP 에서 같은 내용을 다룰 것이지만 지금은 일단 이 내용을 충실하게 살펴보도록 하자.

노말맵핑이 뭔지 설마 모르지 않겠지만 그래도 여기에 더 자세한 정보가 담겨 있다.

굳이 노말맵이 더 궁금하다면 전직 렌더링 프로그래머였던 포프님의 유투브 체널을 시청 해 보자.

영상을 볼 때 노말맵의 Y 체널에 대한 이야기가 언급 되는데 이 부분은 좀 더 분명하게 이해 해야 할 필요가 있는 것 같다.

영상으로 포프님이 만들다 보니 설명이 조금 터프 한 느낌이다.

왼손 좌표계 오른손 좌표계 까지 이야기를 하는 것은 너무 복잡한 내용이 될 수 있기 떄문에 간단히 정리 해 보자.

다이렉트 엑스 렌더링과 오픈지엘 렌더링의 경우 UV 의 원점 위치가 일단 다르다.

결국 이게 범인이다.

다시 본론으로 돌아 가자.

Unity3D built-in Shader 코드를 참조 하자.

URP Core 의 ShaderLinrary/Packing.hlsl 을 참조 했다.

// Unpack from normal map
real3 UnpackNormalRGB(real4 packedNormal, real scale = 1.0)
{
    real3 normal;
    normal.xyz = packedNormal.rgb * 2.0 - 1.0;
    normal.xy *= scale;
    return normalize(normal);
}

위 두 함수를 참조 하여 코드를 목적에 부합 하도록 수정 하고 새로운 함수를 추가 하자.
추가 한 새로운 함수는 Amplify Shader Editor 의 Custom Expression 을 사용 하여 테스트 해 볼 것이다.

분명한 것은 이 새로운 함수의 목적이다.
1. 두 장의 노말맵을 한장의 노말맵으로 합쳐서 한번의 Set texture 만 발생 하도록 하자.
2. 모바일 게임용의 최적화 기본 방안을 생각하자. 예로 들어서 ALU 를 최소화 하기 위한 Approximation 에 대한 편차 있는 결과에 대해서 너그럽게 받아들이자.

두 개의 함수 형태로 재구성 했다.

JP_UnpackNormalRG_SafeNormal

inline float3 JP_UnpackNormalRG_SafeNormal( half2 normalXY )
	{
		half3 normal;
		normal.xy = normalXY.xy * 2 - 1;
		normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
		return normalize(normal);
	}

JP_UnpackNormalRG_SafeNormal_Optimal

inline float3 JP_UnpackNormalRG_SafeNormal_Optimal( half2 normalXY )
	{
	     return normalize(half3(normalXY.xy * 2 - 1 , 1));
	}

P_UnpackNormalRG_SafeNormal
JP_UnpackNormalRG_SafeNormal_Optimal

두 함수를 시각적으로 Debug 하기 위해 TransformDirection 을 사용 하고 비교 했다.


위 두 식을 컴파일 하고 ALU 수량도 비교 해 보자.

Disassemble code 상에서 명령어 수량은 3개 차이가 있다.

JP_UnpackNormalRG_SafeNormal 의 Disassemble code block.

// SV_Target                0   xyzw        0   TARGET   float   xyzw
      ps_4_0
      dcl_constantbuffer CB0[5], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v1.xy
      dcl_output o0.xyzw
      dcl_temps 1
   0: mad r0.xy, v1.xyxx, cb0[4].xyxx, cb0[4].zwzz
   1: sample r0.xyzw, r0.xyxx, t0.xyzw, s0
   2: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), l(-1.000000, -1.000000, 0.000000, 0.000000)
   3: dp2 r0.w, r0.xyxx, r0.xyxx
   4: min r0.w, r0.w, l(1.000000)
   5: add r0.w, -r0.w, l(1.000000)
   6: sqrt r0.z, r0.w
   7: dp3 r0.w, r0.xyzx, r0.xyzx
   8: rsq r0.w, r0.w
   9: mad o0.xyz, r0.xyzx, r0.wwww, l(0.000010, 0.000010, 0.000010, 0.000000)
  10: mov o0.w, l(1.000000)
  11: ret 
// Approximately 0 instruction slots used

JP_UnpackNormalRG_SafeNormal_Optimal 의 Disassemble code block.

// SV_Target                0   xyzw        0   TARGET   float   xyzw
      ps_4_0
      dcl_constantbuffer CB0[5], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v1.xy
      dcl_output o0.xyzw
      dcl_temps 1
   0: mad r0.xy, v1.xyxx, cb0[4].xyxx, cb0[4].zwzz
   1: sample r0.xyzw, r0.xyxx, t0.xyzw, s0
   2: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), l(-1.000000, -1.000000, 0.000000, 0.000000)
   3: mov r0.z, l(1.000000)
   4: dp3 r0.w, r0.xyzx, r0.xyzx
   5: rsq r0.w, r0.w
   6: mad o0.xyz, r0.xyzx, r0.wwww, l(0.000010, 0.000010, 0.000010, 0.000000)
   7: mov o0.w, l(1.000000)
   8: ret 
// Approximately 0 instruction slots used

또한 결론적으로 두 장의 노말맵을 한번에 모아서 처리 하고 있기 때문에 더 많은 명령어도 절약 된 결과가 되었다.
또는 여러 다른 텍스처 셋트가 요구 될 때 NormalMap 의 B 와 A 체널에 다른 텍스처 정보를 기록 할 수도 있다.

최적화 함수의 경우 z 에 대한 부분을 단순히 고정상수로 정의 했기 때문에 Pixel normal 의 각 pixel 당 계산도 최소화 한 것이 된다.

그리고 두 번의 Set texture 처리를 한 번의 Set texture 로 처리 했기 때문에 CPU 병목지점에 대한 최적화도 수행 한 결과가 된다.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s