Version: 2020.2
스크립트와 게임플레이 방법론
스크립트 최적화

렌더링 최적화

이 섹션에서는 렌더링 최적화에 대한 전문적인 지식을 소개합니다. 더 나은 성능을 위해 조명 결과물을 베이크하는 방법과 Shadowgun 개발자들이 조명이 베이크된 고대비 텍스처를 활용하여 훌륭한 시각적 효과를 보이는 게임을 만든 방법 등을 설명합니다. 모바일에 최적화된 게임이 어떤 모습인지에 대한 일반 정보를 얻으려면 그래픽스 방법론 페이지를 참조하십시오.

예술성을 발휘해야 합니다

때로는 게임의 렌더링 최적화를 위해 궂은 일을 해야 할 때도 있습니다. Unity가 제공하는 모든 구조는 더 빠른 속도를 쉽게 얻을 수 있도록 해 주지만, 제한된 하드웨어에서 최상급의 충실도를 제공하고자 한다면 이러한 구조를 회피하여 스스로 제작하는 것이 정답입니다. 주요 구조 변화를 통해 훨씬 더 빠른 게임을 만들 수 있기 때문입니다. 이 때 선택해야 할 툴은 에디터 스크립트, 단순 셰이더, 그리고 전통적인 방식의 아트 프로덕션입니다.

어떻게 뛰어들 것인가

먼저, 셰이더 작성 방법 소개 페이지를 확인해야 합니다.

  • 빌트인 셰이더
    • 빌트인 셰이더의 소스 코드를 살펴볼 수 있습니다. 기존과 다른 동작을 하는 새로운 셰이더를 만들고자 할 경우, 이미 존재하는 기존의 두 셰이더의 일부분을 가져다가 하나로 합치는 방식으로 만들 수도 있습니다.
  • 표면 셰이더 디버깅(#pragma debug)
    • 모든 표면 셰이더로부터 CG 셰이더가 생성되며, 그 후 거기에서 완전히 컴파일됩니다. 표면 셰이더의 맨 위에 #pragma debug를 추가하면, 컴파일된 셰이더를 인스펙터로 열었을 때 CG 중간코드를 볼 수 있습니다. 이 방법은 셰이더의 특정 부분이 실제로 어떻게 산출되는지를 살펴보는 데 유용하며, 또한 표면 셰이더에서 원하는 특정 측면을 파악하여 CG 셰이더에 적용하는 데에도 유용합니다.
  • 셰이더 포함 파일
    • 다수의 셰이더 헬퍼 코드가 모든 셰이더에 포함되어 있으며 보통 사용되지는 않습니다. 그러나 이러한 헬퍼 코드가 존재하기 때문에, WorldReflectionVector와 같이 어디에도 정의되지 않은 듯한 함수를 셰이더가 종종 호출할 수 있습니다. Unity는 이러한 헬퍼 정의를 보유하는 여러 빌트인 셰이더 포함 파일을 제공합니다. 특정 함수를 찾고자 한다면, 서로 다른 포함 사항 모두를 검색해야 합니다.
    • 이러한 파일은 Unity가 셰이더 작성을 쉽게 하기 위해 사용하는 내부 구조의 주요 구성 부분입니다. 파일은 실시간 그림자, 서로 다른 광원 타입, 라이트맵, 다중 플랫폼 지원 등을 제공합니다.
  • 하드웨어 문서
    • 잠시 시간을 할애하여 Apple 문서 중 셰이더 작성 베스트 프랙티스 페이지를 참조하십시오. Unity는 부동 소수점 정밀도 힌트에 있어서 좀 더 공격적으로 보는 것을 제안합니다.

Shadowgun에 대한 상세한 정보

Shadowgun은 게임이 실행되는 하드웨어를 고려할 때 훌륭한 그래픽 결과를 보여줍니다. 아트 품질이 해결의 열쇠인 것처럼 보이지만, 아티스트가 잠재력을 최대한 발휘할 수 있도록 하기 위해 프로그래머들이 품질을 이끌어내려고 사용한 몇 가지 트릭이 있습니다.

그래픽스 방법론 페이지에서는 Shadowgun의 황금 조각상을 훌륭한 최적화의 예제로 들고 있습니다. Shadowgun은 각 조각상을 높은 선명도로 표현하기 위해 노멀 맵을 사용하는 대신, 조명 디테일을 텍스처에 베이크하였습니다. 이와 유사한 기법을 게임 개발에 어떻게 활용하는지, 그리고 왜 그렇게 해야 하는지에 대해 보여 드리겠습니다.

실시간 셰이더(Real-Time Shader) 코드 vs. 베이크된 황금 조각상 셰이더(Baked Golden Statue Shader) 코드

// This is the pixel shader code for drawing normal-mapped
// specular highlights on static lightmapped geometry

// 5 texture reads, lots of instructions

SurfaceOutput o;

fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
fixed4 c = tex * _Color;
o.Albedo = c.rgb;

o.Gloss = tex.a;
o.Specular = _Shininess;

o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));

float3 worldRefl = WorldReflectionVector (IN, o.Normal);
fixed4 reflcol = texCUBE (_Cube, worldRefl);
reflcol *= tex.a;
o.Emission = reflcol.rgb * _ReflectColor.rgb;
o.Alpha = reflcol.a * _ReflectColor.a;

fixed atten = LIGHT_ATTENUATION(IN);
fixed4 c = 0;

half3 specColor;
fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);

const float3x3 unity_DirBasis = float3x3( 
float3( 0.81649658,  0.0, 0.57735028),
float3(-0.40824830,  0.70710679, 0.57735027),
float3(-0.40824829, -0.70710678, 0.57735026) );

half3 lm = DecodeLightmap (lmtex);

half3 scalePerBasisVector = DecodeLightmap (lmIndTex);

half3 normalInRnmBasis = saturate (mul (unity_DirBasis, o.Normal));
lm *= dot (normalInRnmBasis, scalePerBasisVector);

return half4(lm, 1);
// This is the pixel shader code for lighting which is
// baked into the texture

// 2 texture reads, very few instructions

fixed4 c = tex2D (_MainTex, i.uv.xy);   

c.xyz += texCUBE(_EnvTex,i.refl) * _ReflectionColor * c.a;

return c;

반사 범프 스페큘러 반사를 포함하는 베이크된 광원

텍셀 렌더링

실시간 광원이 확실한 고품질을 보장한다면 베이크된 광원은 엄청난 퍼포먼스 향상을 가져옵니다. 어떻게 그럴 수 있을까요? 텍셀 렌더링(Render to Texel )이라 불리는 에디터 툴이 바로 이러한 작업을 수행합니다. 이 툴은 다음 과정을 거쳐 광원을 텍스처로 베이크합니다.

  • 스크립트를 통해 탄젠트 공간 노멀 맵을 월드 공간으로 변환합니다.
  • 스크립트를 통해 월드 공간 포지션 맵을 생성합니다.
  • 두 개의 기존 맵을 사용하여 전체 텍스처의 전체화면 패스를 텍스처로 렌더링합니다. 이때 광원당 하나의 패스를 추가합니다.
  • 여러 다른 관점에서 결과값의 평균을 냅니다. 이를 통해 모든 방향에서, 또는 최소한 게임 내에서 보통 바라보는 시각에서 그럴듯하게 보이는 결과물을 만들 수 있습니다.

이것이 최상의 그래픽스 최적화가 이루어지는 방법입니다. 엄청난 수의 연산을 에디터에서 또는 게임 실행 전에 수행함으로써 연산 수를 줄입니다. 일반적으로 다음과 같이 하면 됩니다.

  • 성능은 걱정하지 말고 시각적으로 훌륭한 결과물을 만들어야 합니다.
  • Unity의 라이트매퍼, 텍셀 렌더링, 스프라이트 패커 등 에디터 확장 프로그램을 사용하여 렌더링하기 아주 간단한 요소에 베이크하십시오.
    • 직접 툴을 만드는 것이 최상의 방법으로, 게임에서 드러나는 모든 문제를 처리할 수 있는 완벽한 툴을 만들 수 있습니다.
  • 셰이더와 스크립트를 생성해서 베이크된 결과물에 일종의 “빛나는” 효과를 더해야 합니다. 눈길을 끄는 효과를 통해 동적 광원이 적용된 것 같은 착시를 유도할 수 있습니다.

광원 주파수의 개념

오디오 음원의 저음과 고음처럼, 이미지에도 역시 High-Frequency와 Low-Frequency 컴포넌트가 있습니다. 마치 스테레오에서 서브우퍼와 트위터를 사용하여 충실한 사운드를 만들어내듯이, 렌더링할 때에도 컴포넌트는 서로 다른 방식으로 다루는 것이 좋습니다. 이미지의 서로 다른 광원 주파수(Light Frequency)를 시각화하는 방법 중 하나는 Photoshop에서 “하이패스” 필터를 사용하는 것입니다. 필터->기타->하이패스. 오디오 작업을 해 본 적이 있다면 하이패스라는 이름이 익숙할 것입니다. 기본적으로 필터가 하는 일은 필터로 전달하는 파라미터 X보다 작은 모든 주파수를 제거하는 것입니다. 이미지에서는 가우시안 블러가 로우 패스의 역할을 합니다.

실시간 그래픽스에서도 이러한 개념을 차용하는데, 주파수를 활용하면 전체를 여러 부분으로 나누고 각 부분을 어떻게 처리해야 할지 쉽게 결정할 수 있기 때문입니다. 예를 들어, 기본 라이트맵 환경에서 최종 이미지는 주파수가 낮은 광원을 처리한 라이트맵과 주파수가 높은 광원을 처리한 텍스처를 합성하여 얻어집니다. Shadowgun에서는 주파수가 낮은 광원은 라이트 프로브를 통해 캐릭터에 빨리 적용되고, 주파수가 높은 광원은 임의의 광원 방향과 함께 단순 범프맵 셰이더를 사용하여 인위적으로 꾸며집니다.

일반적으로 광원을 렌더링할 때 주파수에 따라 서로 다른 방법(예: 베이크 vs 동적, 오브젝트당 vs 레벨당, 픽셀당 vs 버텍스당 등)을 사용함으로써 제한된 하드웨어에서 충실한 이미지를 생성할 수 있습니다. 스타일 측면에서의 선택은 차치하고, 다양한 컬러나 값을 낮은 주파수의 광원과 높은 주파수의 광원 양쪽에 적용해보는 것도 좋은 방법입니다.

실전에서의 광원 주파수: Shadowgun 분석

  • 상단 열
    • 울트라 로우 프리퀀시 스페큘러 버텍스 광원 (동적) | 하이 프리퀀시 알파 채널 | 로우 프리퀀시 라이트맵 | 하이 프리퀀시 알베도
  • 중간 열
    • 스페큘러 버텍스 광원 * 알파 | 하이 프리퀀시 추가 세부 사항 | 라이트맵 * 컬러 채널
  • 하단
    • 최종 결과물

참고: 보통 이러한 분석은 디퍼드 렌더러에서 단계를 나타내나, 여기서는 그렇지 않습니다. 모든 것이 오직 하나의 패스에서 진행되었습니다. 구성은 두 개의 관련 셰이더를 기반으로 이루어졌습니다.

버텍스당 추가되는 버추얼 글로스가 있는 라이트맵

Shader "MADFINGER/Environment/Virtual Gloss Per-Vertex Additive (Supports Lightmap)" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    //_MainTexMipBias ("Base Sharpness", Range (-10, 10)) = 0.0
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125
    _ScrollingSpeed("Scrolling speed", Vector) = (0,0,0,0)
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;
    samplerCUBE _ReflTex;

    #ifdef LIGHTMAP_ON
    float4 unity_LightmapST;
    sampler2D unity_Lightmap;
    #endif

    //float _MainTexMipBias;
    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float4 _ScrollingSpeed;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        #ifdef LIGHTMAP_ON
        float2 lmap : TEXCOORD1;
        #endif
        fixed3 spec : TEXCOORD2;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);

        o.uv = v.texcoord + frac(_ScrollingSpeed * _Time.y);

        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float3 viewPos = UnityObjectToViewPos(v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        #ifdef LIGHTMAP_ON
        o.lmap = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
        #endif
        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c = tex2D (_MainTex, i.uv);

            fixed3 spec = i.spec.rgb * c.a;

            #if 1
            c.rgb += spec;
            #else           
            c.rgb = c.rgb + spec - c.rgb * spec;
            #endif

            #ifdef LIGHTMAP_ON
            fixed3 lm = DecodeLightmap (tex2D(unity_Lightmap, i.lmap));
            c.rgb *= lm;
            #endif

            return c;
        }
        ENDCG 
    }   
}
}

버텍스당 추가되는 버추얼 글로스가 있는 라이트 프로브

Shader "MADFINGER/Environment/Lightprobes with VirtualGloss Per-Vertex Additive" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (1, 1, 1, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125    
    _SHLightingScale("LightProbe influence scale",float) = 1
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #pragma multi_compile _ LIGHTMAP_ON
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;


    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float _SHLightingScale;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 refl : TEXCOORD1;
        fixed3 spec : TEXCOORD3;
        fixed3 SHLighting: TEXCOORD4;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv = v.texcoord;

        float3 worldNormal = UnityObjectToWorldDir(v.normal);       
        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float4 viewPos = UnityObjectToViewPos(v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos.xyz - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        o.SHLighting    = ShadeSH9(float4(worldNormal,1)) * _SHLightingScale;

        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c    = tex2D (_MainTex, i.uv);

            c.rgb *= i.SHLighting;
            c.rgb += i.spec.rgb * c.a;

            return c;
        }
        ENDCG 
    }   
}
}

베스트 프랙티스

GPU 최적화: 알파 테스팅

일부 GPU, 특히 모바일 디바이스용 GPU는 알파 테스팅(또는 픽셀 셰이더에서 discardclip 사용 시)에 있어 높은 성능 부하를 유발합니다. 따라서 가급적 알파 테스트 셰이더를 알파 블렌디드 셰이더로 교체하여야 합니다. 알파 테스팅을 피할 수 없는 경우, 표시되는 알파 테스트된 픽셀 전체 수를 최소한으로 유지해야 합니다.

iOS 텍스처 압축

일부 이미지, 특히 iOS/Android PVR 텍스처 압축을 사용하는 이미지는 알파 채널에서 의도하지 않은 시각적 결함을 발생시키는 경향이 있습니다. 이러한 경우 PVRT 압축 파라미터를 이미징 소프트웨어에서 직접 조절해야 할 수도 있습니다. PVR 익스포트 플러그인을 설치하거나, 또는 PVRTC 포맷을 제작한 Imagination Tech의 PVRTexTool을 사용하면 됩니다. .pvr 확장자를 가지는 압축 이미지 결과물 파일은 Unity 에디터에서 직접 임포트할 수 있고, 명시된 압축 파라미터가 그대로 유지됩니다. PVRT 압축 텍스처의 화질이 충분하지 않거나, GUI 텍스처 등을 위해 특별히 선명한 이미지가 필요할 경우 32비트 대신 16비트 텍스처를 사용하는 것을 고려해 보십시오. 16비트 텍스처를 사용하면 메모리 대역폭과 스토리지 요구 사항을 절반으로 줄일 수 있습니다.

Android 텍스처 압축

OpenGL ES 2.0을 지원하는 모든 Android 디바이스는 ETC1 compression format도 지원합니다. 그러므로 가능하다면 선호하는 텍스처 포맷으로 항상 ETC1을 사용할 것을 권장합니다.

Nvidia Tegra 또는 Qualcomm Snapdragon 등의 특정 그래픽스 아키텍처를 타겟으로 한다면, 이러한 아키텍처에서 사용 가능한 전용 압축 포맷 사용을 고려해보는 것도 좋습니다. Android Market 또한 지원 텍스처 압축 포맷에 기반한 필터링을 허용합니다. 예를 들어 DXT 압축 텍스처를 포함하는 배포 아카이브 파일(.apk)은 압축 포맷을 지원하지 않는 디바이스에서는 다운로드되지 않도록 제한됩니다.

연습

텍셀 렌더링을 다운로드합니다. 모델에 조명을 베이크해야 합니다. Photoshop에서 위 결과물에 하이패스 필터를 실행해야 합니다. 텍셀 렌더링 패키지에 포함된 “ Mobile/Cubemapped” 셰이더를 편집하여, 제거된 낮은 주파수의 광원 디테일이 버텍스 광원으로 대체되도록 하십시오.

스크립트와 게임플레이 방법론
스크립트 최적화