【Unity URP】一次对卡通渲染仿动画摄影的探索

Posted by FlowingCrescent on 2021-06-06
Estimated Reading Time 26 Minutes
Words 5.8k In Total
Viewed Times

本文初发布于zhihu
https://zhuanlan.zhihu.com/p/363790714

前言

本篇文章是笔者个人对卡通渲染中运用动画摄影的一次探索与实践,对于文章内容笔者并不能保证其实用性,读者完全可以将它当做笔者的个人笔记看待,但笔者个人希望本篇文章能够成为后人对此方面研究时的垫脚石,为大家引出思考与灵感,若此抛砖引玉的奢望能够实现,便再好不过。

本文目录为:

  1. 对动画摄影的介绍
  2. 模仿摄影的实践
  3. 光感等效果在角色Shader中的具体实现
    1. 废弃方案
    2. 改进方案
    3. 最终方案
  4. 结语

笔者所用Unity版本为2019.4.6f1,URP 7.3.1
笔者经验甚少,才浅学疏,难以避免文中出现错误,还请大家不吝斧正,只求轻喷。

对动画摄影的介绍

笔者第一次发觉“赛璐璐风格卡通渲染可以引入动画摄影的流程来提升画面观感”,是在读蓝色协议的技术分享时
原文链接:
https://game.watch.impress.co.jp/docs/news/1275034.html
中文翻译/解读(by flashyiyi):
https://zhuanlan.zhihu.com/p/229621134
image.png
它正是在模仿日本动画制作中的“摄影”这一环节

或许各位游戏业者对这一动画制作阶段并不熟悉,笔者在此简单介绍一下。
正如电影、电视剧制作需要有影视后期,动画制作也需要一个“动画后期”的环节,而这个环节正是“摄影”。
这一环节的工作内容,最基本的便是将人物层和背景层相合成(因为角色图和背景图是分开画的),
还有便是增加一些“特效”,比如表现物体发光、表现场景的光影、制作粒子特效、修正线条还有镜头运动等等……
而上文中所引用的蓝色协议,正是在模仿摄影环节中的“表现光与影”。
在《After Effects for アニメーション Crimax》一书中,便有对这个“表现光与影”的介绍及实现过程:
image.png
image.png
フレア(flare)表现光
image.png
パラ(para)表现影
我们可以看见,其实就是简单地叠了白色以及黑色的图层上去,但确实非常影响最终画面观感。

如果你还想更加深入了解动画摄影及其相关技术手法,可以参考:
动画摄影后期流程解密:https://zhuanlan.zhihu.com/p/20202161
日本动画中的摄影是一个什么样的岗位?:https://www.zhihu.com/question/24787555/answer/28993271
“动画感”从何而来:https://www.zhihu.com/question/67482841/answer/258068843
动画后期(动画摄影) 业内向:https://www.bilibili.com/video/BV1Rx411L7nx
《AfterEffects for アニメーション》系列书籍:https://tieba.baidu.com/p/3093345184?red_tag=2538494334


模仿摄影的实践

那么笔者也实践模仿了动画摄影,如图
image.png
效果似乎还算可以,想用完全PBR的场景实现很“动画风”的效果,还是有些难度,笔者已经尽力了。


光照及色彩方面的参考图,出自《吹响!悠风号》

场景使用的是:https://assetstore.unity.com/packages/3d/environments/japanese-school-classroom-18392
首先实现了后处理基于深度/法线描边:https://alexanderameye.github.io/outlineshader.html
从OFF到ON,主要添加的效果有那么几个:

  1. 一个主要用于调整场景色彩的Color Adjustments

增大了场景的对比度和饱和度,因为原本PBR的场景的颜色实在跟动画的色彩相差太大了
image.png
image.png

  1. 以一个Threshold为0的Bloom来模拟“柔光”效果

动画摄影中可能会将整张图快速模糊之后,以Lighten混合模式叠加到原图上,来进行柔光效果的制作,这常常也是大家看动画截图会感受到一种“光晕感”或者说“朦胧感”的原因。
image.png
但是实时渲染要进行模糊之后再叠加想想就有点耗,还得再造个轮子……等等,这不是跟Bloom差不多吗,只是把Bloom的那一步“筛选足够亮的区域”去掉了而已,那就姑且这样试试吧。
image.jpg
现在场景已经明亮许多了
image.png
顺便用Tint进行了调色

那么是时候放入人物了,
但是
image.png
image.png
角色无情过曝,那怎么办呢……算了,先继续做着。

  1. 开启景深,用Vignette模拟场景的パラ,后处理叠加一张图作为フレア(flare)

在动画中,如果人物作为前景,那么背景通常是要被模糊处理的

image.png
在画面左边,笔者添加了一个较为微弱的Vignette:
image.png

在右上角,叠加了一个“光晕”图:
image.png
PS做的带Alpha通道的一张图
image.png
以RenderFeature做的后处理

当然,这样还是没解决角色过曝的问题。

  1. 给人物添加光感影パラ以及ハイライト(Highlight)

此处第一次提到“光感”这个词,可以参考下图,
image.png
出自《动画基础知识大百科》
有点像是与光源方向相关的边缘光,不过很难实现完全像AE这样的效果,因为我们很难获得“人物边界”,也很难确定较为准确的“渐变的起点和终点”。

影パラ就像光学核心所演示的那样,也是与光源方向有关,但这次是相乘来模拟影。
image.png
image.png
image.png
原理上与光感非常相似,应当可以一起实现。
ハイライト是说什么呢,其实仍然还是人物边缘光。
image.png
出自《AfterEffects for アニメーション Expert》

可以看见它在角色在朝向光源的头发边界添加了一个高光

image.png
较为微弱的光感

image.png
添加了ハイライト

个人认为添加了这个Highlight之后才跟《动画基础知识大百科》那案例差不多……

image.png
影パラ将角色稍微压暗一些

具体的实现方法由于相对复杂一些,将在下文详细阐述
总之,将光感+ハイライト叠加上去,将影パラ乘上去,就获得我们的最终结果了
image.png
影パラ顺便解决了角色过曝问题……该说是歪打正着吧


光感和影パラ以及ハイライト在角色Shader中的具体实现

其实笔者最初正是想要实现角色光感才去试着模仿动画摄影,这个效果可说是本文的起点也不为过。
实现效果的过程中笔者走了不少现在回头看来可能显得有些滑稽的弯路,均会大致记录在本文中,提醒自己以此为鉴。
如果你对笔者的制杖心路历程不感兴趣,可以直接跳到改进/最终方案的部分。

那么要实现光感,我们需要哪些数据?
其实光感,说白了就是一个有方向性的渐变,而一个渐变,无非就是需要起点与终点,以及渐变方法(线性or非线性)。
渐变方法姑且用线性,但是在3D世界,这个渐变的起点和终点如何确定呢。
显然这会是一个屏幕空间的效果,我们将光照方向映射到屏幕空间,再定一个人物的中心点,以这个中心点为基准进行渐变,但是渐变的长度怎么确定?
如果写定一个屏幕空间渐变长度,那人物时远时近又如何协调,能否找到一个不受角色远近影响的方案?

废弃方案 直接使用Billboard

此时笔者突然想到,在AE中作为效果层的Layer,不是跟我们在游戏中使用的Billboard很像吗,它们都永远朝向观众,而且Billboard也受透视影响,如果将其绑为角色的子物体,那它也会随角色的远近而产生透视变化,岂不美哉?

笔者采用的Billboard算法参考自Colin:
https://github.com/ColinLeung-NiloCat/UnityURP-BillboardLensFlareShader

因此笔者决定给每个角色绑定一个足够在各个角度遮挡人物的Billboard为子物体,在Billboard的shader中以uv和光照方向来进行渐变计算,然后将Billboard通过Render Feature绘制在一个RT中,而Framebuffer中不显示。

image.png
Billboard演示
image.png
一张额外的Render Texture
image.png
角色以屏幕空间uv采样buffer后

这样确实在原理上能够实现效果,但是也有相应的问题:

  1. 计算量虽然不大,但切换RT和采样buffer总是不尽人意
  2. 如果有多个人物在场,那么billboard会产生相互交叠的问题,billboard之间的绘制顺序问题也很难解决

改进方案 在角色Shader中模拟Billboard

虽然直接使用Billboard的方案不行,但是Billboard这一想法本身还是不错的,
后来笔者突然发觉,完全可以在角色的Shader中模拟一个Billboard,创建一个基于屏幕空间的2D坐标系。
Billboard无非就是需要一个中心点,以及它的长宽,那么完全可以在角色Shader中用这几个量去模拟Billboard。
这样可能还是不太易懂,笔者再说得更详细一些。

首先,我们设置了一个空物体在角色的“中心点”,这也是我们之后建立坐标系的中心点,如图:
image.png

通过脚本,将这个“中心点”的世界坐标传入角色Shader中。
然后在代码中如此书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
            //计算中心点的屏幕坐标
float2 centerPosSS = ConvertPosWSToScreenUV(_CenterPosWS.xyz);

//计算中心点的摄像机空间坐标
float4 centerPosVS = mul(UNITY_MATRIX_V, float4(_CenterPosWS.xyz, 1));

//在摄像机空间设置Billboard的上边界与右边界
//_LightRampSize为我们设置的新坐标系的单位长度对应摄像机空间的值
//.xy实际设置均为1,若无法理解,忽略即可
float4 rightPosVS = centerPosVS + float4(_LightRampSize.x, 0, 0, 0);
float4 topPosVS = centerPosVS + float4(0, _LightRampSize.y, 0, 0);

//将两个边界的位置都转化为屏幕空间坐标值,使用函数为自定义函数,非Unity自带
float2 rightPosSS = ConvertPosVSToScreenUV(rightPosVS);
float2 topPosSS = ConvertPosVSToScreenUV(topPosVS);

//获得屏幕空间下的坐标系“单位长度”
float lightRampHeight = (topPosSS.y - centerPosSS.y) * 2;
float lightRampWidth = (rightPosSS.x - centerPosSS.x) * 2;

//正式建立一个坐标系,获取每个像素对应的uv值
float2 newUV_SS = (scrPos - centerPosSS) / float2(lightRampWidth, lightRampHeight);

此时输出float4(newUV_SS, 0, 0),可得到如下结果
image.png
可见,就是以我们设置的“中心点”为原点,建立一个基于屏幕空间的坐标系。
至于为什么说是“基于”而不完全是,因为不仅原点不同,单位长度也不同……简单理解为屏幕空间坐标系也可以。而这个坐标系随距离远近变化,它在人物上的表现也不会发生变化,非常符合我们的需求。

然后参考之前用Billboard写的渐变就行了,写法完全一样。

1
2
3
4
5
6
7
8
9
10
//将原点移到“左下角”
newUV_SS = newUV_SS + float2(0.5, 0.5);
lightDirVS.xy = normalize(lightDirVS.xy);
float2 newUV_SS = (scrPos - centerPosSS)/float2(lightRampWidth, lightRampHeight) + float2(0.5, 0.5);
int adjustX = lightDirVS.x > 0 ? 0 : -1;

int adjustY = lightDirVS.y > 0? 0 : -1;
float lightRamp = dot(lightDirVS.xy, float2(newUV_SS.x + adjustX, newUV_SS.y + adjustY));

return pow(lightRamp, 8) * _LightRampColor;

当主光源随Y轴旋转时,渐变的动态表现如下(场景光照是烘焙所得,所以其他物体不受灯光旋转影响)
image.gif
我们可以发现,当阳光近乎从上往下时,由于lightDirVS.x迅速接近0,导致lightDirVS.y值迅速增大,让渐变在这个时候仿佛在“弹跳”一般,动态来看显得有些诡异。

那干脆不单位化lightDirVS.xy了吧,结果会是这样的:
image.png
渐变的“弹跳”已经缓解很多了,但由于lightDirVS.x归零时,lightDirVS.y又没多大,此时渐变的值就会比较小了……总的来说还能接受,但笔者个人觉得还是有些不适。

最终方案 将渐变的起点轨迹调整为圆弧

既然方块不行,那干脆用圆得了,让渐变的起始点都在圆上,那它的渐变旋转想必是非常平滑的,
那么这次的代码就非常简洁了:

1
2
3
4
float2 newUV_SS = (scrPos - centerPosSS) / float2(lightRampWidth, lightRampHeight);

float2 intersectPoint = normalize(lightDirVS.xy) * _RampCircleSize.x;
float lightRamp = max(0, 1 + dot(normalize(lightDirVS.xy), newUV_SS - IntersectPoint));

image.png
经过简单的调整,这个方案果然让渐变的旋转“自然”了许多。

而影パラ就是用光感的值进行一些调整后获得,就不再多说了。

那么接下来讲讲关于Highlight的部分,
其实Highlight很明显就是边缘光,那先按边缘光的做法做着,
关于边缘光,其实大家也非常熟悉了,个人采用的算法是
pow(saturate(1 - dot(viewDirectionWS, normal)), _RimLightSmoothness)
非常易懂的算法,笔者也尝试过使用次表面散射来模拟,但发现跟普通边缘光的效果相差无几,还是采用了上述简单易懂的边缘光算法。
image.png
接下来结合之前算的渐变值来进行边缘光的范围限制就行了
image.png
效果正如各位之前所看到的那样
这一部分的Shader代码如下:

1
2
3
4
5
6
7
8
9
10
            //对渐变的计算结果进行一些调整
float4 characterLightRamp = saturate(pow(lightRamp, _LightRampSmoothness)) * _LightRampColor * _LightRampIntensity;
//计算边缘光
float rim = pow(saturate(1 - dot(viewDirectionWS, normal)), _RimLightSmoothness);
float4 addRim = rim * _LightRampColor;
characterLightRamp.xyz += addRim * smoothstep(0.1, 0.2, length(characterLightRamp));

//影パラ
float darkRamp = saturate(pow(lightRamp, _DarkRampSmoothness));

一如既往的,奉上角色和灯光分别以不同速度旋转的演示视频
这次为了表现渐变效果“不受角色远近影响”,特意使摄像机会远近移动

最后奉上角色的完整Shader代码(Unity URP7.3.1)
只是在https://zhuanlan.zhihu.com/p/232450616的基础上增加了“Light Ramp”的部分
里面有很大一部分是被注释掉的曾尝试过的方案,还有一些实际没用到的函数,完全删掉它们也不影响使用,笔者保留着它们只是想纪念一下自己的心路历程……
使用前记得传入“中心点”的世界坐标到"_CenterPosWS"变量。

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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
Shader "Custom/CelShadingWithLightRamp"
{
Properties
{
[MainTexture]_BaseMap ("Base Map", 2D) = "white" { }
_BaseColor ("Base Color", Color) = (0, 0.66, 0.73, 1)

[Header(Shading)]
_BrightColor ("BrightColor", Color) = (1, 1, 1, 1)
[HDR]_MiddleColor ("MiddleColor", Color) = (0.8, 0.1, 0.1, 1)
_DarkColor ("DarkColor", Color) = (0.5, 0.5, 0.5, 1)
_CelShadeMidPoint ("CelShadeMidPoint", Range(0, 1)) = 0.5
_CelShadeSmoothness ("CelShadeSmoothness", Range(0, 1)) = 0.1
[Toggle(_IsFace)] _IsFace ("IsFace", Float) = 0.0
_HairShadowDistace ("_HairShadowDistance", Float) = 0.008

[Header(Rim)]
_RimColor ("RimColor", Color) = (1, 1, 1, 1)
_RimSmoothness ("RimSmoothness", Range(0, 10)) = 10
_RimStrength ("RimStrength", Range(0, 1)) = 0.1

[Header(OutLine)]
_OutLineColor ("OutLineColor", Color) = (0, 0, 0, 1)
_OutLineThickness ("OutLineThickness", float) = 0.5
[Toggle(_UseColor)] _UseColor ("UseVertexColor", Float) = 0.0

[Header(heightCorrectMask)]
_HeightCorrectMax ("Height Correct Mask", float) = 1.6
_HeightCorrectMin ("Height Correct Min", float) = 1.51

[Header(LightRamp)]
[Toggle(_UseLightRamp)] _UseLightRamp ("Use Light Ramp", Float) = 0.0
_LightRampSize ("Light Ramp Size", Vector) = (1, 1, 0, 0)
_LightRampColor ("Light Ramp Color", Color) = (1, 1, 1, 1)
_RampCircleSize ("Ramp Circle Size", Vector) = (0.84, 1.3, 0, 0)
_LightRampSmoothness ("Light Ramp Smoothness", Range(0, 20)) = 9.7
_LightRampIntensity ("Light Ramp Intensity", Range(0, 20)) = 7.3
_DarkRampSmoothness ("Dark Ramp Smoothness", Range(0, 10)) = 1.5
_DarkRampIntensity ("Dark Ramp Intensity", Range(0, 1)) = 1
_RimLightSmoothness ("Rim Light Smoothness", Range(0, 10)) = 3
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _SHADOWS_SOFT


CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor, _BrightColor, _DarkColor, _OutLineColor, _MiddleColor, _RimColor;
float _CelShadeMidPoint, _CelShadeSmoothness, _OutLineThickness;
float _RimSmoothness, _RimStrength, _HairShadowDistace, _HeightCorrectMax, _HeightCorrectMin;
float4 _CenterPosWS, _LightRampSize, _LightRampColor, _RampCircleSize;
float _LightRampSmoothness, _LightRampIntensity, _DarkRampSmoothness, _DarkRampIntensity;
float _RimLightSmoothness;
CBUFFER_END
ENDHLSL

Pass
{
Name "BaseCel"
Tags { "LightMode" = "UniversalForward" }

Cull off

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

#pragma multi_compile _UseLightRampOff _UseLightRamp
#pragma multi_compile _NotOnlyLightRamp _OnlyLightRamp
#pragma shader_feature _IsFace


struct a2v
{
float4 positionOS: POSITION;
float2 uv: TEXCOORD0;
float4 normal: NORMAL;
float3 color: COLOR;
};

struct v2f
{
float4 positionCS: SV_POSITION;
float2 uv: TEXCOORD0;
float3 positionWS: TEXCOORD1;
float3 normal: TEXCOORD2;
float4 positionSS: TEXCOORD3;
#if _IsFace
float posNDCw: TEXCOORD4;
float4 positionOS: TEXCOORD5;
#endif
float3 color: TEXCOORD6;
};

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
TEXTURE2D(_HairSoildColor);
SAMPLER(sampler_HairSoildColor);
//TEXTURE2D(_LightRampColor);
//SAMPLER(sampler_LightRampColor);


v2f vert(a2v v)
{
v2f o;

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = positionInputs.positionCS;
o.positionWS = positionInputs.positionWS;
o.positionSS = ComputeScreenPos(positionInputs.positionCS);

#if _IsFace
o.posNDCw = positionInputs.positionNDC.w;
o.positionOS = v.positionOS;
#endif

o.uv = TRANSFORM_TEX(v.uv, _BaseMap);

VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(v.normal);
o.normal = vertexNormalInput.normalWS;


o.color = v.color;
return o;
}

float2 ConvertPosWSToScreenUV(float3 posWS)
{
float4 posCS = mul(UNITY_MATRIX_VP, float4(posWS, 1));//posWS -> posCS
float2 posNDCxy = posCS .xy / posCS .w;//posCS -> posNDC
float2 screenUV = posNDCxy * 0.5 + 0.5; //posNDC -> screen [0,1] uv
screenUV.y = 1 - screenUV.y;
return screenUV;
}

float2 ConvertPosVSToScreenUV(float4 posVS)
{
float4 posCS = mul(UNITY_MATRIX_P, posVS);//posVS -> posCS
float2 posNDCxy = posCS .xy / posCS .w;//posCS -> posNDC
float2 screenUV = posNDCxy * 0.5 + 0.5; //posNDC -> screen [0,1] uv
screenUV.y = 1 - screenUV.y;
return screenUV;
}

float SubsurfaceScattering(float3 viewDir, float3 lightDir, float3 normalDir,
float frontSubsurfaceDistortion, float backSubsurfaceDistortion, float frontSSSIntensity, float thickness)
{
//分别计算正面和反面的次表面散射
float3 frontLitDir = normalDir * frontSubsurfaceDistortion - lightDir;
float3 backLitDir = normalDir * backSubsurfaceDistortion + lightDir;
float frontsss = saturate(dot(viewDir, -frontLitDir));
float backsss = saturate(dot(viewDir, -backLitDir));

float result = saturate(frontsss * frontSSSIntensity + backsss) * thickness;
return result;
}

//Ray must through point(0, 0)
float2 GetRayIntersectWithSqure(float2 squareWidHei, float2 rayDir)
{
float K = rayDir.y / rayDir.x;

float2 verticalLine1Point = float2(-squareWidHei.y / 2, -squareWidHei.y / 2 * K);
float2 horizonalLine1Point = float2(squareWidHei.x / K / 2, squareWidHei.x / 2);
float2 horizonalLine2Point = float2(-squareWidHei.x / K / 2, -squareWidHei.x / 2);

float2 horizonalLinePoint = K > 0 ? horizonalLine2Point: horizonalLine1Point;

float2 leftPoint;
float2 rightPoint;

if (length(verticalLine1Point) < length(horizonalLinePoint))
{
leftPoint = verticalLine1Point;
rightPoint = -verticalLine1Point;
}

else
{
leftPoint = horizonalLinePoint;
rightPoint = -horizonalLinePoint;
}

float2 returnPoint = rayDir.x > 0 ? rightPoint: leftPoint;

return returnPoint;
}

float2 GetRayIntersectWithCircle(float2 radiusAndHeight, float2 rayDir)
{
float K = rayDir.y / rayDir.x;
float r = radiusAndHeight.x / 2;
float rightPointX = sqrt(r * r / (K * K + 1));


float2 rightPoint = float2(rightPointX, K * rightPointX);
float2 leftPoint = -rightPoint;

float2 returnPoint = rayDir.x > 0 ? rightPoint: leftPoint;
returnPoint.y *= radiusAndHeight.y;
return returnPoint;
}

float fit01(float minValue, float maxValue, float value)
{
return saturate((value - minValue) / (maxValue - minValue));
}


half4 frag(v2f i): SV_Target
{
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
float4 shadowCoord = TransformWorldToShadowCoord(i.positionWS.xyz);
Light light = GetMainLight(shadowCoord);
float3 normal = normalize(i.normal);

//receive shadow-------------
Light mainLight;
#if _MAIN_LIGHT_SHADOWS
mainLight = GetMainLight(TransformWorldToShadowCoord(i.positionWS));
#else
mainLight = GetMainLight();
#endif
real shadow = mainLight.shadowAttenuation * mainLight.distanceAttenuation;

//basic cel shading-------------
float CelShadeMidPoint = _CelShadeMidPoint;
float halfLambert = dot(normal, light.direction) * 0.5 + 0.5;
half ramp = smoothstep(0, CelShadeMidPoint, pow(saturate(halfLambert - CelShadeMidPoint), _CelShadeSmoothness));

float2 scrPos = i.positionSS.xy / i.positionSS.w;
//face shadow-------------
#if _IsFace
//we are not use heightCorrect in this sample
float heightCorrect = smoothstep(_HeightCorrectMax, _HeightCorrectMin, i.positionWS.y);
//return heightCorrect;


float4 scaledScreenParams = GetScaledScreenParams();
float3 viewLightDir = normalize(TransformWorldToViewDir(mainLight.direction)) * (1 / i.posNDCw) ;

float2 samplingPoint = scrPos + _HairShadowDistace * viewLightDir.xy * float2(1, scaledScreenParams.x / scaledScreenParams.y);

//if (depth = z/w * 0.5 + 0.5), we will have a float precision problem
//but it's just right when we are far away from character to hide the shadow
float depth = (i.positionCS.z / i.positionCS.w);
float hairDepth = SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).g;

float depthCorrect = depth/* * heightCorrect*/ < hairDepth + 0.001 ? 0: 1;
//return depthCorrect;

//float hairShadow = 1 - SAMPLE_TEXTURE2D(_HairSoildColor, sampler_HairSoildColor, samplingPoint).r;
//0为影,1为非影
float hairShadow = lerp(0, 1, depthCorrect);
ramp *= hairShadow;
#else

//ramp *= shadow;

#endif


half3 diffuse = lerp(_DarkColor.xyz, _BrightColor.xyz, ramp);
diffuse *= baseMap.xyz;

//rim light-------------
half3 viewDirectionWS = SafeNormalize(GetCameraPositionWS() - i.positionWS.xyz);
float rimStrength = pow(saturate(1 - dot(normal, viewDirectionWS)), _RimSmoothness);
float3 rimColor = _RimColor.xyz * rimStrength * _RimStrength;

//Character Light Ramp-------------
//计算中心点的屏幕坐标
float2 centerPosSS = ConvertPosWSToScreenUV(_CenterPosWS.xyz);
//计算中心点的摄像机空间坐标
float4 centerPosVS = mul(UNITY_MATRIX_V, float4(_CenterPosWS.xyz, 1));
//在摄像机空间设置Billboard的上边界与右边界
float4 rightPosVS = centerPosVS + float4(_LightRampSize.x, 0, 0, 0);
float4 topPosVS = centerPosVS + float4(0, _LightRampSize.y, 0, 0);
//将两个边界的位置都转化为屏幕空间坐标值
float2 rightPosSS = ConvertPosVSToScreenUV(rightPosVS);
float2 topPosSS = ConvertPosVSToScreenUV(topPosVS);

//获得屏幕空间下的坐标系“单位长度”
float lightRampHeight = (topPosSS.y - centerPosSS.y) * 2;
float lightRampWidth = (rightPosSS.x - centerPosSS.x) * 2;


float3 lightDirVS = normalize(TransformWorldToViewDir(mainLight.direction));


//----------Method 1----------
// float2 newUV_SS = (scrPos - centerPosSS)/float2(lightRampWidth, lightRampHeight) + float2(0.5, 0.5);
// int adjustX = lightDirVS.x > 0 ? 0 : -1;

// int adjustY = lightDirVS.y > 0? 0 : -1;
// float lightRamp = dot(lightDirVS.xy, float2(newUV_SS.x + adjustX, newUV_SS.y + adjustY));


// return pow(lightRamp, 8) * _LightRampColor;

// float4 characterLightRamp = SAMPLE_TEXTURE2D(_LightRampColor, sampler_LightRampColor, scrPos);

//return darkRamp;

//----------Method 2----------

// float2 newUV_SS = (scrPos - centerPosSS) / float2(lightRampWidth, lightRampHeight);
// float2 IntersectPoint = GetRayIntersectWithSqure(_RampCircleSize.xy, lightDirVS.xy);

// float K = -lightDirVS.x / lightDirVS.y;
// float A = 1;
// float B = -K;
// float C = K * IntersectPoint.x - IntersectPoint.y;
// float lightRamp = 1 - abs(A * newUV_SS.y + B * newUV_SS.x + C) / sqrt(A * A + B * B);
// return lightRamp;

//----------Method 3----------
float2 newUV_SS = (scrPos - centerPosSS) / float2(lightRampWidth, lightRampHeight);


float2 IntersectPoint = normalize(lightDirVS.xy) * _RampCircleSize.x / 2;
IntersectPoint.y *= _RampCircleSize.y;
//float2 IntersectPoint = GetRayIntersectWithCircle(_RampCircleSize.xy);
//点到直线的距离
// float K = -lightDirVS.x / lightDirVS.y;
// float A = 1;
// float B = -K;
// float C = K * IntersectPoint.x - IntersectPoint.y;
// float lightRamp = 1 - abs(A * newUV_SS.y + B * newUV_SS.x + C) / sqrt(A * A + B * B);

float lightRamp = max(0, 1 + dot(normalize(lightDirVS.xy), newUV_SS - IntersectPoint));

//---------------------------------
float4 characterLightRamp = saturate(pow(lightRamp, _LightRampSmoothness)) * _LightRampColor * _LightRampIntensity;

//Highlight
float rim = pow(saturate(1 - dot(viewDirectionWS, normal)), _RimLightSmoothness);
float4 addRim = rim * _LightRampColor;
characterLightRamp.xyz += addRim.xyz * smoothstep(0.1, 0.2, length(characterLightRamp));

//影パラ
float darkRamp = saturate(pow(lightRamp, _DarkRampSmoothness));
darkRamp = lerp(1, darkRamp, _DarkRampIntensity);

#if _OnlyLightRamp
return characterLightRamp;
#endif


#if _UseLightRamp
float4 finCol = float4(diffuse + rimColor + characterLightRamp.xyz, 1);
return finCol * darkRamp;
#else
return float4(diffuse + rimColor, 1);
#endif
}
ENDHLSL

}

Pass
{
Name "OutLine"
Cull Front
ZWrite On

HLSLPROGRAM

#pragma shader_feature _UseColor
#pragma vertex vert
#pragma fragment frag

struct a2v
{
float4 positionOS: POSITION;
float4 normalOS: NORMAL;
float4 tangentOS: TANGENT;
#if _UseColor
float3 color: COLOR;
#endif
};

struct v2f
{
float4 positionCS: SV_POSITION;
};

v2f vert(a2v v)
{
v2f o;

VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(v.normalOS, v.tangentOS);

#if _UseColor
float3 color = v.color * 2 - 1;

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz + float3(color.xy * 0.001 * _OutLineThickness, 0));
o.positionCS = positionInputs.positionCS/* + float4(color.xy * 0.001 * _OutLineThickness * positionInputs.positionCS.w, 0, 0)*/;
#else
float3 normalWS = vertexNormalInput.normalWS;
float3 normalCS = TransformWorldToHClipDir(normalWS);

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = positionInputs.positionCS + float4(normalCS.xy * 0.001 * _OutLineThickness * positionInputs.positionCS.w, 0, 0);
#endif



return o;
}

half4 frag(v2f i): SV_Target
{
float4 col = _OutLineColor;

return col;
}
ENDHLSL

}

//This Pass copy from https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }

//we don't care about color, we just write to depth
ColorMask 0

HLSLPROGRAM

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

#pragma vertex ShadowCasterPassVertex
#pragma fragment ShadowCasterPassFragment

struct Attributes
{
float3 positionOS: POSITION;
half3 normalOS: NORMAL;
half4 tangentOS: TANGENT;
float2 uv: TEXCOORD0;
};

struct Varyings
{
float2 uv: TEXCOORD0;
float4 positionWSAndFogFactor: TEXCOORD2; // xyz: positionWS, w: vertex fog factor
half3 normalWS: TEXCOORD3;

#ifdef _MAIN_LIGHT_SHADOWS
float4 shadowCoord: TEXCOORD6; // compute shadow coord per-vertex for the main light
#endif
float4 positionCS: SV_POSITION;
};

Varyings ShadowCasterPassVertex(Attributes input)
{
Varyings output;

// VertexPositionInputs contains position in multiple spaces (world, view, homogeneous clip space)
// Our compiler will strip all unused references (say you don't use view space).
// Therefore there is more flexibility at no additional cost with this struct.
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS);

// Similar to VertexPositionInputs, VertexNormalInputs will contain normal, tangent and bitangent
// in world space. If not used it will be stripped.
VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);

// Computes fog factor per-vertex.
float fogFactor = ComputeFogFactor(vertexInput.positionCS.z);

// TRANSFORM_TEX is the same as the old shader library.
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);

// packing posWS.xyz & fog into a vector4
output.positionWSAndFogFactor = float4(vertexInput.positionWS, fogFactor);
output.normalWS = vertexNormalInput.normalWS;

#ifdef _MAIN_LIGHT_SHADOWS
// shadow coord for the light is computed in vertex.
// After URP 7.21, URP will always resolve shadows in light space, no more screen space resolve.
// In this case shadowCoord will be the vertex position in light space.
output.shadowCoord = GetShadowCoord(vertexInput);
#endif

// Here comes the flexibility of the input structs.
// We just use the homogeneous clip position from the vertex input
output.positionCS = vertexInput.positionCS;

// ShadowCaster pass needs special process to clipPos, else shadow artifact will appear
//--------------------------------------------------------------------------------------

//see GetShadowPositionHClip() in URP/Shaders/ShadowCasterPass.hlsl
float3 positionWS = vertexInput.positionWS;
float3 normalWS = vertexNormalInput.normalWS;


Light light = GetMainLight();
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, light.direction));

#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
output.positionCS = positionCS;

//--------------------------------------------------------------------------------------

return output;
}

half4 ShadowCasterPassFragment(Varyings input): SV_TARGET
{
return 0;
}

ENDHLSL

}

//This Pass copy from https://cyangamedev.wordpress.com/2020/06/05/urp-shader-code/10/
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "DepthOnly" }

ZWrite On
ColorMask 0

HLSLPROGRAM

// Required to compile gles 2.0 with standard srp library
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x gles
//#pragma target 4.5

// Material Keywords
#pragma shader_feature _ALPHATEST_ON
#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

// GPU Instancing
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON

#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"

// Again, using this means we also need _BaseMap, _BaseColor and _Cutoff shader properties
// Also including them in cbuffer, except _BaseMap as it's a texture.

ENDHLSL

}
}
}

结语

本文只是笔者对实时赛璐璐风卡通渲染模仿动画摄影流程的一次粗浅模仿,正如前言中所说,可能并不具有什么实用价值,但鉴于游戏界对动画摄影的学习资料实在过少,笔者才斗胆分享自己的拙见,希望能让更多的人了解这方面的知识,推动卡通渲染的画面发展。


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。