이것저것
[UNITY] 카툰, 디퓨즈 랩 Cartoon, Diffuse Wrap 본문
이번 포스팅에서는 카툰 셰이더(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.1, 0.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(fDotl, 0.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나 없는 역광을 만든다거나 잘만 쓰면 여러 가지 독특한 것들을 만들 수 있다고 한다.
'[유니티] Unity3D > Unity Shader' 카테고리의 다른 글
[UNITY] 물, 굴절 쉐이더 (1) | 2019.06.16 |
---|---|
[UNITY] 리플렉션 Reflection (0) | 2019.06.08 |
[UNITY] 라이팅 3장 - Phong, Blinn Phong (0) | 2019.05.03 |
[UNITY] 라이팅 2장 - Custom Light, Half Lambert, Rim (0) | 2019.04.26 |
[UNITY] 라이팅 1장 - 기본 이론 (0) | 2019.04.20 |