Unlit Shader 뜯어보기
먼저 빛의 영향을 받지 않는 Unlit shader부터 뜯어보자.
먼저 shader를 unlit shader를 하나 생성해주고, material 또한 생성해 unlit으로 변경해준다.
이를 현재 object에 drag & drop 해보면 음영처리가 없고, 밋밋하게 바뀐다.
입체감이 전혀 없다.
먼저 생성한 shader를 클릭해서 안으로 들어가면 다음과 같은 script가 적혀있을것이다.
Shader "Unlit/TestShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
그 전에 알아야 할 것이 DirectX 11과는 다르게 Vertex Shader 부분과 Fragment(Pixel) Shader부분이 통합되어있다.
한줄씩 뜯어보자.
셰이더 코드 뜯어보기
경로 및 이름
Shader "Unlit/TestShader"
위 코드를 변경하면, 셰이더를 재질에 할당하기 위해 선택해야 하는 경로가 변경된다.
특성
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
Inspector 창에서 보여질 특성을 여기서 선언한다. 기본적으로 Unlit에서는 하나의 텍스처만 선언되어 있다. (어떠한 것들은 선언할 필요가 없다)
하위 셰이더
SubShader
{
여기서 부터는 한 개 이상의 SubShader를 둘 수 있다. 하위 셰이더에는 몇 가지 종류가 존재하는데, 셰이더를 로딩할 때 유니티는 여러 셰이더 중 GPU에서 지원하는 첫 번째 하위 셰이더를 사용/한다.
각각의 하위 셰이더는 렌더링 패스의 리스트를 포함한다.
태그
Tags { "RenderType"="Opaque" }
태그는 정보를 표현하는 키와 값의 쌍으로 구성되어 있다. 정보의 예를 들어보자면 위 코드처럼 렌더링 큐의 사용 여부 같은 것이다. 투명과 불투명한 게임 오브젝트는 각기 다른 렌더링 큐에서 렌더링 되므로 여기서 RenderType을 Opaque(불투명)로 지정한 이유이다.
Pass/CGPROGRAM/ENDCG
Pass
{
CGPROGRAM
......생략
}ENDCG
Pass는 블록( {…} )으로 구현된다.
먼저 Pass에는 렌더링을 위한 정보와 실제로 셰이더에서 계산하는 코드와 같은 정보를 포함한다. 이는 C#에서 하나씩 분리돼 수행할 수 있다.
Pass
{
Name "BASE"
...//생략
} ENDCG
}
Pass
{
Name "TEXT"
...//생략
} ENDCG
}
쉽게 말해 위와같은 구조를 가진다는 것이다. (c#에서 선택적으로 호출할 수 있다는 개념은 아닌거 같다.)
PRAGMA 선언
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
이는 셰이더 컴파일러에게 정보를 전달하는 방법이다. 즉 여기선 vertex shader와 pixel shader에 사용할 함수를 지정해준 것이다.
입출력 구조체
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
슬슬 DX에서 본 익숙한 구조들이 많이 나온다. Vertex Shader의 output은 Pixel Shader의 Input이 되는것이 Pipeline의 기본적인 흐름이었다. 즉 여기서 v2f는 Vertex to Fragment의 정보를 표현한 구조체이다.
appdata는 정점(vertex) 셰이더는 입력 구조체를 통해 특정 input을 요청할 수 있는데 이 구조체가 appdata이다. (쉽게 말하면 vertex의 input구조체 이다.)
변수 선언 이후에 있는 : POSITION 같은 구조를 Simentics(시맨틱) 이라고 한다. 이는 컴파일러에게 구조체 내 특정한 맴버 내애 어떤 종류를 저장할 것인지 알려주는 역할이다. (예를 들어 SV_POSITION은 화면상의 정점의 위치를 의미한다)
참고로 SV는 System Value의 약자로 이는 파이프라인의 특정한 위치를 참조한다.
변수 선언
sampler2D _MainTex;
float4 _MainTex_ST;
이전에 특성 블록에 저장한 모든 특성은 CGPROGRAM안에 따로 정의해주어야 한다. 여기서는 특성에서 하나의 Main Texture를 받았으므로, Sampler2D와 MainTex_ST로 정의했다.
정점 함수와 프래그먼트 함수
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
pragma선언에서 지정해준 vertex와 fragment 셰이더로 동작할 함수이다.
처음 볼만한 것은 UnityObjectToClipPos일텐데, 이는 정점의 위치가 한 3D 좌표 공간에서 다음에 수행할 계산에 더 적합한 3D 좌표로 사영해주는 변환 함수이다.