본문 바로가기

Graphics/Ronja's Unity Shader tutorials

커스텀 라이팅 (램프 효과)

 

 

Custom Lighting

Summary Surface shaders are wonderful and being able to use the Standard PBR model is very powerful. But we don’t always want the PBR light. Sometimes we want to change the way we treat lighting to get a different, often more cartoonish, look. Custom lig

www.ronja-tutorials.com

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

 

목차

  • 개요
  • 커스텀 라이팅 함수 사용하기
  • 라이팅 램프 구현하기

 


 

개요

서피스 셰이더는 스탠다드 PBR (물리 기반 렌더링) 모델을 사용할 수 있다는 점에서 매우 유용합니다. 하지만 항상 PBR 조명이 필요하진 않고, 가끔은 조명 처리를 다르게 변경하여 더 만화적인 느낌을 주길 원할 때도 있습니다. 커스텀 라이팅 함수는 바로 이러한 작업을 가능하게 해줍니다.

이 튜토리얼은 서피스 셰이더에 특화된 기능에 대한 내용입니다. 기본적인 조명에 관한 내용들은 다른 셰이더와 동일하지만, 서피스 셰이더가 아닌 셰이더로 같은 결과를 내기 위해서는 더 많은 코드를 작성해야 합니다. 따라서 이번 튜토리얼에서는 서피스 셰이더 외의 내용은 다루지 않겠습니다.

이 튜토리얼은 서피스 셰이더 기초 튜토리얼의 결과물을 기반으로 진행되므로, 해당 내용을 먼저 이해하고 진행하는 것을 추천합니다.

 

 


 

커스텀 라이팅 함수 사용하기

먼저 라이팅 함수를 직접 작성하는 커스텀 라이팅 함수로 바꾸는 것부터 시작하겠습니다.

 

// 이 셰이더는 서피스 셰이더로,
// 백그라운드에서 유니티가 고급 조명 및 기타 기능을 추가하여 확장합니다.
// 우리의 서피스 셰이더 함수는 'surf'라고 불리며, 커스텀 라이팅 모델을 사용합니다.
// fullfowardshadows 는 셰이더에 필요한 그림자 패스를 유니티가 추가하도록 합니다.
#pragma surface surf Custom fullforwardShadows

 

그 다음 셰이더에 라이탕 함수 역할을 할 메소드를 추가합니다. 이 함수의 이름은 Lighting'X' 형식이어야 하며, X는 서피스 셰이더 정의에서 참조하는 라이팅 메소드의 이름입니다. 여기서 사용하는 함수의 정의에서는 서피스 셰이더부터 반환받는 SurfaceOutput 값과, 광원이 표면을 비추는 방향 (lightingDir), 그리고 감쇠(Attenuation) 값을 매개변수로 가집니다. (감쇠 값이 무엇을 하는지에 대해서는 나중에 설명하겠습니다.)

 

// 커스텀 라이팅 함수로, 조명 당 한 번씩 호출됩니다.
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    return 0;
}

 

제가 매개변수 SurfaceOutputStadard 대신 SurfaceOutput 구조체를 사용했다는 것을 눈치치채셨을 수도 있습니다. 이는 우리의 커스텀 라이팅 모델에서 Metalness 와 smoothness 를 사용하지 않을 것이기 때문이며, 따라서 PBR이 아닌 메테리얼을 위한 구조체를 사용하는 것입니다. (만약 원한다면 커스텀 라이팅 함수에도 SurfaceOutputStandard 구조체를 사용할 수 있지만, 추가로 UnityPBSLighting.cginc 파일을 임포트해야 합니다.) SurfaceOutput 구조체를 사용하려면, 서피스 셰이더 함수에서도 이 구조체를 반환해야 하며, 기존에 metalness와 smoothness 값을 설정했던 부분들을 제거해줘야 합니다.

추가로, 셰이더 변수와 프로퍼티에서 더 이상 사용하지 않는 metalness와 smoothness를 지워주었습니다. 이 부분은 필수는 아닙니다.

 

// 라이팅 함수에서 사용하는 매개변수를 설정하는 서피스 함수
void surf (Input i, inout SurfaceOutput o) {
    // 알베도 텍스처를 처리하고 틴트 값을 적용합니다.
    fixed4 col = tex2D(_MainTex, i.uv_MainTex);
    col *= _Color;
    o.Albedo = col.rgb;

    //o.Emission = _Emission;
}

 

 

이렇게 작업해준 뒤에는 유니티가 사용하는 라이팅 함수가 준비되지만, 현재는 0을 반환하므로 아무런 조명도 볼 수 없습니다. 그런데도 물체가 완전히 까맣게 보이는 대신 어느정도 구분할 수 있는 이유는, 유니티가 글로벌 일루미네이션(GI)을 수행하여 스카이박스를 기반으로 환경광을 근사적으로 적용하기 때문입니다.

만약 조명 탭에서 환경광을 검정색으로 변경하게 된다면, 완전히 검은색인 물체를 볼 수 있을 것입니다. 하지만 커스텀 조명 함수는 상관없이 잘 동작하기 때문에, 원하는 대로 조명 효과를 조절하며 게임에 어울리는 최적의 비주얼을 찾아보셔도 좋습니다. (저는 기본 설정으로 유지하겠습니다.)

 


 

라이팅 램프 구현하기

다음으로 단순한 라이팅 모델을 구현해보겠습니다. 첫번째 단계는 표면에서 광원을 향하는 벡터와 노말 벡터 사이의 내적을 구하는 것입니다. 다행히 유니티는 두가지 벡터 모두를 이미 월드스페이스에 맞게 제공하며, 또한 정규화도 되어 있기 때문에 별도로 변환해줄 필요가 없습니다.

두 값의 내적은 표면이 빛을 향해 얼마나 기울어져 있는지를 알려줍니다. 이 값이 0이면 표면의 노말 벡터가 빛의 방향과 평행한 것이고, 1이면 표면이 빛을 정면으로 향한 것이며, -1이면 표면의 노말 벡터가 빛과 수직이 되는 것입니다.

 

// 커스텀 라이팅 함수로, 조명 당 한 번씩 호출됩니다.
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // 노말이 빛을 향해 얼마나 기울어져 있는가
    float towardsLight = dot(s.Normal, lightDir);
    return towardsLight;
}

 

 

구현할 라이팅 메소드는 매우 간단하면서도 활용도가 높습니다. 텍스처의 값을 가져온 뒤 표면이 얼마나 빛을 향해 있는지의 값을 사용해 밝기로 나타내는 것입니다.

이를 위해, 내적 값의 범위를 기존의 -1~1 에서 0~1 사이의 값으로 변경해야 합니다. (왜냐하면 UV 변수가 0~1 의 값을 가지기 때문입니다.) 내적 값에 0.5를 곱하고 (곱한 뒤의 값 범위은 -0.5~0.5 가 됩니다), 0.5를 더해 0~1 범위를 가지도록 변환합니다.

다음으로, 셰이더에 새로운 텍스처를 변수와 프로퍼티로 추가합니다. ramp 라는 이름을 지어주었는데, 일반적으로 이러한 라이팅 테크닉은 toon ramp 라고 불리기 때문입니다. 그 후, 라이팅 함수에서 ramp 텍스처를 읽어와 반환합니다. 절반은 검정색, 절반은 흰색인 텍스처를 사용해서 모델 위에서 명확하게 음영의 경계를 확인할 수 있도록 할 것입니다.

 

// 인스펙터에서 조작하는 값들을 보여줍니다
Properties {
    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
    [HDR] _Emission ("Emission", color) = (0,0,0)

    _Ramp ("Toon Ramp", 2D) = "white" {}
}

//...

sampler2D _Ramp;

 

아래의 이미지가 예제에서 사용된 텍스처입니다.

 

// 커스텀 라이팅 함수로, 조명 당 한 번씩 호출됩니다.
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // 노말이 빛을 향해 얼마나 기울어져 있는가
    float towardsLight = dot(s.Normal, lightDir);
    // 값을 -1~1 범위에서 0~1 범위로 재매핑
    towardsLight = towardsLight * 0.5 + 0.5;

    // 툰 램프 텍스처에서 값을 읽어옵니다.
    float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

    return float4(lightIntensity, 1);
}

 

 

그림자 부분에서 알베도가 보이는 것을 확인할 수 있는데, 이는 유니티가 백그라운드에서 추가한 환경광 계산 때문입니다. 하지만 곧 괜찮게 보이도록 만들 것입니다.

더 괜찮게 보이도록 하기 위해서, 광원의 세기(intensity)에 메테리얼의 알베도 값을 곱해 올바른 색상이 나타나도록 하고, 감쇠(attenuation) 값도 곱해줍니다. 이 감쇠 값에는 그림자와 빛의 감쇠 효과가 포함되어 있어 거리에 따라 어두워지게 됩니다. 마지막으로 빛의 색상(_LightColor0) 값도 곱해주어 물체가 빛의 색상에 영향을 받을 수 있게 합니다.

 

// 커스텀 라이팅 함수로, 조명 당 한 번씩 호출됩니다.
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // 노말이 빛을 향해 얼마나 기울어져 있는가
    float towardsLight = dot(s.Normal, lightDir);
    // 값을 -1~1 범위에서 0~1 범위로 재매핑
    towardsLight = towardsLight * 0.5 + 0.5;

    // 툰 램프 텍스처에서 값을 읽어옵니다.
    float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

    // 색상을 혼합합니다.
    float4 col;
    // 이전에 계산한 빛의 세기, 표면의 색상(알베도), 감쇠(빛의 거리별 감소 및 그림자), 광원의 색상
    col.rgb = lightIntensity * s.Albedo * atten * _LightColor0.rgb;
    // 셰이더를 투명하게 만들 경우를 대비한 알파값 (현재는 관련 없음)
    col.a = s.Alpha; 

    return col;
}

 

 

완성된 셰이더입니다. 이 셰이더의 장점은 색상 램프를 포함한 다양한 종류의 툰 램프를 적용할 수 있다는 점입니다. 아래의 예제와 같이 앞면은 따뜻한 느낌을 주고 뒷면은 푸른 색의 차가운 느낌을 주는 과장된 램프를 사용할 수 있습니다. 이 램프는 유니티 공식 예제에서 가져왔습니다.

 

사용된 램프 텍스처

 

이번 셰이더에서 작성하지 않았지만 잘 작동하는 하나는 이미션(emission)입니다. 이미션은 물체가 스스로 발산하는 빛이기 때문에 다른 조명과는 독립적으로 처리되며, 라이팅 함수에서 계산되지 않습니다.

이 툰 셰이더는 멋있고 유연해서 많은 곳에서 사용되는 것을 보아왔습니다.

일반적으로 라이팅 함수는 매우 유용하고 강력합니다. 하지만 기억해야할 하나는, 이러한 함수들은 포워드 렌더링에서만 동작한다는 것입니다. 디퍼드 렌더링으로 렌더 방식을 변경하더라도 변함없이 물체를 볼 수는 있지만, 디퍼드 렌더링의 장점을 활용하지는 못합니다. (만약 두 렌더링 방식의 차이를 모른다면, 포워드 렌더링 방식을 계속 사용하는 것을 추천드립니다.)

 

아래는 완성된 셰이더 코드 전문입니다.

Shader "Tutorial/013_CustomSurfaceLighting" {
    // 인스펙터에서 조작하는 값들을 보여줍니다.
    Properties {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
        [HDR] _Emission ("Emission", color) = (0,0,0)

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

        CGPROGRAM

        // 이 셰이더는 서피스 셰이더로,
        // 백그라운드에서 유니티가 고급 조명 및 기타 기능을 추가하여 확장합니다.
        // 우리의 서피스 셰이더 함수는 'surf'라고 불리며, 커스텀 라이팅 모델을 사용합니다.
        // fullfowardshadows 는 셰이더에 필요한 그림자 패스를 유니티가 추가하도록 합니다.
        #pragma surface surf Custom fullforwardShadows

        sampler2D _MainTex;
        fixed4 _Color;
        half3 _Emission;

        sampler2D _Ramp;

        // 커스텀 라이팅 함수로, 조명 당 한 번씩 호출됩니다.
        float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
            // 노말이 빛을 향해 얼마나 기울어져 있는가
            float towardsLight = dot(s.Normal, lightDir);
            // 값을 -1~1 범위에서 0~1 범위로 재매핑
            towardsLight = towardsLight * 0.5 + 0.5;

            // 툰 램프 텍스처에서 값을 읽어옵니다.
            float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

            // 색상을 혼합합니다.
            float4 col;
            // 이전에 계산한 빛의 세기, 표면의 색상(알베도), 감쇠(빛의 거리별 감소 및 그림자), 광원의 색상
            col.rgb = lightIntensity * s.Albedo * atten * _LightColor0.rgb;
            // 셰이더를 투명하게 만들 경우를 대비한 알파값 (현재는 관련 없음)
            col.a = s.Alpha; 

            return col;
        }

        // 유니티에 의해 자동으로 채워지는 구조체
        struct Input {
            float2 uv_MainTex;
        };

        // 라이팅 함수에서 사용하는 매개변수를 설정하는 서피스 함수
        void surf (Input i, inout SurfaceOutput o) {
            // 알베도 텍스처를 처리하고 틴트 값을 적용합니다.
            fixed4 col = tex2D(_MainTex, i.uv_MainTex);
            col *= _Color;
            o.Albedo = col.rgb;

            //o.Emission = _Emission;
        }
        ENDCG
    }
    FallBack "Standard"
}

 

 

 

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

프레넬  (0) 2025.02.02
체크 무늬 만들기  (0) 2024.08.19
트라이플래너 매핑 (Triplanar Mapping)  (0) 2024.07.08
색상 보간  (0) 2024.06.15
평면 매핑 (Planar Mapping)  (0) 2024.05.05