2015년 8월 30일 일요일

[유니티 최적화 기법] - 셰이더 Variants

유니티 셰이더를 작성할 때 쉽게 간과하는 것이 있다.

다음은 흔한 셰이더 코드다. 빨간색 부분을 보자.
Shader "Toon/PMO_toon_mask_skin" {
    Properties {
         _Color ("Main Color", Color) = (1.0, 1.0, 1.0, 1.0)
         _UnderTex ("Under (RGB)", 2D) = "white" {}
         _MainTex ("Skin (RGB)", 2D) = "white" {}
         _RimPower ("Rim Power", Range(0.5,8.0)) = 2.0
         _RimIntesity ("Rim Intensity", float) = 2.0
         _ColorIntensity("Skin Intensity", float) = 1
         _PickColor ("Pick Color", Color) = (1, 1, 1, 1)
     }
  
     SubShader {
         Tags {"RenderTexture" = "RenderTextureMask" "IgnoreProjector"="True" "RenderType"="Opaque"}
         LOD 200
         
         CGPROGRAM
         #pragma surface surf ToonRampFace
 
         sampler2D _UnderTex;
         sampler2D _MainTex;
         half4     _Color;
         half      _RimPower;
         half4     _PickColor;
         half      _RimIntesity;
         half      _ColorIntensity;
 
         struct Input
         {
             half2 uv_MainTex : TEXCOORD0;
             half3 viewDir;
         };

         struct SurfOut
         {
             half3 Albedo;
             half3 Normal;
             half3 Emission;
             half  Specular;
             half  Alpha;
             half3 viewDir;
         };

         #pragma lighting ToonRampFace exclude_path:prepass
         inline half4 LightingToonRampFace (SurfOut s, half3 lightDir, half atten)
         {
             half4 c;
             half rim = 1.0 - saturate(dot (normalize(s.viewDir) , s.Normal));
             c.rgb = (s.Albedo * _LightColor0.rgb + _PickColor.rgb * _LightColor0.rgb * pow (rim, _RimPower) * _RimIntesity)  * atten;              
             c.a = s.Alpha;
             return c;
         }
   
         void surf (Input IN, inout SurfOut o) 
         {                      
             half4 skin = tex2D(_MainTex, IN.uv_MainTex) * _Color;
             half4 under = tex2D(_UnderTex, IN.uv_MainTex) * _Color;
             if(under.a > 0.48f)
             {
                o.Albedo = under.rgb * under.a * _ColorIntensity + (1.0f - under.a) * skin.rgb;
             }
             else
             {
                o.Albedo = skin.rgb * _ColorIntensity;
             }          
            
             o.Alpha = skin.a;
             o.viewDir = IN.viewDir;            
         }
 
         ENDCG
    }
    FallBack "Diffuse"
}
[코드-1] 흔한 Surface 셰이더 코드

보다시피 흔히 알고 있는 Surface셰이더를 사용중이며, #pragma를 사용해서 각종 컴파일 규칙을 정의해주고 있다.

이 셰이더는 컴파일될 때 과연 어떻게 될까?

유니티의 셰이더코드는 PC의 것과 매우 다르다. PC는 알파의 유무, 뼈대의 개수, 라이트의 개수, 라이맵의 유무 등에 따라서 각각의 경우마다 셰이더를 따로 작성해야 했고, 이를 감당할수 없게 되었을때 우리는 셰이더 폭발(!)이라고 불렀다. 유니티를 사용해보면 이런 부분들이 매우 편하게 되어 있어서 더 이상 셰이더 폭발을 걱정할 필요가 없을 줄 알았다.

특히 Surface셰이더를 사용하면 CgHLSL을 직접 사용한 코딩보다 훨씬 강력하게 지원되기 때문에 즐겨 사용하게 된다. 문제는 이러한 지원이 너무나 강력하다보니 원치 않는 부작용이 나타난다는 것이다.

유니티에서는 이러한 알아서 제멋대로 강력함을 최소화하기 위해서 #pragma 명령어로 다양한 제어를 할 수 있게 해놓았다. 만약 light probe를 원치않으면 novertexlights를 추가하면 된다.

#pragma surface surf ToonRampFace novertexlights

이렇게 하면 light probe 기능이 없는 셰이더 코드를 생성해주며, 당연히 Light probe코드가 제거되었기 때문에 셰이더 코드가 훨씬 가벼워진다. #pragma 명령어와 관련된 다양한 옵션은 매뉴얼(http://docs.unity3d.com/Manual/SL-SurfaceShaders.html) 을 참고하도록 하자.

오늘 말하고자 하는 것은 의외로 간과하게 되는 유니티에서의 셰이더 폭발에 관한 것이다

사실 모바일(유니티)에서도 셰이더 폭발은 발생한다. 우리가 그것을 잘 모르고 있을 뿐이다. [그림-1]을 보도록 하자.

[그림-1] 셰이더 코드의 Inspector화면

제일 하단의 Variants값이 14로 되어 있는데, 이것이 의미하는 것은 바로 옆의 [Show]버튼을 눌러보면 알 수 있다.

// Total snippets: 2
// -----------------------------------------
// Snippet #0 platforms ffffffff:

9 keyword variants:

DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE VERTEXLIGHT_ON


// -----------------------------------------
// Snippet #1 platforms ffffffff:

5 keyword variants:

POINT
DIRECTIONAL
SPOT
POINT_COOKIE
DIRECTIONAL_COOKIE


[코드-2] 변종 셰이더 코드의 종류

이것이 의미하는 것이 무얼까?

PC에서 셰이더를 개발해본 사람은 단박에 알 수 있을 것이다. 라이트맵이 있는 경우, 없는 경우, 그림자가 있는 경우 없는 경우, 정점조명이 있는 경우, 없는 경우 등등에 따라서 셰이더 코드를 모두 다르게 만들어야 하고, 이러한 과정 때문에 셰이더 대폭발이라는 참사가 발생하는데, 유니티에서는 사실 우리 눈에 보이지 않았을뿐 내부적으로 폭발이 발생하고 있었던 것이다너무 잘 숨겨놔서 잘 모를뿐이다
저 14라는 숫자는 다시 말해서 나는 비록 셰이더를 1개만 만들었지만, 다양한 경우에 적용될수 있는 변종이 유니티에 의해서 14개 생성되었다는 것이다. 이런 망할 ㅋㅋㅋ

그나마 모바일은 캐릭터 스키닝을 일반적으로 CPU에서 하니까 뼈대 개수에 따른 가중치별 분화는 없어서 다행이랄까? (유니티 4.5에서 GPU 스키닝을 지원하도록 컴파일해보면 몇몇 폰에서 이상 현상을 발생시킨다.)

문제는 이렇게 많은 변종 셰이더가 있을 경우 로딩에 치명적인 악영향을 끼치게 된다. 실제 스테이지를 구성하는 메시, 텍스처, 애니 데이터 로딩 시간을 모두 합친 급에 육박하는 셰이더 로딩이라는 불상사를 직접 격고 나서야 나도 이러한 문제점을 깨달았다.

방법은 의외로 간단하다. 앞서 말한 #pragma를 사용해서 필요 없는 변종들은 생성하지 않도록 지정하는 것이다다음과 같이 변경해보자

변경전: #pragma surface surf ToonRampFace
변경후: #pragma surface surf ToonRampFace nolightmap

 
[그림-2] 줄어든 Variants값

이 상태로 저장하면 [그림-2]처럼 Variants11로 줄어든 것을 목격할 수 있다. [show]버튼을 눌러서 확인해보면 [코드-3]처럼 분기되는 경우의 수가 줄어들어 있다.

// Total snippets: 2
// -----------------------------------------
// Snippet #0 platforms ffffffff:

6 keyword variants:

DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE VERTEXLIGHT_ON


// -----------------------------------------
// Snippet #1 platforms ffffffff:

5 keyword variants:

POINT
DIRECTIONAL
SPOT
POINT_COOKIE
DIRECTIONAL_COOKIE



이 외에도 줄일 수 있는 여지가 이것 저것 더 있으니 꼭 직접 찾아보기 바란다

지금 이 예의 경우에는 캐릭터 전용 셰이더기 때문에 라이트맵이 필요없었다. 따라서 #pragma를 사용해 라이트맵 지원 셰이더 생성을 막은 것이다. 이처럼 특정 용도로 제한된 셰이더(주로 캐릭터, FX, Post Process)들은 #pragma로 제한을 두어서 변종의 생성을 억제할 필요가 있다



댓글 없음:

댓글 쓰기