원문: https://unity3d.com/kr/learn/tutorials/topics/graphics/gentle-introduction-shaders


(최대한 매끄러운 번역을 위해서 곳곳에 의역된 부분이나 추가/삭제된 내용이 존재하니 이 점 참고하시기 바랍니다.)


 Unity는 많은 사람들이 더욱 쉽게 게임을 개발할 수 있게 해주는 멋진 게임 엔진입니다. 그런데 아직 갈 길이 먼 그 무엇인가가 존재하니, 그것은 의심할 여지없이 셰이더 코딩이라고 할 수 있습니다. 이놈은 종종 신비함으로 둘러싸여 있지만, 사실 셰이더는 그냥 GPU 상에서 실행하기 위해 만든 프로그램일 뿐입니다. 여러분의 게임에 특별한 겉보기나 효과를 주고 싶다면 셰이더 코드를 작성하는 방법을 배우는 것은 중요하다고 할 수 있습니다. 게다가 Unity는 후처리를 위해서도 셰이더를 사용하는데, 이것은 2D게임에서는 필수라고 할 수 있습니다.


 이 글 연재는 셰이더 지식이 거의 없는 개발자들을 대상인 여러분에게 셰이더 코딩에 대해서 소개해 드릴 것입니다.



서론

 아래의 다이어그램은 Unity의 렌더링 워크플로우 안에서 작동하는 각기 다른 3가지 개체를 대략적으로 나타내고 있습니다.


 3D 모델은 본질적으로 버텍스라고 불리우는 3D 좌표들의 모음입니다. 그들은 삼각형을 이루기 위해서 서로 간에 연결이 되어 있습니다. 각 버텍스에는 몇가지 다른 정보들을 포함할 수 있습니다. 색상값, 방향(흔히 노멀이라고 함) 그리고 UV값이라고 불리는 텍스처 매핑을 위한 좌표들이 있습니다.


 모델은 머티리얼(material) 없이는 렌더링될 수 없습니다. 머티리얼은 셰이더와 그 셰이더 속성값을 포함하는 일종의 래퍼(wapper)입니다. 서로 다른 머티리얼은 서로 다른 데이터를 가진 상태로 동일한 셰이더를 함께 공유할 수 있습니다.



셰이더 구조

 Unity는 서피스 셰이더(surface shader)와 버텍스/프래그먼트 셰이더(vertex and fragment shader) 이렇게 두 가지 타입의 셰이더를 지원합니다. 사실 고정함수 셰이더(fixed function shader)라고 불리우는 한 가지 타입을 더 지원하긴 합니다. 하지만 이것은 현재 잘 안 쓰이므로, 이 글 연재에서는 다루지 않을 것입니다. (역주: 혹시나 고정 함수 셰이더에 관한 내용을 알고 싶은 분은  연재  참고하시기 바랍니다.)  


여러분이 필요한 셰이더의 타입이 어떤 것이든 관계없이, 쉐이더의 구조는 모두 아래와 같이 동일합니다:

Shader "MyShader"

{

Properties

{

// The properties of your shaders

// - textures

// - colours

// - parameters

// ...

}


SubShader

{

// The code of your shaders

// - surface shader

//    OR

// - vertex and fragment shader

//    OR

// - fixed function shader

}

}

 SubShader 섹션은 여러 개가 될 수 있습니다. 여기에는 실질적인 GPU에 대한 명령(instruction)을 담겨 있습니다. Unity 엔진은 그래픽 카드에 호환되는 놈을 찾을 때까지 이것들을 순서대로 실행을 시도합니다. 이로써 하나의 파일 안에 있는 동일한 셰이더의 다양한 버전을 갖출 수 있기 때문에, 다른 여러 플랫폼에 대해 고려하여 코딩할 때에 유용합니다, 



프로퍼티(Properties)

 셰이더의 프로퍼티(속성)들은 마치 C# 스크립트에서의 public 필드와 같이 보일 것입니다; 셰이더 프로퍼티는 머티리얼의 인스펙터 안에서 볼 수 있습니다. 따라서 여러분은 인스펙터를 통해 그 값을 변경할 수 있습니다. 스크립트를 사용하는 것과는 달리, 머티리얼은 애셋(asset)입니다: 에디터 상에서, 게임을 실행하고 있을 동안 머티리얼의 프로퍼티값 변경은 그대로 남아있게 됩니다. 따라서 게임을 멈춘 후에도, 여러분은 머티리얼에서 변경했던 부분이 남아있는 것을 확인하실 수 있습니다.


다음 아래의 코드 부분은 셰이더 안에서 사용할 프로퍼티의 모든 기본 타입들에 대한 정의입니다.

Properties

{

_MyTexture ("My texture", 2D) = "white" {}

_MyNormalMap ("My normal map", 2D) = "bump" {} // Grey


_MyInt ("My integer", Int) = 2

_MyFloat ("My float", Float) = 1.5

_MyRange ("My range", Range(0.0, 1.0)) = 0.5


_MyColor ("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A)

_MyVector ("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)

}

 첫번째와 두번째 프로퍼티에 사용된 2D 타입은 파라미터가 텍스처라는 것을 뜻합니다. 이들은 white, black, gray로 초기화될 수 있습니다. 또한 텍스처가 노멀 맵으로 사용될 것을 의미하는 bump를 사용할 수도 있습니다. 이 경우에, 자동으로 색상 #808080으로 초기화됩니다. Vector와 Color는 각각 XYZW와 RGBA 이렇게 항상 4가지 요소를 가집니다.


 아래의 그림은 일단 셰이더가 머티리얼에 어태치되었을 때에 셰이더 프로퍼티들이 인스펙터 안에서 어떻게 보이는가를 나타냅니다.


 안타깝지만, 단지 이렇게만 설정해서는 여러분이 셰이더 프로퍼티를 실제로 다루고 제어할 수 없습니다. Properties 섹션은 단지 셰이더 내 숨겨진 변수들을 에디터 인스펙터로 통해 접근하기 위해서 Unity에 의해서 사용되는 인터페이스일 뿐입니다. 따라서 이 숨겨진 변수들이 셰이더 실제 바디 안에서 정의되어야 합니다. 이것은 SubShader 섹션에 포함됩니다.

SubShader

{

// Code of the shader

// ...

sampler2D _MyTexture;

sampler2D _MyNormalMap;


int _MyInt;

float _MyFloat;

float _MyRange;


half4 _MyColor;

float4 _MyVector;


// Code of the shader

// ...

}

텍스처를 위해 사용되는 타입은 sampler2D입니다. 벡터는 float4이고 컬러는 보통 half4입니다.(각각 32와 16비트를 사용합니다.) 쉐이더를 작성하기 위해 사용되는 언어는 Cg / HLSL 입니다. 파라미터 이름은 앞서 정의된 것과 정확히 일치해야 합니다. 그러나 타입에 대해서는 일치시킬 필요는 없습니다: 즉, _MyRange를 float 대신 half로 선언해도 에러가 나지 않을 것입니다. 만약 Vector 타입의 프로퍼티를 정의하고 이것을 float2변수에 연결할 수 있는데, 나머지 2개의 값은 Unity에 의해서 무시됩니다. 이런 부분이 여러분에게 다소 혼란을 줄 수 있으니 유의해야 하겠습니다. 



렌더링 오더

 이미 언급했던 것처럼, SubShader 섹션은 셰이더의 실제 코드를 담고 있습니다. 이 코드는 C와 비슷한 Cg/ HLSL 로 작성됩니다. 대략적으로 말하면, 셰이더의 바디는 이미지의 각각의 픽셀을 위해서 실행됩니다; 여기 성능이 크리티컬합니다. GPU의 아키텍처로 인해, 셰이더 안에서 여러분이 수행할 수 있는 명령의 수에 제한이 존재합니다. 계산을 여러 패스(pass)로 나누어 이런 것을 피할 수도 있습니다. 하지만 이런 내용은 이 튜토리얼의 범위에서 벗어나므로 넘어가겠습니다.


셰이더의 바디는 전형적으로 다음과 같은 모습입니다:

SubShader

{

Tags

{

"Queue" = "Geometry"

"RenderType" = "Opaque"

}

CGPROGRAM

// Cg / HLSL code of the shader

// ...

ENDCG

}

CGPROGRAM과 END 지시자에 의해 묶여 있는 부분은 실제 Cg코드를 포함합니다.


그 이전의 코드 섹션은 Tag의 개념을 보여줍니다. Tag는 우리가 작성 중인 셰이더의 특정 프로퍼티에 대해 Unity에게 얘기하는 수단입니다. 여기서 말하는 특정 프로퍼티를 예를 들자면, 렌더링이 되어야 하는 순서를 뜻하는 Queue 그리고 모델을 어떻게 렌더링이 되어야 하는지를  뜻하는 RenderType 같은 것들이 있습니다.


Unity는 각 머티리얼의 렌더링 순서를 제어하기 위해서 Queue태그를 지정할 수 있습니다. Queue는 양의 정수를  지정합니다 ( 더 작은 값일 수록 더 먼저 그리게 됩니다). 다음과 같은 특정 레이블도 사용될 수 있습니다:

Background(1000): 배경과 스카이박스를 위해 사용됩니다.

Geometry(2000): 대부분의 불투명 오브젝트를 위해 사용되는 기본 레이블입니다.

Transparent(3000): 투명 프로퍼티를 가지는 머티리얼(유리, 불, 파티클, 물 등등)에서 사용됩니다.

Overlay(4000): 렌즈 플레어같은 이펙트, GUI, 텍스트와 같은 것을 그릴 때 사용됩니다.


또한 Unity는 Background+2 와 같은 식으로 상대적인 순서를 지정할 수 있습니다. 이것은 queue값이 1002라는 것을 의미합니다. Queue가 복잡해져 버리면, 다른 모델에 의해 덮혀져야 되는 오브젝트가 항상 그려져 버리는 등의 거지같은 상황이 발생할 수도 있으니 유의해야 합니다. 



ZTest

 그런데 사실 상 보면, Transparent 큐의 오브젝트가 반드시 Geometry 큐의 모든 오브젝트 위에 그려질 필요는 없는 것이 현실입니다. 그렇다면 어떻게 이것을 해결해야 할까요? GPU는 기본적으로 ZTest라는 테스트를 수행합니다. 이것은 숨겨진 픽셀들이 그려지는 것을 중지시킵니다. 이 작업을 하기 위해서 렌더링할 스크린과 동일한 크기의 추가 버퍼를 사용합니다. 각 픽셀은 픽셀 안에 그려진 오브젝트의 깊이(depth)값을 담고 있습니다. 만약 현재의 깊이보다 더 먼 픽셀일 경우, 이 픽셀은 버려집니다. 따라서 Z테스트는 스크린 상에 그려지는 순서와 관계없이 다른 오브젝트에 의해 가려지는 픽셀은 버립니다. 



서피스 셰이더 vs. 버텍스/프래그먼트 셰이더

 마지막은 실제 셰이더 코드를 보도록 하겠습니다. 그 전에, 우리는 어떤 타입의 셰이더를 사용할 지를 결정해야겠죠. 우선 이 섹션에서는 셰이더가 어떤 모양새인지 살짝 살펴보고, 자세히는 설명하지 않겠습니다. 서피스 셰이더와 버텍스/프래그먼트 셰이더 모두 다 다음에 연재되는 튜토리얼에서 전반적으로 설명할 것이니까요.


 서피스 셰이더

 머티리얼이 매 순간 빛에 의해 영향 받으며 현실감 넘치게 시뮬레이션하고 싶다면, 여러분은 피스 셰이더를 사용해야 할 것입니다. 서피스 셰이더는 라이트(빛)가 반영되는 방법에 대한 계산은 내부에 숨기며, surf라는 함수 안에서 알베도, 노멀, 반사율 등과 같은 “직관적인” 속성들만 여러분이 지정하도록 합니다. 이 속성 값들은 각 픽셀에 대해 최종 rgb값을 계산/출력하기 위해서 라이팅 모델로 넘겨집니다. 물론 이미 제공되는 라이팅 모델이 아닌 여러분 자신만의 라이팅 모델을 작성할 수도 있습니다. 하지만 보통은 고급 효과를 위해서만 사용하게 됩니다.


일반적인 서피스 쉐이더의 Cg코드는 아래와 같습니다:

CGPROGRAM

// Uses the Labertian lighting model

#pragma surface surf Lambert


sampler2D _MainTex; // The input texture


struct Input {

float2 uv_MainTex;

};


void surf (Input IN, inout SurfaceOutput o) {

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}

ENDCG



코드를 보면, 텍스처 인풋을 받는다는 것을 알 수 있습니다. 그리고 이 텍스처는 surf 함수 안에서 머티리얼의 알베도 속성으로 설정됩니다. 이 셰이더는 램버시안 라이팅 모델을 사용합니다. 램버시안 라이팅 모델은 라이트가 오브젝트 표면에서 어떻게 반사되는지를 모델링하는 가장 일반적인 방법입니다. 단지 알베도 프로퍼티만 사용하는 셰이더는 일반적으로 디퓨즈(diffuse)라고 불리웁니다.


 버텍스/프래그먼트 셰이더

 버텍스/프래그먼트 셰이더는 거의 GPU가 삼각형을 렌더링하는 방법대로 동작하게 됩니다. 그리고 라이트와 어떻게 작용하는지에 대해 특별히 내장되거나 정해진 컨셉이 없습니다. 모델의 지오메트리는 먼저 버텍스를 변경할 수 있는 vert라는 함수로 전달됩니다. 그 후에, 각 픽셀에 대해서 최종 RGB 색상을 결정하는 frag라는 함수로 전달됩니다. 보통 이 유형의 셰이더는 2D 이펙트, 후처리 또는 서피스 셰이더로는 표현되기 어려운 복잡한 3D 특수 이펙트를 구현하는 데에 유용합니다.


아래의 버텍스/프래그먼트 셰이더는 라이팅이 없는 단순히 빨간 색을 갖는 오브젝트를 만듭니다:

Pass {

CGPROGRAM


#pragma vertex vert             

#pragma fragment frag


struct vertInput {

float4 pos : POSITION;

};  


struct vertOutput {

float4 pos : SV_POSITION;

};


vertOutput vert(vertInput input) {

vertOutput o;

o.pos = mul(UNITY_MATRIX_MVP, input.pos);

return o;

}


half4 frag(vertOutput output) : COLOR {

return half4(1.0, 0.0, 0.0, 1.0); 

}

ENDCG

}


vert함수 안을 보면, 버텍스들을 그 원래의 3D 공간에서 스크린 상의 최종 2D 포지션으로 변환하는 코드가 있습니다. Unity는 이 변환에 대한 수학적인 내용을 감추기 위해서 UNITY_MATRIX_MVP를 사용하여 단순화하였습니다. 그 후 frag함수에서는 모든 픽셀이 빨간 색상을 갖도록 값을 줍니다. 버텍스/프래그먼트 셰이더의 Cg부분은 Pass섹션 안에 들어가야 한다는 것을 기억하세요.



결론

 이 포스트는 Unity에서 사용할 수 있는 두 가지 타입의 셰이더를 살짝 소개하였습니다. 이 다음 몇 개의 포스트에 걸쳐서는, 이 셰이더들을 어떻게 구현하는지를 상세히 설명할 것입니다. 그리고 나머지 포스트에서는 2D 이미지 후처리에 사용되는 스크린 셰이더에 대해 소개할 예정입니다.


 감사합니다.




Posted by 흑 기사
,