본문 바로가기

Graphics/Ronja's Unity Shader tutorials

유니티 서피스 셰이더 기본

 

Surface Shader Basics

Summary In addition to writing shaders almost from the ground up, unity also allows us to define some parameters and let unity generate the code which does the complex light calculations. Those shaders are called “surface shaders”. To understand surfac

www.ronja-tutorials.com

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

 

 

목차

  • 개요
  • 간단한 서피스 셰이더로의 변환
  • 표준 라이팅 프로퍼티
  • 몇 가지 라이팅 프로퍼티 구현
  • 작은 개선점들

 


 

개요

처음부터 셰이더를 작성하지 않아도 되도록 유니티는 일부 매개변수를 정의하고, 복잡한 조명 계산을 수행하는 코드를 생성하도록 할 수 있습니다. 이런 셰이더를 '서피스 셰이더' 라고 합니다.

서피스 셰이더를 이해하기 위해서는, 기본 언릿(Unlit) 셰이더를 알아두는 것이 좋습니다. 해당하는 내용의 튜토리얼은 링크에 있습니다.

 


 

간단한 서피스 셰이더로의 변환

서피스 셰이더를 사용할 때엔, 서피스 셰이더를 사용하지 않았을 때 해줘야 했던 몇몇 작업들을 할 필요가 없습니다. 유니티가 이를 대신 처리해주기 때문입니다.

서피스 셰이더로의 변환에선, 기존에 사용했던 버텍스 셰이더를 완전히 지워도 됩니다. 버텍스 프래그먼트 함수에서의 pragma 정의 부분과 v2f(버텍스에서 프래그먼트로) 구조체를 삭제합니다. 텍스처 스케일링을 위한 MainTex_ST 변수를 지우고, UnityCG include 파일을 포함시키는 부분도 삭제해줍니다. 패스로 감싸주는 부분도 제거하면, 유니티가 대신 패스를 생성해주게 됩니다.

기본적인 부분을 제외한 나머지를 다 지운 셰이더는 아래와 같습니다.

 

Shader "Tutorial/005_surface"
{
    Properties
    {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
    	Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM

        sampler2D _MainTex;
        fixed4 _Color;

        fixed4 frag (v2f i) : SV_TARGET
        {
            fixed4 col = tex2D(_MainTex, i.uv);
            col *= _Color;
            return col;
        }
        
        ENDCG
    }
    FallBack "Standard"
}

 

이야기한 부분들을 지우며 셰이더를 망가뜨렸으니, 몇 가지를 추가해서 다시 서피스 셰이더로 기능할 수 있게끔 하겠습니다.

먼저, 새로운 구조체를 추가하고 Input 이라는 이름을 지정해줍니다. Input 구조체는 표면의 색상을 설정하는데 필요한 모든 정보들을 담을 것입니다. 이번에 만드는 간단한 셰이더에선 UV 좌표만 해당 구조체에 들어갑니다. UV 좌표의 자료형은 이전에 작업한 셰이더와 마찬가지로 2차원 float 입니다. 여기서, UV 좌표 변수의 네이밍이 중요합니다. 이 변수를 uv_MainTex 라고 이름지어줄텐데, 이렇게 하면 변수는 MainTex 텍스처의 타일링과 오프셋 값을 가지게 됩니다. 만약 텍스처가 다른 이름이라면, uvTextureName 식으로 네이밍해 해당 텍스처에 맞는 좌표를 가져와야 합니다.
(관련 유니티 메뉴얼 - Surface Shader Input Structure 부분 참고)

프로퍼티를 정의하는 부분에서 텍스처 변수명을 _MainTex 로 지었으므로, UV좌표 변수의 이름은 uv_MainTex 로 해줍니다.

 

struct Input
{
	float2 uv_MainTex;
};

 

다음엔, 프래그먼트 함수를 서피스 함수로 바꿔줄 차례입니다. 확실하게 바뀌었다고 알 수 있게, 함수명을 surf 로 변경해줍니다. 그리고는 함수가 아무 값도 반환하지 않도록, 반환 타입 (함수명 앞의 자료형) 을 void 로 바꿔줍니다.

그리고 이 함수가 인자 두개를 가질 수 있도록 합니다. 첫번째는 방금 정의한 Input 구조체의 인스턴스로, 버텍스 단에서 정의된 정보에 접근할 수 있도록 해줍니다. 두번째는 SurfaceOutputStandard 라는 이름의 구조체입니다. 이름에서 알아볼 수 있듯이, 이 구조체를 사용해서 셰이더의 생성된 부분으로 값들을 반환해줍니다. '반환하는' 부분이 작동할 수 있도록, inout 키워드를 앞에 작성해 줍니다. 이 두번째 구조체는 유니티가 라이팅 계산에 사용하는 모든 데이터를 가지고 있습니다. 라이팅 계산은 물리 기반으로 이루어집니다. (추후에 해당 변수들에 대해 글에서 다뤄보겠습니다.) 

다음으로 메서드에서 SV_TARGET 어트리뷰트를 삭제합니다. 이 부분 또한 다른 부분들처럼 유니티에서 처리해주기 때문입니다.

서피스 메서드가 동작할 수 있도록 마지막으로 처리해줄 변경점은 return 문을 제거해주는 것입니다. (함수의 반환 타입을 void 로 바꾼 이유기도 합니다.) 컬러 값을 반환값 대신 출력 구조체의 알베도 값으로 설정해 줍니다.

 

void surf (Input i, inout SurfaceOutputStandard o)
{
    fixed4 col = tex2D(_MainTex, i.uv_MainTex);
    col *= _Color;
    o.Albedo = col.rgb;
}

 

셰이더가 다시 동작하고 빛을 맞게 처리하게 하는 마지막 단계는, pragma 선언문을 추가해 셰이더의 종류와 사용된 메서드를 선언해주는 것입니다. (기본 셰이더의 버텍스 프래그먼트 메서드에서 선언했던 방식과 비슷합니다.)

이 문은 #pragma 로 시작하고, 우리가 선언한 셰이더의 종류 (surface), 서피스 메서드의 이름 (surf) 그리고 마지막으로 사용하려는 라이팅 모델 (Standard) 순으로 적습니다.

이러한 작업들을 통해, 셰이더는 다시 동작하고 올바른 라이팅을 보여줍니다.

 

Shader "Tutorial/005_surface"
{
    Properties
    {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    
    SubShader
    {
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input
        {
        	float2 uv_MainTex;
        };

        void surf (Input i, inout SurfaceOutputStandard o)
        {
            fixed4 col = tex2D(_MainTex, i.uv_MainTex);
            col *= _Color;
            o.Albedo = col.rgb;
        }
        ENDCG
    }
}

 


 

표준 라이팅 프로퍼티

셰이더를 확장하기 위해서 더 많은 메테리얼 프로퍼티들을 사용할 수 있습니다. 출력 구조체의 다른 값들은 아래와 같습니다.

 

알베도 (Albedo)

알베도는 메테리얼의 베이스 컬러값입니다. 알베도는 비추는 조명에 의해 색조가 달라지고, 그림자 속에선 어둡게 표현됩니다. 알베도 색상은 스페큘러 라이팅에 영향을 주지 않으므로, 알베도를 검정색으로 만들어도 반짝이는 광택이 보이게 할 수 있습니다. 알베도 값은 3차원 색상 벡터로 저장됩니다.

 

노말 (Normal)

메테리얼의 노말 값입니다. 노말은 '탄젠트 공간'에 있습니다. 이는 노말을 반환하면 월드에 상대적인 노말 값으로 변경된다는 것을 의미합니다. 만약 탄젠트 공간의 노말 변수값을 (0, 1, 0) 으로 작성하면 노말은 위를 가리키지 않고, 표면으로부터 멀어지게 됩니다. (이것이 정보를 노말맵에서 변수로 직접 복사할 수 있게, 노말이 노말맵으로 인코딩되는 방식입니다.) 노말 값은 3차원 방향 벡터로 저장됩니다.

 

이미션 (Emission)

이미션을 통해서는 메테리얼이 빛나도록 할 수 있습니다. 만약 출력 구조체에서 이미션 값만 작성한다면, 이전에 작성한 언릿 셰이더처럼 라이팅이 적용되지 않고 보여지게 됩니다. 하지만, 이미션으로 처리하는 방식이 더 비쌉니다. 이미시브 컬러는 조명에 영향을 받지 않으므로 항상 밝게 보여지는 지점을 만들 수 있습니다. HDR컬러를 사용해 렌더한다면 (카메라 설정에서 이를 사용하도록 설정할 수 있습니다), 이미션 채널에 1보다 더 큰 값을 작성해 물체를 정말 밝게 보여지도록 만들 수 있으며 포스트 프로세싱에서 블룸(Bloom) 효과를 사용할 때의 블룸 효과를 더 강하게 만들 수도 있습니다. 이미시브 컬러 또한 3차원 컬러 벡터로 저장됩니다.

 

메탈릭 (Metalic)

물체가 금속일 때, 재질은 다르게 보여집니다. 메탈릭 값을 조정해서 물체가 금속으로 보이게 할 수 있습니다. 메탈릭은 메테리얼이 다른 방식으로 반사하게 만들고, 알베도 값은 메탈릭이 아닐 때의 디퓨즈 라이팅 대신에 이 반사 값에 따라 색조가 조정됩니다. 메탈릭 값은 스칼라 (1차원) 값으로 저장되며, 0은 비금속 재질을, 1은 완전한 금속 재질을 나타냅니다.

 

스무스니스 (Smoothness)

스무스니스 값으론 메테리얼이 얼마나 매끄러운지 정해줄 수 있습니다. 스무스니스 값이 0인 메테리얼은 거칠어 보이고, 빛이 모든 방향으로 반사되어 스페큘러 하이라이트 또는 환경의 반사를 볼 수 없습니다. 스무스니스 값이 1인 메테리얼은 엄청 광택이 나게 보여집니다. 환경들을 적절히 설정해주었을 때, 메테리얼에 환경이 반사되는 것을 볼 수 있습니다. 스무스니스 값이 1일때 또한, 너무 매끄럽기 때문에 스페큘러 하이라이트가 무한히 작아져서 이를 관찰할 수 없게 됩니다.

스무스니스 값을 1보다 아래로 설정했을 때부터, 주변 조명의 스페큘러 하이라이트를 볼 수 있게 됩니다. 스무스니스 값을 낮추는 만큼, 하이라이트의 강도는 약해지고 크기는 커지게 됩니다. 스무스니스 값 또한 스칼라 값으로 저장됩니다.

 

오클루젼 (Occlusion)

오클루젼은 메테리얼에서 조명을 제거합니다. 오클루젼을 통해 모델의 틈새 부분들로 빛이 들어가지 않는 것처럼 만들 수 있습니다. 하지만 초현실적인 스타일을 추구하는 경우를 제외하고는, 이를 사용하는 일은 드물 것입니다. 오클루젼 값 또한 스칼라 값으로 저장됩니다. 다만 기존의 값들과는 반대로, 오클루젼 값은 1일 때 픽셀이 자신의 색을 완전한 명도로 표현하고 있음을, 0일 땐 어둡게 표현된다는 것을 의미합니다.

 

알파 (Alpha)

알파는 메테리얼의 투명도 값입니다. 우리의 현재 메테리얼은 'opaque' 로, 이는 이 메테리얼이 적용된 물체가 렌더될 때 투명한 픽셀이 없다는 것을 의미하며, 알파값은 렌더되는 픽셀에 아무 영향을 미치지 않습니다. 투명 셰이더에서, 알파값은 물체가 렌더된 픽셀에서 물체가 얼마나 보여질지를 정의합니다. 값이 1일 때는 완전히 보여지며, 0일 땐 완전히 투명해서 물체가 보이지 않습니다. 알파값도 스칼라 값으로 저장됩니다.

 


 

몇 가지 라이팅 프로퍼티 구현

이제 이러한 기능 중 일부를 셰이더에 추가해볼수 있습니다. 일단 이미션, 메탈릭과 스무스니스 값들을 사용하겠지만, 다른 값들도 구현할 수 있습니다.

첫 번째로 스무스니스와 메탈릭이라는 2개의 스칼라 값을 추가합니다. 자료형을 half (서피스 출력 구조체에서 사용되는 자료형) 로 설정하고 전역으로 (global scope) 값들을 선언해줍니다.

 

half _Smoothness;
half _Metallic;

 

그리고 값들을 프로퍼티에도 추가해줘, 인스펙터에서 이 값들을 변경할 수 있도록 해줍니다. 프로퍼티는 이 값들이 half 자료형임을 모르므로, 변수가 float 형임을 추가해서 알려줍니다. 이제 변수가 인스펙터에 표시되지만, 아직 이 값들을 사용할 수 없습니다.

 

Properties
{
    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
    _Smoothness ("Smoothness", float) = 0
    _Metallic ("Metalness", float) = 0
}

 

컬러값을 알베도에 할당한 방식과 마찬가지로, 스무스니스와 메탈릭 값들을 출력 구조체의 스무스니스와 메탈릭 변수 값으로 할당할 수 있습니다.

 

void surf (Input i, inout SurfaceOutputStandard o)
{
    fixed4 col = tex2D(_MainTex, i.uv_MainTex);
    col *= _Color;
    o.Albedo = col.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Smoothness;
}

 

이렇게 값을 할당해주면, 잘 작동하게 됩니다. 하지만 지금은 1보다 크거나 0보다 작은 값들을 변수에 할당하기 쉬워 잘못된 결과를 가질 수도 있고, 값이 어느 정도로 높은지 직관적으로 확인하기 어렵습니다. 값을 float 대신 range 프로피티로 지정해 이를 해결할 수 있습니다. Range 프로퍼티는 최소값과 최대값을 정의할 수 있게 해주며, 인스펙터에서 슬라이더로 표시됩니다.

 

Properties
{
    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
    _Smoothness ("Smoothness", Range(0, 1)) = 0
    _Metallic ("Metalness", Range(0, 1)) = 0
}

 

Range 프로퍼티가 인스펙터에서 슬라이더로 표시되는 모습

 

다음은 이미시브 컬러를 추가할 차례입니다. 먼저 HLSL 코드에 변수로 추가해주고, 그 다음 프로퍼티 단락에서 프로퍼티로도 추가해 줍니다. 이미시브 컬러에는 틴트값처럼 color 프로퍼티 타입을 사용합니다. 변수는 half3 자료형으로 저장하는데, 이는 이미션 값이 알파가 없는 RGB 컬러이고, 1보다 큰 값을 가질 수 있기 때문입니다. 그리고 서피스 출력 구조체에 이전에 했던 것처럼 값을 할당해 줍니다.

 

// ...

// 프로퍼티 단락
_Emission ("Emission", Color) = (0,0,0,1)

// ...

// HLSL 내부
half3 _Emission;

// ...

// surf 함수 내부
o.Emission = _Emission;

 

Emission 값을 적용한 모습

 

지금은 메테리얼의 이미션 값에 HDR 색상 값이 아닌 일반 색상만 넣을 수 있습니다. 이를 수정하기 위해, HDR 태그를 이미션 프로퍼티 앞에 추가해줍니다. 이렇게 변경해준 뒤엔, 더 높은 값으로 명도를 설정해줄 수 있습니다. 이미션을 더 잘 사용하려면 텍스처를 사용하는 편이 좋습니다. 알베도에 사용한 메인 텍스처처럼, 별도의 텍스처를 추가해 사용할 수 있습니다.

 

[HDR] _Emission ("Emission", Color) = (0,0,0,1)

 

HDR 컬러 피커

 


 

작은 개선점들

마지막으로 셰이더를 조금 나아 보이도록 만드는 두가지의 작은 것들을 소개하겠습니다.

먼저 SubShader 아래에 FallBack 셰이더를 추가할 수 있습니다. 이는 유니티로 하여금 셰이더에 추가하지 않은 다른 셰이더의 기능을 사용할 수 있게 해줍니다. FallBack을 표준 셰이더로 설정해주면, 유니티가 'Shadow Pass'를 해당 셰이더에서 빌려와 처리해서 메테리얼이 적용된 물체가 다른 물체에 그림자를 드리울 수 있게 합니다.

다음으로, pragma 지시문을 늘릴 수 있습니다. 서피스 셰이더 지시문에 fullfowardshadow 파라미터를 더해서 더 나은 그림자를 가질 수 있습니다. 또한 셰이더 빌드 타겟을 3.0으로 설정하는 지시문을 추가해서 조금 더 예쁜 라이팅을 위해 유니티가 고정밀도 값들을 사용할 수 있게 할 수 있습니다.

 

Shader "Tutorial/005_surface"
{
    Properties
    {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
        _Smoothness ("Smoothness", Range(0, 1)) = 0
        _Metallic ("Metalness", Range(0, 1)) = 0
        [HDR] _Emission ("Emission", color) = (0,0,0)
    }
    
    SubShader
    {
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;
        fixed4 _Color;

        half _Smoothness;
        half _Metallic;
        half3 _Emission;

        struct Input
        {
        	float2 uv_MainTex;
        };

        void surf (Input i, inout SurfaceOutputStandard o)
        {
            fixed4 col = tex2D(_MainTex, i.uv_MainTex);
            col *= _Color;
            o.Albedo = col.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Smoothness;
            o.Emission = _Emission;
        }
        ENDCG
    }
    FallBack "Standard"
}

 

 

 

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

스프라이트 셰이더  (0) 2024.03.03
기본 투명 셰이더  (0) 2024.01.21
기본 셰이더  (0) 2023.08.06
변수  (0) 2023.04.22
HLSL  (0) 2022.11.09