-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathindex.html
More file actions
407 lines (372 loc) · 35.5 KB
/
Copy pathindex.html
File metadata and controls
407 lines (372 loc) · 35.5 KB
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
<!DOCTYPE html>
<html lang="en">
<head>
<title>카툰 렌더링</title>
<!-- Meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="카툰렌더링 기초 | 한별(StellaMagna)">
<meta name="author" content="Xiaoying Riley at 3rd Wave Media">
<link rel="shortcut icon" href="favicon.ico">
<!-- Google Font -->
<link href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700&display=swap" rel="stylesheet">
<!-- FontAwesome JS-->
<script defer src="./pages/fontawesome/js/all.min.js"></script>
<!-- Plugins CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.2/styles/atom-one-dark.min.css">
<link rel="stylesheet" href="./pages/plugins/simplelightbox/simple-lightbox.min.css">
<!-- Theme CSS -->
<link id="theme-style" rel="stylesheet" href="./pages/css/theme.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body);"></script>
</head>
<body class="docs-page">
<header class="header fixed-top">
<div class="branding docs-branding">
<div class="container-fluid position-relative py-2">
<div class="docs-logo-wrapper">
<button id="docs-sidebar-toggler" class="docs-sidebar-toggler docs-sidebar-visible me-2 d-xl-none" type="button">
<span></span>
<span></span>
<span></span>
</button>
<div class="site-logo"><a class="navbar-brand"><img class="logo-icon me-2" src="./pages/images/coderdocs-logo.png" alt="logo"><span class="logo-text">카툰 <span class="text-alt">렌더링</span></span></a></div>
</div><!--//docs-logo-wrapper-->
<div class="docs-top-utilities d-flex justify-content-end align-items-center">
<div class="top-search-box d-none d-lg-flex">
<form class="search-form">
<input type="text" placeholder="Search the docs..." name="search" class="form-control search-input">
<button type="submit" class="btn search-btn" value="Search"><i class="fas fa-search"></i></button>
</form>
</div>
<ul class="social-list list-inline mx-md-3 mx-lg-5 mb-0 d-none d-lg-flex">
<li class="list-inline-item"><a href="https://github.com/Hanbyori" target='_blank'><i class="fab fa-github fa-fw"></i></a></li>
<li class="list-inline-item"><a href="https://x.com/onestar_1337" target='_blank'><i class="fab fa-twitter fa-fw"></i></a></li>
<li class="list-inline-item"><a href="https://blog.naver.com/twinkle_onestar" target='_blank'><i class="fa fa-rss"></i></a></li>
</ul><!--//social-list-->
</div><!--//docs-top-utilities-->
</div><!--//container-->
</div><!--//branding-->
</header><!--//header-->
<div class="docs-wrapper">
<div id="docs-sidebar" class="docs-sidebar">
<div class="top-search-box d-lg-none p-3">
<form class="search-form">
<input type="text" placeholder="Search the docs..." name="search" class="form-control search-input">
<button type="submit" class="btn search-btn" value="Search"><i class="fas fa-search"></i></button>
</form>
</div>
<nav id="docs-nav" class="docs-nav navbar">
<ul class="section-items list-unstyled nav flex-column pb-3">
<li class="nav-item section-title mt-3"><a class="nav-link scrollto" href="#section-1"><span class="theme-icon-holder me-2"><i class="fa fa-bell"></i></span>안내사항</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-1-1">대상</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-1-2">기본 설정</a></li>
<li class="nav-item section-title mt-3"><a class="nav-link scrollto" href="#section-2"><span class="theme-icon-holder me-2"><i class="fa fa-code"></i></span>시작</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-1">Diffuse</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-2">Normal</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-3">Shadow</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-4">Ramp</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-5">Face Shadow</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-6">Ambient Occlusion</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-7">Specular</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-8">Emission</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-9">RimLight</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-10">Outline</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-11">Cast/Receive Shadow</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-12">Additional Light</a></li>
<li class="nav-item"><a class="nav-link scrollto" href="#item-2-13">Fake Shadow</a></li>
<li class="nav-item section-title mt-3"><a class="nav-link scrollto" href="#section-3"><span class="theme-icon-holder me-2"><i class="fa fa-heart"></i></span>마무리</a></li>
</ul>
</nav><!--//docs-nav-->
</div><!--//docs-sidebar-->
<div class="docs-content">
<div class="container">
<article class="docs-article" id="section-1">
<header class="docs-header">
<h1 class="docs-heading">안내사항 <span class="docs-time">마지막 수정: 2024-08-26</span></h1>
<section class="docs-intro">
<p>카툰 렌더링의 기초 정리 문서입니다.</p>
<p>카툰 렌더링은 조명 모델과 같이 통일된 공식이 없습니다. 클라이언트의 요구사항이나 작업자의 방식에 따라 여러 다양한 트릭을 통해 표현됩니다.<br>이 문서 또한 카툰 렌더링의 교과서 같은게 아닙니다. 단지 하나의 솔루션일 뿐입니다.</p>
</section><!--//docs-intro-->
</header>
<section class="docs-section" id="item-1-1">
<h2 class="section-heading">대상</h2>
<p>그래픽스 이론을 어느정도 알고 있으면서 카툰 렌더링이 궁금한 독자를 대상으로 합니다.<br>필자는 말하는 감자에 불과하기에 정말 많은 오류가 내포되어 있을 수 있습니다. 참고용으로만 가볍게 읽어주세요.</p>
<p>아래 나열된 지식이 없다면 본문을 이해하기 어려울 수 있습니다.<br>
<a href="https://medium.com/@thkim2/%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%ED%86%B5%ED%95%B4-%EB%93%9C%EB%A1%9C%EC%9A%B0%EC%BD%9C-%ED%95%98%EA%B8%B0-2-rendering-pipeline-ec0cf6f4c4bc" target="_blank">렌더링 파이프라인에게 드로우콜 하기 (2) : Rendering Pipeline</a> - 렌더링 파이프라인 소개<br>
<a href="https://www.cyanilux.com/tutorials/urp-shader-code/" target="_blank">Shader Code in Universal RP (v2)</a> - URP 셰이더 작성 방법<br>
<a href="http://www.codinglabs.net/article_world_view_projection_matrix.aspx" target="_blank">Article - World, View and Projection Transformation Matrices</a> - 좌표 공간과 변환 행렬<br>
<a href="https://www.opengl-tutorial.org/kr/intermediate-tutorials/tutorial-13-normal-mapping/" target="_blank">Tutorial 13 : Normal Mapping</a> - Normal과 TBN<br>
<a href="https://woo-dev.tistory.com/225" target="_blank">조명(Lighting) 기초</a> - 라이팅 기초<br>
<a href="https://ko.wikipedia.org/wiki/Z_%EB%B2%84%ED%8D%BC%EB%A7%81" target="_blank">Z 버퍼링</a> - ZBuffer 개념<br>
<a href="https://youtu.be/y-SEiDTbszk?si=oYFmzuGz80jOQHlb" target="_blank">[TUTORIAL] Stencil buffer in Unity URP (cutting holes, impossible geometry, magic card)</a> - StencilBuffer와 Test<br>
<a href="https://learn.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps?redirectedfrom=MSDN" target="_blank">Common Techniques to Improve Shadow Depth Maps</a> - ShadowMap의 생성과 적용 원리<br>
</section><!--//section-->
<section class="docs-section" id="item-1-2">
<h2 class="section-heading">기본 설정</h2>
<p>Unity 2022.3.16f1 URP를 기준으로 작성되었으며, 다른 버전과의 호환성은 장담할 수 없습니다.</p>
<p>기본적인 구조체는 다음과 같이 작성하며 이후 다룰 셰이더는 Fragment만 다룹니다.
<script src="https://gist.github.com/Hanbyori/3b6c961f07f4470f2bc74c9a9449c5b0.js"></script></p>
</section><!--//section-->
</article>
<article class="docs-article" id="section-2">
<header class="docs-header">
<h1 class="docs-heading">시작</h1>
<section class="docs-intro">
<div class="callout-block callout-block-info">
<div class="content">
<h4 class="callout-title">
<span class="callout-icon-holder me-1">
<i class="fas fa-info-circle"></i>
</span><!--//icon-holder-->
Note
</h4>
<p>URP와 BIRP의 메소드가 혼용되어 사용되는 경우가 있을 수 있습니다.</p>
</div><!--//content-->
</div><!--//callout-block-->
<p>카툰 렌더링에 사용되는 기법들을 나열합니다. 프로터피에 대한 선언과 ShaderLab 구조는 본문에 포함되지 않으며, 오로지 Fragment 계산만 포함합니다.</p>
</section><!--//docs-intro-->
</header>
<section class="docs-section" id="item-2-1">
<h2 class="section-heading">Diffuse</h2>
<p>셰이더를 생성하고 기본적인 텍스쳐를 매핑합니다.</p>
<p><img src="./pages/images/Diffuse01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/Diffuse02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/29048920a9f633ed2cccdcda15d677ad.js"></script></p>
</section><!--//section-->
<section class="docs-section" id="item-2-2">
<h2 class="section-heading">Normal</h2>
<p>Normal Map을 매핑합니다.</p>
<p><script src="https://gist.github.com/Hanbyori/c6310d215d8053c2fc673fdfe1061c19.js"></script><p>
<p>Tangent Space에서 World Space로 변환하기 위한 3개의 Vector는 모두 월드 공간에서 정의된 Vector입니다. 이 Vector들을 기반으로 3x3 행렬을 구성합니다.<br>구체적으로, 이 변환행렬은 다음과 같습니다.</p>
<p>$$ M = \begin{bmatrix}
\text{Tangent}_x & \text{Bitangent}_x & \text{Normal}_x \\
\text{Tangent}_y & \text{Bitangent}_y & \text{Normal}_y \\
\text{Tangent}_z & \text{Bitangent}_z & \text{Normal}_z
\end{bmatrix} $$</p><br>
<p>Normal Map은 기본적으로 Tangent Space에서 정의됩니다. 이 Vector를 World Space로 변환하기 위해 다음과 같은 계산을 수행해 normalWS을 얻습니다.</p>
<p>$$ \text{normalWS} = M \times \text{bump} $$</p>
</section><!--//section-->
<section class="docs-section" id="item-2-3">
<h2 class="section-heading">Shadow</h2>
<p>Diffuse Coloring의 기본인 Shadow 계산입니다.</p>
<p><img src="./pages/images/Shadow01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/Shadow02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/7975ee3a82998fba762a67cddaeba6cc.js"></script></p>
<p>Lighting을 하기 위해 먼저 Normal과 Light를 내적하는 속칭 \( \text{NdotL} \) 연산을 먼저 수행합니다.<br>연산으로 얻은 \( \cos \)값 자체가 Lambert 라이팅입니다.</p>
<p>이 Lambart는 \( \left[-1 \sim 1\right] \) 범위를 가지고 있으므로, 문제가 생길 수 있는 음수 값과 음영의 급격한 변화를 방지하기 위해 Half-Lambert 공식을 사용합니다.</p>
<p><img src="./pages/images/halfLambert01.jpg" style="max-width: 60%; height: auto;">
<img src="./pages/images/halfLambert02.png" style="max-width: 30%; height: auto;"></p>
<p>Half-Lambert는 \( \text{NdotL} \times 0.5 + 0.5\) 공식으로 계산됩니다. 이 공식은 \( \left[-1 \sim 1\right] \) 범위를 \( \left[0 \sim 1\right] \) 범위로 만들어 줍니다.</p><br>
<p><code>smoothstep</code>은 Hermite 보간으로 음영의 경계를 부드럽게 계산합니다.</p>
<p><img src="./pages/images/smooth01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/smooth02.png" style="max-width: 30%; height: auto;"></p>
<p>현재 계산에서 <code>_ShadowSmooth</code>가 증가되면 음영이 전진하면서 보간됩니다.<br>기존 음영이 후퇴하면서 전진 방향으로 Smooth 효과를 내고싶다면, 다음과 같이 계산을 수정합니다.
<script src="https://gist.github.com/Hanbyori/e2bc5b3679d41a0a97c23820da02745d.js"></script></p>
</section><!--//section-->
<section class="docs-section" id="item-2-4">
<h2 class="section-heading">Ramp</h2>
<p>Diffuse Coloring의 다른 방법인 Ramp 계산입니다.</p>
<p><img src="./pages/images/ramp01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/ramp02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/dcf5298d321f35023bce64eccc591207.js"></script></p><br>
<p>HalfLambert로 게산한 Shadow 영역을 Ramp Texture로 샘플링합니다.</p>
<p><img src="./pages/images/ramp03.png" style="max-width: 60%; height: auto;"></p>
<p>\( V \)값의 \( 0.5 \)는 Ramp 맵의 Y축 중심을 샘플링 한다는 의미입니다. HalfLambart 값 범위가 \( \left[0 \sim 1\right] \)이므로 Ramp 맵의 X축 왼쪽부터 오른쪽까지 이 Lambert 계수 값으로 샘플링합니다.</p><br>
<p><img src="./pages/images/ramp04.png" style="max-width: 60%; height: auto;"></p>
<p>예로 Genshin Impact에서 사용하는 Ramp Map의 경우 상단 5줄은 낮 환경에서 샘플링 하고, 하단 5줄은 밤 환경에서 샘플링 하는 방식으로 Y축 값의 따라 재질 별 다른 색상 그라데이션을 샘플링 합니다.</p>
<p><img src="./pages/images/ramp05.jpg" style="max-width: 60%; height: auto;"></p>
</section><!--//section-->
<section class="docs-section" id="item-2-5">
<h2 class="section-heading">Face Shadow</h2>
<p>얼굴에 드리우는 그림자가 제대로 표시되기 위한 방법입니다.</p>
<p><img src="./pages/images/smoothface01.gif" style="max-width: 30%; height: auto;">
<img src="./pages/images/smoothface02.gif" style="max-width: 30%; height: auto;"></p>
<p>카툰 렌더링의 얼굴 그림자를 계산할 때 두 가지 주요 방법이 있습니다. 먼저 얼굴 Mesh의 법선을 수정하는 방식입니다.<br>비단 얼굴 부분에만 사용하는 것이 아니라 법선의 각도 수정은 기타 다른 Mesh의 면 정리에도 필요한 수단입니다.</p><br>
<p>아래 그림은 구형의 법선을 얼굴 Mesh에 전달하는 과정입니다.</p>
<p><img src="./pages/images/smoothface03.webp" style="max-width: 60%; height: auto;"></p>
<p>이 방식은 작업이 매우 간단합니다. 하지만 그림자가 단순한 구형에 불과하여, 표현하고자 하는 효과가 비교적 제한적입니다.</p><br>
<p>다른 방법은 <a href="https://en.wikipedia.org/wiki/Signed_distance_function" target='_blank'>SDF</a> 입니다. 일반적으로 글꼴 렌더링에 사용되지만 거리 필드를 사용하여 그림자를 기록한 다음 광원의 방향에 따라 해당 그림자 모양을 추출하는 방식입니다.</p>
<p><img src="./pages/images/smoothface01.gif" style="max-width: 30%; height: auto;">
<img src="./pages/images/smoothface03.gif" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/ea777557736b07190c1447062fc89927.js"></script></p>
<p>이 방법의 이점은 그림자 모양을 완전히 자유롭게 정의할 수 있습니다. 눈 밑이나 코 등의 그림자 모양도 손쉽게 표현하면서 동시에 자연스러운 전환이 가능합니다.</p><br>
<p><img src="./pages/images/a.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/b.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/c.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/d.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/e.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/f.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/g.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/h.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/i.png" style="max-width: 10%; height: auto;"><br>
<img src="./pages/images/a_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/b_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/c_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/d_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/e_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/f_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/g_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/h_1.png" style="max-width: 10%; height: auto;">
<img src="./pages/images/i_1.png" style="max-width: 10%; height: auto;"></p>
<p>위 사진은 얼굴 정면에서 조명 각도 \( 180^\circ, 157.5^\circ, 135^\circ, 112.5^\circ, 90^\circ, 67.5^\circ, 45^\circ, 22.5^\circ, 0^\circ \)에 해당하는 그림자를 그리고 거리 필드를 생성한 결과입니다.</p><br>
<p><img src="./pages/images/SDF.png" style="max-width: 30%; height: auto;"></p>
<p>스무딩된 이미지의 픽셀별 합을 그림자 맵의 갯수로 나누어 최종 SDF 맵을 생성합니다.</p><br>
<p><img src="./pages/images/SDF_Calc.webp" style="max-width: 60%; height: auto;"></p>
<p>계산 방식은 간단합니다. SDF 맵은 한 방향만 기록하므로 \( \vec{V}_{right} \cdot \vec{V}_{light} \)결과로 왼쪽, 오른쪽 여부를 판단하여 SDF를 뒤집어서 사용합니다.<br>이후 \( \vec{V}_{front} \cdot \vec{V}_{light} \) 결과로 SDF 맵에서 표시해야 할 임계 값을 구하고 출력합니다.</p>
<p>SDF는 정확한 정보를 유지하기 위해 이미지 압축을 권장하지 않습니다. 또한, Y축 변형에 대한 정보도 제공하지 않습니다. 이에 대한 해결책으로 <a href="https://zhuanlan.zhihu.com/p/670837192" target='_blank'>360 SDF 솔루션</a>을 다룬 글이 있습니다.</p>
<p>Cygames사에서 발표한 방법(<a href="https://youtu.be/T2iI9hbNqLI?si=P_YTLBdCLspWuj3p&t=862" target='_blank'>영상</a>)으로 SDF 그림자를 그리고 법선 또한 수정하여 광원과 혼합해서 그림자를 표현하는 방법도 있습니다.<br>해당 영상은 그림자 솔루션 이외에도 좋은 정보를 다수 내포하고 있습니다. 꼭 한번 보시기를 권장드립니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-6">
<h2 class="section-heading">Ambient Occlusion</h2>
<p>차폐 계산입니다. NPR에서는 그림자 한계 값으로도 불립니다.</p>
<p><img src="./pages/images/ao01.png"style="max-width: 30%; height: auto;">
<img src="./pages/images/ao02.png"style="max-width: 30%; height: auto;">
<img src="./pages/images/ao03.png"style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/f015f8f9d8d22dd44ce145f6b14e60f7.js"></script></p>
<p>AO는 기본적으로 주광 \( + \) 환경광 \( \times \) AOMask로 연산되지만, 여기선 주광에 직접 곱해 사용합니다.</p><br>
<p><img src="./pages/images/ao05.webp"style="max-width: 30%; height: auto;">
<img src="./pages/images/ao04.gif"style="max-width: 30%; height: auto;"></p>
<p>이 AO맵을 사용하여 라인아트를 표현하는 경우도 있습니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-7">
<h2 class="section-heading">Specular</h2>
<p>비금속과 금속에 대한 하이라이트 계산입니다.</p>
<p><img src="./pages/images/ao03.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/spec01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/spec02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/ab66f0f98e18b2c1d53501fe97feef0f.js"></script></p><br>
<p>하이라이트를 표시할 때, <code>step</code> 함수를 사용하여 specularMask에서 비금속과 금속 부분을 나누어 처리합니다.<br>전통적인 Blinn-Phong 셰이딩 모델을 사용하며, 금속 부분은 표현을 위해 Matcap을 추가로 사용합니다.</p><br>
<p>Specular를 계산할 때, Hair에 비치는 하이라이트를 따로 계산하는 경우가 많습니다.</p>
<p><img src="./pages/images/spec03.jpg" style="max-width: 60%; height: auto;"></p>
<p>이때 Mask 맵을 사용하여 하이라이트가 단순히 비춰질 부분을 계산하는 것이 아니라, 하나의 형상으로 계산합니다. Blinn-Phong 기반으로 계산하기 때문에 라이트와 카메라 각도에 따라 더 나은 결과물을 얻을 수 있습니다.</p>
<img src="./pages/images/spec04.gif" style="max-width: 30%; height: auto;">
</section><!--//section-->
<section class="docs-section" id="item-2-8">
<h2 class="section-heading">Emission</h2>
<p>발광에 대한 계산입니다.</p>
<p><img src="./pages/images/spec02.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/emi01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/emi02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/8e3389a796db3e234bc4ae1207d02264.js"></script></p>
</section><!--//section-->
<section class="docs-section" id="item-2-9">
<h2 class="section-heading">RimLight</h2>
<p>후광 효과의 계산입니다. 가장자리에 빛을 가상으로 시뮬레이션합니다.</p>
<p><img src="./pages/images/emi02.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/rim01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/rim02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/13003a40f6cb96c2b58fc666fd842b8b.js"></script></p>
<p>Fresnel만 사용해도 충분히 RimLight를 표현할 수 있는데, 굳이 다른 방식을 사용해야 하냐는 의문이 생길 수 있습니다.<br>제가 생각하는 RimLight의 가장 이상적인 형태는, 두께가 균일하고 움직임이 있더라도 오브젝트에 대해 RimLight의 두께가 항상 일정하게 유지되는 것입니다.</p><br>
<p><img src="./pages/images/rim03.gif" style="max-width: 60%; height: auto;"></p>
<p>캐릭터가 움직일 때, Fresnel로 계산된 RimLight에서 두께가 불균일하게 나타나는 경우가 있습니다.</p><br>
<p><img src="./pages/images/rim04.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/rim05.png" style="max-width: 30%; height: auto;"></p>
<p>정지된 상태에서도 Fresnel로 계산된 RimLight가 불균일한 두께로 나타나는 현상을 볼 수 있습니다.</p><br>
<p>계산이 복잡해 보일 수 있지만, 원리는 간단합니다.</p>
<p><img src="./pages/images/rim06.png" style="max-width: 20%; height: auto;">
<img src="./pages/images/rim08.png" style="max-width: 20%; height: auto;">
<img src="./pages/images/rim07.png" style="max-width: 20%; height: auto;">
<img src="./pages/images/rim09.png" style="max-width: 20%; height: auto;"></p>
<p>화면 공간에서 Depth 맵을 생성한 후, 각 정점의 위치를 법선 방향으로 일정 거리만큼 오프셋한 뒤에 또 하나의 Depth 맵을 생성합니다.</p><br>
<p><img src="./pages/images/rim10.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/rim12.png" style="max-width: 30%; height: auto;"></p>
<p>두 Depth 맵을 비교한 후, 지정된 임계값을 초과하면 그 지점을 RimLight로 판단합니다. 이후 Fresnel을 사용하여 보간합니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-10">
<h2 class="section-heading">Outline</h2>
<p>외곽선에 대한 계산입니다. 새로운 Pass에서 계산합니다.</p>
<p><img src="./pages/images/rim02.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/outline01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/outline02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/d829db546ada0ef760478e26c487a9f5.js"></script></p>
<p>일반적으로는 법선 방향으로 정점을 확장하여 외곽선을 표현하지만, 보다 나은 효과를 얻기 위한 3가지 제안사항이 있습니다.</p>
<p>1. 외곽선 두께가 카메라와의 거리와 상관없이 일정하게 유지되도록 합니다.<br>2. 화면의 종횡비에 따라 외곽선이 변하지 않도록 합니다.<br>3. 외곽선이 끊어지는 문제를 수정합니다.</p><br>
<p>외곽선을 계산할 때 가장 큰 문제 중 하나는 하드 엣지에서 외곽선이 끊어지는 현상입니다.</p>
<p><img src="./pages/images/outline03.webp" style="max-width: 60%; height: auto;"></p>
<p>이 문제를 해결하기 위해 런타임에 SmoothNormal을 계산하는 스크립트를 사용할 수 있습니다.<script src="https://gist.github.com/Hanbyori/4878938dbdbcc462e6199db3593483b4.js"></script></p>
<p>하지만 이 솔루션은 Unity를 종료하거나 씬이 리로드될 때마다 다시 계산해야 하는 문제가 있습니다.</p><br>
<p>그래서 저는 <a href="https://assetstore.unity.com/packages/vfx/shaders/toony-colors-pro-2-8105" target='_blank'>Toony Colors Pro 2</a>에 내장된 플러그인을 사용하는 것을 선호합니다.</p>
<p><img src="./pages/images/outline04.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/outline05.png" style="max-width: 30%; height: auto;"></p>
<p>이 플러그인을 사용하면 Mesh의 법선 정보를 스무딩하여 Tangent에 저장할 수 있습니다. 따라서 위 계산 코드에서도 tangentWS를 사용해 계산했습니다.</p><br>
<p>머리카락 끝 부분 외곽선 두께 조절과 같은 세밀한 부분의 계산을 쉽게 하기 위해 Vertex Color를 함께 사용합니다.</p>
<p><img src="./pages/images/outline06.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/outline07.png" style="max-width: 30%; height: auto;"></p><br>
<p><a href="https://ameye.dev/notes/edge-detection-outlines/" target="_blank">후처리를 통해 외곽선을 검출하는 방법</a>도 있지만, 다중 레이어 처리가 필요한 경우 복잡해지기 때문에 제가 선호하는 방식은 아닙니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-11">
<h2 class="section-heading">Cast/Receive Shadow</h2>
<p>그림자를 그리고 받는 계산입니다.<br>Cast Shadow는 단순히 ShadowCaster Pass를 추가하면 됩니다.</p>
<p><img src="./pages/images/receive.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/receive01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/receive02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/263d9777605940994e7a443f4fd95bae.js"></script></p>
<p>이제 MainLight의 Bias와 Distance 등의 영향을 받게 됩니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-12">
<h2 class="section-heading">Additional Light</h2>
<p>추가 조명에 대한 계산입니다.</p>
<p><img src="./pages/images/add01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/add02.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/add03.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/97274ae3babe59a95355f3a5a0ded5db.js"></script></p>
<p>Forward+ 환경에서는 조명이 Cluster에 저장되므로, 특수한 루프인 <code>LIGHT_LOOP</code>를 사용합니다.<br>Forward 환경에서는 pixelLightCount를 기준으로 루프를 돌고, Forward+ 환경에서는 InputData를 기준으로 루프가 실행됩니다.</p><br>
<p>추가 조명을 계산할 때 고려해야 할 사항이 매우 많습니다. 각 조명이 서로 그림자 감쇠값과 그림자 마스크에 영향을 미치는지, 기본 조명 색상이 어떻게 혼합되는지, 그리고 어느 부분까지 함께 혼합되어야 하는지 등 요구사항에 맞게 신중하게 처리해야 합니다.</p>
</section><!--//section-->
<section class="docs-section" id="item-2-13">
<h2 class="section-heading">Fake Shadow</h2>
<p>머리카락 그림자 투영에 대해 가짜로 표현하는 계산입니다.</p>
<p><img src="./pages/images/fake01.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/fake02.png" style="max-width: 30%; height: auto;">
<script src="https://gist.github.com/Hanbyori/18dd9bb2efefff65b15477a5c48ad15c.js"></script>
<script src="https://gist.github.com/Hanbyori/af7ab373e89da279f0f1007393d5cb10.js"></script></p>
<p>원리는 매우 간단합니다. 먼저 머리카락 모양의 단색 버퍼를 만든 후, Stencil 테스트를 통과한 객체에 이 버퍼를 추가로 그려주면 됩니다.<br>이를 구현하기 위해 URP의 기능 중 하나인 RenderFeature를 사용합니다.</p><br>
<p>기존의 라이트로 생성된 그림자를 사용하지 않고, FakeShadow 기법과 같은 트릭을 사용하는 이유가 있습니다.</p>
<p><img src="./pages/images/fake03.png" style="max-width: 30%; height: auto;">
<img src="./pages/images/fake04.png" style="max-width: 30%; height: auto;"></p>
<p>머리카락과 얼굴 사이의 공간이 매우 좁기 때문에, ShadowMap으로 구현하려고 하면 ShadowMap의 정밀도가 비현실적으로 높아야 하는 문제가 발생합니다.</p><br>
<p>이 효과는 단순히 버퍼를 하나 더 그리는 방식이기 때문에, 다양한 각도와 머리카락의 형태 등에 많은 영향을 받을 수밖에 없습니다.<br>그래서 많은 사람들이 ShadowMap의 정밀도 문제를 피하기 위해 다양한 기법을 사용해 캐릭터의 그림자를 최상의 퀄리티로 구현하려고 합니다.</p>
<p><img src="./pages/images/fake05.jpeg" style="max-width: 60%; height: auto;"></p>
<p><a href="https://youtu.be/egHSE0dpWRw?si=S3iVOq2g1HjOZEaS&t=1048" target='_blank'>Unite2018에서 소개된 'Honkai Impact 3rd'</a>는 캐릭터별로 별도의 그림자 맵을 렌더링하고 화면에 구현하는 기능을 발표했습니다.<br>발표자인 'He Jia'는 캐릭터의 바운딩 박스 경계를 기반으로 기술을 구현한다고 설명했습니다.</p>
<p>감사하게도 <a href="https://zhuanlan.zhihu.com/p/501739296" target='_blank'>발표 내용을 바탕으로 기능을 구현한 글</a>이 있습니다. 저는 주로 이 방법에 다른 기법들을 더해 그림자를 표시합니다.</p><br>
<p>또한 Unity는 객체별 Shadow Map을 지원하지 않기 때문에, Decal Projector에서 사용되는 기술을 응용하여 객체별 그림자 맵을 생성하는 <a href="https://github.com/GavinKG/PerObjectShadowSRP" target='_blank'>PerObjectShadow</a> 기법을 구현한 사례도 있습니다.</p>
</section><!--//section-->
</article><!--//docs-article-->
<article class="docs-article" id="section-3">
<header class="docs-header">
<h1 class="docs-heading">마무리</h1>
<section class="docs-intro">
<p>다음은 본문에 설명된 솔루션을 적용하여 얻은 결과입니다.</p>
<p><img src="./pages/images/result01.png" style="max-width: 40%; height: auto;">
<img src="./pages/images/result02.png" style="max-width: 40%; height: auto;"></p>
<p>전체 소스 코드는 <a href="https://github.com/Hanbyori/ToonShader" target='_blank'>Github</a>에 업로드해 두었습니다. 이 코드는 오로지 본문 작성을 위해 새로 작성된 것이며, 정리가 되어 있지 않고 대부분 하드코딩된 상태입니다.</p><br>
<p>사실 제 작업 환경은 본문에서 설명한 것과는 많이 다릅니다. Coloring을 Ramp 방식으로 진행하고, Batch 수를 줄이기 위해 각 요소의 필요한 텍스처를 하나로 묶어 RGBA 채널로 관리하며, 강도 역시 텍스처로 관리합니다.<br>추가 조명 역시 의도적으로 RimLight 부분에만 색상이 영향을 미치도록 설정합니다.</p><br>
<p>저는 전문가가 아니고, 셰이더를 독학으로 작성하는 사람이라 여러분이 훨씬 더 잘 구현하실 수 있을 거라 생각합니다.<br>독자분들께서도 잘 아실 <a href="https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample/tree/master" target='_blank'>NiloToon</a>은 예제 샘플이 공유되어 있으니, 이를 분석해보는 것이 훨씬 더 도움이 될 것입니다. 꼭 한번 살펴보세요 …_〆(゚▽゚*)</p>
</section><!--//docs-intro-->
</header>
</article><!--//docs-article-->
<footer class="footer">
<div class="container text-center py-5">
<!--/* This template is free as long as you keep the footer attribution link. If you'd like to use the template without the attribution link, you can buy the commercial license via our website: themes.3rdwavemedia.com Thank you for your support. :) */-->
<small class="copyright">Designed with <span class="sr-only">love</span><i class="fas fa-heart" style="color: #fb866a;"></i> by <a class="theme-link" href="http://themes.3rdwavemedia.com" target="_blank">Xiaoying Riley</a> for developers</small>
</div>
</footer>
</div>
</div>
</div><!--//docs-wrapper-->
<!-- Javascript -->
<script src="./pages/plugins/popper.min.js"></script>
<script src="./pages/plugins/bootstrap/js/bootstrap.min.js"></script>
<!-- Page Specific JS -->
<script src="./pages/plugins/smoothscroll.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.8/highlight.min.js"></script>
<script src="./pages/js/highlight-custom.js"></script>
<script src="./pages/plugins/simplelightbox/simple-lightbox.min.js"></script>
<script src="./pages/plugins/gumshoe/gumshoe.polyfills.min.js"></script>
<script src="./pages/js/docs.js"></script>
</body>
</html>