이것저것

[UNITY] 카툰, 디퓨즈 랩 Cartoon, Diffuse Wrap 본문

[유니티] Unity3D/Unity Shader

[UNITY] 카툰, 디퓨즈 랩 Cartoon, Diffuse Wrap

Patch_JA 2019. 6. 2. 00:46
728x90

 

이번 포스팅에서는 카툰 셰이더(Cartoon Shader)와 디퓨즈 랩(Diffuse Wrap)을

실습해보고자 한다.

 


Cartoon Shader | Toon Shading, Toon Rendering, Cell Shading

셀 셰이딩은 3D 그래픽을 만화 같은 느낌을 주는 렌더링 방식이다.

 

PR과 NPR

 

앞 포스팅에서 다루었던 Lambert나 Blinn phong 같은 라이트들은 공통점이 존재한다.

'실재 사물의 재질과 비슷하게 만들려고 하는 시도'를 PR(Photo Realistic) 렌더링이라 한다.

 

반면 실제 사물과 다르게 재밌게 비슷하지 않게 만드는 것이 NPR(Non-Photo Realistic) 렌더링

이라고 하고, 셀셰이딩은 NPR렌더링에 속한다.

 

 

2 Pass

 

셀 셰이딩을 구현하기 위해서는 외곽선을 만들어야 하는데 2 Pass를 사용하여 만들 수 있다.

한번 그리는 것을 1 Pass, 두 번 그리면 2 Pass, 세 번 그리면 3 Pass이다.

 

 

2 Pass를 사용해야 하는 것을 알았으니 한번 바로 구현해보자.

기본적으로 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
Shader "Custom/WorkToon"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
      
        CGPROGRAM
        #pragma surface surf Lambert
 
        struct Input
        {
            float _Blank;
        };
 
        void surf(Input IN, inout SurfaceOutput o)
        {
            o.Albedo = 1;
        }
 
        ENDCG
    }
    FallBack "Diffuse"
}
 
 

 

위 코드는 한번 그리고 있는 1 Pass이니 이상태로 한번 더 그리게 할 것이다

그러기 위해서는 CGPROGRAM~ENDCG까지 그대로 복사해서 ENDCG밑에

붙여 넣어주면 된다.

 

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
Shader "Custom/WorkToon"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
      
        //1Pass
        CGPROGRAM
        #pragma surface surf Lambert
 
        struct Input
        {
            float _Blank;
        };
 
        void surf(Input IN, inout SurfaceOutput o)
        {
            o.Albedo = 1;
        }
 
        ENDCG
 
        //2Pass
        CGPROGRAM
        #pragma surface surf Lambert
 
        struct Input
        {
            float _Blank;
        };
 
        void surf(Input IN, inout SurfaceOutput o)
        {
            o.Albedo = 1;
        }
 
        ENDCG
    }
    FallBack "Diffuse"
}
 

 

 

결과를 확인해보면 1 Pass때랑 똑같은걸 볼 수 있다. 왜냐하면 같은 오브젝트를

그대로 한 번 더 그려주었기 때문에 겹쳐져있어서 달라진 게 없어 보일 것이다.

이번에는 1 Pass의 Vertex Normal 크기를 조절하여 검게 만들어 보겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//1Pass
CGPROGRAM
#pragma surface surf Lambert vertex:vert
 
struct Input
{
    float _Blank;
};
 
void vert(inout appdata_full v)
{
    v.vertex.xyz += v.normal.xyz * 0.01;
}
 
void surf(Input IN, inout SurfaceOutput o)
{
    o.Albedo = 0;
}
 

 

 

실행해보니 정말로 검게 나오는데 잘 보면 아래 하얗게 뭔가 가려진 게 보인다.

코드에서 각 Pass부분 CGPROGRAM위에 1 Pass에는 cull front, 2 Pass에는 cull back을

작성하여 다시 실행해본다.

 

 

1 Pass에 cull front를 하여 면을 뒤집어서 외곽선처럼 보이게 되었다.

이번에는 2 Pass에서 CustomLight 함수를 만들어서 본격적인 셀 셰이딩을

구현해보자.

 

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
//2Pass
cull back
CGPROGRAM
#pragma surface surf _CustomCell
 
struct Input
{
    float _Blank;
};
 
void surf(Input IN, inout SurfaceOutput o)
{
    o.Albedo = 1;
}
 
float4 Lighting_CustomCell(SurfaceOutput o, float3 lightDir, float atten)
{
    float fNDotl = dot(o.Normal, lightDir) * 0.7 + 0.3;
    
    if (fNDotl > 0.5)
        fNDotl = 1;
    else if (fNDotl > 0.3)
        fNDotl = 0.3;
    else
        fNDotl = 0.1;
 
    float4 fResult;
    fResult.rgb = fNDotl * o.Albedo * _LightColor0.rgb * atten;
    fResult.a = o.Alpha;
 
    return fResult;
}
 
ENDCG
}
 

 

 

fNDotl을 if문으로 3번을 걸쳐서 그림자를 나누었다.

이번엔 여기에 Specular까지 적용해보겠다.

 

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
49
50
//2Pass
cull back
CGPROGRAM
#pragma surface surf _CustomCell
 
struct Input
{
    float _Blank;
};
 
void surf(Input IN, inout SurfaceOutput o)
{
    o.Albedo = 0.6;
}
 
float4 Lighting_CustomCell(SurfaceOutput o, float3 lightDir, float3 viewDir, float atten)
{
    // Diffuse
    float fNDotl = dot(o.Normal, lightDir) * 0.7 + 0.3;        
    fNDotl *= atten;
    if (fNDotl > 0.5)
        fNDotl = 1;
    else if (fNDotl > 0.3)
        fNDotl = 0.3;
    else
        fNDotl = 0.1;
 
    // Specular
    float3 fH = normalize(lightDir + viewDir);
    float fH_Dot = saturate(dot(o.Normal, fH));
    fH_Dot = pow(fH_Dot, 10);
 
    if (fH_Dot > 0.8)
    {
        fH_Dot = 1;
    }
    else
    {
        fH_Dot = 0;
    }
 
    // Result
    float4 fResult;
    fResult.rgb = (fNDotl * o.Albedo * _LightColor0.rgb) + fH_Dot;
    fResult.a = o.Alpha;
 
    return fResult;
}
 
ENDCG
 

 

 

여기에 이제 노말맵과 스펙큘러맵까지 추가해서 최종 완성 하도록 해보겠다.

 

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "Custom/WorkToon"
{
    Properties
    {
        _MainTex("Main Texture"2D= "white" {}
        _BumpTex("Normal Textrue"2D= "bump" {}
        _SpecTex("SpecularMap"2D= "white" {}
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
 
        //1Pass
        cull front
        CGPROGRAM
        #pragma surface surf Lambert vertex:vert
 
        struct Input
        {
            float _Blank;
        };
 
        void vert(inout appdata_full v)
        {
            v.vertex.xyz += v.normal.xyz * 0.01;
        }
 
        void surf(Input IN, inout SurfaceOutput o)
        {
            o.Albedo = 0;
        }
 
        ENDCG
 
        //2Pass
        cull back
        CGPROGRAM
        #pragma surface surf _CustomCell noambient
            
        sampler2D _MainTex;
        sampler2D _BumpTex, _SpecTex;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex, uv_SpecTex;
        };
 
        void surf(Input IN, inout SurfaceOutput o)
        {
           float4 mainTex = tex2D(_MainTex, IN.uv_MainTex);
            float4 specTex = tex2D(_SpecTex, IN.uv_SpecTex);
 
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
            o.Albedo = mainTex.rgb;            
            o.Gloss = specTex.a;
        }
 
        float4 Lighting_CustomCell(SurfaceOutput o, float3 lightDir, float3 viewDir, float atten)
        {
            // Diffuse
            float fNDotl = dot(o.Normal, lightDir) * 0.7 + 0.3;        
            fNDotl *= atten;
        
            if (fNDotl > 0.5)
                fNDotl = 1;
            else if (fNDotl > 0.3)
                fNDotl = 0.4;
            else
                fNDotl = 0.2;
 
            // Specular
            float3 fHResult;
            float3 fH = normalize(lightDir + viewDir);
            float fH_Dot = saturate(dot(o.Normal, fH));
            fH_Dot = pow(fH_Dot, 20);
            if (fH_Dot > 0.7)
            {
                fH_Dot = 1;
            }
            else
            {
                fH_Dot = 0;
            }
            fHResult = fH_Dot * o.Gloss;            
 
            // Result
            float4 fResult;
            fResult.rgb = (fNDotl * o.Albedo * _LightColor0.rgb) + fHResult ;
            fResult.a = o.Alpha;
 
            return fResult;
        }
 
        ENDCG
    }
    FallBack "Diffuse"
}
 

 

 

스펙큘러맵까지 이용하여 머리부분에는 스펙큘러가 적용안된것처럼 보이도록 하였다.

더 자세한 내용은 라이팅 3장 내용에 있다.

 

 

 

Diffuse Warping

밸브사의 팀포트리스2 Diffuse 렌더링 기술문서에서 Half Lambert 외에도

Wrapped Diffuse라는 용어가 등장하게 된다. 

빛이 넘어가는 부분의 색상을 자연스럽게 할 수 있다고 하는데..

일단은 한번 구현해보자...

 

우선 기본적인 머테리얼과 쉐이더를 만들어주는데

CustomLight함수와 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
Shader "Custom/Work3"
{
    Properties
    {
        _MainTex("Albedo (RGB)"2D= "white" {}
    }
        SubShader
    {
        Tags { "RenderType" = "Opaque" }
 
        CGPROGRAM
        #pragma surface surf _CustomRamp noambient
 
        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;
        }
 
        float4 Lighting_CustomRamp(SurfaceOutput o, float3 lightDir, float atten)
        {
            float fDotl = dot(o.Normal, lightDir) * 0.5 + 0.5;            
            return fDotl;
        }
        ENDCG
    }
        FallBack "Diffuse"
}
 

 

 

기본 세팅이 완료되었다면 텍스쳐 한 장을 추가적으로 받아올 수 있도록 추가하고,

이때 Input구조체에 텍스쳐 uv 변수는 만들지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    Properties
    {
        _MainTex("Albedo (RGB)"2D= "white" {}
        _RampTex("RampTexture"2D= "white" {}
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
 
        CGPROGRAM
        #pragma surface surf _CustomRamp noambient
 
        sampler2D _MainTex;
        sampler2D _RampTex;
 
        ...
    }
 

 

1
2
3
4
5
6
7
8
float4 Lighting_CustomRamp(SurfaceOutput o, float3 lightDir, float atten)
{
    float fDotl = dot(o.Normal, lightDir) * 0.5 + 0.5;            
 
    float4 fRamp_Tex = tex2D(_RampTex, float2(0.10.1));
 
    return fRamp_Tex;
}
 

 

여태까지와 다르게 tex2D 함수를 surf 함수가 아닌 CustomLight함수 내에서

사용하였다..  심지어 텍스쳐 uv변수를 넣기 위해서 원래

 

text2D(_RampTex, IN.uv_RampTex);

 

이런 식으로 작성해야 했었는데 CustomLight함수에서는 Input구조체를 받아올 수 없어서

float2로 직접 입력 UV값을 넣어주었다.

이 방법 외에도 Custom SurfaceOutput을 만들어서 넘겨주는 방법도 있긴 하다.. 여하튼

 

_RampTex 변수에 넣을 이미지를 하나 준비하는데

아래와 같은 텍스쳐를 넣어준다.

 

실행해보면..

 

 

붉은색으로 출력되었다...(?)

하지만 당연한 결과이다. 

 

text2D(_RampTex, float2 (0.1, 0.1));

 

UV 위치에 0.1, 0.1을 지정해주었으니 아래와 같은 위치에 있기 때문이다.

 

 

그렇다면 X축에 fDotl 변수를 넣으면 어떻게 될까?

 

1
2
3
4
5
6
7
8
float4 Lighting_CustomRamp(SurfaceOutput o, float3 lightDir, float atten)
{
    float fDotl = dot(o.Normal, lightDir) * 0.5 + 0.5;            
 
    float4 fRamp_Tex = tex2D(_RampTex, float2(fDotl0.1));
 
    return fRamp_Tex;
}
 

 

마치 음영이 부드럽게 딱딱 나눠진 것처럼 보인다.

 

fDotl변수에 노말 벡터와 조명 벡터를 내적 하고 하프 램버트 공식을 이용하면 0~1까지

값이 나오는데 이 값을 X축에 넣어주어서 그렇다.

 

Ramp 텍스쳐를 보면 X축과 달리 Y축에는 색상이 바뀌는 게 없기 때문에

Y축을 0~1 사이 값을 넣어도 변화가 없어 보일 것이다.

 

이제부터 이걸 이용한 충격과 공포가 등장한다..

 

 

이런 식으로 텍스쳐를 만들고 Y축에 넣은 뒤,

카메라가 바라보는 방향으로 출력해버리면 어떻게 될까?

 

1
2
3
4
5
6
7
8
float4 Lighting_CustomRamp(SurfaceOutput o, float3 lightDir, float3 viewDir, float atten)
{
    float fDotl = dot(o.Normal, lightDir) * 0.5 + 0.5;            
    float fDotV = saturate(dot(o.Normal, viewDir));
    float4 fRamp_Tex = tex2D(_RampTex, float2(fDotl, fDotV));
 
    return fRamp_Tex;
}
 

 

이렇게 코드를 추가 수정해준 뒤 확인해보면..

 

 

 

외곽선이 생겨버렸다..  심지어 검은색을 흰색으로 변경하면 림 라이트를 쉽게 구현할 수 있다..

그런데 자세히 보면 뭔가 이상한 점 같은 그림자가 지는 걸 볼 수 있다.

 

 

텍스쳐에서 Wrap Mode에서 Repeat를 Clamp로 변경하면 해결이 된다.

 

 

이런 식으로 맨 위에 흰색으로 칠해놓으면 텍스쳐 한 장으로도 Specular를 만들 수가 있다..

이번에도 한번 위와같은 방법으로 조금 이것저것 추가해보겠다.

 

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Shader "Custom/Work3"
{
    Properties
    {
        _MainTex("Albedo (RGB)"2D= "white" {}
        _BumpTex("Normal Textrue"2D= "bump" {}
        _SpecTex("SpecularMap"2D= "white" {}
        _RampTex("RampTexture"2D= "white" {}
 
        _SpecCol("Specular val"float= 30
        _SpecColSmooth("Specular Smooth"float= 0.01
 
        [HDR]_Color("Color"Color= (1,1,1,1)
        [HDR]_SpecularColor("SpecularColor"Color= (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
 
        CGPROGRAM
        #pragma surface surf _CustomRamp noambient
 
        sampler2D _MainTex;
        sampler2D _BumpTex, _SpecTex;
        sampler2D _RampTex;
 
        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex, uv_SpecTex;
        };
 
        float4 _Color, _SpecularColor;
        float _SpecCol, _SpecColSmooth;
 
        void surf(Input IN, inout SurfaceOutput o)
        {
            float4 mainTex = tex2D(_MainTex, IN.uv_MainTex);
            float4 specTex = tex2D(_SpecTex, IN.uv_SpecTex);
 
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
            o.Albedo = mainTex.rgb;
            o.Gloss = specTex.a;
        }
 
        float4 Lighting_CustomRamp(SurfaceOutput o, float3 lightDir, float3 viewDir, float atten)
        {
            // Diffuse 
            float fDotl = dot(o.Normal, lightDir) * 0.7 + 0.3;            
            float fDotV = saturate(dot(o.Normal, viewDir));
            float4 fRamp_Tex = tex2D(_RampTex, float2(fDotl, fDotV));
 
            // Specular
            float3 fSpecResult;
            float3 fH = normalize(lightDir + viewDir);
            float fHDot = saturate(dot(o.Normal, fH));
 
            float fLightSmooth = smoothstep(0, _SpecColSmooth, fDotl);
            fHDot = pow(fHDot * fLightSmooth, _SpecCol * o.Gloss);
            if (fHDot > 0.8)
            {
                fHDot = 1;
            }
            else
            {
                fHDot = 0;
            }
            fSpecResult = fHDot * _SpecularColor * o.Gloss;
 
            // Result
            float4 fResult;
            fResult.rgb = (fRamp_Tex * o.Albedo * _LightColor0.rgb * _Color) + fSpecResult;
            fResult.a = o.Alpha;
 
            return fResult;
        }
        ENDCG
    }
        FallBack "Diffuse"
}
 

 

 

노말맵과 스펙큘러와 스펙큘러맵을 적용시켰다..

 

이외에도 셀 셰이딩뿐만 아니라 일반적인 라이팅에서도 사용될 수 있고

Fake SSS나 없는 역광을 만든다거나 잘만 쓰면 여러 가지 독특한 것들을 만들 수 있다고 한다.

 

 

Comments