본문 바로가기

진리는어디에

[Unity] 쉐이더(Shader) 기초

들어가며

스프라이트를 클릭하면 외곽선을 하이라이트 시켜 선택되었다고 인지할 수 있는 기능을 만들고 싶었는데, 유니티에서 이런 기능을 만들려면 쉐이더를 써야 한다고 한다. 필자는 프로그래머로 오래 일을 해오긴 했지만 쉐이더 사용에는 전혀 문외한인지라 쉐이더를 공부하면서..아니다 공부라는 단어를 사용하기에는 너무 깊게 들어가지는 않을거라 '공부'라는 단어 보다는 기존 쉐이더를 분석하고 사용하는 방법을 알아간 과정을 기록하도록 하겠다.

쉐이더란?

화면에 출력할 픽셀의 위치와 색상을 계산하는 함수

쉐이더란 "화면에 출력할 픽셀의 위치와 색상을 계산하는 함수"라고 [여기]에서 쉽게 정의를 내리고 있다. 쉐이더는 크게 버텍스 쉐이더(vertex shader)와 픽셀 쉐이더(pixel shader)로 나뉘어진다. 버텍스 쉐이더를 이용해 각 3D 버텍스들의 위치를 화면 좌표로 변화하고 픽셀 쉐이더를 이용해 화면에 출력될 픽셀의 색상을 계산한다. 이 포스트에서는 딱 여기까지만 알도록하자. 필자가 지금하고 싶은 것은 쉐이더의 오의를 깨달는게 아니라 그냥 스프라이트에 아웃라인을 그려주고 싶을 뿐이다.

[여기]에 버텍스 쉐이더와 픽셀 쉐이더에 대해 간략히 정리 해놓은 블로그가 있다. 궁금하면 클릭해보자.

쉐이더 언어로는 HLSL(High Level Shading Language), OpenGL에서 사용하는 GLSL(OpenGL Shading Language), 앤비디아와 마이크로소프트의 합작으로 나옴 CG(C for Graphics)가 있다. 이중 유니티가 사용하는 쉐이더 언어는 CG다. 하지만 URP 부터는 HLSL을 사용하고 있다고 한다. 언리얼도 HLSL을 사용한다.

ShaderLab & Surface Shader

그럼 쉐이더를 사용하기 위해서는 위의 언어들을 모두 배워야 하는가? 유니티를 사용한다면 일단은 그렇지 않다. 유니티에서는 ShaderLab이라고 멀티 플랫폼 엔진에서 다양한 디바이스, 다양한 플랫폼을 지원하기위한 공통 인터페이스를 가진 ShaderLab이라는 쉐이더 언어를 사용한다. 단점으로는 호환성은 좋지만 할 수 있는게 그렇게 많지는 않다. 그래서 ShaderLab 내부에 CG를 포함할 수 있는 Surface Shader라는 것을 주로 사용한다. 좀 더 발전된 버전으로 CG를 좀더 매뉴얼하게 다룰 수 있는 'Vertex & Fragment Shader'가 있지만 필자가 필요한것은 단순히 스프라이트 외곽에 아웃라인을 그려주는것 뿐이므로 Surface Shader면 충분하다.

Surface Shader 만들기

프로젝트 뷰에서 우클릭을 하고 'Create > Shader > Standard Surface Shader'를 선택한다.

쉐이더의 이름을 'Outline'이라고 생성하고 IDE에서 쉐이더 코드를 열면 아래와 같은 구조의 코드가 생성 된다

Shader "name" { [Properties] Subshaders [Fallback] [CustomEditor] }

name 은 쉐이더의 이름뿐 아니라 경로를 포함한다. 만일 쉐이더가 많아져 분류가 필요하다면 name을 디렉토리 형식으로 만들어 주면된다. 대괄호로 감싸진 Properties, Fallback, CusomEditor는 없어도 되지만 최소 하나 이상의 Subshader는 필수다. 이제 부터 쉐이더 코드의 구조를 살펴 보자

Properties

프로퍼티는 유니티 엔진에서 쉐이더 변수들을 조작할 수 있는 인터페이스를 정해주는 곳이다. 인스펙터를 통해 값을 수정 할 수 있는 UI를 만들어 주는 곳이라 생각하자.

Properties
{
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
}

유니티에서 기본적으로 생성해주는 쉐이더 코드는 위와 같다.

변수이름 ("인스펙터에 표시 되는 이름", 프로퍼티) = 초기값

아래 그림을 참고하면 _Color라는 변수는 'Color'라는 이름으로 인스펙터에 표시 되고 컬러 픽커를 가진 초기값 흰색(1, 1, 1, 1)을 가지게 된다. 상황에 따라 적절한 프로퍼티를 사용하면 된다.

사용할 수 있는 프로퍼티들은 다음과 같다. 프로퍼티에 대한 보다 자세한 정보는 [여기]에서 찾을 수 있다.

// Numbers and Slider
name ("display name", Range (min, max)) = number
name ("display name", Float) = number
name ("display name", Int) = number

// Colors and Vectors
name ("display name", Color) = (number,number,number,number)
name ("display name", Vector) = (number,number,number,number)

// Textures
name ("display name", 2D) = "defaulttexture" {}
name ("display name", Cube) = "defaulttexture" {}
name ("display name", 3D) = "defaulttexture" {}

 

SubShader

쉐이더는 최소 하나 이상의 서브 쉐이더 블록을 가지고 있어야 하며, SubShader는 쉐이더의 내용을 결정하는 아주 중요한 블록이다.

Subshader { [Tags] [CommonState] Passdef [Passdef ...] }

이곳에서 쉐이더의 조명 계산 설정, 사용 쉐이더 버전등을 명시하며, 쉐이더에서 사용할 변수들을 선언 한다. 그리고 가장 중요하게 봐야할 surf 함수에서는 색상이나 이미지가 어떻게 출력될지를 결정한다. 쉐이더 코드에 대한 이해를 위해  아래의 유니티에서 생성한 코드를 살펴 보도록하자.

SubShader
{
    Tags { "RenderType"="Opaque" }
    LOD 200
    
    CGPROGRAM // CG 프로그래밍이 시작 되는 곳
    #pragma surface surf Standard fullforwardshadows
    #pragma target 3.0
    
    sampler2D _MainTex;
    
    struct Input
    {
        float2 uv_MainTex;
    };
    
    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_INSTANCING_BUFFER_END(Props)
    
    void surf (Input IN, inout SurfaceOutputStandard o)
    {
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        o.Metallic = _Metallic;
        o.Smoothness = _Glossiness;
        o.Alpha = c.a;
    }
    ENDCG
}

Tags

SubShader는 태크를 사용하여 언제 어떻게 렌더링 엔진에서 렌더링할지 나타낸다.

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }

태그는 기본적으로 Key-Value페어다. SubShader에서 태그는 SubShader 순서 및 기타 파라메터를 판정하는데 사용된다. 위 예제에서 사용된 RenderType 태그는 불투명한 쉐이더를 사용하겠다는 의미다. 태그에 대한 보다 자세한 설명은 [여기]를 참조한다.

CGPROGRAM ~ ENDCG

이 블록 내에서 CG코드를 사용하겠다는 선언문으로 CGPROGRAM 부터 ENDCG까지 CG 언어로 작성된다는 것을 의미한다. CG 언어를 통해 우리는 Surface Shader 프로그래밍을 할 수 있다.

#pragma 지시자

먼저 CGPROGRAM ~ ENDCG 블록에는 순수 CG 코드 외에 여러가지 컴파일 지시자를 포함할 수 있다. 기본적으로 포함된느 지시자는 #pragma다. 위 예제에서 #pragma surface는 Surface Shader를 작성하겠다는 의미다. Unity의 Surface Shader는 낮은 수준(어려운)의 vertex/pixel shader를 사용하는것 보다 훨씬 간단하게 쉐이더를 사용하는 방법이다. 이외에도 '#pragma vertex', '#pragma fragment'를 통해 버텍스/픽셀(fragment 라고 적혀 있지만 같은 의미다) 쉐이더를 작성할수도 있다.

다시 Surface Shader로 돌아가 '#pragma surface surf Standard fullforwardshadows'의 의미를 살펴보자.

#pragma surface surfaceFunction lightModel [optionalparams]
  • surfaceFunction - 앞에서 말했듯이 surface는 Surface Shader를 사용한다는 의미이고, 바로 뒤의 surfaceFunction, 즉, 예제에서 surf는 CG로 작성된 Shurface Shader의 함수 이름이다. 이 함수는 void surf(Input IN, inout SurfaceOutput o)의 형태를 가지고 있어야 한다.
  • lightModel - Surface Shader에서 사용하는 라이팅 모델을 지정한다. 내장 라이팅 모델은 심플한 비물리기반의 Lambert(diffues)와 BlinnPhong(specular)SurfaceOutput 외에도 물리 기반 라이트닝 모델을 위해  Standard와 StandardSpecular를 제공한다.
    위 예제에서 사용된 Standard 라이팅 모델은 inout 파라메터로 SurfaceOutputStandard를 사용한다. 물론, StandardSpecular이 설정되어 있다면 SurfaceOutputStandardSpecular를 사용해야 한다.
  • optionalprams - 옵션 파라메터에는 여러가지 기타 옵션들을 설정해줄 수 있다. 예제의 fullforwardshadows는..무슨 말인지 모르겠다. 영어 번역이 문제가 아니라 한글로 봐도 무슨 말인지 이해 못하고 있다.

#pragma target 3.0은 쉐이더 3.0 버전으로 컴파일 하라는 지시자다.

Input 구조체

이제 앞서 언급 되었던 Input 구조체에 대해 살펴 보도록 하겠다. 서피스 쉐이더 함수의 파라메터인 Input 구조체는 일반적으로 쉐이더에 의해 요구되는 텍스처 좌표가 필요하다. 텍스처 좌표의 이름은 uv_뒤에 텍스처 이름이 오는 형태로 해야한다(두번째 텍스쳐 좌표 세트를 사용하려면 uv2로 시작).

위 예제에서 텍스처를 나타내는 변수의 이름은 _MainTex다. Input 구조체는 엔진으로 부터 텍스처 좌표를 얻어 오기 위해 uv_MainTex라는 변수를 가지고 있다.

surf - 서피스 쉐이더 함수

실질적으로 쉐이더가 무슨 일을해야하는지 기술하는 곳이다. tex2D는 CG/HLSL에서 제공하는 함수로써 텍스처와 uv값을 넘기면 이에 해당하는 RGB 값을 반환한다. 예제에서는 tex2D 함수를 이용해 _MainTex에 Input 파라메터로 넘어온 uv값을 이용해 RGB 값을 얻고 거기에 _Color를 곱했다(지금은 쉐이더를 공부하는 중이라 그래픽적으로 이 연산이 뭘하는 것인지는 아직 모르겠다). 그 외에 연산들은 그냥 값을 대입하는 것이라 딱히 설명이 필요하진 않겠다.

서피스 쉐이더에 대한 자세한 사항은 [여기]를 참고하자.

쉐이더를 머트리얼에 적용하기

프로젝트 머트리얼을 하나 만들고 거기에 우리가 만든 Custom/Outline 쉐이더를 적용해보자

제대로 적용이 되었다면 다음과 인스펙터에 다음과 같이 나올 것이다.

위에서 우리가 Properties를 통해 연결해주었던 여러 변수들이 보일 것이다. 이제 이 머트리얼을 스프라이트라던지 필요로 하는 곳에 사용하면 된다.

마치며

스프라이트에 아웃라인을 그리고 싶었을 뿐인데 정작 아웃라인에 관한 내용은 하나도 없고 유니티 쉐이더 프로그래밍의 기본에 대해서만 알아 보았다. 스프라이트에 아웃라인을 따는 쉐이더 프로그래밍은 다음 포스트에서 알아 보도록 하자.

부록 1. 같이 읽으면 좋은 글

부록 2. 참고

 

[Shader] 쉐이더(Shader)란?

책 '유니티 쉐이더 스타트업' 을 보고 공부했습니다. 인터넷 '포프의 쉐이더 입문강좌' 를 참고했습니다. ( https://kblog.popekim.com/2011/11/01-part-1.html ) 정의 책에서는 이렇게 정의를 내립니다. '3D 컴퓨

mingyu0403.tistory.com

 

 

유니티 셰이더 Unity Shader - 01. 쉐이더 기초

24bit 컬러에서 흰색을 만들려면 R 255 G255 B255의 값을 주어야 합니다.최솟값인 0, 0, 0은 당연히 검정이겠죠? 색상 슬라이더나 색상 피커를 이용해보신다면 좀 더 빠르게 이해하실 수 있을 겁니다.

celestialbody.tistory.com

 

 

Creating a Surface Shader - Unity Learn

Surface shaders, as the name implies, define the physical characteristics of Materials. They are responsible for both calculating the final color of each pixel within a Material and performing the light calculations that define the shading of each pixel on

learn.unity.com

 

 

유니티 - 매뉴얼: 서피스 쉐이더 작성

서피스 쉐이더 작성 라이팅과 상호 작용하는 쉐이더 기술은 복잡합니다. 각종 라이트, 각종 그림자 옵션, 각종 렌더링 패스(포워드 및 지연 렌더링)가 있으며, 쉐이더는 그 복잡성을 처리해야 합

docs.unity3d.com

 

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!