본문 바로가기

Graphics/Ronja's Unity Shader tutorials

유니티 셰이더 구조

 

Structure

Shader Structure When talking about shaders I want to start at explaining the rough outline of how shaders are set up so we can understand how to customize them. Most modern shaders have a variable pipeline that consists out of at least a vertex shader and

www.ronja-tutorials.com

Ronja 님의 허락을 받고 번역한 튜토리얼입니다. 원문은 위 링크에서 확인하실 수 있습니다.
몇몇 부분은 생략·추가하였습니다.
의역과 오역이 넘쳐날 수 있으니 편하게 봐주시고 잘못된 부분은 알려주시면 감사하겠습니다!

 

 

목차

  • 셰이더 구조
  • ShaderLab
  • ShaderLab 이란?
  • 셰이더 / 서브셰이더 / 패스
  • 프로퍼티와 태그

 


 

셰이더 구조

셰이더에 관해 설명할 때, 셰이더를 어떻게 커스터마이즈 가능한지 이해할 수 있도록 셰이더가 어떻게 구성되어 있는지 그 대략적인 개요부터 이야기하고 싶습니다.

대부분의 현대 셰이더들은 적어도 버텍스 셰이더와 프래그먼트 셰이더로 구성된 다양한 파이프라인을 가지고 있습니다.
여기에 지오메트리와 테셀레이션 스테이지를 더하는 것 또한 가능하지만, 거의 필요하지는 않습니다.

버텍스 셰이더(버텍스 스테이지 또는 함수라고도 함)는 모델을 정의하는 데이터가 렌더될 수 있도록 화면 공간으로 변환합니다(행렬 곱셈을 사용하는데, 일단 그런게 동작한다고만 하고 넘어갑시다). 커스텀 버텍스 셰이더를 사용하면 메쉬 데이터를 변경하지 않고 버텍스들의 위치를 움직이고, 프래그먼트 셰이더에 더 많은 정보를 보낼 수 있습니다.

버텍스들이 화면 위 어디에 위치하는지 정의하고 난 후엔 버텍스 사이의 삼각형들이 래스터라이저를 통해 픽셀로 변환됩니다. 래스터라이저는 오브젝트에서 어떤 픽셀이 렌더될지를 결정하는 것 외에도 버텍스 셰이더의 출력 내 모든 데이터를 보간해서 버텍스 사이의 픽셀들이 그 사이의 값을 가지게 합니다.

어떤 픽셀들이 렌더될 지 결정하고 난 뒤엔 프래그먼트 셰이더(픽셀 셰이더라고 부르기도 함)는 픽셀의 색을 결정합니다.

 

 

이건 셰이더의 기본적인 구조일 뿐입니다.

이후의 튜토리얼에서는 셰이더를 어떻게 작성하고, '공간'들이 어떤 것인지, 스테이지 사이에서 어떤 데이터를 움직이는지와 그 데이터가 어디에서 왔는지에 대해서 설명하려고 합니다. 하지만, 그냥 보는 것 만으로도 셰이더가 가진 단계를 이해할 수 있기를 바랍니다.

이건 대부분의 그래픽스 언어와 환경 속에서도 동일합니다. 버텍스와 프래그먼트 스테이지에 대한 개념이 없는 노드 기반의 셰이더도 내부적으론 이 스테이지들을 생성하고 있습니다.

 


 

ShaderLab

유니티의 일반적인 셰이더는 C# 스크립트의 파일명이 .cs 로 끝나듯이 .shader 로 끝나는 텍스트 파일입니다.
프로젝트 창에서 우클릭 > Create > Shader > 에서 템플릿 중 하나를 선택하는 것으로 쉽게 셰이더를 만들 수 있습니다.

작성하기 쉽도록 Unlit Shader 템플릿과 상당히 유사한 셰이더로 시작하겠습니다.
주요한 차이점은 우리의 셰이더는 안개(fog)효과에 제대로 반응하지 않고, 단색의 텍스처를 생성하지 않고도 해당 색을 가진 오브젝트를 만들 수 있도록 최종 색에 곱해지는 틴트 컬러와, 텍스처도 같이 가진다는 것입니다.

전체 셰이더는 이렇게 생겼습니다. 처음 몇 가지의 튜토리얼을 통해 모든 부분을 조금씩 설명해 나가려고 합니다.
셰이더를 배우는 시작점이므로, 만약 튜토리얼을 이해하는데에 어려움이 있다면 다른 학습자들을 위해 튜토리얼을 개선시킬 수 있도록 문제점들을 편하게 이야기해주세요.

 

Shader "Tutorial/001-004_Basic_Unlit"{
    //인스펙터에서 조작할 수 있는 값들이 보여집니다.
    Properties
    {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        //메테리얼은 완전히 불투명하고, 다른 불투명 지오메트리와 같은 타이밍에 렌더됩니다.
        Tags{ "RenderType"="Opaque" "Queue"="Geometry" }

        Pass
        {
            CGPROGRAM

            //유니티에서 지원하는 유용한 CG함수들을 포함해줍니다.
            #include "UnityCG.cginc"

            //버텍스와 프래그먼트 셰이더 함수를 정의합니다.
            #pragma vertex vert
            #pragma fragment frag

            //텍스처와 텍스처 좌표
            sampler2D _MainTex;
            float4 _MainTex_ST;

            //텍스처의 컬러 틴트
            fixed4 _Color;

            //버텍스 셰이더가 읽는 메쉬 데이터
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            //버텍스에서 프래그먼트 셰이더로 래스터라이저를 통해 보간되어 전달되는 데이터
            struct v2f
            {
                float4 position : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            //버텍스 셰이더 함수
            v2f vert(appdata v)
            {
                v2f o;

                //맞게 렌더될 수 있도록 버텍스 좌표를 오브젝트 공간(Object Space)에서
                //클립 공간(Clip space)으로 변환합니다.
                o.position = UnityObjectToClipPos(v.vertex);

                //텍스처 트랜스폼을 UV좌표에 적용해서 v2f 구조체로 넘깁니다.
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            //프래그먼트 셰이더 함수
            fixed4 frag(v2f i) : SV_TARGET
            {
                //UV좌표의 텍스처 컬러를 읽습니다.
                fixed4 col = tex2D(_MainTex, i.uv);

                //텍스처 컬러에 틴트 컬러를 곱합니다.
                col *= _Color;

                //화면에 그려질 최종 색상을 반환합니다.
                return col;
            }

            ENDCG
        }
    }
    Fallback "VertexLit"
}

 


 

ShaderLab 이란?

유니티 셰이더는 'ShaderLab'이라고 불리는 사용자 선언형 언어로 작성됩니다.
ShaderLab은 대부분 유니티에서 어떤 모델이 그려지는 문맥에 대해서만 정의하고 실제 셰이더 프로그램은 ShaderLab 내부의 블록에 HLSL, GLSL 또는 CG 중 하나의 셰이더 언어로 작성됩니다.

 

 

위에서 보이듯 '순수한' ShaderLab 코드는 셰이더의 작은 부분만을 차지하고 있습니다. 이렇게 작은 부분인 이유는, ShaderLab은 실행되지 않고, 추상적인 형태로 표현할 뿐이기 때문입니다. 이와 같은 간단한 셰이더에선 대부분의 경우에 기본 설정은 문제가 없습니다.

 


 

셰이더 / 서브셰이더 / 패스

셰이더 코드에서 중괄호로 된 블록 여럿이 있는 것을 발견하셨을텐데, 일단 그 블록들을 먼저 살펴봅시다.

Shader 는 셰이더 전체를 정의합니다.
메테리얼 내의 셰이더 메뉴에서 보여지는 셰이더의 이름은 셰이더 블록의 첫 부분에서 설정됩니다. 이름을 설정할 때, 슬래시(/)를 더해 셰이더를 카테고리로 묶을 수 있습니다. 저는 튜토리얼에서 작성한 모든 셰이더를 Tutorial 카테고리에 넣고 튜토리얼에 여러개의 셰이더가 있을 경우엔 새로운 서브 카테고리를 만들었지만, 편하게 자신에게 맞는 카테고리를 만들어 사용하면 됩니다.

셰이더 파일 하나마다 하나의 셰이더만을 정의할 수 있고, 하나의 파일에 여러 셰이더를 정의하는 것은 불가능합니다.
다만, 셰이더의 상위 부분에서 폴백 셰이더를 정의하는 것은 가능합니다. 폴백 셰이더를 정의하면, 폴백 셰이더 내의 모든 서브셰이더가 셰이더 파일 안에 붙여넣어진 것처럼 동작하게 됩니다.

Unity 매뉴얼 - ShaderLab : 셰이더 오브젝트 정의 ↗

 

셰이더 블록은 하나 또는 여러개의 서브셰이더를 가지고 있을 수 있습니다.
여러개의 서브셰이더는 셰이더가 동작하는 하드웨어에 따라 다른 셰이더를 사용하도록 할 수 있지만, 리소스가 부족한 상황에서 어떤 서브셰이더를 사용해야 하는지에 대한 문서와 제 경험에 따르면 일반적으로 하나의 서브셰이더만으로 충분할 것입니다.

예외적으로 종종 그림자 패스를 정의해 사용하고 싶지 않을 때 폴백 셰이더를 지정하면, 유니티는 셰이더 내에서 그림자 패스를 찾을 수 없는 경우에 폴백 셰이더의 그림자 패스를 사용합니다. 대부분의 셰이더들은 그림자를 얻기 위해 VertexLit 셰이더를 폴백으로 사용하는데, VertexLit 셰이더가 값싸고 단순하기 때문입니다.
(아마 돌아다니는 코드를 복사해 사용하고 있기 때문인 것 같습니다. 솔직히 말하면, 저도 사용되는 여러가지의 VertexLit 셰이더에 대해 잘 모릅니다.)

서브셰이더 내부에서는 서브셰이더 태그뿐만 아니라 다양한 셰이더 패스들과 서브셰이더 내의 패스에 적용될 속성들을 정의할 수 있습니다.

Unity 매뉴얼 - ShaderLab : 서브셰이더 정의 ↗

 

패스는 화면에 그려지는 어떠한 것에 대한 하나의 단위입니다.

기본 렌더 파이프라인에서 패스를 여러 개 정의한다면, 차례대로 패스들이 그려집니다. (URP는 제가 아는 한 하나의 패스만 그립니다.) 이러한 셰이더를 멀티패스 셰이더라고도 합니다.

패스는 선택적으로 넣을 수 있는 이름과 함께, 패스 태그와 서브셰이더에서 정의할 수 있는 변수들 (패스 하나를 기준으로, 패스가 하나인 서브셰이더에서는 속성이 패스 또는 서브셰이더 중 어디 내에 위치하는지는 상관이 없습니다.) 과 렌더링을 관리하는 코드 또한 가지고 있습니다.

Unity 매뉴얼 - ShaderLab : 패스 정의하기 ↗

 


 

프로퍼티와 태그

ShaderLab 부분에서 아직 이야기하지 않은 두 가지 블록이 있을 것입니다. 바깥쪽 블록에는 프로퍼티가, 서브셰이더 내부에는 태그가 있습니다.

만약 다른 프로그래밍 언어의 Dictionary 자료형을 안다면 태그와 비교해볼 수 있습니다. 태그는 엔진이 사용할 수 있는 Key-Value 페어를 들고 있습니다.
서브셰이더 태그는 에디터에서 셰이더가 적용된 메테리얼이 어떻게 보여지는지, 언제 렌더되어야 하는지와 어떤 연산이 적용될 수 있는지를 주로 정의합니다. 패스 태그는 레거시 파이프라인에서 어떤 빛 연산 단계에 어떤 패스가 사용되는지를 정의하기 위해 주로 사용됩니다.

서브셰이더 태그에 관해서는 여기, 패스 태그에 관해서는 여기에서 찾아볼 수 있습니다.

 

프로퍼티는 메테리얼 에디터에서 변수들을 표시하기 위해 사용됩니다.

프로퍼티는 메테리얼 에디터를 통해 프로퍼티를 설정하면 해당 메테리얼을 사용하는 오브젝트에 모두 같은 값이 적용되는 한계를 가지고 있습니다. 그래서 오브젝트 또는 메쉬의 작은 부분마다 프로퍼티를 다르게 설정하고 싶다면 다른 기술을 사용해야 합니다. 하지만 텍스처 좌표에 접근하고 난 다음부터는 이러한 프로퍼티를 통해 텍스처를 설정할 수 있기 때문에 많은 것을 해결할 수 있습니다.

다음 튜토리얼 중 하나에서 프로퍼티에 대한 더 자세한 설명을 하려고 합니다.

 

약간 추상화된 아래의 코드는 셰이더의 대략적인 구조가 전체적으로 어떻게 생겼는지를 보여줍니다.

Shader "Category/Name"
{
    Properties
    {
        //프로퍼티
    }

    Subshader
    {
        Tags
        {	
            //서브셰이더 태그
        }

        //모든 패스에 적용되는 설정

        Pass
        {
            Tags
            {
            	//패스 태그
            }

            //패스 설정

            CGPROGRAM
            
            //셰이더 코드
            
            ENDCG
        }
    }
}

 

 

'Graphics > Ronja's Unity Shader tutorials' 카테고리의 다른 글

기본 투명 셰이더  (0) 2024.01.21
유니티 서피스 셰이더 기본  (0) 2023.11.19
기본 셰이더  (0) 2023.08.06
변수  (0) 2023.04.22
HLSL  (0) 2022.11.09