안녕하세요. 여러분 모두 즐거운 추석 보내시고 계신지요. 이번 포스트에서는 반복 작업과 분기 작업에 매우 유용한 쉐이더 전처리기(Pre-processer)를 전체적으로 다뤄볼 것입니다.
쉐이더 스니핏(Snippet)
(쉐이더를 짜면서 셀 수 없이 많이 봤던 친구들)
이 친구들은 쉐이더에서 도대체 무슨 일을 하는 걸까요?
#pragma surface surf
vert와 frag 그리고 surf 같은 함수를 쉐이더로 컴파일(Compile) 하라고 요청하는 역할을 합니다. 저희 같은 초보 아티스트들은 컴파일도 무슨 뜻인지 잘 모르겠으니까 간단히 설명을 하고 넘어가야겠습니다.
사실 저희가 짜놓은 쉐이더 코드는 컴퓨터가 바로 알아 들을 수 없습니다. (충격)
저희가 일상 생활에서 사용하는 한국어는 자연어로서 같은 언어를 쓰는 사람들 끼리만 소통 할 수 있습니다.
가엽게도 제 컴퓨터 역시 한국어를 할 줄 모릅니다. 그래서 컴퓨터가 이해할 수 있는 쉐이더 언어를 통해 직접 명령을 내리는 것이라고 생각했지만 사실 많은 과정이 생략되어 있었습니다.
저희에게는 유능한 통역-번역가가 필요합니다. 바로 컴파일러 라는 친구입니다.
(개발자 - 고급 쉐이더 언어 - 어셈블리어 - 기계어 - 컴퓨터)
이처럼 컴퓨터에게 명령을 전달하기 위해 쉐이더 언어를 어셈블리어를 거쳐 이진수 기계어로 번역하는 과정이 바로 컴파일입니다.
스니핏에서 vert와 frag 그리고 surf는 일반적인 함수가 아니고 쉐이더라고 컴파일러에게 알려주는 것입니다.
(vert & frag)
Both vertex and fragment programs must be present in a shader snippet. Excluding it from compilation.
위와 같이 경고문이 나오고 익숙한 마젠타가 저희를 반겨줍니다. 스니핏으로 컴파일러에게 요청 해야만 vert와 frag 그리고 surf 함수가 버텍스와 프래그먼트 그리고 서피스 쉐이더로서 작동하는 것입니다.
#pragma fragment asdf
심지어 'frag' 대신 함수 이름에 'asdf'를 넣어도 함수 asdf가 프래그먼트 쉐이더로서 작동하게 된다는 것입니다.
멀티 컴파일(multi_compile)
놀랍게도 저희는 하나의 쉐이더를 화면에 출력하기 전에 여러가지로(multi) 사전 컴파일 할 수 있습니다. 쉐이더를 여러가지 초기 조건에 따른 분기를 생성 할 수 있는 것이지요. 이것을 쉐이더 배리언트(Variants) 라고 합니다.
(사전 컴파일 : 여러 기능을 사용 할 수 있도록 런타임 전에 설정 할 수 있는 옵션)
토글(Toggle)
ON/OFF 체크박스를 하나 만들어서 켜면 흰색(1,1,1)이고 끄면 검은색 (0,0,0)이 출력되도록 쉐이더를 작성해봅시다. 매우 간단합니다.
[Toggle] _Toggle("ON/OFF", float) = 0
프로퍼티에 [Toggle] 을 써주면 체크 박스가 생깁니다.
체크를 풀었을때 0, 체크했을때 1으로 0과 1의 값만을 갖게 됩니다.
#pragma multi_compile _TOGGLE_OFF _TOGGLE_ON
중요한 멀티 컴파일 구문입니다. 모두 대문자로 작성해야 하며 띄어쓰기로 쉐이더 배리언트를 구분합니다. 서피스 쉐이더도 같은 문법을 사용합니다.
토글의 경우 접미사로 0에 해당하는 '_OFF'와 1에 해당하는 '_ON' 두가지 분기의 쉐이더 배리언트가 생성됩니다.
(기존 if문과 전처리기 연산자 #if문)
if(_Toggle == 1) { #if _TOGGLE_ON col.rgb = float3(1,1,1); col.rgb = float3(1,1,1); } #else else col.rgb = float3(0,0,0); { #endif col.rgb = float3(0,0,0); }
다음은 멀티 컴파일과 같은 전처리기 연산자로 쓰이는 #if문 입니다. _TOGGLE_ON일때 흰색이고 그렇지 않을때는 검은색을 반환합니다. 기존의 if문과 비교 했을 때 몇가지 차이가 있습니다.
float _Toggle;
#if문에서 멀티 컴파일에만 사용하는 변수일 경우, 프로퍼티에만 써주고 쉐이더 내부에 위와 같이 프로퍼티에서 토글로 사용하는 변수를 따로 선언 해주지 않아도 잘 작동 됩니다.
multi_compile / shader_feature / if
#pragma multi_compile _TOGGLE_OFF _TOGGLE_ON
#pragma shader_feature _TOGGLE_OFF _TOGGLE_ON
'쉐이더 피쳐'라는 '멀티 컴파일'과 아주 유사하지만 조금 다른 스니핏이 있습니다.
멀티 컴파일과 쉐이더 피쳐의 유일한 차이점은 빌드 했을때 어떤 쉐이더 배리언트를 게임 빌드에 포함 할 지 결정하는 부분입니다.
사용되지 않는 쉐이더 배리언트를 빌드에 포함하지 하지 않고 싶을 경우 shader_feature를 사용하는 것이 맞습니다.
빌드에 포함되어 인게임에서 다른 배리언트로 전환 될 수 있도록 쉐이더가 짜여진 경우에는 multi_compile을 사용하는 것이 맞습니다.
기존의 if문은 '묻지도 따지지도 않고' 모든 분기의 컴파일 결과를 쉐이더에 저장하므로 가급적 사용을 피해야합니다.
이넘(Enum)
체크박스 _ON/_OFF 뿐만 아니라 이넘(Enum)을 사용할 수 있습니다.
이넘이란 직역하면 열거 데이터, 쉽게 말해서 메뉴판 같은 개념입니다.
김밥지옥 메뉴판에 국밥, 라면, 떡볶이가 있고 하나를 주문한다고 생각하시면 됩니다.
위 자료 처럼 여러가지 키워드를 대입해서 특정한 옵션을 프로퍼티에서 결정 할 수 있게 합니다.
(키워드 이넘, KeywordEnum)
[KeywordEnum(Kukbab, Ramen, Dduk)] _Food("Food", float) = 0
인스펙터에서 세가지(국밥, 라면, 떡볶이) 문자열을 메뉴로 출력하여 사용자가 고를 수 있게 합니다. 이것을 키워드 이넘이라고 합니다. 국밥과 라면과 떡볶이는 순서대로 각각 숫자 0,1,2에 대응합니다.
[HideInInspector]_MainTex ("Texture", 2D) = "white" {}
하이드 인 인스펙터(Hide in inspector)를 이용하여 인스펙터 창에서 특정 프로퍼티가 출력되지 않도록 할 수 있습니다. 미리 삽입된 텍스쳐를 보이지 않게 가려놨습니다.
(shader_feature _FOOD)
전처리 구문은 토글과 비슷하면서 다릅니다. 모두 대문자로 써줘야 하는 것은 같지만 접미어로 _OFF가 아닌 지정한 문자열 그대로 써주면 됩니다.
#if문 문법에 대한 자세한 정보는 위의 링크에서 참고할 수 있습니다. 언젠가 따로 포스트를 쓸 계획입니다.
이렇게 동시에 세가지 이상의 쉐이더 배리언트를 컴파일 할 수 있습니다. 하지만 이넘의 기능은 여기서 끝이 아닙니다.
(이넘, Enum)
[Enum(Zero, 0, One, 1, DstColor, 2, SrcColor, 3, OneMinusDstColor, 4, SrcAlpha, 5)] _Blend("Blend", float) = 0
바로 블렌드 공식도 직접 조작 할 수 있습니다. 또한 프로퍼티와 헤더(Header)와 스페이스(Space)를 이용하여 프로퍼티의 가독성을 올려 줄 수 도 있습니다.
헤더의 괄호 내부에 원하는 문자(한글은 안 됨)를 적어줄 수 있고, 스페이스는 프로퍼티 사이의 행간(行間)을 조절합니다.
(no #pragma)
Blend [_Blend] OneMinusSrcAlpha
심지어 아무런 프래그마(pragma) 전처리 구문 없이 동작합니다. 블렌드 공식에 변수 이름인 _Blend를 대괄호[]를 사용해서 넣어주면 원하는 문자열을 집어넣어 줄 수 있습니다.
하지만 프로퍼티에 블렌드 공식을 전부 쓰기에는 프로퍼티가 너무 길어집니다. 위에서 제로부터 소스-알파까지 여섯개만 썼는데도 가독성이 떨어집니다. 하지만 다 방법이 있지요.
(Enumeration)
[Enum(UnityEngine.Rendering.BlendMode)] _SrcFactor("Src Factor", float) = 5
[Enum(UnityEngine.Rendering.BlendMode)] _DstFactor("Dst Factor", float) = 10
짜잔. 이넘(Enum)은 위와 같이 유니티 엔진 내부에 존재하는 API에 접근해서 미리 짜여있는 목록(Enumeration)을 가져올 수 있습니다.
원한다면 이렇게 짜는 것도 가능합니다. 이넘의 자유도는 무궁무진 합니다. 유니티 메뉴얼에 소개된 유니티 렌더링 API의 모든 목록을 사용할 수 있습니다.
다수의 멀티 컴파일 결합
(complex multi compile combine)
저희는 앞서 작성했던 토글과 이넘을 합쳐서 둘 이상의 멀티 컴파일 구문을 결합하여 사용할 것입니다.
#pragma multi_compile _TOGGLE_OFF _TOGGLE_ON #pragma multi_compile _FOOD_KUKBAB _FOOD_RAMEN _FOOD_DDUK
두 줄 이상의 멀티 컴파일을 선언 할 수 있고 쉐이더 피쳐와 혼용해서 작성하는 것도 충분히 가능합니다.
두 가지의 멀티 컴파일을 결합하기 위해 저희는 다중 #if문을 구성해야 합니다.
간단합니다. 조건이 참일 경우와 거짓일 경우, 각각 내부에서 돌아가는 또 다른 조건문을 만들 수 있습니다.
먼저 토글이 ON일 때만 텍스쳐가 나오고 그렇지 않은 경우(OFF)에는 검은색을 출력하도록 써줍시다.
다음은 #if문 속에 내부 #if를 하나 더 넣어서 토글이 ON이고, 텍스쳐가 각각 국밥/라면/떡볶이로 설정 되어있을 경우를 만들어 줍니다. 내부의 #if를 종료하기 위해 #endif를 써주어야 합니다.
키워드 제한
이렇게 하면 토글 ON/OFF 두개 * 텍스쳐 키워드 세개로 총 여섯가지 경우의 수가 발생하는데, 같은 숫자 만큼의 쉐이더 배리언트가 컴파일 되는 것입니다. 여기에는 함정이 있습니다.
다수의 멀티 컴파일을 결합해서 사용하는 것은 매우 쉽고 강력하지만, 컴파일 하는 횟수가 많을 수록 당연히 필요한 연산과 저장할 용량이 지수함수적으로 증가합니다.
예를 들어 옵션이 각각 두 개인 멀티 컴파일이 열 개가 있으면 (2^10)셰이더 배리언트가 총 1,024개 생성됩니다. 엄청나게 무거워지는 것이죠.
이러한 쉐이더 배리언트는 유니티 프로젝트 전체의 '전역 배리언트'가 최대 256개(2^8)로 제한되어 있는데다가 약 60개의 전역 배리언트가 이미 유니티 프로젝트 내부에서 디폴트로 사용되고 있기 때문에 제한을 초과하여 오류가 나지 않게 해야 합니다.
그래서 해결책으로 쉐이더마다 제공되는 '로컬 배리언트' 최대 64개(2^6)를 사용하면 됩니다.
#pragma multi_compile_local _TOGGLE_OFF _TOGGLE_ON #pragma multi_compile_local _FOOD_KUKBAB _FOOD_RAMEN _FOOD_DDUK
만약 외부에서 스크립트로 접근하지 않아도 되는 '쉐이더 키워드'라면 간단히 접미사 _local 으로 각각 shader_feature_local / multi_compile_local 을 사용하여 '로컬 배리언트'로 처리 되어 문제가 해결 됩니다.
다음 포스트에서는 전처리기 인클루드(#Include)로 .cginc 파일 임포트를 해볼 것입니다. 읽어주셔서 감사합니다.
תגובות