이 섹션에서는 렌더링 최적화에 대한 전문적인 지식을 소개합니다. 더 나은 성능을 위해 조명 결과물을 베이크하는 방법과 Shadowgun 개발자들이 조명이 베이크된 고대비 텍스처를 활용하여 훌륭한 시각적 효과를 보이는 게임을 만든 방법 등을 설명합니다. 모바일에 최적화된 게임이 어떤 모습인지에 대한 일반 정보를 얻으려면 그래픽스 방법론 페이지를 참조하십시오.
때로는 게임의 렌더링 최적화를 위해 궂은 일을 해야 할 때도 있습니다. Unity가 제공하는 모든 구조는 더 빠른 속도를 쉽게 얻을 수 있도록 해 주지만, 제한된 하드웨어에서 최상급의 충실도를 제공하고자 한다면 이러한 구조를 회피하여 스스로 제작하는 것이 정답입니다. 주요 구조 변화를 통해 훨씬 더 빠른 게임을 만들 수 있기 때문입니다. 이 때 선택해야 할 툴은 에디터 스크립트, 단순 셰이더, 그리고 전통적인 방식의 아트 프로덕션입니다.
먼저, 셰이더 작성 방법 소개 페이지를 확인해야 합니다.
#pragma debug
)
#pragma debug
를 추가하면, 컴파일된 셰이더를 인스펙터로 열었을 때 CG 중간코드를 볼 수 있습니다. 이 방법은 셰이더의 특정 부분이 실제로 어떻게 산출되는지를 살펴보는 데 유용하며, 또한 표면 셰이더에서 원하는 특정 측면을 파악하여 CG 셰이더에 적용하는 데에도 유용합니다.Shadowgun은 게임이 실행되는 하드웨어를 고려할 때 훌륭한 그래픽 결과를 보여줍니다. 아트 품질이 해결의 열쇠인 것처럼 보이지만, 아티스트가 잠재력을 최대한 발휘할 수 있도록 하기 위해 프로그래머들이 품질을 이끌어내려고 사용한 몇 가지 트릭이 있습니다.
그래픽스 방법론 페이지에서는 Shadowgun의 황금 조각상을 훌륭한 최적화의 예제로 들고 있습니다. Shadowgun은 각 조각상을 높은 선명도로 표현하기 위해 노멀 맵을 사용하는 대신, 조명 디테일을 텍스처에 베이크하였습니다. 이와 유사한 기법을 게임 개발에 어떻게 활용하는지, 그리고 왜 그렇게 해야 하는지에 대해 보여 드리겠습니다.
// 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 )이라 불리는 에디터 툴이 바로 이러한 작업을 수행합니다. 이 툴은 다음 과정을 거쳐 광원을 텍스처로 베이크합니다.
이것이 최상의 그래픽스 최적화가 이루어지는 방법입니다. 엄청난 수의 연산을 에디터에서 또는 게임 실행 전에 수행함으로써 연산 수를 줄입니다. 일반적으로 다음과 같이 하면 됩니다.
오디오 음원의 저음과 고음처럼, 이미지에도 역시 High-Frequency와 Low-Frequency 컴포넌트가 있습니다. 마치 스테레오에서 서브우퍼와 트위터를 사용하여 충실한 사운드를 만들어내듯이, 렌더링할 때에도 컴포넌트는 서로 다른 방식으로 다루는 것이 좋습니다. 이미지의 서로 다른 광원 주파수(Light Frequency)를 시각화하는 방법 중 하나는 Photoshop에서 “하이패스” 필터를 사용하는 것입니다. 필터->기타->하이패스. 오디오 작업을 해 본 적이 있다면 하이패스라는 이름이 익숙할 것입니다. 기본적으로 필터가 하는 일은 필터로 전달하는 파라미터 X보다 작은 모든 주파수를 제거하는 것입니다. 이미지에서는 가우시안 블러가 로우 패스의 역할을 합니다.
실시간 그래픽스에서도 이러한 개념을 차용하는데, 주파수를 활용하면 전체를 여러 부분으로 나누고 각 부분을 어떻게 처리해야 할지 쉽게 결정할 수 있기 때문입니다. 예를 들어, 기본 라이트맵 환경에서 최종 이미지는 주파수가 낮은 광원을 처리한 라이트맵과 주파수가 높은 광원을 처리한 텍스처를 합성하여 얻어집니다. Shadowgun에서는 주파수가 낮은 광원은 라이트 프로브를 통해 캐릭터에 빨리 적용되고, 주파수가 높은 광원은 임의의 광원 방향과 함께 단순 범프맵 셰이더를 사용하여 인위적으로 꾸며집니다.
일반적으로 광원을 렌더링할 때 주파수에 따라 서로 다른 방법(예: 베이크 vs 동적, 오브젝트당 vs 레벨당, 픽셀당 vs 버텍스당 등)을 사용함으로써 제한된 하드웨어에서 충실한 이미지를 생성할 수 있습니다. 스타일 측면에서의 선택은 차치하고, 다양한 컬러나 값을 낮은 주파수의 광원과 높은 주파수의 광원 양쪽에 적용해보는 것도 좋은 방법입니다.
참고: 보통 이러한 분석은 디퍼드 렌더러에서 단계를 나타내나, 여기서는 그렇지 않습니다. 모든 것이 오직 하나의 패스에서 진행되었습니다. 구성은 두 개의 관련 셰이더를 기반으로 이루어졌습니다.
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;
#ifndef LIGHTMAP_OFF
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;
#ifndef LIGHTMAP_OFF
float2 lmap : TEXCOORD1;
#endif
fixed3 spec : TEXCOORD2;
};
v2f vert (appdata_full v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord + frac(_ScrollingSpeed * _Time.y);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_MV, v.normal);
float4 viewPos = mul(UNITY_MATRIX_MV, 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;
#ifndef LIGHTMAP_OFF
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
#ifndef LIGHTMAP_OFF
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_OFF 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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
float3 worldNormal = mul((float3x3)_Object2World, v.normal);
float3 viewNormal = mul((float3x3)UNITY_MATRIX_MV, v.normal);
float4 viewPos = mul(UNITY_MATRIX_MV, 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는 알파 테스팅(또는 픽셀 셰이더에서 discard 및 clip 사용 시)에 있어 높은 성능 부하를 유발합니다. 따라서 가급적 알파 테스트 셰이더를 알파 블렌디드 셰이더로 교체하여야 합니다. 알파 테스팅을 피할 수 없는 경우, 표시되는 알파 테스트된 픽셀 전체 수를 최소한으로 유지해야 합니다.
일부 이미지, 특히 iOS/Android PVR 텍스처 압축을 사용하는 이미지는 알파 채널에서 의도하지 않은 시각적 결함을 발생시키는 경향이 있습니다. 이러한 경우 PVRT 압축 파라미터를 이미징 소프트웨어에서 직접 조절해야 할 수도 있습니다. PVR 익스포트 플러그인을 설치하거나, 또는 PVRTC 포맷을 제작한 Imagination Tech의 PVRTexTool을 사용하면 됩니다. .pvr 확장자를 가지는 압축 이미지 결과물 파일은 Unity 에디터에서 직접 임포트할 수 있고, 명시된 압축 파라미터가 그대로 유지됩니다. PVRT 압축 텍스처의 화질이 충분하지 않거나, GUI 텍스처 등을 위해 특별히 선명한 이미지가 필요할 경우 32비트 대신 16비트 텍스처를 사용하는 것을 고려해 보십시오. 16비트 텍스처를 사용하면 메모리 대역폭과 스토리지 요구 사항을 절반으로 줄일 수 있습니다.
OpenGL ES 2.0을 지원하는 모든 Android 디바이스는 ETC1 compression format도 지원합니다. 그러므로 가능하다면 선호하는 텍스처 포맷으로 항상 ETC1을 사용할 것을 권장합니다.
Nvidia Tegra 또는 Qualcomm Snapdragon 등의 특정 그래픽스 아키텍처를 타겟으로 한다면, 이러한 아키텍처에서 사용 가능한 전용 압축 포맷 사용을 고려해보는 것도 좋습니다. Android Market 또한 지원 텍스처 압축 포맷에 기반한 필터링을 허용합니다. 예를 들어 DXT 압축 텍스처를 포함하는 배포 아카이브 파일(.apk)은 압축 포맷을 지원하지 않는 디바이스에서는 다운로드되지 않도록 제한됩니다.
텍셀 렌더링을 다운로드합니다. 모델에 조명을 베이크해야 합니다. Photoshop에서 위 결과물에 하이패스 필터를 실행해야 합니다. 텍셀 렌더링 패키지에 포함된 “ Mobile/Cubemapped” 셰이더를 편집하여, 제거된 낮은 주파수의 광원 디테일이 버텍스 광원으로 대체되도록 하십시오.