이것저것

[UNITY] 라이팅 2장 - Custom Light, Half Lambert, Rim 본문

[유니티] Unity3D/Unity Shader

[UNITY] 라이팅 2장 - Custom Light, Half Lambert, Rim

Patch_JA 2019. 4. 26. 22:17
728x90

 

 

오늘 포스팅에서는 Custom Light와 Half Lambert 그리고 Rim Light에 대해서 다룰 예정이다.

 

 

벡터의 내적 연산

벡터는 기본적으로 float3을 가지고 있는데 내적 연산으로 더하면 각도를 숫자로 나타내

float3 A와 B를 dot 하면 float1이 나오게 된다.

dot은 코사인 그래프와 연관이 있다.

 

 

코사인 값이 0도일 때는 1, 90도일 때는 0

180도 일 때는 -1 인 것을 알 수 있는데,

이것을 알고 있으면 라이트 벡터노말 벡터의 연산을 알 수 있게 된다.

 

 

 

Lambert 기본 셰이더 코드

셰이더 기초 포스팅 때 기본 코드가 있던 것처럼

라이팅에서도 기본적으로 베이스로 둘 코드를 소개하겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Shader "Custom/Lighting1" 
{ 
    Properties 
    { 
        _MainTex ("Albedo (RGB)", 2D) = "white" {} 
    } 
    SubShader 
    { 
        Tags { "RenderType"="Opaque" } 
 
        CGPROGRAM 
        #pragma surface surf Lambert 
        #pragma target 3.0 
 
        sampler2D _MainTex; 
 
        struct Input 
        { 
            float2 uv_MainTex; 
        }; 
 
        void surf (Input IN, inout SurfaceOutput o) 
        { 
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex); 
            o.Albedo = c.rgb; 
            o.Alpha = c.a; 
        } 
        ENDCG 
    } 
    FallBack "Diffuse" 
}

 

특별히 수정해야 하는 부분들이 두 곳 정도가 있다.

 

#pragma부분 Standard -> Lambert
surf함수 인자 부분 SurfaceOutputStandard -> SurfaceOutput

 

앞으로 기본적으로 라이팅 세팅을 한다고 하면 이 코드로 준비를 해두면 된다.

 

 

 

커스텀 라이트 (Custom Light) 만들기

커스텀 라이트를 만들기 위해 위에서 준비했던 Lambert 기본 코드를 준비한다.

 

1
2
3
CGPROGRAM
#pragma surface surf MyLight noambient
#pragma target 3.0

 

기존에 Lambert부분을 지우고 자신이 원하는 이름으로 수정해준다.

필자는 MyLight라고 임의로 작성하였다.

 

이제 새로운 함수를 만들어야 한다.

커스텀 조명을 만들기 위해 유니티에서 제공되는 Lighting <Name> 함수를 이용해서

LightingMyLight 이렇게 붙여서 작성해야 한다. 그렇지 않으면 에러를 뱉어낸다.

 

float4를 반환하는 함수로

인자 값은 3가지가 사용되는데 변수명들은 그대로 사용하자.

  • SurfaceOutput s
  • float3 lightDir
  • float atten
1
2
3
4
5
6
7
8
9
10
11
12
13
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
 
        float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
        {
            return float4(1, 0, 0, 1);
        }

 

float4로 반환 값을 가지고 있어서 일단은 빨간색으로 나오게끔 반환 해주었다.

 

혹시라도 프로그래머 강박증으로 이름이 마음 어딘가 불편하다면

MyLight를 _MyLight 식으로 이름을 수정해서

Lighting_MyLight()로 바꾸는 것도 하나의 방법이다...

 

< https://docs.unity3d.com/2018.3/Documentation/Manual/SL-SurfaceShaderLighting.html >

 

이번에는 디렉셔널 라이트 영향을 받도록 해보자.

 

1
2
3
4
5
6
float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
{
    float fDot = dot(s.Normal, lightDir);
    return fDot;
}
 

 

함수 인자 값에 lightDir을 이용해서 디렉셔널 라이트 영향을 받는 것을 볼 수 있다.

혹시라도 noambient때문에 확인이 애매하다면 해당 코드를 삭제하고 확인해본다.

 

디폴트 머티리얼을 하나 만들어서 비교하면 커스텀 쪽이 음영이 엄청 진거를 볼 수 있는데,

마이너스 값이 있어서 그렇다. 이 문제를 해결하려면 유니티에서 제공되는 함수를 사용해야 한다.

 

< saturate 공식 >

saturate라는 수학 함수를 사용해야 하는데 쉽게 설명하면 0 이하는 0으로 1 이상은 1로 고정시켜주는 것이다.

그래서 마이너스 값이 들어가지 않게 방지해준다. 

유니티 쉐이더에서는 기본적으로 제공되어 사용할 수 있으니 바로 사용하면 된다.

 

1
2
3
4
5
 
float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
{
    float fDot = saturate(dot(s.Normal, lightDir));
    return fDot;
}
 
 
 
 

 

 

 

Half Lambert

하프라이프를 개발한 밸브에서 기존 Lambert를 개량하여 Half Lambert라는 것을 공개했다.

당시 GI를 만들기에는 연산이 커서 꼼수로 GI를 구현하고자 만든것인데 만드는 법은 간단하다.

Half Lambert에는 마법의 공식이 존재하는데..

 

1
2
3
4
5
6
float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
{
    float fDot = dot(normalize(s.Normal), normalize(lightDir)) * 0.5 + 0.5;
    return fDot;
}
 

 

 

*0.5 + 0.5 가 추가되었는데 이것이 바로 마법의 공식이다..

기존에 0~1 값을 0.5~1 값으로 바꿔준다.

물리적으로 좋은 라이팅은 아니지만 예쁘게 보이기 위한 라이팅 방법이다..

 

 

커스텀 라이트의 문제점?

커스텀 라이팅에는 사실 그냥 쓰면 몇 가지 문제들이 있다.

먼저 디렉셔널 라이트에서 색을 바꿔보게 되면

 

기본 머티리얼을 가진 친구는 라이트 색상에 따라 색이 바뀌었지만

커스텀으로 만든 친구는 전혀 영향을 받지 않고 있다.

이문제를 해결하기 위해서는 빛을 받도록 코드를 작성해야 한다.

 

1
2
3
4
5
6
7
8
9
10
float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
{
    float fDot = dot(normalize(s.Normal), normalize(lightDir)) * 0.5 + 0.5;
    
    float4 fLight;
    fLight.rgb = fDot * _LightColor0.rgb;
    fLight.a = s.Alpha;
 
    return fLight;
}
 

 

_LightColor0이라는 변수가 뜬금없이 등장한다. 먼저 유니티 도큐먼트를 확인해보면

 

_LightColor0이라는 내장 변수가 존재하며 라이트 색상을 가져온다.

그러나 아직 문제가 남아있다.

 

그림자 영향을 받고 있지 않다!.. 이것 또한 간단하게 해결할 수 있다. 

인자 값에 있는 atten을 활용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
{
    float fDot = dot(normalize(s.Normal), normalize(lightDir)) * 0.5 + 0.5;
    
    float4 fLight;
    fLight.rgb = fDot * _LightColor0.rgb * atten;
    fLight.a = s.Alpha;
 
    return fLight;
}
 

 

그러면 그림자 영향을 받는 것을 볼 수 있다.

 

 

Half Lambert Texture 와 Normal

이번에는 머리가 있는 모델에 Half Lambert로 텍스처와 노말 맵을 한번 적용해보자.

 

위에서 진행했었던 셰이더의 머테리얼에서 텍스처만 넣게 되면 위와 같이 뭔가 흐린(?)것처럼 보인다.

코드를 전부 완성시켜 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Shader "Custom/Lighting1"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _BumpTex("Normal Map", 2D) = "bump" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
 
        CGPROGRAM
        #pragma surface surf MyLight
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _BumpTex;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex;
        };
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
 
        float4 LightingMyLight(SurfaceOutput s, float3 lightDir, float atten)
        {
            float fDot = saturate(dot(normalize(s.Normal), normalize(lightDir)) * 0.5 + 0.5);
            
            float4 fLight;
            fLight.rgb = fDot * _LightColor0.rgb * atten * s.Albedo;
            fLight.a = s.Alpha;
 
            return fLight;
        }
        ENDCG
    }
    FallBack "Diffuse"
}
 

 

기존에 노말 맵을 쓰는 것처럼 똑같이 쓰면 작동을 한다.

하지만 결과물만 보면 크게 노말 맵이 적용된 것처럼 보이지는 않는다..

Lambert에서는 노말 맵이 그냥 봐서는 잘 보이지 않는다.. Specular 없기 때문이다. 

확인을 하려면 BlinnPhong을 이용해서 확인하면 알 수 있을 것이다.

 

 

림 라이트(Rim Light) 만들기

물체를 바라보는 방향 외곽 테두리에 빛이 밝게 보이는 것인데,

Back Light, Edge Light라고도 불리는 라이팅 기법이다.

림 라이트를 시작하기 전에 Lambert 기본 셰이더를 준비해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Shader "Custom/LimLightWork"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
 
        CGPROGRAM
        #pragma surface surf Lambert
        #pragma target 3.0
 
        sampler2D _MainTex;
 
        struct Input
        {
            float2 uv_MainTex;
            float3 viewDir;
        };
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
 
            float rim = dot(IN.viewDir, o.Normal);
            o.Emission = rim;
        }
        ENDCG
    }
    FallBack "Diffuse"
}
 

 

Input 구조체 안에 못 보던 변수가 생긴 것을 볼 수 있다.

float3 viewDir; 

해당 변수는 유니티 쉐이더에서 제공되는 내장 변수인데 뷰 방향을 제공한다.

카메라가 보는 벡터를 말한다.

 

 

surf 함수를 보면 노말 벡터와 카메라 벡터의 내적을 이용해서 조명 연산을 하도록 하였다.

그러나 우리가 원하는 것은 내부가 아닌 외부가 밝게 빛나야 한다.

반전시켜보자.

 

1
2
3
4
5
6
7
void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
 
    float rim = saturate(dot(normalize(IN.viewDir), o.Normal));
    o.Emission = 1-rim;
}
 

 

1-rim을 함으로써 라이트를 반전시켜 주었다. 

그러나 밝은 부분이 생각보다 너무 차지하는 부분이 많다.

밝은 부분을 제곱을 통하여 해결할 수 있다.

 

1
o.Emission = (1-rim) * (1-rim);

 

 

무식한 방법이긴 하지만 제곱이 되긴 했다..

이렇게 되면 제곱이 많아질수록 길어지게 되고, 

주로 이런 방법은 모바일 게임에서 최적화할 때 좋은 방법이라고 한다.

 

최적화가 목적이 아니라면 pow라는 제곱 함수가 존재한다.

pow(구하려는 값, 지수); 

 

1
o.Emission = pow((1 - rim), 4);

 

쉽게 구현되는 것을 볼 수 있다. 상황에 따라 선택해서 사용하면 좋을 듯하다.

 

림 라이트(Rim Light) two side 적용

이번에는 모델에 2 side를 적용시키려고 한다.

Tags 아래 cull off 코드만 한 줄 추가해주면 적용이 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
 SubShader
{
    Tags { "RenderType"="Opaque" }
 
    cull off
 
    CGPROGRAM
    #pragma surface surf Lambert
    #pragma target 3.0
 
    ...
    ...
}
 

 

 

그런데 생각했던 것과 달리 하얗게 나와버린다.

2 side는 사실 노말을 역으로 돌려서 사용하는 것이 아니라 버텍스의 노말 값은 같은

상태이기 때문에 값이 1이면 2 side도 1이고 값이 -1이면 2 side도 -1 값을 가진다.

 

그래서 이를 해결하기 위해서는 음수를 양수로 바꿔버리면 된다.

편하게도 abs라는수학 함수가 존재한다.

 

1
2
3
4
5
6
7
 void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
 
    float rim = abs(dot(normalize(IN.viewDir), o.Normal));
    o.Emission = pow((1 - rim), 4);
}
 

 

 

 

림 라이트(Rim Light) 컬러 넣기

외곽에 하얗게 된 라이트를 한번 다른 색상으로 변경해보자.

Emission에 float3로 컬러 값을 지정해주면 된다. 

 

1
2
3
4
5
6
7
 void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
 
    float rim = abs(dot(IN.viewDir, o.Normal));
    o.Emission = pow((1 - rim), 4* float3(abs(sin(_Time.y*5)), 00);
}
 

 

float3(1, 0, 0) 식으로 레드 색상만 띄울 수 있었지만

여기서 한 번 더 abs, sin함수와 _Time 변수를 활용해서 반짝이게 구현하였다.

sin이 음수까지 내려가기 때문에 abs를 통해서 통통 튀는 것처럼 반짝거린다.

 

가장 좋은 건 인터페이스로 빼두어서 활용해보는 것도 좋다.

 

 

응용하여 홀로그램 만들기

홀로그램을 만들기 전에 먼저 알파 값 처리를 해야 한다.

셰이더 기초에서 배웠던 알파가 먹도록 코드를 수정해준다.

 

1
2
3
4
5
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
 
CGPROGRAM
#pragma surface surf Lambert alpha:fade
#pragma target 3.0
 
 

 

다음 노말 맵을 하나 얹을 예정이다. 이것 또한 전에 배웠던 것처럼

똑같이 노말 맵 텍스쳐를 한 장 받을 수 있도록 해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Properties
{
    ...       
    _BumpTex("Normal", 2D) = "bump" {}
}
SubShader
{
    ...
    sampler2D _BumpTex;
    struct Input
    {
        ...
        float2 uv_BumpTex;
        float3 viewDir;
    };
    void surf (Input IN, inout SurfaceOutput o)
    {
        ...
        o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
        ...
    }
    ENDCG
}
 

 

보면 알겠지만 림 라이트이기 때문에 Input 구조체에 viewDir 변수가 들어간다.

 

여기까지 따라왔다면 위와 같은 결과가 나올 것이다.

이제 여기서 림 라이트 때 코드를 추가해서 알파에 넣어보자.

 

1
2
3
4
5
6
7
8
9
10
11
void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
    
    o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
 
    float rim = abs(dot(IN.viewDir, o.Normal));
 
    o.Emission = c.rgb;
    o.Alpha = pow(1 - rim, 2);
}
 

 

실행해보면 투명화되면서 노말 맵이 적용된 것을 볼 수 있다.

이상태로 배웠던 것들을 응용하여 최종적으로 노말 맵이 움직이고 림 라이트 색상도 바꿔보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Shader "Custom/Hologram"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _BumpTex("Normal", 2D) = "bump" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
 
        CGPROGRAM
        #pragma surface surf Lambert alpha:fade
        #pragma target 3.0
 
        sampler2D _MainTex;
        sampler2D _BumpTex;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex;
            float3 viewDir;
        };
 
        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
 
            float rim = abs(dot(IN.viewDir, o.Normal));
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex - float2(0, _Time.y*0.2)));
            o.Emission = c.rgb *  float3(abs(sin(_Time.y)), abs(cos(_Time.y)), 0);
            o.Alpha = pow(1 - rim, 2);
        }
        ENDCG
    }
    FallBack "Diffuse"
}
 

 

이로써 그럴듯하게(?) 홀로그램이 구현되었다.

 

 

 

 

[출처]

Custom Lighting Models : https://docs.unity3d.com/2018.3/Documentation/Manual/SL-SurfaceShaderLighting.html

 

Custom Lighting Example : https://docs.unity3d.com/2018.3/Documentation/Manual/SL-SurfaceShaderLightingExamples.html

 

Lighting 내장 변수 : https://docs.unity3d.com/kr/530/Manual/SL-UnityShaderVariables.html

 

Comments