유니티 셰이더 (Unlit Shader) 뜯어보기
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 좌표로 사영해주는 변환 함수이다.