URL : http://catlikecoding.com/unity/tutorials/rendering/part-2/
Shader Fundamentals
[셰이더를 이용하여 질감을 포함해 보자.]
§ 정점변화
§ 픽셀별 색상
§ 셰이더 속성(properties)사용
§ 데이터를 정점에서 분할하여 전달
§ 컴파일된 셰이더 코드 검증
§ 타일링(Tiling)과 offset을 이용하여 텍스처를 샘플링
1 Default Scene
[1. 유니티 기본 셰이더 구체생성 ]
Menu : GameObject >> 3D >> Sphere
유니티가 자체적으로 여러 가지 렌더링 과정이 진행되고 있기에 그러한 렌더링 기능들을 분해하여 끄는 것이 일차적인 목표입니다.
1.1 Stripping It Down
[1.1 기본 렌더링 과정 끄기]
Menu: Window >> Lighting >> Settings
Lighting 창 등장
Lighting: Environment > Skybox Material >> None으로 변경
(추가적으로 Auto Generate도 비활성화)
환경 광을 종료해서, 방향 광만 남은 상태
Hierarchy: Directional Light >> Inspector >> 체크해제(비활성화)
환경 광을 종료한다.
모두 꺼진 경우로 그림자도 사라진 상태이다.
반대로 기본 환경 광만 사용한다면 이 모습이 된다.
또한
Hierarchy: Main Camera >> Camera
에서 배경색(background)을 조정이 가능하다.
유니티 오브젝트의 Mesh가 이미지를 그리는 핵심이 됩니다.
CPU에 저장된 Mesh, Transform, Material을 이용하여
GPU에 올라간 Shader 코드를 이용하여 이미지를 그리기 시작합니다.
Assets::Create >> Shader >> Unlit Shader
Name : MyFirstShader 생성
이 파일을 열면 속에 여러 내용이 이미 있다, 우선 전부 삭제한다.
Shader "Custom/My First Shader" { }
이 상태에서는 문법에 맞지 않아 지원되지 않는다고 한다.
Material을 생성하고 여기에 Shader 코드를 넣기
Assets::Create >> Material
Name : MyMaterial 생성
그 후 Assets창의 MyFirstShader를 끌어다가 MyMaterial에 넣는다.
유니티에서 셰이더에 문제가 있다면 마젠타 색을 보여준다.
우선 이렇게 만들어진 MyMaterial 을 Hierarchy의 Sphere에게 적용한다.
유니티에서는 다양한 빌드 플랫폼마다 셰이더를 다르게 적용할 수 있어 SubShader을 이용하여 각각의 셰이더를 제공할 수 있다, 하지만 우리는 하나만 이용할 생각이라 하나만 만든다.
Shader "Custom/My First Shader" { SubShader { } }
또한, 하나의 셰이더마다 하나 이상의 Pass가 필요하다, Pass는 실제로 Object가 렌더링 되는 곳으로 두 개 이상의 Pass면 여러 번 렌더링 된다.
Shader "Custom/My First Shader" { SubShader { Pass { } } }
이제 기본적인 셰이더의 형태가 나타났다.
CGPROGRAM와 ENDCG으로 코드의 시작과 끝을 알려줍니다.
Pass { CGPROGRAM ENDCG }
#pragma를 이용하여 어느 Program을 사용할지 알려줘야한다.
CGPROGRAM #pragma vertex MyVertexProgram #pragma fragment MyFragmentProgram ENDCG
이때 또다시 셰이더가 오류를 띄우는데, 셰이더의 구조는 만들어졌으나 가르키는 Program들이 없기 때문이다.
CGPROGRAM #pragma vertex MyVertexProgram #pragma fragment MyFragmentProgram void MyVertexProgram () { } void MyFragmentProgram () { } ENDCG
각각의 프로그램을 추가하여 오류를 지워준다.
유니티에서의 셰이더는 대상 플랫폼에 따라 변화 해야 함 (Direct3D for Windows, OpenGL for Macs, OpenGL ES for mobiles)
즉, 어느 대상으로 한 플랫폼 마다 특정한 결과를 보여줄 수 있어야 함( 위의 예제의 경우 Open GL이나 DirectX 11 환경에서는 사용가능하나, DirectX 9에서는 사용이 불가능 함)
Assets창에서 .shader의 Inspector 창에서 "Compile and show code"를 클릭해서 발생한 에러를 볼 수 있음
셰이더 코드의 Inspector >> Compiled Code에서
위와 같이 대상 플렛폼을 설정 할 수 있으며, 대상 해당 플랫폼의 방식으로 코드가 생성이 됩니다.
공통 변수나 함수와 정의 등의 코드를 #include를 이용하여 다른 파일에서 가져올 수 있습니다.
그래서 유니티에서 기능들을 지원해주고 있는 "UnityCG.cginc" 을 이용하여 진행합니다.
CGPROGRAM #pragma vertex MyVertexProgram #pragma fragment MyFragmentProgram #include "UnityCG.cginc" void MyVertexProgram () { } void MyFragmentProgram () { } ENDCG
"UnityShaderVarialbels.cginc"는 렌더링에 필요한 Transformation, camera, light 같은 변수들의 정보를 정의해서 가지고 있음
"HLSLSupport.cginc"는 플랫폼별 데이터 유형 등의 상관없이 동일한 코드를 사용할 수 있게 해줌
"UnityInstancing.cginc "는 Instancing을 사용할 때 (다양한 플랫폼에서 구동될 수 있도록 하기 위해 만들어진 구조에서) 호출을 줄이기 위한 특정 렌더링 기술을 지원해주고 있으며, 이러한 부분은 UnityShaderVariables이 어떻게 되어 있느냐에 따라 다릅니다.
2.5 Producing Output
Mesh에서 전달된 정점의 좌표 x y z +w를 출력하기 위해 "SV_POSITION" 정점 정보로 float4를 전달한다.
에러를 방지하기 위해 임시로 return 0을 반환한다.
//void MyVertexProgram () { //}
float4 MyVertexProgram () : SV_POSITION { return 0; // float4(0,0,0,0)과 같음 }
//void MyFragmentProgram(){ //}
float4 MyFragmentProgram ( float4 position : SV_POSITION ) : SV_TARGET { return 0; }
우선 Mesh에서 정점을 받아와 변형을 해야 한다
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION { return position; }
위의 이미지와 같이 변형되어 카메라의 위치에 상관없이 보이는 검은 찌그러진 구가 보이게 되는데 이를 해결하기 위해 객체-공간 위치일 뿐인 position에서 카메라에서부터의 위치로 변형해야 합니다.
이를 위해 "UnityShaderVariables"의 UNITY_MATRIX_MVP. 모델뷰프로젝션을 이용하여 아래와 같이 곱해줍니다.
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION { return mul(UNITY_MATRIX_MVP, position); }
위와 같이 정상적인 형태로 출력 되게 됩니다.
우선 색상을 상수를 이용하여 노란색으로 만들어봅니다.
float4 MyFragmentProgram ( float4 position : SV_POSITION ) : SV_TARGET { return float4(1, 1, 0, 1); }
셰이더 properties 창에 추가될 정보를 띄어봅니다.
Shader "Custom/My First Shader" { Properties { } SubShader { //기존 코드들 } }
우선 임의로 이름을 _Tint으로 합니다. 혹여나 중복의 가능성을 없애기 위해 '_' 이후에 이름을 만듭니다.
또한 나타날 이름을 " "안에 적으며, 자료의 형태를 정의 해줍니다, 우선 기본 값으로 0,1,0,1으로 녹색을 해봅니다.
Properties { _Tint ("Tint", Color) = (0, 1, 0, 1) }
Assets >> MyMaterial >> Inspector에서 값을 수정이 가능해 집니다.
Properties에서 정의해준 _Tint가 Pass안에서의 _Tint에 적용되게 된다.
#pragma vertex MyVertexProgram #include "UnityCG.cginc" float4 _Tint; float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION { return mul(UNITY_MATRIX_MVP, position); } float4 MyFragmentProgram ( float4 position : SV_POSITION ) : SV_TARGET { return _Tint; }
Green sphere.
위의 코드들은 모든 픽셀에 대해서 동일한 처리를 했습니다, MyFragmentProgram에서 받은 position은 이미 변형된 위치로 그다지 유용하지 않습니다.
즉 정점에서 픽셀에게 중간 단계를 보간하여 전달해줄 필요가 있습니다.
정점의 위치에 따라 표면의 색깔을 지정하기
float4 MyFragmentProgram ( float4 position : SV_POSITION, float3 localPosition : TEXCOORD0 ) : SV_TARGET { return float4(localPosition, 1); }
TEXCOORD0 : localPostion 데이터를 Texture 코드로 사용하겠다는 의미
float4 MyVertexProgram ( float4 position : POSITION, out float3 localPosition : TEXCOORD0 ) : SV_POSITION { localPosition = position.xyz; return mul(UNITY_MATRIX_MVP, position); }
float4 _Tint; struct Interpolators { float4 position : SV_POSITION; float3 localPosition : TEXCOORD0; }; Interpolators MyVertexProgram (float4 position : POSITION) { Interpolators i; i.localPosition = position.xyz; i.position = mul(UNITY_MATRIX_MVP, position); return i; } float4 MyFragmentProgram (Interpolators i) : SV_TARGET { return float4(i.localPosition, 1); }
3.5 Tweaking Colors
어둡게 보이는 이유는 구의 지역 좌표의 경우 -0.5~+0.5사이의 값에 존재하기 때문이다 그렇기에 0.5를 가산 해준다.
return float4(i.localPosition + 0.5, 1);
또한 이렇게 나온 값에 Properties으로 받아온 값을 이용해보자
return float4(i.localPosition + 0.5, 1) * _Tint;
Mesh에 더 많은 정보를 넣기 위해 삼각형이나 정보를 더 추가하지 않고, 텍스처를 Mesh 삼각형 위에 투영하여 다양한 상세한 정보를 추가가 가능합니다.
Texture 좌표 투영을 제어하는 데 사용되며 텍스처의 실제 가로와 세로의 비율에 관계없이 0~1의 값으로 이미지 전체를 덮는 2D 좌표 한 쌍입니다. 수평은 'U' 수직은 'V'로 알려져 UV 좌표라 부릅니다.
'U' 좌표는 왼쪽에서 오른쪽에서 증가합니다. 즉 왼쪽은 '0' 중간은 '0.5' 우측은 '1' 입니다, 'V'는 동일한 방식으로 수직으로 작동합니다, 하지만 OpenGL과 Direct3D의 'v' 증가 방향은 반대입니다, 유니티에서는 V가 위쪽이 1이다.
유니티의 기본 Mesh에는 텍스쳐 맵핑을 위한 기본적인 UV 값이 들어가 있습니다. 시멘틱 'TEXCOORD0'을 이용하여 버텍스 프로그램에서 접근이 가능하게 만들어 봅니다.
Interpolators MyVertexProgram ( float4 position : POSITION
, float2 uv : TEXCOORD0 ) { Interpolators i; i.localPosition = position.xyz; i.position = mul(UNITY_MATRIX_MVP, position); return i; }
위와 같이 값을 입력을 받을 수 도 있지만, 구조체로 묶어서 처리도 가능합니다.
struct VertexData { float4 position : POSITION; float2 uv : TEXCOORD0; }; Interpolators MyVertexProgram (VertexData v) { Interpolators o; o.localPosition = v.position.xyz; o.position = mul(UNITY_MATRIX_MVP, v.position); return i; }
그리고 받아온 UV좌표를 넘겨 줍니다.
struct Interpolators { float4 position : SV_POSITION; //float3 localPosition : TEXCOORD0; float2 uv : TEXCOORD0; }; Interpolators MyVertexProgram (VertedData v) { Interpolators o; //o.localPosition = position.xyz; o.position = mul(UNITY_MATRIX_MVP, v.position); i.uv = v.uv; return o; }
우선 임시로 R에는 U, G에는 V, B에는 1을 넣어봅니다.
float4 MyFragmentProgram (Interpolators i) : SV_TARGET { return float4(i.uv, 1, 1); }
공의 앞면과 뒷면
공의 왼편과 오른편로 색의 차이로 보아 즉 원의 왼편 중앙이 U의 1과 0이다.
공의 위와 아래 위쪽이 V가 크다.
텍스처 다운로드
이 텍스처를 Assets에 추가 시키세요
Properties 창에서 새로운 택스처를 불러오기 위한 작업을 합니다, = "white" 기본값으로 "Gray"와 같이 다른 색상들도 지정이 가능합니다.
Properties { _Tint ("Tint", Color) = (1, 1, 1, 1) _MainTex ("Texture", 2D) = "white" }
(255,255,0,1)일 경우
_ST는 스케일및 변환등을 나타내는 접미사로 offset과 Tiling의 값을 들고 오는 변수로 취급합니다.
sampler2D _MainTex; float4 _MainTex_ST;
Interpolators MyVertexProgram (VertexData v) { Interpolators i; i.position = mul(UNITY_MATRIX_MVP, v.position); i.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw
return i; }
기본모습
Offset. //기본적으로 UV에 +값으로 사용된다.
Tiling. //기본적으로 UV *값으로 사용된다.
UnityCG.cginc 기본 메크로 아래와 같이 처리도 기본적인 기능으로 처리가 가능하다.
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
텍스처의 기능들에 대해서 알아보기 위해 몇가지 작업을 진행 한다.
Hierarchy >> 3D Object >> Plane
MyMaterial 추가
Assets >> MyMaterial >> Inspector
tiling값 3 3
offset값 -1 -1
Assets >> texture >> Inspector
우선 Wrap Mode에 대해서 설명한다.
: Repeat
원래의 이미지를 반복합니다,기본 값 입니다.
: Clamp
0~1 사이의 값으로 고정됩니다.
: Mirror
리피터와 비슷하나 0~1을 넘어 가면 좌표를 한번 뒤집습니다.
: Mirror Once
클램프에 미러를 섞은 형태와 비슷합니다, -좌표가 나올 경우 뒤집습니다.
: Per-axis
U,V각각 다른 위의 옵션을 줄 수 있다.
이해를 하기에 앞서 Camera와 Plane을 수정한다.
Hierarchy >> Main Camera
Hierarchy >> Plane
Filter Mode의 기능들은, 텍스처의 픽셀인 Texels과 투사되는 픽셀이 정확하게 일치하지 않으면 일종의 보정을 해줘야 합니다.
Assets >> texture >> inspector >>
Point (no filter) //보간 없음
UV가 샘플링 될 때 가장 가까운 텍셀을 블럭 단위로 이용합니다.
bilinear
기본 방식으로 그려져야 할 픽셀이 두 텍셀 사이에 존재한다면 텍셀 사이의 값으로 보간이 됩니다.|
하지만 이 방법은 두 개의 텍셀 사이에 픽셀이 있어야 하기 때문에 텍스처의 해상도가 디스플레이의 해상도보다 떨어져야 합니다. 이를 해결하기 위해서 Trilinear는 밉맵을 이용하여 텍셀의 밀도가 너무 높아 질 경우 해상도가 낮은 텍스처로 보간을 합니다.
밉맵 인식하기
밉맵은 하나의 텍스처를 여러 개의 해상도로 만들어 두는 것입니다.
Hierarchy >> Main Camera
Assets >> texture >> inspector >>
밉맵이 없을 경우에는 화면에서 멀어질수록 픽셀이 깨지는 듯한 현상이 보입니다.
밉맵이 있으면 자연스럽게 흐릿해져 갑니다.
밉맵은 하나의 텍스처를 일정 거리에 따라 해상도를 나눠나 가까운 거리의 텍스처일수록 더욱 고화질의 텍스처를 사용하는 기술입니다.
Fade Out Mipmaps
Hierarchy >> Main Camera
Assets >> texture >> inspector >>
이처럼 텍스처가 멀어져 밉맵이 변하다가 더 그릴 수 없으면 기본색을 그립니다.
Aniso Level을 조정할 경우 좀더 손쉽게 볼 수 있습니다.
Fade Range로 거리를 조절이 가능합니다.
보간을 하지 않을 경우에 거리에 따른 흐릿함이 제대로 구현되지 않아 위의 이미지처럼 너무 깔끔하게 잘려 보일 수 있습니다.