-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
568 lines (269 loc) · 338 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>远行's Blog</title>
<subtitle>STEP BY STEP</subtitle>
<link href="http://xiaopengcheng.top/atom.xml" rel="self"/>
<link href="http://xiaopengcheng.top/"/>
<updated>2024-11-30T13:41:45.603Z</updated>
<id>http://xiaopengcheng.top/</id>
<author>
<name>远行</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>玻璃效果和窗户室内效果模拟</title>
<link href="http://xiaopengcheng.top/2023/03/18/%E7%8E%BB%E7%92%83%E6%95%88%E6%9E%9C%E5%92%8C%E7%AA%97%E6%88%B7%E5%AE%A4%E5%86%85%E6%95%88%E6%9E%9C%E6%A8%A1%E6%8B%9F/"/>
<id>http://xiaopengcheng.top/2023/03/18/%E7%8E%BB%E7%92%83%E6%95%88%E6%9E%9C%E5%92%8C%E7%AA%97%E6%88%B7%E5%AE%A4%E5%86%85%E6%95%88%E6%9E%9C%E6%A8%A1%E6%8B%9F/</id>
<published>2023-03-18T06:20:22.000Z</published>
<updated>2024-11-30T13:41:45.603Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、玻璃效果"><a href="#一、玻璃效果" class="headerlink" title="一、玻璃效果"></a>一、玻璃效果</h1><p>首先来讲如何模拟玻璃效果。玻璃的渲染包括三部分,普通场景物体的渲染、反射和折射模拟、毛玻璃模拟。作为场景物体,那么类似其它场景物体Shader一样,可以使用PBR、BlingPhong或者Matcap,甚至三阶色卡通渲染都可以。玻璃比较特殊的地方是模拟对环境的反射和折射,以及模拟玻璃污渍效果。<br>对于场景物体的基础着色部分不再赘述,下面来介绍环境反射和折射、玻璃污渍模拟部分。</p><h2 id="1-1-环境反射和折射"><a href="#1-1-环境反射和折射" class="headerlink" title="1.1 环境反射和折射"></a>1.1 环境反射和折射</h2><p>对于不要求实时反映环境变化的效果,那么采样静态贴图进行模拟,是一种性能和效果都更优的方式。从效果上来说,美术可以自由定制贴图,那么可以方便控制效果;从性能上来说,不要求实时blit出当前的colorbuffer,性能远超实时反射和折射。</p><h3 id="1-1-1-静态Cubemap模拟"><a href="#1-1-1-静态Cubemap模拟" class="headerlink" title="1.1.1 静态Cubemap模拟"></a>1.1.1 静态Cubemap模拟</h3><p>最常见的方式是使用Cubemap来模拟环境的反射和折射。</p><h4 id="反射"><a href="#反射" class="headerlink" title="反射"></a>反射</h4><ol><li>计算当前着色像素的反射方向。</li><li>使用反射方向去从Cubemap中采样出反射颜色</li></ol><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">half3 reflectVector = reflect(-inputData.viewDirectionWS, inputData.normalWS);</span><br><span class="line">half3 reflectColor = SAMPLE_TEXTURECUBE_LOD(_EnvironmentCubeMap, sampler_EnvironmentCubeMap, reflectVector, _EnvironmentCubemapLod).rgb * _EnvironmentReflectionColor * _EnvironmentReflectionIntensity;</span><br></pre></td></tr></tbody></table></figure><p>如上述代码,使用内置函数reflect即可计算视线到当前像素的反射方向,然后用该方向去采样Cubemap即可。具体相关数学原理,比较简单,不再赘述。</p><h4 id="折射"><a href="#折射" class="headerlink" title="折射"></a>折射</h4><ol><li>计算当前着色像素的折射方向。</li><li>使用折射方向去从Cubemap中采样出折射颜色。</li></ol><p><strong>如何计算折射方向?</strong></p><ol><li>-viewDirectionWS。最简单的方式是假定折射方向没有发生偏转,那么简单使用相机到该像素点的方向即可,即-inputData.viewDirectionWS。由于,这本来就是一种近似效果,因此简单使用视线方向得到的结果也能差强人意。</li><li>refract。即使用折射定律来计算折射方向,直接调用refract函数即可,需要提供参数来调整折射率。</li><li>Refraction Model。生活中真正的玻璃,光线是先折射进入玻璃,然后再折射出来到空气中,我们需要的是最终的方向,而不是到玻璃内的折射方向。要模拟真实的折射方向,可以使用简化的模拟来模拟,比如假设折射是通过一定厚度的球体或者立方体。相关内容和代码,在HDRP内已经使用,参考文档:<a href="https://docs.unity3d.com/Packages/[email protected]/manual/Refraction-in-HDRP.html">Refraction in the High Definition Render Pipeline</a>的Refraction Model部分。代码在com.unity.render-pipelines.core内,因此urp也可以使用。如果需要使用该折射模型,搜索RefractionModelBox或者RefractionModelSphere即可。</li></ol><h4 id="最终结果"><a href="#最终结果" class="headerlink" title="最终结果"></a>最终结果</h4><p> 使用fresnel定律,将反射和折射颜色叠加起来作为最终的环境颜色。这部分的关键在于正确计算出反射和折射的贡献比例,并不一定需要严格计算fresnel定律,只需要接近该定律的现象即可。<br> fresnel定律的基本意思是:视线方向与法线的角度越大,反射越明显。对于基本只剩下反射的区域,也可以叫做掠角。<br>因此,最终结果可以使用下述代码叠加起来。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">float fresnel = pow(saturate(1 - dot(inputData.viewDirectionWS, inputData.normalWS)), 5.0);</span><br><span class="line">half3 color = reflectColor * fresnel + refractColor * (1 - fresnel);</span><br></pre></td></tr></tbody></table></figure><p>最终效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/GlassEnv.png"></p><h3 id="1-1-2-Matcap模拟"><a href="#1-1-2-Matcap模拟" class="headerlink" title="1.1.2 Matcap模拟"></a>1.1.2 Matcap模拟</h3><p>使用Matcap来模拟的话,思路与Cubemap类似。问题转换成如何从2D的Matcap贴图中计算反射颜色和折射颜色。</p><h4 id="反射-1"><a href="#反射-1" class="headerlink" title="反射"></a>反射</h4><p>反射其实可以理解为高光,那么可以参考Matcap如何实现高光的模拟部分:<a href="https://xiaopengcheng.top/2022/09/19/Matcap%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8/">Matcap模拟高光</a>。</p><h4 id="折射-1"><a href="#折射-1" class="headerlink" title="折射"></a>折射</h4><p>折射更像一个扭曲的过程,因为折射后方向发生了改变。那么,可以直接对uv进行扭曲,比如采样噪声图对uv进行叠加,再去采样一张折射matcap。</p><h4 id="最终结果-1"><a href="#最终结果-1" class="headerlink" title="最终结果"></a>最终结果</h4><p>与使用Cubemap类似,都需要使用计算fresnel定律计算折射和反射的混合比例。</p><h3 id="1-1-3-实时反射和折射模拟"><a href="#1-1-3-实时反射和折射模拟" class="headerlink" title="1.1.3 实时反射和折射模拟"></a>1.1.3 实时反射和折射模拟</h3><p>实时反射和折射,与前面两个算法的区别,是用反射和折射方向去采样当前的渲染结果,作为反射和折射的计算结果。</p><h3 id="获得ColorBuffer"><a href="#获得ColorBuffer" class="headerlink" title="获得ColorBuffer"></a>获得ColorBuffer</h3><p>需要在管线内插入一个Pass,将ColorBuffer进行Blit到一个低分辨率的RT上,然后对该RT进行采样。对于URP渲染管线,我们只要设置请求OpaqueTexture后,既可以在Shader对_CameraOpaqueTexture进行采样。</p><h3 id="反射-2"><a href="#反射-2" class="headerlink" title="反射"></a>反射</h3><p>由于OpaqueTexture是屏幕空间纹理,那么需要在屏幕空间内计算反射方向,可以参考文章<a href="https://xiaopengcheng.top/2021/08/22/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%BB%E7%BB%93/">反射效果的实现总结</a>的屏幕空间反射部分。<br>或者更直接参考GitHub上的开源项目:<a href="https://github.com/ColinLeung-NiloCat/UnityURP-MobileScreenSpacePlanarReflection">UnityURP-MobileScreenSpacePlanarReflection</a>。在屏幕空间计算反射,算法部分比较复杂,不再赘述,请参考相关资料。</p><h3 id="折射-2"><a href="#折射-2" class="headerlink" title="折射"></a>折射</h3><p>由于反射要求方向精确,但是折射就没有这种要求,因此最简单的方式是计算出当前像素的屏幕空间位置后,然后对该位置进行扭曲,再采样OpaqueTexture即可获得折射结果。<br>当然如果要计算精确的折射方向,类似屏幕空间反射,都需要在屏幕空间内使用类似算法进行精确的方向计算, 然后再去采样屏幕空间RT。</p><h4 id="最终结果-2"><a href="#最终结果-2" class="headerlink" title="最终结果"></a>最终结果</h4><p>与使用Cubemap类似,都需要使用计算fresnel定律计算折射和反射的混合比例。</p><h2 id="1-2-玻璃污渍模拟"><a href="#1-2-玻璃污渍模拟" class="headerlink" title="1.2 玻璃污渍模拟"></a>1.2 玻璃污渍模拟</h2><p>该效果是对玻璃角落通常会出现污渍现象的模拟。通过观察,玻璃或者窗户一般是四个角落积累污渍。因此,可以计算与角落或者中心的距离,以这个距离归一化为默认的污渍强度。再结合一个污渍掩码贴图和污渍强度噪声贴图就可以让美术精细控制污渍了,当然不提供任何贴图也有默认的角落污渍。<br>具体功能,参考下图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/GlassFrost.png"></p><h3 id="1-2-1-角落污渍"><a href="#1-2-1-角落污渍" class="headerlink" title="1.2.1 角落污渍"></a>1.2.1 角落污渍</h3><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">float uDis = min(abs(_FrostCenter.x - uv.x), abs(1 - _FrostCenter.x - uv.x));</span><br><span class="line">float vDis = min(abs(_FrostCenter.y - uv.y), abs(1 - _FrostCenter.y - uv.y));</span><br><span class="line">float dis = length(float2(uDis, vDis)) / 0.707;//斜边距离,然后归一化</span><br><span class="line">dis = _FrostReverse * (1 - dis) + (1 - _FrostReverse) * dis;//反转距离</span><br></pre></td></tr></tbody></table></figure><p>如上述代码,_FrostCenter定义的是污渍的中心,这个通常是(0.5,0.5),即UV的中心。然后,计算当前uv到中心的归一化距离,用该距离作为污渍强度。</p><h3 id="1-2-2-污渍强度Noise"><a href="#1-2-2-污渍强度Noise" class="headerlink" title="1.2.2 污渍强度Noise"></a>1.2.2 污渍强度Noise</h3><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">half noiseDistance = SAMPLE_TEXTURE2D(_FrostNoiseMap, sampler_FrostNoiseMap, TRANSFORM_TEX(uv, _FrostNoiseMap)).r * _FrostNoiseIntensity;</span><br><span class="line">dis *= smoothstep(0, _FrostNoiseMax, noiseDistance);</span><br></pre></td></tr></tbody></table></figure><p>从贴图内读取噪声强度,然后对强度进行smoothstep归一化,再乘以到原来的强度距离上。</p><h3 id="1-2-3-污渍Mask"><a href="#1-2-3-污渍Mask" class="headerlink" title="1.2.3 污渍Mask"></a>1.2.3 污渍Mask</h3><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">float4 frostMask = SAMPLE_TEXTURE2D(_FrostMaskMap, sampler_FrostMaskMap, TRANSFORM_TEX(uv, _FrostMaskMap));</span><br><span class="line">dis *= lerp(1, lerp(frostMask.x, 1 - frostMask.x, _FrostMaskReverse), _FrostBlendFactor);//blend with mask</span><br></pre></td></tr></tbody></table></figure><p>从贴图内读取mask,然后将mask乘到原有的强度上。至于_FrostMaskReverse则是强度反转控制,_FrostBlendFactor是mask比例控制。</p><h3 id="1-2-4-将距离转化为污渍颜色"><a href="#1-2-4-将距离转化为污渍颜色" class="headerlink" title="1.2.4 将距离转化为污渍颜色"></a>1.2.4 将距离转化为污渍颜色</h3><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">float weight = smoothstep(0, _FrostDistance / 0.707, dis);</span><br><span class="line">color.rgb += weight * _FrostColor * _FrostIntensity;</span><br></pre></td></tr></tbody></table></figure><p>_FrostDistance/0.707是归一化的最大距离。然后对dis进行smoothstep后就可以得到污渍强度。最后,将<br>污渍强度与污渍颜色、污渍整体强度相乘后叠加到最终颜色上即可。当然,也可以有其它的应用方式,比如用污渍weight来改变法线等。<br>最终效果如图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/GlassFrostDemo.png"></p><h1 id="二、窗户室内模拟"><a href="#二、窗户室内模拟" class="headerlink" title="二、窗户室内模拟"></a>二、窗户室内模拟</h1><p>这是另外一种窗户模拟效果,跟玻璃效果差距较大,但是也可以作为通用Shader的一部分整合进来,因此放在一起讲述。详细的效果和算法可以参考文章:<a href="https://zhuanlan.zhihu.com/p/376762518">案例学习——Interior Mapping 室内映射(假室内效果)</a>。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/GlassInterior.png"><br>该算法的整体思想,是计算当前视线与室内的交点,然后从室内环境Cubemap获取交点的颜色作为最终颜色。计算交点有两种方式,一种是在模型空间计算,一种是在切线空间计算。在模型空间计算,依赖模型空间坐标系的具体范围,更通用的方式是在切线空间计算。</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">input.<span class="property">interiorViewDir</span>.<span class="property">z</span> *= <span class="number">1</span> / (<span class="number">1</span> - _InteriorDepthScale) - <span class="number">1</span>;</span><br><span class="line"> half3 revseseViewDir = <span class="title class_">SafeNormalize</span>(-input.<span class="property">interiorViewDir</span>);</span><br><span class="line"></span><br><span class="line">#<span class="keyword">if</span> _INTERIOR_TANGENT</span><br><span class="line">float2 interiorUV = <span class="title function_">frac</span>(<span class="title function_">TRANSFORM_TEX</span>(uv, _InteriorCubemap) + <span class="number">0.0001</span>);</span><br><span class="line"><span class="comment">// raytrace box from tangent view dir</span></span><br><span class="line">float3 pos = <span class="title function_">float3</span>(interiorUV * <span class="number">2.0</span> - <span class="number">1.0</span>, <span class="number">1.0</span>);</span><br><span class="line">#<span class="keyword">else</span></span><br><span class="line">float3 pos = <span class="title function_">frac</span>(input.<span class="property">positionOS</span> * _InteriorCubemap_ST.<span class="property">xyx</span> + _InteriorCubemap_ST.<span class="property">zwz</span> + <span class="number">0.0001</span>);</span><br><span class="line"> <span class="comment">// raytrace box from object view dir</span></span><br><span class="line"> <span class="comment">// transform object space uvw( min max corner = (0,0,0) & (+1,+1,+1)) </span></span><br><span class="line"> <span class="comment">// to normalized box space(min max corner = (-1,-1,-1) & (+1,+1,+1))</span></span><br><span class="line"> pos = pos * <span class="number">2.0</span> - <span class="number">1.0</span>;</span><br><span class="line">#endif</span><br><span class="line"></span><br><span class="line">float3 id = <span class="number">1.0</span> / revseseViewDir;</span><br><span class="line">float3 k = <span class="title function_">abs</span>(id) - pos * id;</span><br><span class="line">float kMin = <span class="title function_">min</span>(<span class="title function_">min</span>(k.<span class="property">x</span>, k.<span class="property">y</span>), k.<span class="property">z</span>);</span><br><span class="line">pos += kMin * revseseViewDir;</span><br><span class="line"></span><br><span class="line">refractColor += <span class="title function_">SAMPLE_TEXTURECUBE</span>(_InteriorCubemap, sampler_InteriorCubemap, pos.<span class="property">xyz</span>).<span class="property">rgb</span> * _InteriorIntensity;</span><br></pre></td></tr></tbody></table></figure><p>_InteriorDepthScale表示室内的深度,对室内进行远近拉伸,默认是0.5,表示没有拉伸。详细的推导算法请参考上述文章。<br>具体效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/GlassInteriorDemo.png"><br>外表凹凸不平的是玻璃本身的颜色贴图和法线贴图效果,内部是室内模拟效果。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><blockquote><p><a href="https://docs.unity3d.com/Packages/[email protected]/manual/Refraction-in-HDRP.html">Refraction in the High Definition Render Pipeline</a><br><a href="https://xiaopengcheng.top/2022/09/19/Matcap%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8/">Matcap模拟高光</a><br><a href="https://xiaopengcheng.top/2021/08/22/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%BB%E7%BB%93/">反射效果的实现总结</a><br><a href="https://github.com/ColinLeung-NiloCat/UnityURP-MobileScreenSpacePlanarReflection">UnityURP-MobileScreenSpacePlanarReflection</a><br><a href="https://zhuanlan.zhihu.com/p/376762518">案例学习——Interior Mapping 室内映射(假室内效果)</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、玻璃效果"><a href="#一、玻璃效果" class="headerlink" title="一、玻璃效果"></a>一、玻璃效果</h1><p>首先来讲如何模拟玻璃效果。玻璃的渲染包括三部分,普通场景物</summary>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/Rendering/"/>
<category term="玻璃" scheme="http://xiaopengcheng.top/tags/%E7%8E%BB%E7%92%83/"/>
<category term="窗户" scheme="http://xiaopengcheng.top/tags/%E7%AA%97%E6%88%B7/"/>
</entry>
<entry>
<title>多层融合地形Shader</title>
<link href="http://xiaopengcheng.top/2023/01/18/%E5%A4%9A%E5%B1%82%E8%9E%8D%E5%90%88%E5%9C%B0%E5%BD%A2Shader/"/>
<id>http://xiaopengcheng.top/2023/01/18/%E5%A4%9A%E5%B1%82%E8%9E%8D%E5%90%88%E5%9C%B0%E5%BD%A2Shader/</id>
<published>2023-01-18T08:02:00.000Z</published>
<updated>2024-11-17T13:46:51.729Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、多层融合地形Shader介绍"><a href="#一、多层融合地形Shader介绍" class="headerlink" title="一、多层融合地形Shader介绍"></a>一、多层融合地形Shader介绍</h1><p>所谓多层融合地形Shader,即多层地形效果过渡融合的Shader,比如黄色的土地上,融合淡绿色的草地,然后再点缀红色的花,这样就是三层效果融合。虽然,该效果常用在地形上,但是并不局限于地形,普通的场景模型等照样可以使用这样方式融合多层效果,比如静态的雪地,就可以使用该技术达到很精细的效果。<br>对于单独的一层效果,可以是PBR也可以是BlingPhong,甚至可以是Matcap。关键在于,如何将多层效果比较自然得融合起来。<br>类似下面的地形Shader效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/TerrainOptions.png"><br>可以看出该Shader,支持权重混合和高度混合,同时支持四层融合。</p><h1 id="二、多层融合效果的基本实现思路"><a href="#二、多层融合效果的基本实现思路" class="headerlink" title="二、多层融合效果的基本实现思路"></a>二、多层融合效果的基本实现思路</h1><p>概况的说,一句话可以总结:<strong>对着色模型或者说光照模型的输入进行多层混合</strong>。比如,PBR的基本输入是基础色、法线、金属度、粗糙度、AO;BlingPhong的基本输入是基础色、法线、AO。其它的着色模型也是类似的思路,同样可以对Matcap和卡通渲染进行多层融合。</p><h1 id="二、权重融合"><a href="#二、权重融合" class="headerlink" title="二、权重融合"></a>二、权重融合</h1><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/TerrainWeight.png"><br>权重融合的实现方式比较简单,用一个权重贴图控制四层的权重,同时提供数值缩放。这样,第一层的权重就是权重贴图的R通道乘以第一层的缩放(First Layer Weight),以此类推。然后,用得到的四个权重,应用到四层的基本输入上做加权平均,得到最终的输入。</p><h1 id="三、高度融合"><a href="#三、高度融合" class="headerlink" title="三、高度融合"></a>三、高度融合</h1><p>权重融合的缺点是边界过渡比较生硬,无法实现平滑的过渡。而高度融合是解决过渡生硬的一个好的方式。效果和思路可以参考文章:<a href="https://zhuanlan.zhihu.com/p/26383778">基于高度的纹理混合shader</a><br>基本思路:</p><ol><li>求四层最大高度。</li><li>用最大权重减去过渡因子作为开始高度。</li><li>每一层的高度减去开始高度作为该层权重。</li><li>然后进行四层加权混合。<br>具体代码可以参考下面函数:<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">half LayerHeightBlend(half4 height, half input1, half input2, half input3, half input4)</span><br><span class="line">{</span><br><span class="line">half heightStart = max(max(height.r, height.g), max(height.b, height.a)) - _HeightBlendFactor;</span><br><span class="line">half b1 = max(height.r - heightStart, 0);</span><br><span class="line">half b2 = max(height.g - heightStart, 0);</span><br><span class="line">half b3 = max(height.b - heightStart, 0);</span><br><span class="line">half b4 = max(height.a - heightStart, 0);</span><br><span class="line">return max((input1 * b1 + input2 * b2 + input3 * b3 + input4 * b4) / (b1 + b2 + b3 + b4), 0.0001);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>效果对比:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/TerrainWeightDemo.png"><br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/TerrainHeightDemo.png"></li></ol><p>第二个效果与第一个效果对比,有明显的过渡区域,而且过渡区域是有明显融合的,而单纯的权重融合只是效果的加权平均。</p><h1 id="四、融合控制贴图编辑工具"><a href="#四、融合控制贴图编辑工具" class="headerlink" title="四、融合控制贴图编辑工具"></a>四、融合控制贴图编辑工具</h1><p>为了方便美术所见即所得的编辑和预览最终的混合效果,那么实现一个编辑器工具,方便美术用笔刷实时来修改效果,同时在Unity编辑器内预览最终效果是非常有意义的,可以显著提高生产效率。<br>类似如下工具:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/TerrainPaint.png"><br>该工具的功能并不复杂,读取地形材质的设置,比如控制贴图信息和四层的贴图信息,提供笔刷功能,在Scene窗口去刷模型,壁画操作的结果再写回控制贴图。由于当前模型使用的正是笔刷引用的地形材质和控制贴图,因此就可以实时预览最终的渲染效果。<br>网上应该也有类似的插件或者开源代码。这里只是抛砖引玉,具体代码不会提供。</p></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、多层融合地形Shader介绍"><a href="#一、多层融合地形Shader介绍" class="headerlink" title="一、多层融合地形Shader介绍"></a>一、多层融合地形Shade</summary>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/Rendering/"/>
<category term="多层融合" scheme="http://xiaopengcheng.top/tags/%E5%A4%9A%E5%B1%82%E8%9E%8D%E5%90%88/"/>
<category term="地形" scheme="http://xiaopengcheng.top/tags/%E5%9C%B0%E5%BD%A2/"/>
</entry>
<entry>
<title>通用特效Shader</title>
<link href="http://xiaopengcheng.top/2022/11/22/%E9%80%9A%E7%94%A8%E7%89%B9%E6%95%88Shader/"/>
<id>http://xiaopengcheng.top/2022/11/22/%E9%80%9A%E7%94%A8%E7%89%B9%E6%95%88Shader/</id>
<published>2022-11-22T07:10:00.000Z</published>
<updated>2024-11-10T14:33:04.074Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、通用特效Shader介绍"><a href="#一、通用特效Shader介绍" class="headerlink" title="一、通用特效Shader介绍"></a>一、通用特效Shader介绍</h1><h2 id="1-1-什么是通用特效材质"><a href="#1-1-什么是通用特效材质" class="headerlink" title="1.1 什么是通用特效材质"></a>1.1 什么是通用特效材质</h2><p>Unity支持SRP Batcher后,使用UberShader的优势非常明显。所谓,UberShader,即一个超级Shader,覆盖一类功能,而不是多个分散的小Shader,比如一个通用特效Shader,整个项目内的所有特效都使用该Shader来制作所有的粒子特效或者模型特效,其它需求也类似,比如物体、地形等。</p><h2 id="1-2-通用特效材质的意义"><a href="#1-2-通用特效材质的意义" class="headerlink" title="1.2 通用特效材质的意义"></a>1.2 通用特效材质的意义</h2><h3 id="1-2-1-方便使用和沟通"><a href="#1-2-1-方便使用和沟通" class="headerlink" title="1.2.1 方便使用和沟通"></a>1.2.1 方便使用和沟通</h3><p>整个项目只有一个特效Shader的情况下,美术很容易熟悉该Shader有哪些功能,能够尽可能复用该通用Shader的多个功能制作复杂特效;同时,避免美术与技术美术或者技术重复沟通类似的功能。</p><h3 id="1-2-2-方便维护"><a href="#1-2-2-方便维护" class="headerlink" title="1.2.2 方便维护"></a>1.2.2 方便维护</h3><p>对于技术美术或者开发同学来说,维护一个Shader的成本比维护多个的成本低很多。对于重复功能不需要再重复开发,优化性能时候也不用满世界扫描美术到底用的是什么Shader,只需要优化这一个Shader性能即可。</p><h3 id="1-2-3-批次更低"><a href="#1-2-3-批次更低" class="headerlink" title="1.2.3 批次更低"></a>1.2.3 批次更低</h3><p>由于支持SRP Batcher,Unity使用的是最终的变体级别合批。与多个分散小Shader相比,一个Shader质制作的特效更可能被SRP Batcher,因为使用的同一个变体的概率很高,而不同的Shader天然就是不同的变体,不可能被SRP Batcher。因此,只要复用率越高,被合并的批次越多。</p><h2 id="1-2-通用特效材质主要模块"><a href="#1-2-通用特效材质主要模块" class="headerlink" title="1.2 通用特效材质主要模块"></a>1.2 通用特效材质主要模块</h2><p>主要包括基础功能、Mask、Distortion(扭曲)、Dissolve(溶解)、Emission(自发光)、Reflection(反射)、其它等。每个大的模块都有变体开关,部分消耗大的小功能也需要有变体开关。这里只是列出本人项目中用到过的一些功能,基本上覆盖了常见的特效功能,但是不同项目会有不同的需求,因此除了常见的扭曲、溶解等还会有不少定制功能。这篇文章的目的,不在于介绍这些功能的实现细节,而是说明一个超级特效Shader在项目中使用的意义。</p><h1 id="二、通用特效具体模块"><a href="#二、通用特效具体模块" class="headerlink" title="二、通用特效具体模块"></a>二、通用特效具体模块</h1><h2 id="2-1-Surface-Options(基础设置)"><a href="#2-1-Surface-Options(基础设置)" class="headerlink" title="2.1 Surface Options(基础设置)"></a>2.1 Surface Options(基础设置)</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsSurfaceOptions.png"><br>Surface Type:不透明或者半透明<br>Render Face:Front(剔除背面)、Back(剔除前面)、Both(不剔除)<br>Custom ZWrite:是否覆盖深度写入<br>ZTest:深度测试<br>ColorMask:输出颜色通道<br>Alpha Clipping:Alpha裁剪<br>Enable Billboard:是否作为Billboard渲染(朝向相机)</p><h2 id="2-2-Surface-Inputs-(基础输入)"><a href="#2-2-Surface-Inputs-(基础输入)" class="headerlink" title="2.2 Surface Inputs (基础输入)"></a>2.2 Surface Inputs (基础输入)</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsSurfaceInputs.png"><br>Base Map:基础颜色贴图<br>Base Color:基础颜色<br>Enable BackFace Base Color:开启该选项,可以给背面单独制定基础颜色<br>Apply Vertex Color:开启该选项,顶点颜色应用到基础色上<br>BaseMap UvType:该设置有UV、ScreenUV、ReflectionUV三种类型。UV模式是使用模型UV0,ScreenUV是使用屏幕空间位置作为UV,ReflectionUV使用反射向量的两个分量比如xz作为UV。ScreenUV和ReflectionUV可以实现一些特殊效果。<br>BaseMap Rotation:UV模式下,对模型UV0旋转。<br>BaseMap U Speed:UV的U移动速度。<br>BaseMap V Speed:UV的V移动速度。<br>BaseMap Custom Speed (CustomData1.xy, UV0.zw):该功能是使用Unity粒子的CustomData1.xy来作为UV。如下所示的CustomData1.xy是曲线:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/EffectsCustomData.png"><br>Normal Map:法线贴图,只在需要使用法线方向时候有意义,比如反射、Matcap等<br>Normal Scale:法线强度缩放<br>Normal U Speed:法线的U移动速度。<br>Normal V Speed:法线的V移动速度。</p><h2 id="2-3-Mask-Options"><a href="#2-3-Mask-Options" class="headerlink" title="2.3 Mask Options"></a>2.3 Mask Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsMask.png"><br>Mask贴图的作用是修改透明度,在半透明模式下可以只显示通过Mask的区域。</p><p>Mask Map (R):Mask贴图,默认使用R通道。<br>Mask Channel Mask (Default R):Mask贴图的通道掩码。<br>Mask Rotation:Mask贴图的UV旋转。<br>Mask U Speed:Mask贴图的U移动速度。<br>Mask V Speed:Mask贴图的V移动毒素。<br>Mask Custom Speed (CustomData1.zw, UV1.xy):使用Unity粒子的CustomData1.zw来作为Mask的UV。<br>Mask Intensity: Mask的强度缩放。<br>Mask Min:Mask的强度最小值。<br>Mask Max:Mask的强度最大值。最终的Mask强度会在该范围内SmoothStep。<br>下面是使用Mask的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsMaskTest.gif"></p><h2 id="2-4-Distortion-Options"><a href="#2-4-Distortion-Options" class="headerlink" title="2.4 Distortion Options"></a>2.4 Distortion Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsDistortion.png"><br>Distortion的功能就是扭曲其它模块的采样UV,比如基础色、自发光、溶解等。<br>Distortion Map (RG) Mask(A):扭曲贴图,RG通道是扭曲强度,A通道是扭曲Mask。<br>Distortion Rotation:扭曲的UV旋转。<br>Distortion U Speed:扭曲的U移动速度。<br>Distortion V Speed:扭曲的V移动速度。<br>Distortion Intensity:扭曲的强度缩放。<br>下面是使用扭曲的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsDistortionTest.gif"></p><h2 id="2-5-Dissolve-Options"><a href="#2-5-Dissolve-Options" class="headerlink" title="2.5 Dissolve Options"></a>2.5 Dissolve Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsDissolveOptions.png"><br>Dissolve Map (R):溶解贴图,默认使用R通道。<br>Dissolve Channel Mask (Default R):溶解贴图的通道掩码。<br>Dissolve Rotation:溶解贴图的UV旋转。<br>Dissolve U Speed:溶解贴图的U移动速度。<br>Dissolve V Speed:溶解贴图的V移动速度。<br>Dissolve Intensity:溶解强度,注意不是溶解贴图的输入强度缩放。<br>Dissolve Width:溶解宽度。<br>Dissolve Edge Color:溶解边缘颜色。<br>Dissolve Edge Intensity:溶解边缘颜色强度缩放。<br>Dissolve Hard Edge:是否硬边溶解。<br>Dissolve Custom Intensity (CustomData2.x, UV1.z):使用粒子系统的CustomData2.x作为溶解强度。<br>Dissolve Custom Width (CustomData2.y, UV1.w):使用粒子系统的CustomData2.y作为溶解宽度。<br>溶解的功能比较复杂,最基本的思想是用一张贴图作为溶解强度输入,然后通过在溶解强度和宽度之间计算出溶解阈值,用这个阈值去修改透明度;溶解的边缘颜色则是将溶解阈值应用到单独是边缘颜色上再叠加到输出颜色上;至于硬边溶解是直接将溶解阈值取sign。<br>有兴趣还原的可以参考下面代码:<br></p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">half disslove = dot(SAMPLE_TEXTURE2D(_DissolveMap, sampler_DissolveMap, input.uv3.xy + distortion), _DissolveChannelMask);</span><br><span class="line">half dissloveIntensity = input.uv3.z;</span><br><span class="line">half dissolveWidth = input.uv3.w;</span><br><span class="line">half dissolveWithParticle = (dissloveIntensity * (1 + dissolveWidth) - dissolveWidth);</span><br><span class="line">half dissolveAlpha = saturate(smoothstep(dissolveWithParticle, (dissolveWithParticle + dissolveWidth), disslove));</span><br><span class="line">color.a *= _DissolveHardEdge ? sign(dissolveAlpha) : dissolveAlpha;</span><br><span class="line">color.rgb += _DissolveEdgeColor.rgb * _DissolveEdgeColor.a * _DissolveEdgeIntensity * (_DissolveHardEdge ? sign(1 - dissolveAlpha) : (1 - dissolveAlpha));</span><br></pre></td></tr></tbody></table></figure><br>下面是使用溶解的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsDissolveTest.gif"><p></p><h2 id="2-6-Emission-Options"><a href="#2-6-Emission-Options" class="headerlink" title="2.6 Emission Options"></a>2.6 Emission Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsEmissionOptions.png"><br>Emission Map:自发光贴图。<br>EmissionColor:自发光颜色。<br>Use WorldPos As UV:特殊需求,使用世界空间位置作为UV采样自发光贴图。<br>Emission U Speed:自发光的U移动速度。<br>Emission V Speed:自发光的V移动速度。<br>Emission Intensity:自发光的强度缩放。<br>自发光的原理比较简单,采样一张额外的自发光贴图叠加颜色,不再赘述。</p><h2 id="2-7-RimLight-Options"><a href="#2-7-RimLight-Options" class="headerlink" title="2.7 RimLight Options"></a>2.7 RimLight Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsRimLightOptions.png"><br>RimLight Color:边缘光颜色。<br>RimLight Width:边缘光宽度。<br>RimLight Smoothness:边缘光光滑度。<br>RimLight Intensity:边缘光强度缩放。<br>RimLight Min:边缘光最小强度。<br>RimLight Max:边缘光最大强度。<br>RimLight Reverse:是否反转强度。<br>边缘光是实际上是计算法线与视线的夹角来判断边缘,当夹角越大边缘光越强,最终将边缘光叠加回输出颜色上。具体代码如下:<br></p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">half cosTheta = dot(normalWS, viewDirWS);</span><br><span class="line">half rimLightStrength = pow(saturate(1 - 1 / _RimLightWidth * cosTheta), _RimLightSmoothness);</span><br><span class="line">rimLightStrength = (_RimLightReverse ? 1 - rimLightStrength : rimLightStrength) * _RimLightIntensity;</span><br><span class="line">rimLightStrength = smoothstep(_RimLightMinValue, _RimLightMaxValue, rimLightStrength);</span><br><span class="line">color.rgb += _RimLightColor * rimLightStrength;</span><br></pre></td></tr></tbody></table></figure><p></p><h2 id="2-8-Light-Options"><a href="#2-8-Light-Options" class="headerlink" title="2.8 Light Options"></a>2.8 Light Options</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectsLightOptions.png"><br>Real Time Light Strength:实时光强度<br>Real Time Shadow Strength:实时光阴影强度<br>Real Time Shadow Color:实时光阴影颜色<br>Real Time Shadow Color Strength:实时光阴影颜色强度<br>强调一下:这里不是让特效Shader走完整的光照计算,而只是用光源的信息去修改最终的输出颜色。这样性能高效,看起来也在接受光源阴影。</p><h2 id="2-9-其它功能"><a href="#2-9-其它功能" class="headerlink" title="2.9 其它功能"></a>2.9 其它功能</h2><h3 id="2-9-1-Depth-Bias"><a href="#2-9-1-Depth-Bias" class="headerlink" title="2.9.1 Depth Bias"></a>2.9.1 Depth Bias</h3><p>深度偏移,特效有时候需要偏移深度来强制放在某些物体之前,这个功能比较有效果。</p><h3 id="2-9-2-Reflection-Options"><a href="#2-9-2-Reflection-Options" class="headerlink" title="2.9.2 Reflection Options"></a>2.9.2 Reflection Options</h3><p>使用反射方向采样Cubemap,将结果叠加到输出颜色上即可。</p><h3 id="2-9-2-Matcap"><a href="#2-9-2-Matcap" class="headerlink" title="2.9.2 Matcap"></a>2.9.2 Matcap</h3><p>参考上一篇文章的Matcap,对于特效的matcap可以简单实现即可。</p></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、通用特效Shader介绍"><a href="#一、通用特效Shader介绍" class="headerlink" title="一、通用特效Shader介绍"></a>一、通用特效Shader介绍</h1></summary>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/Rendering/"/>
<category term="特效" scheme="http://xiaopengcheng.top/tags/%E7%89%B9%E6%95%88/"/>
</entry>
<entry>
<title>Matcap的原理和应用</title>
<link href="http://xiaopengcheng.top/2022/09/19/Matcap%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8/"/>
<id>http://xiaopengcheng.top/2022/09/19/Matcap%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8/</id>
<published>2022-09-19T09:15:51.000Z</published>
<updated>2024-11-09T14:34:32.170Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、概念和原理"><a href="#一、概念和原理" class="headerlink" title="一、概念和原理"></a>一、概念和原理</h1><h2 id="1-1-什么是Matcap"><a href="#1-1-什么是Matcap" class="headerlink" title="1.1 什么是Matcap"></a>1.1 什么是Matcap</h2><p> 什么是Matcap?Matcap实际上是Material Capture的缩写,即材质捕捉。实际上,这是一种离线渲染方案。类似光照烘焙,将光照或者其它更复杂环境下的渲染数据存储到一张2D贴图上, 再从这张2D贴图进行采样进行实时渲染。</p><p><a href="https://help.sketchfab.com/hc/en-us/articles/115003065883-Materials-MatCap-">Materials (MatCap)</a>这篇文章对Matcap的定义是:<br>MatCap (Material Capture) shaders are complete materials, including lighting and reflections. They work by defining a color for every vertex normal direction relative to the camera. </p><h2 id="1-2-如何理解Matcap"><a href="#1-2-如何理解Matcap" class="headerlink" title="1.2 如何理解Matcap"></a>1.2 如何理解Matcap</h2><p>Matcap是一种在视线空间下使用单位法线采样单位球的离线渲染算法。</p><ul><li>为什么是视线空间?因为视线空间下,相机变化就可以看到不同的渲染结果。</li><li>为什么使用法线去采样了?法线是描述表面朝向的向量,与渲染结果强相关,法线跟物体的曲率强相关等,因此这种算法经常用于 sculpting上。</li></ul><h2 id="1-3-Matcap的特点"><a href="#1-3-Matcap的特点" class="headerlink" title="1.3 Matcap的特点"></a>1.3 Matcap的特点</h2><p>Matcap的特点总结如下:</p><ul><li>使用视线空间下的法线向量采样2D贴图,作为光照和反射结果。</li><li>在缺乏光照烘焙的环境下,可以一定程度上替代或者模拟光图。</li><li>但是,Matcap代表的2D贴图不局限于光照信息,也可以理解为某种环境下的最终渲染结果。</li><li>由于是离线方案,因此计算非常廉价,很适合低端机器或者特定场合下使用。</li></ul><h1 id="二、如何实现Matcap"><a href="#二、如何实现Matcap" class="headerlink" title="二、如何实现Matcap"></a>二、如何实现Matcap</h1><h2 id="2-1-如何获得Matcap贴图"><a href="#2-1-如何获得Matcap贴图" class="headerlink" title="2.1 如何获得Matcap贴图"></a>2.1 如何获得Matcap贴图</h2><p>按照定义,matcap贴图是一张2D贴图,内部包含一个单位球,表示光照信息。如何获得这样的贴图了?</p><ul><li>从网上的材质库下载<br>比如,<a href="https://github.com/nidorx/matcaps">matcaps</a></li><li>引擎预览材质球然后截图。<br><img alt="材质预览matcap" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/材质预览matcap.png"><br>如上图,可以把右边的预览结果紧贴着球体进行截图。<br>当然,如果严格按照定义,Matcap表示的是光照信息,不是所有材质预览的结果都可以当作Matcap贴图。</li></ul><h2 id="2-2-如何采样Matcap贴图"><a href="#2-2-如何采样Matcap贴图" class="headerlink" title="2.2 如何采样Matcap贴图"></a>2.2 如何采样Matcap贴图</h2><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// -------------------------------</span><br><span class="line">// Vertex</span><br><span class="line">// -------------------------------</span><br><span class="line">VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);</span><br><span class="line">output.normalWS = normalInput.normalWS;</span><br><span class="line"></span><br><span class="line">// -------------------------------</span><br><span class="line">// Fragment</span><br><span class="line">// -------------------------------</span><br><span class="line">float3 viewNormal = mul((float3x3)GetWorldToViewMatrix(), normalWS);</span><br><span class="line">float2 matCapUV = viewNormal.xy * 0.5 + 0.5;</span><br><span class="line">half3 matcapColor = SAMPLE_TEXTURE2D(_Matcap, sampler_Matcap, matCapUV).rgb;</span><br></pre></td></tr></tbody></table></figure><p>从上述glsl代码可以看出,需要把法线转换到视线空间,然后再将法线偏移到[0,1]的范围内,然后取xy分量作为uv,对matcap纹理进行采样。</p><h1 id="三、Matcap的问题"><a href="#三、Matcap的问题" class="headerlink" title="三、Matcap的问题"></a>三、Matcap的问题</h1><h2 id="3-1-边缘瑕疵"><a href="#3-1-边缘瑕疵" class="headerlink" title="3.1 边缘瑕疵"></a>3.1 边缘瑕疵</h2><p>有时候使用Matcap渲染,模型上会出现一条线或者缝隙。可能的原因是采样到了贴图的边缘部分,而有些matcap贴图制作上不太好,边缘区域过大。<br><img alt="matcap边缘瑕疵" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap边缘瑕疵.png"><br>如上图所示:左边的matcap贴图就是一个非常不规范的matcap贴图,球没有紧贴边缘,而是出现大量空白部分,导致兔子的边缘出现大量的灰色边缘。<br>解决方式有两种,一种是强制采样内部的像素;另一种方式是修改采样算法,使得更合理避免出现边缘区域。</p><h2 id="3-2-单点采样"><a href="#3-2-单点采样" class="headerlink" title="3.2 单点采样"></a>3.2 单点采样</h2><p>对于平面来说,其法线朝着同一个方向的,因此会出现整个平面获得的matcap颜色都是同一个像素点,与正常的光照结果相差很大。我们希望的是,即使是一个平面,不同的像素点也是有不同的光照结果。</p><h2 id="3-3-解决办法"><a href="#3-3-解决办法" class="headerlink" title="3.3 解决办法"></a>3.3 解决办法</h2><h3 id="3-1-1-缩放uv"><a href="#3-1-1-缩放uv" class="headerlink" title="3.1.1 缩放uv"></a>3.1.1 缩放uv</h3><p>第一种方式是对matcapUV进行缩放,比如缩小uv可以使得避免采样边缘区域。</p><figure class="highlight cpp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">float2 matCapUV = viewNormal.xy * <span class="number">0.5</span> * _MatcapUVScale + <span class="number">0.5</span>;</span><br></pre></td></tr></tbody></table></figure><p>这种方式可以简单的解决边缘瑕疵问题,但是无法解决单点采样。</p><h3 id="3-1-2-使用视线空间下单位球的法线"><a href="#3-1-2-使用视线空间下单位球的法线" class="headerlink" title="3.1.2 使用视线空间下单位球的法线"></a>3.1.2 使用视线空间下单位球的法线</h3><p><img alt="matcap优化" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap优化.png"><br>如上图所示,在Matcap的定义中,我们处于视线空间内,视线方向始终是(0,0,1)。我们最终要使用的是单位球的N方向。假设反射方向是R,可以计算得到N是(Rx,Ry,Rz+1)。那么问题转化为求反射向量R。我们可以用视线空间的顶点和法线求得视线空间下的R,然后用视线空间的R去代替单位球上的反射向量R即可,即使两个方向向量不能等价,也可以得到相应正确的结果。<br>这种算法可以显著优化平面的单点采样问题。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">#if _MATCAP_FIX_EDGE_FLAW</span><br><span class="line">float3 r = reflect(input.positionVS, viewNormal);</span><br><span class="line">r = normalize(r);</span><br><span class="line">float m = 2.82842712474619 * sqrt(r.z + 1.0); </span><br><span class="line">float2 matCapUV = r.xy / m * _MatcapUVScale + 0.5;</span><br><span class="line">#else</span><br><span class="line">float2 matCapUV = viewNormal.xy * 0.5 * _MatcapUVScale + 0.5;</span><br><span class="line">#endif</span><br></pre></td></tr></tbody></table></figure><p><img alt="matcap优化对比" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap优化对比.png"><br>从上图可以看出,对于平面来说,两种算法的效果差异非常明显。</p><h1 id="四、Matcap与其它效果的结合"><a href="#四、Matcap与其它效果的结合" class="headerlink" title="四、Matcap与其它效果的结合"></a>四、Matcap与其它效果的结合</h1><p>下面的测试均以如下Matcap贴图为例。<br><img alt="matcap输入" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap输入.png"></p><h2 id="4-1-基础颜色"><a href="#4-1-基础颜色" class="headerlink" title="4.1 基础颜色"></a>4.1 基础颜色</h2><p>如果把Matcap当作光照的结果,那么可以额外提供基础颜色来控制最终结果。比如,提供基础颜色贴图和基础颜色,乘以到matcap上作为最终输出。<br><img alt="matcap基础色" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap基础色.png"></p><h2 id="4-2-法线贴图"><a href="#4-2-法线贴图" class="headerlink" title="4.2 法线贴图"></a>4.2 法线贴图</h2><p>既然matcap需要用到法线,那么可以额外提供法线贴图去修改像素的法线。<br><img alt="matcap法线" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap法线.png"><br>从上图可以看出,法线对最终的渲染结果影响显著。</p><h2 id="4-3-自发光"><a href="#4-3-自发光" class="headerlink" title="4.3 自发光"></a>4.3 自发光</h2><p>类似正常的光照计算,可以在matcap的结果之上,再叠加自发光。</p><h2 id="4-4-模拟高光"><a href="#4-4-模拟高光" class="headerlink" title="4.4 模拟高光"></a>4.4 模拟高光</h2><p>matcap本身已经是光照计算的结果,因此理论上贴图内带有了漫反射、高光、反射的信息。但是,通常情况下,matcap主要包括的还是漫反射信息,或者说表现不出明显的高光信息。<br>有一种简单模拟高光的方式,提供一个高光阈值,使用matcap减去该颜色阈值,然后除以1-阈值。最终结果再用原matcap颜色相乘避免过曝。<br><img alt="matcap高光" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap高光.png"></p><h2 id="4-5-Cubemap反射"><a href="#4-5-Cubemap反射" class="headerlink" title="4.5 Cubemap反射"></a>4.5 Cubemap反射</h2><p>同时,可以额外利用cubemap计算静态反射结果叠加到最终着色上。<br><img alt="matcap反射" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap反射.png"></p><h2 id="4-6-模拟边缘光"><a href="#4-6-模拟边缘光" class="headerlink" title="4.6 模拟边缘光"></a>4.6 模拟边缘光</h2><p>利用dot(normalWS, viewDirWS)计算出边缘光的强度,再将边缘光颜色与强度相乘叠加到最终着色结果上即可。</p><h2 id="4-7-模拟折射"><a href="#4-7-模拟折射" class="headerlink" title="4.7 模拟折射"></a>4.7 模拟折射</h2><p>折射一种扭曲的效果,因此我们可以通过扭曲matcap的采样位置和反射的采样位置来模拟折射。同时,可以乘以边缘光的强度来模拟菲尼尔效应,也就是边缘光强的地方折射更强。然后,利用这个扭曲强度去偏移matcap的uv和反射向量,即可在一定程度上模拟折射的效果。<br><img alt="matcap折射" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/matcap折射.png"><br>如上图所示,边缘的红色是边缘光;同时,噪声贴图作为折射扭曲强度贴图让边缘光看起来比较细碎,用来模拟折射效果。</p><h2 id="4-8-光照强度"><a href="#4-8-光照强度" class="headerlink" title="4.8 光照强度"></a>4.8 光照强度</h2><p>同时,也可以计算出真实的光照强度,将光照强度乘以matcap颜色,让matcap的着色结果受到灯光影响。不过,这跟matcap的初衷不太一致。</p><h1 id="五、参考资料"><a href="#五、参考资料" class="headerlink" title="五、参考资料"></a>五、参考资料</h1><blockquote><p><a href="[Matcap的原理和应用](xsjapp://doc/b62819d2-d505-4107-8b34-5a176c25bc82#xsj_1702740535139">Materials (MatCap)</a>)<br><a href="https://github.com/nidorx/matcaps">https://github.com/nidorx/matcaps</a><br><a href="https://zhuanlan.zhihu.com/p/79040521">MatCap Shader 改进:解决平面渲染和环境反射问题</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、概念和原理"><a href="#一、概念和原理" class="headerlink" title="一、概念和原理"></a>一、概念和原理</h1><h2 id="1-1-什么是Matcap"><a hr</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="Matcap" scheme="http://xiaopengcheng.top/tags/Matcap/"/>
</entry>
<entry>
<title>角色妆容的实现</title>
<link href="http://xiaopengcheng.top/2022/07/30/%E8%A7%92%E8%89%B2%E5%A6%86%E5%AE%B9%E7%9A%84%E5%AE%9E%E7%8E%B0/"/>
<id>http://xiaopengcheng.top/2022/07/30/%E8%A7%92%E8%89%B2%E5%A6%86%E5%AE%B9%E7%9A%84%E5%AE%9E%E7%8E%B0/</id>
<published>2022-07-30T03:10:00.000Z</published>
<updated>2023-12-16T16:31:14.811Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>前段时间做了下角色妆容的实现,想写个文章记录一下这个事情。妆容看起来很复杂,实际上整理实现思路很简单,主要是两个方面的内容,改变基础色和改变高光(金粉效果)。<br>先贴一个妆容效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/妆容效果1.jpg"></p><p>没有开启妆容的情况下,基础色就是从颜色贴图和基础颜色中获得;开启妆容后,要根据各个状态模块的模板和比例来插值妆容颜色和基本颜色;金粉则是改变特定区域的高光,金粉的浓淡可以用滑块控制,最好同时结合妆容色的浓淡;额外可能需要改变特定妆容区域的光滑度,比如唇彩。</p><h1 id="妆容界面"><a href="#妆容界面" class="headerlink" title="妆容界面"></a>妆容界面</h1><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/妆容0.png"><br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/妆容1.png"></p><h2 id="妆容开关"><a href="#妆容开关" class="headerlink" title="妆容开关"></a>妆容开关</h2><p>最上面有一个Toggle来开关妆容模块,可以使用shader_feature_local的关键字,比如_MAKEUP来区分。</p><h2 id="妆容金粉"><a href="#妆容金粉" class="headerlink" title="妆容金粉"></a>妆容金粉</h2><p>最上面的界面是指定金粉贴图贴图和相应的缩放,使用同样的UV缩放和位移的好处是避免重复读取金粉贴图,提高性能,避免每个妆容模块都要去读一次贴图。毕竟贴图是存储在内存中,要读取到GPU内的话,如果没有Cache中,则速度相比一个计算来说要慢一个数量级的可能。</p><h2 id="妆容模块"><a href="#妆容模块" class="headerlink" title="妆容模块"></a>妆容模块</h2><p>接下来是具体的妆容模块,虽然模块比较多,实际上大同小异。功能都是通过通道贴图去改变指定位置的基础颜色,有些通道贴图还有图案的作用。有一些模块有额外的功能,比如唇彩的光滑度滑块、面纹的UV变化。</p><h1 id="妆容的实现原理"><a href="#妆容的实现原理" class="headerlink" title="妆容的实现原理"></a>妆容的实现原理</h1><p>下面介绍妆容的具体实现原理。</p><h2 id="妆容颜色"><a href="#妆容颜色" class="headerlink" title="妆容颜色"></a>妆容颜色</h2><p>这里的妆容颜色实际上对应的就是界面上具体的妆容模块。以第一个腮红为例子来说明,参考如下代码:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">half3 makeup = baseColor;</span><br><span class="line">half4 blush = SAMPLE_TEXTURE2D(_BlushMap, sampler_BlushMap, uv0);</span><br><span class="line">makeup = lerp(makeup, blush.rgb * _BlushColor, blush.a * _BlusIntensity);</span><br></pre></td></tr></tbody></table></figure><p>从代码可以看到妆容色makeup是基础颜色和妆容颜色的插值结果。妆容颜色是从通道贴图读取的rgb和腮红颜色的结合,同样插值比例是通道贴图的a和腮红比例的结合。不过大部分妆容模块的妆容颜色不需要通道贴图的rgb,这种通道贴图实际上可以做合并处理。</p><h3 id="妆容模块的结合"><a href="#妆容模块的结合" class="headerlink" title="妆容模块的结合"></a>妆容模块的结合</h3><p>妆容模块有一定的叠加顺序,最底部的是基础色,然后是按照顺序叠加的妆容模块,比如界面上的妆容模块顺序。那么,计算的时候,首先也是一个个按照顺序插值过来,比如先插值基础颜色和腮红,然后用插值结果继续和下一个妆容模块做插值,这样得到的最终妆容颜色就是多个妆容模块的结合。</p><h2 id="妆容金粉-1"><a href="#妆容金粉-1" class="headerlink" title="妆容金粉"></a>妆容金粉</h2><p>金粉实际上改变的是高光。没有金粉的话,高光就是默认的情况,比如pbr的金属流高光或者Bling-Phong的高光。有金粉的话,根据金粉计算出一个高光,同时与默认高光进行插值,插值的因子同具体妆容模块的颜色计算。<br>可以参考以下代码实现:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">half3 golddustUV0 = SAMPLE_TEXTURE2D(_GolddustMap, sampler_GolddustMap, _GolddustUVTile * uv0 + _GolddustUVSpeed * _Time.x).rgb;</span><br><span class="line">half4 blush = SAMPLE_TEXTURE2D(_BlushMap, sampler_BlushMap, uv0);</span><br><span class="line">half3 specular = lerp((half3)0, _BlusGolddustColor * golddustUV0 * _BlusGolddustIntensity, blush.a * _BlusIntensity);</span><br></pre></td></tr></tbody></table></figure><p>对于Pbr的金属流,默认的高光是0,所以金粉是为了增加额外的高光。关键的一句是在默认的高光和金粉高光之间做插值,插值比例是金粉浓度和妆容的比例。金粉高光是从金粉贴图读取出来同时应用金粉颜色和强度。</p><h2 id="其它功能"><a href="#其它功能" class="headerlink" title="其它功能"></a>其它功能</h2><p>比如唇彩模块可以改变光滑度,这个改变的前提是唇彩的通道贴图a通道是大于0的;另外还有面纹的一些UV变化,实际上这个是简单模仿贴花的功能。</p><h1 id="性能优化"><a href="#性能优化" class="headerlink" title="性能优化"></a>性能优化</h1><p>下面介绍一些妆容性能相关的优化策略。</p><h2 id="妆容通道贴图采样器的合并"><a href="#妆容通道贴图采样器的合并" class="headerlink" title="妆容通道贴图采样器的合并"></a>妆容通道贴图采样器的合并</h2><p>上述界面的妆容模块过多,如果每个妆容通道贴图一个采样器,肯定会超过限制。方式是所有妆容模块共用一个或者几个采样器。不过,理解上来说,一个贴图采样器对应一个贴图设置,所以去改变贴图的设置会不会有一些影响这个待验证。</p><h2 id="妆容通道贴图合并"><a href="#妆容通道贴图合并" class="headerlink" title="妆容通道贴图合并"></a>妆容通道贴图合并</h2><p>其实根本没必要一个妆容模块一个贴图,完全可以做贴图合并,比如不需要使用rgb的妆容模块,那么一个贴图可以对应四个妆容模块了。实际上,跟美术沟通后发现,妆容的效果主要是依赖妆容的通道掩码和妆容颜色,所以基本上不需要使用妆容贴图的rgb。</p><h2 id="妆容颜色渲染到基础贴图"><a href="#妆容颜色渲染到基础贴图" class="headerlink" title="妆容颜色渲染到基础贴图"></a>妆容颜色渲染到基础贴图</h2><p>这个理论上来说算是终极优化吧。妆容会暴露很多参数给美术或者用户,用户调整这些参数后会得到一个化妆后的效果。关键的地方是,调整完成之后,可以理解为妹子化妆完成后,效果已经固定了。那么实际上,我们不需要每次再去计算妆容颜色,而是可以将妆容颜色渲染到一张单独的贴图上或者直接覆盖原本的BaseMap。以后的渲染,就不需要使用妆容模块了。</p><h3 id="具体实现思路"><a href="#具体实现思路" class="headerlink" title="具体实现思路"></a>具体实现思路</h3><p>可以新建一个Pass,将妆容颜色的计算结果单独走一遍Pass,同时结合原本的BaseMap作为基础颜色,渲染目标是一个RT,比如是BaseMap。这个Pass的开关可以提供接口供业务代码控制,在化妆完成后调用来覆盖原本的BaseMap。</p><h1 id="妆容效果"><a href="#妆容效果" class="headerlink" title="妆容效果"></a>妆容效果</h1><p>最后上一点效果图吧,从美术大佬那边要来的图,凑合看看吧。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/妆容效果0.jpg"><br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/妆容效果3.jpg"></p></body></html>]]></content>
<summary type="html"><html><head></head><body><p>前段时间做了下角色妆容的实现,想写个文章记录一下这个事情。妆容看起来很复杂,实际上整理实现思路很简单,主要是两个方面的内容,改变基础色和改变高光(金粉效果)。<br>先贴一个妆容效果:<br><img alt="" data</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="妆容" scheme="http://xiaopengcheng.top/tags/%E5%A6%86%E5%AE%B9/"/>
</entry>
<entry>
<title>Unity手游性能优化的经验总结</title>
<link href="http://xiaopengcheng.top/2022/05/18/Unity%E6%89%8B%E6%B8%B8%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%9A%84%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<id>http://xiaopengcheng.top/2022/05/18/Unity%E6%89%8B%E6%B8%B8%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%9A%84%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/</id>
<published>2022-05-18T03:10:00.000Z</published>
<updated>2022-05-20T01:53:31.188Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、定位游戏性能瓶颈"><a href="#一、定位游戏性能瓶颈" class="headerlink" title="一、定位游戏性能瓶颈"></a>一、定位游戏性能瓶颈</h1><h2 id="1-1-游戏循环"><a href="#1-1-游戏循环" class="headerlink" title="1.1 游戏循环"></a>1.1 游戏循环</h2><p>基本循环:游戏逻辑-渲染提交-等待渲染完成(注意:游戏逻辑指的是除去渲染之外所有的CPU运算)。<br>基本的游戏循环可以理解为先执行游戏逻辑,比如获得输入,然后更新玩家位置,播放动画,物理碰撞等,然后渲染引擎会将要渲染的游戏画面信息提交到GPU,CPU则等待GPU完成该一帧的渲染结果。</p><h3 id="1-1-1-单线程渲染"><a href="#1-1-1-单线程渲染" class="headerlink" title="1.1.1 单线程渲染"></a>1.1.1 单线程渲染</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/singlethreaded_rendering_0.png"><br>上图可以看到主线程直接提交渲染命令GCMD给渲染设备。主线程需要等待图形设备渲染完成。</p><h3 id="1-1-2-多线程渲染"><a href="#1-1-2-多线程渲染" class="headerlink" title="1.1.2 多线程渲染"></a>1.1.2 多线程渲染</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/multithreaded_rendering_0.png"><br>主线程:游戏逻辑-提交渲染到渲染线程-等待渲染完成。<br>渲染线程:提交渲染命令给GPU-等待渲染完成。<br>GPU:等待渲染命令-执行渲染。</p><p>从上图可以看到,主线程将渲染命令封装后提交给渲染线程,渲染再提交给图形设备渲染画面。因此,渲染线程会比主线程延迟一帧。虽然新增了渲染线程来减少CPU等待渲染完成的时间,但是每一帧主线程还是会等待渲染线程完成上一帧的渲染。因此,还是可以按照基本的游戏循环来分析每一帧的运行过程。</p><h2 id="1-2-利用工具确定执行时间"><a href="#1-2-利用工具确定执行时间" class="headerlink" title="1.2 利用工具确定执行时间"></a>1.2 利用工具确定执行时间</h2><p>我们需要使用工具,比如UWA分析报告或者Unity的Profile等来确定主线程的游戏逻辑、渲染提交、渲染等待,这三个部分的整体执行时间。</p><ol><li>或者利用工具获得GPU每帧耗时是多少,是否超过帧率的要求,比如30帧的话,那么一帧是33.3ms,GPU的每帧耗时就不能就不能超过33.3ms,或者不能比CPU的耗时多,否则就是GPU的性能瓶颈。</li><li>如果GPU不存在瓶颈,那么问题就在CPU上,这个时候要进一步分析CPU的瓶颈是游戏逻辑还是渲染提交上,这个UWA的性能分析报告和Unity的Profile都可以查看。</li><li>如果渲染等待时间过长,说明渲染指令过多,这个时候就需要减少渲染指令的提交,一般就是采样各种合批策略,比如SRP合批、静态合批等,或者是合并网格、减少摄像机距离或者层级Culling等。</li><li>如果渲染等待时间比较小,而游戏逻辑占用时间过长,那么要去分析CPU的耗时,具体是哪一块耗时过多,比如物理、动作、逻辑等。</li></ol><h1 id="二、GPU性能优化"><a href="#二、GPU性能优化" class="headerlink" title="二、GPU性能优化"></a>二、GPU性能优化</h1><p>当GPU遇到性能瓶颈时候就要着重进行GPU的性能优化,下面介绍一些常用的优化策略。</p><h2 id="2-1-降低分辨率"><a href="#2-1-降低分辨率" class="headerlink" title="2.1 降低分辨率"></a>2.1 降低分辨率</h2><p>很多手机的显示器分辨率过高,GPU性能却跟不上。因此,降低分辨率或者对分辨率做限制也是常见的优化手段。比如,最高档设置可以限制1080屏幕高度,依次递减,最低档的设备720就可以。</p><h2 id="2-2-减少OverDraw"><a href="#2-2-减少OverDraw" class="headerlink" title="2.2 减少OverDraw"></a>2.2 减少OverDraw</h2><p>OverDraw的意思是一个像素被重复绘制的次数,也就是该像素位置重复执行像素着色器(片元着色器)的次数。我们可以利用工具,比如固定管线的Unity编辑器是可以显示场景的OverDraw的,Urp的需要做一些扩展支持,来可视化游戏的OverDraw。对OverDraw特别高的部分要想办法优化,下面介绍一些Urp管线下的优化策略。</p><h3 id="2-2-1-自定义渲染Pass"><a href="#2-2-1-自定义渲染Pass" class="headerlink" title="2.2.1 自定义渲染Pass"></a>2.2.1 自定义渲染Pass</h3><p>Urp的渲染Pass是渲染顺序中优先级最高的,因此同一个Pass对应的物体都会在一起按照一定的顺序渲染。那么,为了减少OverDraw或者提高合批的命中,要合理的设计相关的渲染Pass。比如,天空盒是最远的物体,会一直被遮挡,这种就没必要在场景或者角色之前渲染,否则会有没必要的OverDraw。那么,就可以对天空盒单独一个Pass,在场景和角色之后执行。同理,如果角色一直靠近摄像机,也可以强制角色在场景之前的Pass渲染。</p><h3 id="2-2-2-渲染队列"><a href="#2-2-2-渲染队列" class="headerlink" title="2.2.2 渲染队列"></a>2.2.2 渲染队列</h3><p>渲染队列是在Shader中设置的,属于同一个Pass内的渲染顺序微调。同样的,还可以设置Render的sortingOrder等。类似的,在默认管线下,通常会调整天空盒的渲染队列为不透明物体之后来避免OverDraw。</p><h3 id="2-2-3-合理的渲染顺序"><a href="#2-2-3-合理的渲染顺序" class="headerlink" title="2.2.3 合理的渲染顺序"></a>2.2.3 合理的渲染顺序</h3><p>Urp渲染管线对不透明物体会按照从前到后渲染(不支持GPU隐藏面消除),透明物体按照从后到前渲染。Shader的RenderType定义是半透明还是不透明物体。另外前述的渲染Pass和渲染队列等也会影响渲染顺序。合理的渲染顺序能够提高Early-Z的命中率,减少OverDraw。</p><h3 id="2-2-4-减少大范围或者全屏特效"><a href="#2-2-4-减少大范围或者全屏特效" class="headerlink" title="2.2.4 减少大范围或者全屏特效"></a>2.2.4 减少大范围或者全屏特效</h3><p>大范围的特效是OverDraw的杀手,尤其特效反复重叠的情况,项目中最好从设计层面规避这种情况的出现,实在没办法的再想办法优化特效本身的执行效率,比如特效的粒子数或者面片数、Shader复杂度等。</p><h3 id="2-2-5-减少UI的重叠层数"><a href="#2-2-5-减少UI的重叠层数" class="headerlink" title="2.2.5 减少UI的重叠层数"></a>2.2.5 减少UI的重叠层数</h3><p>由于UI是按照半透明物体渲染的,因此要尽可能减少UI的重叠。下面的UI要隐藏而不是被覆盖。不过,这一点通常要在UI框架中做好解决,因为不仅仅是有OverDraw,UI的网格计算等CPU消耗也很大。</p><h2 id="2-3-提高Early-Z-Test的命中率"><a href="#2-3-提高Early-Z-Test的命中率" class="headerlink" title="2.3 提高Early-Z Test的命中率"></a>2.3 提高Early-Z Test的命中率</h2><p>理论上来说,从前到后渲染就是尽可能的提高Early-Z的命中率,从而降低OverDraw。所以,这一项优化通常是和优化OverDraw是一起进行的。</p><h3 id="2-3-1-减少Alpha测试"><a href="#2-3-1-减少Alpha测试" class="headerlink" title="2.3.1 减少Alpha测试"></a>2.3.1 减少Alpha测试</h3><p>但是Alpha-Test,也就是在Shader中丢弃像素,即在片元着色器中执行Discard指令,可能会破坏Early-Z Test,因为硬件不执行片元着色器就无法预测最终的深度。因此,要尽量避免大范围的Early-Z Test,除非特殊的渲染要求,比如溶解、植物、头发等,不要使用Alpha-Test。</p><h2 id="2-4-减少半透明物体"><a href="#2-4-减少半透明物体" class="headerlink" title="2.4 减少半透明物体"></a>2.4 减少半透明物体</h2><p>半透明物体一个是渲染顺序必须从后到前,因此OverDraw严重,性能肯定比不上不透明物体。<a href="https://docs.unrealengine.com/4.27/zh-CN/TestingAndOptimization/PerformanceAndProfiling/Guidelines/">按照虚幻的官方文档</a>,不透明最快,蒙版(Alpha测试)其次,半透明最慢(OverDraw严重)。</p><h2 id="2-5-降低GPU带宽"><a href="#2-5-降低GPU带宽" class="headerlink" title="2.5 降低GPU带宽"></a>2.5 降低GPU带宽</h2><h3 id="2-5-1-压缩纹理"><a href="#2-5-1-压缩纹理" class="headerlink" title="2.5.1 压缩纹理"></a>2.5.1 压缩纹理</h3><ol><li>纹理压缩格式:比如现今基本都支持ASTC纹理压缩,通过合理设置不同资源的压缩率,尽可能压缩纹理大小。</li><li>纹理大小:一些贴图根本不需要过大的尺寸,但是美术导入的原始资源通常过大,因此可以在资源导入脚本中强制压缩到一定的尺寸或者写工具扫描压缩一遍。</li><li>去除不必要的纹理通道:比如Alpha通道或者灰度图改成单通道</li></ol><h3 id="2-5-2-减少纹理采样次数"><a href="#2-5-2-减少纹理采样次数" class="headerlink" title="2.5.2 减少纹理采样次数"></a>2.5.2 减少纹理采样次数</h3><ol><li>尽可能关闭各向异性纹理</li><li>降低纹理质量</li><li>UI或者角色展示场景关闭Mipmap</li></ol><p>其中1和2都可以在工程设置的Quality中选择,可以根据不同的设备等级来选择不同的设置。3的话需要在贴图中设置,可以通过贴图导入脚本来设置。</p><h3 id="2-5-3-提高纹理缓存命中率"><a href="#2-5-3-提高纹理缓存命中率" class="headerlink" title="2.5.3 提高纹理缓存命中率"></a>2.5.3 提高纹理缓存命中率</h3><ol><li>减少贴图尺寸</li><li>开启Mipmap<br>GPU的片内缓冲大小是有限的,因此尽可能小的贴图或者Mipmap才更可能被缓存命中。纹理被缓存命中,那么读取速度会比从内存中读取快一个数量级。</li></ol><h3 id="2-5-4-压缩网格"><a href="#2-5-4-压缩网格" class="headerlink" title="2.5.4 压缩网格"></a>2.5.4 压缩网格</h3><ol><li>尽可能减少网格大小:比如限制面数、顶点数</li><li>开启顶点压缩<br>网格的大小也会影响带宽,但是更多的是会影响渲染面数,从而增大GPU的负担。</li></ol><h3 id="2-5-5-减少全屏Blit"><a href="#2-5-5-减少全屏Blit" class="headerlink" title="2.5.5 减少全屏Blit"></a>2.5.5 减少全屏Blit</h3><p>尽可能减少全屏特效,或者合并全屏特效的计算,减少全屏Blit的次数。</p><h2 id="2-6-Shader优化"><a href="#2-6-Shader优化" class="headerlink" title="2.6 Shader优化"></a>2.6 Shader优化</h2><h3 id="2-6-1-降低Shader中的数值精度"><a href="#2-6-1-降低Shader中的数值精度" class="headerlink" title="2.6.1 降低Shader中的数值精度"></a>2.6.1 降低Shader中的数值精度</h3><p>现在版本的Shader中已经不需要使用fixed类型,实际上Urp的Shader中也会编译失败。尽可能使用half类型,减少float类型的使用,float类型主要是用在postion和uv上,对应颜色等属性值尽量都用half。精度更大的话,计算时间就更多。</p><h3 id="2-6-2-减少Shader中的分支和循环"><a href="#2-6-2-减少Shader中的分支和循环" class="headerlink" title="2.6.2 减少Shader中的分支和循环"></a>2.6.2 减少Shader中的分支和循环</h3><p>尽可能避免分支出现,尽量不要使用循环。分支会破坏Shader的并行,严重影响Shader的执行效率。尽可能用其它方式替换,比如条件操作符、乘法等。</p><h3 id="2-6-3-降低Shader的计算复杂度"><a href="#2-6-3-降低Shader的计算复杂度" class="headerlink" title="2.6.3 降低Shader的计算复杂度"></a>2.6.3 降低Shader的计算复杂度</h3><p>分析一些OverDraw高或者屏幕占比高的Shader,尽可能或者根据项目要求来简化其计算复杂度,或者利用Shader LOD,写几个简化版本的Shader来对应中低端机器。</p><h3 id="2-6-4-减少纹理的读取次数"><a href="#2-6-4-减少纹理的读取次数" class="headerlink" title="2.6.4 减少纹理的读取次数"></a>2.6.4 减少纹理的读取次数</h3><p>在Shader中尽量减少纹理的读取次数,比如控制贴图对应的纹元可以一次性读取到变量中,不要反复读取。</p><h3 id="2-6-5-Shader-LOD"><a href="#2-6-5-Shader-LOD" class="headerlink" title="2.6.5 Shader LOD"></a>2.6.5 Shader LOD</h3><p>Unity的Shader支持LOD,可以针对中低端机器编写简化版本的SubShader,然后针对性的运行时切换到简化Shader运行。</p><h2 id="2-7-尽可能剔除不必要的物体渲染"><a href="#2-7-尽可能剔除不必要的物体渲染" class="headerlink" title="2.7 尽可能剔除不必要的物体渲染"></a>2.7 尽可能剔除不必要的物体渲染</h2><ol><li>合理设置摄像机远平面距离,不渲染远处的物体</li><li>给物体设置不同的Layer,给不同的Layer设置不同的摄像机裁剪距离</li><li>使用Unity自带的遮挡剔除(要烘焙数据,占用额外的包体和内存,可能占用额外的CPU,一般不建议使用)</li><li>自定义的剔除算法,比如检测到物体超过摄像机多远,不渲染或者只渲染部分效果(可以参考项目内的WorldCullingManager,这个是利用Unity自带的CullingGroup来检测角色的距离变化,从而缩放角色的渲染效果)。</li></ol><h2 id="2-8-LOD"><a href="#2-8-LOD" class="headerlink" title="2.8 LOD"></a>2.8 LOD</h2><h3 id="2-8-1-模型LOD"><a href="#2-8-1-模型LOD" class="headerlink" title="2.8.1 模型LOD"></a>2.8.1 模型LOD</h3><p>模型也可以使用Lod,默认情况下是随着摄像机距离变大切换到更简单的Lod。不过,也可以根据任意条件来切换Lod,比如机型匹配,帧率下降等。</p><h3 id="2-8-2-动画LOD"><a href="#2-8-2-动画LOD" class="headerlink" title="2.8.2 动画LOD"></a>2.8.2 动画LOD</h3><p>可以在切换模型LOD时候,选择更简单的动画状态机,这个Unity是支持分层动画和Mask的,具体参考相关文档。</p><h3 id="2-8-3-渲染LOD"><a href="#2-8-3-渲染LOD" class="headerlink" title="2.8.3 渲染LOD"></a>2.8.3 渲染LOD</h3><ol><li>不同的距离开启不一样的渲染效果,类似模型LOD,远处的物体采用更简单的渲染方式。</li><li>不同档次设置采用不同级别的渲染效果,比如切换Shader Lod,关闭一些Pass等。</li><li>不同重要度的物体,比如小怪等,可以关闭一些渲染特性或者效果,或者使用更低的Shader Lod等</li><li>同理,类似的其它一些缩放方式。</li></ol><h2 id="2-9-Fake渲染"><a href="#2-9-Fake渲染" class="headerlink" title="2.9 Fake渲染"></a>2.9 Fake渲染</h2><h3 id="2-9-1-假阴影"><a href="#2-9-1-假阴影" class="headerlink" title="2.9.1 假阴影"></a>2.9.1 假阴影</h3><p>比如角色下面的影子是一个黑色面片,或者使用平面阴影(Planar Shadow)。</p><h3 id="2-9-2-公告板"><a href="#2-9-2-公告板" class="headerlink" title="2.9.2 公告板"></a>2.9.2 公告板</h3><p>比如公告板技术实现的远处面片树木、面片房子等</p><h1 id="三、CPU性能优化"><a href="#三、CPU性能优化" class="headerlink" title="三、CPU性能优化"></a>三、CPU性能优化</h1><p>按照第一节的游戏循环的说法,CPU的性能优化主要包括:游戏逻辑和渲染提交。或者更准确的说,应该把CPU性能优化分为渲染优化和其它的优化;其它的优化,主要指的是游戏逻辑相关的优化,具体包括:物体、动画、粒子、UI、资源加载、游戏逻辑(游戏层脚本)、GC等。下面,我们来一个个的做一些经验介绍。</p><h2 id="3-1-渲染优化"><a href="#3-1-渲染优化" class="headerlink" title="3.1 渲染优化"></a>3.1 渲染优化</h2><p>CPU上的渲染主要包括两个部分,一个是计算需要渲染的物体,另一个是提交渲染和等待渲染完成。</p><h3 id="3-1-1-Culling优化"><a href="#3-1-1-Culling优化" class="headerlink" title="3.1.1 Culling优化"></a>3.1.1 Culling优化</h3><p>Culling也可以叫做裁剪,实质上裁剪有很多种算法或者方式。从现今的游戏引擎来说,一般是使用层次包围盒(Bounding Volume Hierarchy),来粗略的和摄像机的可视范围做交集来进行裁剪。这一部分通常是在游戏引擎内的,因此我们不能控制。但是,SRP渲染管线提供了一些可能性来做优化。</p><ol><li>隔帧Culling:如果游戏场景更新不频繁,那么可以隔帧或者过几帧才计算一次Culling。</li><li>修改Urp的Renderer中的SetupCullingParameters:该函数控制渲染引擎的裁剪参数,可以尝试修改该函数以提高特定项目的裁剪效率。</li><li>在主线程中自定义Culling逻辑:和2.6的第四点意思一样。</li></ol><h3 id="3-1-2-渲染批次优化"><a href="#3-1-2-渲染批次优化" class="headerlink" title="3.1.2 渲染批次优化"></a>3.1.2 渲染批次优化</h3><p>本质上是优化主线程提交给渲染线程的指令数目,从而减少渲染线程提交给图形接口的渲染指令数目。对应到Unity上则是各种合批策略的体现。</p><ol><li>自定义Pass:对于要合批的物体,尤其是动态物体,比如特效,最好是定义一个专门的Pass,就不会被其它不相干物体打断动态合批(对于SRP Batcher没发现这种限制)。</li><li>SRP Batcher:尽可能所有的Shader兼容SRP Batcher,除非特别低端的机器,这种合批方式对性能的提高很大。</li><li>静态合批:静态合批通常用于场景(SRP Batcher实际上也兼容静态合批),不过静态合批需要打开网格读写,静态合批只合并材质一样的物体,因此静态合批需要美术那边尽量提高材质的重用度后使用的意义才大;同时因为静态合批会合并网格(比如,同样的网格和材质出现多次,会将网格复制多次合并成一个更大的网格)可能会导致包体和内存显著增长;因此,静态合批最适合材质重复度高,网格重复很少的场景。</li><li>动态合批:静态合批的运行时版本,因为有运行时的CPU消耗,类似静态合批也可能会增大内存消耗;有限制,比如要求顶点属性之和不超过900等;适合于动态的小物体的合批,比如粒子特效、小道具等。</li><li>Draw Instancing:也叫实例化渲染,适合的场景是网格重复多次(只是朝向、缩放等不一样),材质一样(或者材质属性基本一致的)的情况,比如大规模渲染树木和草地。</li><li>手动合并网格和材质:用软件来离线合并场景内的模型和材质;理论上来说,最自由但是最繁琐,如果场景小的话可以这样试试;合理控制的话,不会显著增大内存和包体,也没有运行时消耗,美术乐意的话,何乐而不为?</li></ol><h2 id="3-2-物理优化"><a href="#3-2-物理优化" class="headerlink" title="3.2 物理优化"></a>3.2 物理优化</h2><h3 id="3-2-1-降低Unity的物理更新频率"><a href="#3-2-1-降低Unity的物理更新频率" class="headerlink" title="3.2.1 降低Unity的物理更新频率"></a>3.2.1 降低Unity的物理更新频率</h3><p>可以在工程设置的Physics选项中关闭Auto Simulation,然后选择在框架更新的时候降低频率(比如2倍的Fixed Timestep)来调用Physics.Simulate来更新物理。同样可以直接设置Fixed Timestep来降低更新频率。</p><h3 id="3-2-2-少用或者不用MeshCollider"><a href="#3-2-2-少用或者不用MeshCollider" class="headerlink" title="3.2.2 少用或者不用MeshCollider"></a>3.2.2 少用或者不用MeshCollider</h3><h3 id="3-2-3-减少频繁射线检测的使用"><a href="#3-2-3-减少频繁射线检测的使用" class="headerlink" title="3.2.3 减少频繁射线检测的使用"></a>3.2.3 减少频繁射线检测的使用</h3><p>可以缓存计算结果或者用更快速的检测方式替代,比如boxcast。</p><h3 id="3-2-4-关闭碰撞矩阵中没必要的部分"><a href="#3-2-4-关闭碰撞矩阵中没必要的部分" class="headerlink" title="3.2.4 关闭碰撞矩阵中没必要的部分"></a>3.2.4 关闭碰撞矩阵中没必要的部分</h3><h2 id="3-3-动画优化"><a href="#3-3-动画优化" class="headerlink" title="3.3 动画优化"></a>3.3 动画优化</h2><h3 id="3-3-1-限制骨骼数目"><a href="#3-3-1-限制骨骼数目" class="headerlink" title="3.3.1 限制骨骼数目"></a>3.3.1 限制骨骼数目</h3><p>要求美术制作时候在规定的骨骼数目范围内,骨骼数目会影响动画大小也会影响执行效率。通常80-100已经非常足够了。</p><h3 id="3-3-3-动画的CPU性能优化"><a href="#3-3-3-动画的CPU性能优化" class="headerlink" title="3.3.3 动画的CPU性能优化"></a>3.3.3 动画的CPU性能优化</h3><p>参考UWA的文章:<a href="https://zhuanlan.zhihu.com/p/382656748">Unity性能优化 — 动画模块</a><br>参考Unity文档:<a href="https://docs.unity3d.com/cn/current/Manual/MecanimPeformanceandOptimization.html">性能和优化</a></p><h2 id="3-4-粒子优化"><a href="#3-4-粒子优化" class="headerlink" title="3.4 粒子优化"></a>3.4 粒子优化</h2><p>可以参考UWA的文章:<a href="https://blog.uwa4d.com/archives/UWA_ReportModule3.html">粒子系统优化——如何优化你的技能特效</a><br>特效的优化一般在项目的中后期,快上线的时候,针对性的对战斗这种特效集中度很高的场景进行测试和优化。</p><h3 id="3-4-1-限制特效的最大粒子数"><a href="#3-4-1-限制特效的最大粒子数" class="headerlink" title="3.4.1 限制特效的最大粒子数"></a>3.4.1 限制特效的最大粒子数</h3><p>通常会限制普通特效只能有5-10个粒子或者更小。</p><h3 id="3-4-2-限制特效的批次"><a href="#3-4-2-限制特效的批次" class="headerlink" title="3.4.2 限制特效的批次"></a>3.4.2 限制特效的批次</h3><p>最好是一个特效能在几个批次或者1个批次内渲染完成。</p><h3 id="3-4-3-限制特效使用的贴图尺寸"><a href="#3-4-3-限制特效使用的贴图尺寸" class="headerlink" title="3.4.3 限制特效使用的贴图尺寸"></a>3.4.3 限制特效使用的贴图尺寸</h3><p>特效尽量使用小贴图,比如不超256或者512的,尽量都是128或者更细的贴图或者贴图合集。</p><h3 id="3-4-3-限制特效的重叠层数和范围"><a href="#3-4-3-限制特效的重叠层数和范围" class="headerlink" title="3.4.3 限制特效的重叠层数和范围"></a>3.4.3 限制特效的重叠层数和范围</h3><p>这一部分应该算GPU的优化,可以减少OverDraw和GPU的计算。</p><h2 id="3-5-UI优化"><a href="#3-5-UI优化" class="headerlink" title="3.5 UI优化"></a>3.5 UI优化</h2><p>参考UWA的文章:<a href="https://blog.uwa4d.com/archives/UWA_ReportModule8.html">Unity性能优化 — UI模块</a><br>优化UI的基本原则是:</p><ol><li>减少UI变化重新生成网格:因为UI本质上也是网格加贴图绘制出来的,因此要避免各种操作或者设置到UI频繁变化导致网格重复生成。</li><li>减少UI的射线检测</li><li>UI的图集控制:比如一个界面最多2个图集等</li><li>战斗这种3D场景可以不使用UGUI来绘制3D的UI,而是直接用3D网格来绘制,避免UGUI的各种性能消耗。</li></ol><h2 id="3-6-资源加载优化"><a href="#3-6-资源加载优化" class="headerlink" title="3.6 资源加载优化"></a>3.6 资源加载优化</h2><p>参考UWA的文章:<a href="https://blog.uwa4d.com/archives/UWA_ReportModule2.html">Unity性能优化系列—加载与资源管理</a></p><h3 id="3-6-1-Shader变体的预热"><a href="#3-6-1-Shader变体的预热" class="headerlink" title="3.6.1 Shader变体的预热"></a>3.6.1 Shader变体的预热</h3><p>Shader变体的预热比较耗时,可能需要拆分处理。Shader需要优化关键字数目,尤其是全局关键字数目,这个会显著影响Shader的包体和加载进来的内存。</p><h3 id="3-6-2-游戏对象池"><a href="#3-6-2-游戏对象池" class="headerlink" title="3.6.2 游戏对象池"></a>3.6.2 游戏对象池</h3><p>尽量使用对象池,对象池回收时候可以隐藏GO,也可以选择移动到远处(关闭组件)。</p><h3 id="3-6-3-资源管理方案推荐YooAsset"><a href="#3-6-3-资源管理方案推荐YooAsset" class="headerlink" title="3.6.3 资源管理方案推荐YooAsset"></a>3.6.3 资源管理方案推荐YooAsset</h3><h2 id="3-7-游戏脚本优化"><a href="#3-7-游戏脚本优化" class="headerlink" title="3.7 游戏脚本优化"></a>3.7 游戏脚本优化</h2><p>这里主要讲的是游戏框架和游戏逻辑的代码优化,包括C#和Lua。</p><h3 id="3-7-1-游戏框架优化"><a href="#3-7-1-游戏框架优化" class="headerlink" title="3.7.1 游戏框架优化"></a>3.7.1 游戏框架优化</h3><p>框架应该尽可能优化,尽可能减少对使用者(游戏逻辑层)带来的性能损耗。</p><ol><li>框架代码尽量不要有GC</li><li>框架代码尽量不要占用额外的大内存</li><li>框架代码尽量不要消耗过多的CPU,使用者不用担心性能消耗<h3 id="3-7-2-游戏逻辑优化"><a href="#3-7-2-游戏逻辑优化" class="headerlink" title="3.7.2 游戏逻辑优化"></a>3.7.2 游戏逻辑优化</h3>具体的游戏逻辑优化,跟实际的游戏类型有关,需要针对性优化。</li></ol><h3 id="3-7-3-常见的脚本优化策略"><a href="#3-7-3-常见的脚本优化策略" class="headerlink" title="3.7.3 常见的脚本优化策略"></a>3.7.3 常见的脚本优化策略</h3><ol><li>缓存计算结果:中间结果或者初始参数尽量预计算好</li><li>不产生GC:任何引用类型的对象都尽量使用缓存池内的</li><li>对缓存友好的存储方式:比如尽量使用小数组存储数据,而不用链表或者字典</li><li>限帧法:限制部分逻辑的更新频率,比如2-3帧更新一次</li><li>多线程:部分独立性很强的逻辑,可以考虑多线程处理</li><li>主次法:比如非关键的角色或者物体,使用更少的计算逻辑</li><li>减少项目的MonoBehavior的更新函数入口,游戏逻辑尽量保持一个更新入口。</li></ol><h3 id="3-7-4-Lua代码优化"><a href="#3-7-4-Lua代码优化" class="headerlink" title="3.7.4 Lua代码优化"></a>3.7.4 Lua代码优化</h3><p>参考UWA的文章:<a href="https://blog.uwa4d.com/archives/UWA_ReportModule4.html">Unity性能优化系列—Lua代码优化</a></p><h2 id="3-8-GC优化"><a href="#3-8-GC优化" class="headerlink" title="3.8 GC优化"></a>3.8 GC优化</h2><p>由于Unity的Mono堆在超过最大值或者一定数值后会自动扩容,而且扩容后无法往回缩,因此必须非常关注Mono堆的峰值。</p><ol><li>降低Mono堆的峰值:这样可以避免Mono堆一直增长或者过大</li><li>降低GC的频率:减少不必要的CPU消耗,尽量使用缓存池中的引用对象</li></ol><h1 id="四、资源优化"><a href="#四、资源优化" class="headerlink" title="四、资源优化"></a>四、资源优化</h1><h2 id="4-1-纹理优化"><a href="#4-1-纹理优化" class="headerlink" title="4.1 纹理优化"></a>4.1 纹理优化</h2><p>这个在优化GPU代码有提到。</p><ol><li>合理降低纹理大小</li><li>尽量使用更高的压缩格式(ASTC更高压缩率)</li><li>UI或立绘关闭Mipmap</li><li>减少纹理通道</li><li>提高纹理复用(单色图复用,重复图案复用)</li></ol><h2 id="4-2-网格优化"><a href="#4-2-网格优化" class="headerlink" title="4.2 网格优化"></a>4.2 网格优化</h2><ol><li>关闭网格读写:除了特效外的网格关闭读写</li><li>开启项目的顶点压缩:会降低内存和GPU消耗,应该不会影响资源大小</li><li>MeshCompression:开启据说会降低网格的资源占用,但是不影响内存占用</li><li>尽量减少面数和顶点数:和美术制作规范,正式资源要符合要求</li></ol><h2 id="4-3-动画优化"><a href="#4-3-动画优化" class="headerlink" title="4.3 动画优化"></a>4.3 动画优化</h2><ol><li>压缩方式选择Optimal:官方推荐的方式</li><li>如果没有缩放,去除Scale曲线。</li><li>网上的一些剔除动画原始数据的方法以实测为准,可能剔除后动画文件变小,包体变小了,但是内存中大小不变。</li></ol><h1 id="五、内存优化"><a href="#五、内存优化" class="headerlink" title="五、内存优化"></a>五、内存优化</h1><h2 id="5-1-优化资源"><a href="#5-1-优化资源" class="headerlink" title="5.1 优化资源"></a>5.1 优化资源</h2><p>资源本身都会加载进入内存,因此优化资源本身大小对优化内存大小非常关键,第四节已经讲到。</p><h2 id="5-2-优化打包和资源管理"><a href="#5-2-优化打包和资源管理" class="headerlink" title="5.2 优化打包和资源管理"></a>5.2 优化打包和资源管理</h2><p>减少打AB包时候的重复,以及智能的资源加载管理方案,可以减少AB包加载后的内存占用,以及去除没必要的资源常驻现象,同时也可以优化资源加载的CPU消耗。</p><h2 id="5-3-优化Mono堆"><a href="#5-3-优化Mono堆" class="headerlink" title="5.3 优化Mono堆"></a>5.3 优化Mono堆</h2><p>前述已经提到Mono堆只增不涨,因此优化C#的Mono堆内存非常必要。</p><h2 id="5-4-优化Native内存"><a href="#5-4-优化Native内存" class="headerlink" title="5.4 优化Native内存"></a>5.4 优化Native内存</h2><p>一些插件包括引擎都会占用Native的内存,因此合理使用插件或者检测插件占用的Native内存在某些时候也有意义。如果插件造成的Native内存占用过多,是否可以考虑更换插件?比如音频插件等。</p><h2 id="5-5-优化Lua内存"><a href="#5-5-优化Lua内存" class="headerlink" title="5.5 优化Lua内存"></a>5.5 优化Lua内存</h2><p>Lua同样有虚拟机有自己管理的堆内存,同样是不能无限增长的。因此,Lua代码也要避免频繁创建新的对象造成GC严重或者导致堆内存一直上升。<br>UWA和UPR都有检测Lua内存的选项,可以试试。</p><h2 id="5-6-减少包体二进制大小"><a href="#5-6-减少包体二进制大小" class="headerlink" title="5.6 减少包体二进制大小"></a>5.6 减少包体二进制大小</h2><p>应用都会加载到内存中才会运行,因此更小的二进制包体自然会占用更小的内存。可以尝试剔除一些没有使用的代码(引擎代码或者C#脚本代码),这个Unity打包时候有相关设置。</p><h2 id="5-7-配置优化"><a href="#5-7-配置优化" class="headerlink" title="5.7 配置优化"></a>5.7 配置优化</h2><p>游戏项目到中后期配置可能会增大非常严重,如果一次性加载可能会造成加载时间过长,同时造成Mono内存增长过大。</p><ol><li>避免一次性加载全部配置到C#中</li><li>如果内存占用过高,考虑其它压缩存储方式,比如二进制存储,不要使用Json</li><li>Lua加载配置速度更快,但是配置过大同样内存占用高</li><li>实在占用过高,可以考虑小型数据库存储配置</li><li>是不是该让策划清理或者使用工具清理重复配置,配置本身是否严重冗余?</li></ol><h1 id="六、二进制包体优化"><a href="#六、二进制包体优化" class="headerlink" title="六、二进制包体优化"></a>六、二进制包体优化</h1><h2 id="6-1-代码裁剪"><a href="#6-1-代码裁剪" class="headerlink" title="6.1 代码裁剪"></a>6.1 代码裁剪</h2><p>Unity可以设置裁剪引擎代码,和脚本层代码。经过测试开启引擎代码裁剪问题不大,但是脚本层代码裁剪设置过高可能引起代码丢失问题,可能可以通过link.xml中的设置解决。</p><h2 id="6-2-安卓架构"><a href="#6-2-安卓架构" class="headerlink" title="6.2 安卓架构"></a>6.2 安卓架构</h2><p>通常ARMv7和ARM64只需要打一个架构,当前ARM64的性能更好但是兼容性不够,关闭一个架构能减少包体。</p><h2 id="6-3-其它跟App打包相关的方法"><a href="#6-3-其它跟App打包相关的方法" class="headerlink" title="6.3 其它跟App打包相关的方法"></a>6.3 其它跟App打包相关的方法</h2><p>具体请查阅相关文档,参考并且实验是否有效。</p><h1 id="七、其它"><a href="#七、其它" class="headerlink" title="七、其它"></a>七、其它</h1><p>性能优化是一个迭代的长期工作,关键是底子打好,后期优化压力就小很多;或者明白优化的思路,能够快速定位关键的性能瓶颈。</p></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、定位游戏性能瓶颈"><a href="#一、定位游戏性能瓶颈" class="headerlink" title="一、定位游戏性能瓶颈"></a>一、定位游戏性能瓶颈</h1><h2 id="1-1-游戏循环</summary>
<category term="游戏开发" scheme="http://xiaopengcheng.top/categories/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
<category term="Unity" scheme="http://xiaopengcheng.top/categories/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/Unity/"/>
<category term="性能优化" scheme="http://xiaopengcheng.top/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>Urp渲染管线下的卡通冰效果实现</title>
<link href="http://xiaopengcheng.top/2022/05/04/Urp%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E4%B8%8B%E7%9A%84%E5%8D%A1%E9%80%9A%E5%86%B0%E6%95%88%E6%9E%9C%E5%AE%9E%E7%8E%B0/"/>
<id>http://xiaopengcheng.top/2022/05/04/Urp%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E4%B8%8B%E7%9A%84%E5%8D%A1%E9%80%9A%E5%86%B0%E6%95%88%E6%9E%9C%E5%AE%9E%E7%8E%B0/</id>
<published>2022-05-04T09:10:00.000Z</published>
<updated>2023-12-16T16:31:09.454Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、卡通冰的效果"><a href="#一、卡通冰的效果" class="headerlink" title="一、卡通冰的效果"></a>一、卡通冰的效果</h1><p>先看最终实现的卡通冰材质效果吧,如下所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰.png"><br>也可以调出类似玻璃的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰-玻璃.png"><br>如果对一个球应用卡通冰材质,然后打开各种选项,可以得到如下效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰-球.png"><br>凹凸不平的地方是因为应用了法线贴图。</p><h1 id="二、脚本和最终的材质界面"><a href="#二、脚本和最终的材质界面" class="headerlink" title="二、脚本和最终的材质界面"></a>二、脚本和最终的材质界面</h1><p>最终的材质界面,如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰材质.png"><br>通过材质界面可以清晰看出卡通冰效果的各个模块。<br>另外为了获得屏幕颜色需要挂上一个脚本(PostProcessEffect)表示当前管线需要执行CopyColorPass。</p><h1 id="三、折射"><a href="#三、折射" class="headerlink" title="三、折射"></a>三、折射</h1><p>冰效果最关键的部分是折射,注意是折射而不是半透明。折射是透光冰看过去,后面的背景会发生一定的扭曲;而半透明混合是冰本身的颜色和背景做一定的混合,无法背景实现扭曲的效果。这里的实现思路参考之前的屏幕扭曲特效的实现方式,具体可以参考文章:<a href="https://xiaopengcheng.top/2021/10/22/Urp%E4%B8%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%B9%E6%95%88%E7%AE%A1%E7%BA%BF%E5%92%8C%E5%90%8E%E5%A4%84%E7%90%86%E7%89%B9%E6%95%88%E5%AE%9E%E7%8E%B0/">Urp下自定义特效管线和后处理特效实现</a>。<br>关于如何取得屏幕颜色贴图的方法不再赘述,另外为了优化性能,最终是判断是否需要获取屏幕颜色贴图(比如是否挂了屏幕特效脚本等)来决定是否执行CopyColorPass。</p><h1 id="3-1-折射屏幕扭曲"><a href="#3-1-折射屏幕扭曲" class="headerlink" title="3.1 折射屏幕扭曲"></a>3.1 折射屏幕扭曲</h1><p>获得屏幕颜色贴图后,只需要在屏幕空间下采样就能获得背景的颜色信息,至于扭曲的方式是通过一张扭曲贴图来采样当前位置的扭曲程度,这个扭曲程度加到屏幕空间UV上即可。一定程度的扭曲,能够模仿透过冰这种介质发生光线扭曲的这种效果。</p><h1 id="3-2-折射强度控制"><a href="#3-2-折射强度控制" class="headerlink" title="3.2 折射强度控制"></a>3.2 折射强度控制</h1><p>折射强度主要是通过NDotV来控制,另外提供了折射强度和控制贴图来调节。折射越强,越能透光冰看到后面的场景。至于为什么要使用NDotV,主要是为了贴近菲尼尔效应。根据菲涅尔效应,视线垂直于法线的情况下,反射越强,相应的折射越弱,NDotV越小。</p><h1 id="四、卡通着色"><a href="#四、卡通着色" class="headerlink" title="四、卡通着色"></a>四、卡通着色</h1><p>这里的卡通着色就是一个二阶色的卡通着色,计算halfLambert,然后映射到2个颜色(暗色、亮色),中间的过渡用smoothstep插值。可以参考文章:<a href="https://xiaopengcheng.top/2022/01/22/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E7%9D%80%E8%89%B2%E7%AF%87%EF%BC%88%E4%B8%80%EF%BC%89/">Unity下的日式卡通渲染实现-着色篇(一)</a>中的卡通着色部分。<br>那么卡通着色如何跟折射效果结合了?<br>可以使用折射强度去插值基础颜色和折射颜色,然后再用得到的基础色去计算卡通着色。</p><h1 id="五、高光"><a href="#五、高光" class="headerlink" title="五、高光"></a>五、高光</h1><p>高光就是Blinn-Phong的高光部分,计算NDotH,然后用pow(NDotH, 高光指数)来得到高光结果。比较简单,不再赘述。</p><h1 id="六、边缘光"><a href="#六、边缘光" class="headerlink" title="六、边缘光"></a>六、边缘光</h1><p>边缘光也是通过NDotV来判断边缘光程度,方法是判断NDotV是否小于边缘光宽度。这样不仅可以通过NDotV简单的模仿物体边缘判断,而且可以通过边缘光宽度来调整边缘光的大小。<br>高光和边缘光是叠加在卡通着色基础之上的,叠加比例是1-折射强度。</p><h1 id="七、描边"><a href="#七、描边" class="headerlink" title="七、描边"></a>七、描边</h1><p>描边就是使用沿着法线外扩的卡通渲染描边,可以参考文章:<a href="https://xiaopengcheng.top/2022/03/12/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E6%8F%8F%E8%BE%B9%E7%AF%87%EF%BC%88%E4%B8%89%EF%BC%89/">Unity下的日式卡通渲染实现-描边篇(三)</a>。</p><h1 id="八、溶解"><a href="#八、溶解" class="headerlink" title="八、溶解"></a>八、溶解</h1><p>为了满足特效那边的冰消融的需求,额外添加了一个溶解部分。材质设置如下图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰-溶解材质.png"><br>溶解的实现很简单,提供一个溶解阈值,使用颜色贴图或者控制贴图的Alpha通道,来做AlphaTest。当然具体实现是用clip函数丢弃像素。</p><h2 id="8-1-溶解颜色"><a href="#8-1-溶解颜色" class="headerlink" title="8.1 溶解颜色"></a>8.1 溶解颜色</h2><p>为了模仿消融的效果,提供了一个溶解颜色来表示消融的过渡色,过渡色和本来的颜色通过smoothstep来插值,插值参数是溶解程度,溶解程度即是alpha减去溶解阈值。<br>效果如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通冰-溶解.png"><br>可以看到溶解的边缘有一个溶解过渡颜色。</p><h1 id="九、控制贴图"><a href="#九、控制贴图" class="headerlink" title="九、控制贴图"></a>九、控制贴图</h1><p>为了方便美术控制效果,额外提供了一张控制贴图,四个通道分别控制:高光强度、边缘光强度、折射强度、溶解Alpha值。</p><h1 id="十、参考资料"><a href="#十、参考资料" class="headerlink" title="十、参考资料"></a>十、参考资料</h1><blockquote><p><a href="https://xiaopengcheng.top/2021/10/22/Urp%E4%B8%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%B9%E6%95%88%E7%AE%A1%E7%BA%BF%E5%92%8C%E5%90%8E%E5%A4%84%E7%90%86%E7%89%B9%E6%95%88%E5%AE%9E%E7%8E%B0/">Urp下自定义特效管线和后处理特效实现</a><br><a href="https://xiaopengcheng.top/2022/01/22/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E7%9D%80%E8%89%B2%E7%AF%87%EF%BC%88%E4%B8%80%EF%BC%89/">Unity下的日式卡通渲染实现-着色篇(一)</a><br><a href="https://xiaopengcheng.top/2022/03/12/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E6%8F%8F%E8%BE%B9%E7%AF%87%EF%BC%88%E4%B8%89%EF%BC%89/">Unity下的日式卡通渲染实现-描边篇(三)</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、卡通冰的效果"><a href="#一、卡通冰的效果" class="headerlink" title="一、卡通冰的效果"></a>一、卡通冰的效果</h1><p>先看最终实现的卡通冰材质效果吧,如下所示:</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="Urp" scheme="http://xiaopengcheng.top/tags/Urp/"/>
<category term="卡通冰" scheme="http://xiaopengcheng.top/tags/%E5%8D%A1%E9%80%9A%E5%86%B0/"/>
</entry>
<entry>
<title>Unity下的日式卡通渲染实现-描边篇(三)</title>
<link href="http://xiaopengcheng.top/2022/03/12/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E6%8F%8F%E8%BE%B9%E7%AF%87%EF%BC%88%E4%B8%89%EF%BC%89/"/>
<id>http://xiaopengcheng.top/2022/03/12/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E6%8F%8F%E8%BE%B9%E7%AF%87%EF%BC%88%E4%B8%89%EF%BC%89/</id>
<published>2022-03-12T12:18:30.000Z</published>
<updated>2023-12-16T16:31:14.806Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>这边文章讲述的是项目中用到的一些卡通渲染描边相关技术。</p><h1 id="一、Back-Face外扩描边"><a href="#一、Back-Face外扩描边" class="headerlink" title="一、Back Face外扩描边"></a>一、Back Face外扩描边</h1><p>背面外扩描边和后处理描边是卡通渲染中主要应用到的描边方式。</p><h2 id="1-1-实现原理"><a href="#1-1-实现原理" class="headerlink" title="1.1 实现原理"></a>1.1 实现原理</h2><p>第一个Pass正常渲染物体。第二个Pass只渲染背面,同时顶点沿着法线方向偏移,开启深度测试。<br>第二个Pass开启深度测试的用处一个是重叠部分不会显示出来,另外可以利用Early-Z减少需要处理的片元数量。</p><h2 id="1-2-描边的法线优化"><a href="#1-2-描边的法线优化" class="headerlink" title="1.2 描边的法线优化"></a>1.2 描边的法线优化</h2><p>由于我们是沿着法线偏移顶点,那么最终的描边结果对法线的依赖很大。如果法线分布有问题,可能造成描边断裂的情况。如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通渲染描边断裂.png"></p><p>这是因为四个面的法线都是垂直于面的,在角的地方没有连续性。一种比较好的解决方式是计算平均法线存储在不使用的uv通道内,比如uv8,然后使用这个平均法线去计算描边。<br>何谓平均法线?即顶点周围面法线的平均或者加权平均。<br>如何计算这个平均法线了?这个可以编写外部工具对fbx直接离线修改;或者编写Unity的脚本修改uv8,不过在Unity内已经修改不了Fbx文件了,所以去修改Mesh的uv8数据,实际上这个修改是存储在工程的缓存数据内的,因此需要给Mesh新增一个Tag,如果有这个Tag导入Mesh的时候就需要就需要计算平滑法线。</p><h2 id="1-3-根据摄像机修正描边宽度"><a href="#1-3-根据摄像机修正描边宽度" class="headerlink" title="1.3 根据摄像机修正描边宽度"></a>1.3 根据摄像机修正描边宽度</h2><p>一个是距离摄像机的距离,理论上来说应该是距离摄像机越远描边应该越小,这个可以用摄像机空间的z值来表示。另外一个是Fov,Fov越大描边应该越小。<br>加入这2个修正因子后,描边的粗细会看起来自然很多。</p><h2 id="1-4-描边深度偏移控制消隐"><a href="#1-4-描边深度偏移控制消隐" class="headerlink" title="1.4 描边深度偏移控制消隐"></a>1.4 描边深度偏移控制消隐</h2><p>有些地方美术实际上不希望出现描边。比如,头发的中间部位,美术只希望头发的边缘能看到描边。但是,正对着角色的时候,头发的中间部分实际上也是外扩的边缘,同样会看到描边。<br>这种情况可以通过使用深度偏移来修改顶点着色器的裁剪坐标,从而消隐描边。实际上,就是把不需要看到的描边往里推,从而被角色本身覆盖,就看不到描边了。<br>那么,哪些描边需要消隐了?就是下面要说的顶点色。</p><h2 id="1-5-顶点色控制描边宽度和深度偏移"><a href="#1-5-顶点色控制描边宽度和深度偏移" class="headerlink" title="1.5 顶点色控制描边宽度和深度偏移"></a>1.5 顶点色控制描边宽度和深度偏移</h2><p>我们提供了顶点色的两个通道来分别控制描边的粗细和深度偏移。粗细很好理解,就是有些部位描边宽有些更窄。深度偏移就是上面说的消隐问题,有些地方的描边希望看不到就可以增加一定的深度偏移使其被角色挡住。<br>顶点色需要美术使用DCC工具去涂色,或者也可以在Unity中使用编辑器去涂,然后保存下来。还是类似的问题,在Unity中不能修改原始的Fbx文件,因此涂色后的网格数据只能保存为.asset。<br>当前项目中使用的是这篇文章:<a href="https://zhuanlan.zhihu.com/p/139318012">在Unity中写一个简单的顶点颜色编辑器</a>的顶点色工具,基本需求能够满足。</p><p>最终的描边效果如下所示:<br><img alt="卡通渲染外扩描边" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通渲染外扩描边.jpg"><br>可以看到头顶的头发通过顶点色深度偏移控制了消隐,头发的描边粗细也是通过另一个顶点色通道控制的。</p><h1 id="二、-后处理描边"><a href="#二、-后处理描边" class="headerlink" title="二、 后处理描边"></a>二、 后处理描边</h1><p>后处理描边是在图像空间使用边缘检测因子得到边缘信息,通常是检测深度图或者法线图,比颜色图效果更好。因为,深度或者法线在角色边缘有明显的不连续性。但是,后处理描边的缺点是无法控制描边或者说很难像外扩描边一样精细的控制描边效果,同时还会把内描边也检测出来。<br>对于头发的描边,后处理描边这些缺点就是非常致命的,因为我们主要用的还是外扩描边。</p><h1 id="三、其它描边方式"><a href="#三、其它描边方式" class="headerlink" title="三、其它描边方式"></a>三、其它描边方式</h1><h2 id="3-1-NdotV"><a href="#3-1-NdotV" class="headerlink" title="3.1 NdotV"></a>3.1 NdotV</h2><p>类似简单的边缘光实现方式,也可以用来做描边,但是效果和控制力度肯定是达不到需求的。</p><h2 id="3-2-深度贴图描边"><a href="#3-2-深度贴图描边" class="headerlink" title="3.2 深度贴图描边"></a>3.2 深度贴图描边</h2><p>之前说的深度贴图边缘光和阴影同样可以用来做描边,也能使用顶点颜色提供一定的控制粒度,比如控制粗细,但是也无法做深度偏移消隐等。</p><h1 id="四、内描边"><a href="#四、内描边" class="headerlink" title="四、内描边"></a>四、内描边</h1><p>所谓内描边,指的是物体内部的描边,非物体边缘看到的描边。之前说的技术基本上都是针对外描边的。</p><h2 id="4-1-本村线"><a href="#4-1-本村线" class="headerlink" title="4.1 本村线"></a>4.1 本村线</h2><p>简单来说,是直接在贴图上画描边,同时这些描边基本是跟轴对齐的。不过工作量很大,而且很大细节需要控制,很少有美术愿意采用这种方式,因此不做过多的讨论。<br>如下图:<br>!](<a href="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/本村线描边.jpg">https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/本村线描边.jpg</a>)</p><h2 id="4-2-后处理内描边"><a href="#4-2-后处理内描边" class="headerlink" title="4.2 后处理内描边"></a>4.2 后处理内描边</h2><p>网上有文章提到二之国中的做法是在顶点属性通道中记录边缘程度,然后在后处理中来进行绘制内描边。<br>如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/二之国后处理内描边.jpg"><br>猜测是类似于顶点色的方式,让美术使用工具在顶点色中涂色边缘程度,<br>然后需要一个Pass将顶点色上的边缘程度属性输出到一个RT上,最终在后处理Pass中检测这个RT对应像素的边缘程度完成内描边。<br>至于如何将边缘程度转换为描边,需要参考相关资料才能弄清楚了,这里的日文看不懂啊。</p><h1 id="五、参考资料"><a href="#五、参考资料" class="headerlink" title="五、参考资料"></a>五、参考资料</h1><blockquote><p><a href="https://zhuanlan.zhihu.com/p/109101851">【01】从零开始的卡通渲染-描边篇</a><br><a href="https://zhuanlan.zhihu.com/p/163791090">卡通渲染学习总结</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>这边文章讲述的是项目中用到的一些卡通渲染描边相关技术。</p>
<h1 id="一、Back-Face外扩描边"><a href="#一、Back-Face外扩描边" class="headerlink" title="一、</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="日式卡通渲染" scheme="http://xiaopengcheng.top/tags/%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93/"/>
</entry>
<entry>
<title>Unity下的日式卡通渲染实现-阴影篇(二)</title>
<link href="http://xiaopengcheng.top/2022/02/26/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E9%98%B4%E5%BD%B1%E7%AF%87%EF%BC%88%E4%BA%8C%EF%BC%89/"/>
<id>http://xiaopengcheng.top/2022/02/26/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E9%98%B4%E5%BD%B1%E7%AF%87%EF%BC%88%E4%BA%8C%EF%BC%89/</id>
<published>2022-02-26T10:20:00.000Z</published>
<updated>2023-12-16T16:31:14.806Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>这边文章讲述的是项目中用到的一些卡通渲染阴影相关技术。</p><h1 id="一、SDF面部阴影"><a href="#一、SDF面部阴影" class="headerlink" title="一、SDF面部阴影"></a>一、SDF面部阴影</h1><p>SDF这个概念具体是什么意思了?可以去观看闫令琪在B站上的101课程,有一节专门讲述了SDF的定义和混合SDF能够产生什么效果。其实SDF面部阴影基本的思想就是混合面部的SDF得到一张阴影阈值图,然后利用这张阴影阈值图实现二维的阴影渲染。<br>比较详细的解释可以参考这篇文章,<a href="https://zhuanlan.zhihu.com/p/361716315">卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP)</a>。</p><h2 id="1-1-如何计算SDF"><a href="#1-1-如何计算SDF" class="headerlink" title="1.1 如何计算SDF"></a>1.1 如何计算SDF</h2><p>根据SDF的定义(到边界的最短有符号距离,形状内部为负,外部为正),计算二维的SDF其实很容易,可以使用C++写程序离线处理图片得到对应的SDF图。不过,我们现在需要的是更进一步的阴影阈值图。</p><h2 id="1-2-如何计算阴影阈值图"><a href="#1-2-如何计算阴影阈值图" class="headerlink" title="1.2 如何计算阴影阈值图"></a>1.2 如何计算阴影阈值图</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/特定角度的SDF阴影图.png"><br>阴影阈值图实际上是根据上述的多个角度的阴影图,首先计算每个阴影图的SDF图;然后将所有的SDF图递归混合起来,最终的输出就是阴影阈值图。<br>如何混合SDF图了?根据前后2个SDF图的在对应像素点的差异值来插值前后2个SDF图。<br>不过这些阴影图是有一定的要求的:图片必须连续且后一张必须可以覆盖前一张(可以是暗部覆盖也可以是亮部覆盖,但只能是一种)。<br>这个离线计算阴影阈值图的程序网上已经有人给出来了:<a href="https://zhuanlan.zhihu.com/p/356185096">如何快速生成混合卡通光照图</a>。</p><h2 id="1-3-如何渲染SDF面部阴影"><a href="#1-3-如何渲染SDF面部阴影" class="headerlink" title="1.3 如何渲染SDF面部阴影"></a>1.3 如何渲染SDF面部阴影</h2><p>文章<a href="https://zhuanlan.zhihu.com/p/361716315">卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP)</a>给出的Shader代码基本上问题不大,不过有2个比较明显的问题。<br><strong>朝向问题</strong>:美术出的资源坐标系以及顶点位置不一定是朝着Unity局部坐标系的正Y轴,最好使用脚本传入正Y轴,否则有些模型的阴影就会反了。<br><strong>左右判断问题</strong>:经过验证应该用right去判断还不是left,同样最好是用脚本传入,否则阴影过渡可能出现问题。<br>最后的if判断可以省略:直接用smoothstep计算bias即可达到效果。<br>最终的效果如下:<br><img alt="SDF阴影动画" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/SDF阴影动画.gif"></p><h1 id="二、自阴影"><a href="#二、自阴影" class="headerlink" title="二、自阴影"></a>二、自阴影</h1><p>自阴影实际上是一种ShadowMap的变形,默认的ShadowMap会应用到所有开启了阴影投射的问题上。如果角色还使用这个ShadowMap可能会造成分辨率不够,比如阴影不够清晰、阴影锯齿严重等。因此,对所有的角色重新投影到一个新的ShadowMap上,然后利用这个新的ShadowMap计算自阴影。</p><h2 id="2-1-收集所有激活的角色"><a href="#2-1-收集所有激活的角色" class="headerlink" title="2.1 收集所有激活的角色"></a>2.1 收集所有激活的角色</h2><p>给每个角色增加一个MonoBehavior脚本,该脚本激活的时候收集角色的包围盒,角色删除时候取消<br>对应的包围盒。对所有的角色包围盒计算一个并集,将该并集作为正交相机的渲染范围去渲染下一步的阴影Pass。</p><h2 id="2-2-自定义ShadowCastPass"><a href="#2-2-自定义ShadowCastPass" class="headerlink" title="2.2 自定义ShadowCastPass"></a>2.2 自定义ShadowCastPass</h2><p>增加Urp的自定义Pass,该Pass的渲染模板是额外的ShadowMap对应的RT。在该Pass执行的时候去收集上述的角色包围盒并集,同时将并集作为正交相机的渲染范围,然后去渲染自定义ShadowTag的Pass。<br>Shader内的阴影Pass实现跟Urp默认的阴影Pass基本一致。</p><h2 id="2-3-渲染自阴影"><a href="#2-3-渲染自阴影" class="headerlink" title="2.3 渲染自阴影"></a>2.3 渲染自阴影</h2><p>自阴影的渲染基本与传统的ShadowMap一致。不过,需要注意的是阴影投影矩阵已经变化了,需要从脚本中传入Shader。同时,采样阴影贴图的z值需要增加偏移参数去调整,以获得好的效果。美术可能还需要去控制特定区域的阴影强弱,比如可以使用顶点色来控制阴影强度。<br>效果如下图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通渲染自阴影.jpg"></p><h1 id="三、深度边缘阴影"><a href="#三、深度边缘阴影" class="headerlink" title="三、深度边缘阴影"></a>三、深度边缘阴影</h1><p>深度边缘阴影和上一篇讲的深度边缘光的原理类似,都必须利用深度贴图来判断当前像素处于边缘<br>的程度。同样,角色Shader中需要增加DepthOnly来输出深度到深度贴图了。<br>效果如下图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通渲染深度阴影.jpg"><br>对比自阴影的效果图,可以看到在细节的遮挡区域阴影效果得到了进一步的增强。</p><h1 id="四、默认的Urp阴影"><a href="#四、默认的Urp阴影" class="headerlink" title="四、默认的Urp阴影"></a>四、默认的Urp阴影</h1><p>默认的Urp阴影决定的是主灯的阴影效果,比如角色走入主方向光形成的阴影区域内,那么角色的亮度是否需要做一定的调整了?<br>在urp的shader源码中,这个阴影体现为mainlight的shadowAttenuation大小,该值会去缩放主灯的颜色亮度。如果直接使用该值去缩放灯光,那么会出现一定的自遮挡的阴影,而且这个阴影很丑。因此,可以使用这个shadowAttenuation去缩放卡通着色后的结果,从而体现出角色在不同光照区域有一定的表现差异。<br>不过,如果采用shadowmask的光照烘焙方式,烘焙后场景的静态物体就不会投影阴影了。因此,这个时候通过shadowmap得到的实时阴影就是不正确的,表现就会看起来很怪异。<br>有什么解决办法了?<br>方法一:一种简单的方式是让场景美术摆放一些大的不烘焙的隐藏面片来投影实时阴影。<br>方法二:另一种方式采用遮挡探针(和Unity的光照探针是一起的,实质上就是布置光照探针),类似光照探针一样,可以让动态的物体采用烘焙的阴影信息,不过这个时候主角就不采样shadowmap了。<br>方法三:改成DistanceShadowMask方式,这样子烘焙后的静态物体也会投影实时阴影,可以避免布置光照探针。不过性能损失会增大不少。</p><h1 id="五、阴影如何跟着色结合?"><a href="#五、阴影如何跟着色结合?" class="headerlink" title="五、阴影如何跟着色结合?"></a>五、阴影如何跟着色结合?</h1><p>对于每种阴影可以指定一个阴影颜色,上述的各种算法只是计算对应的阴影强度。最终,使用阴影强度插值卡通着色结果和阴影颜色,就可以得到应用阴影后的效果。</p><h1 id="六、参考资料"><a href="#六、参考资料" class="headerlink" title="六、参考资料"></a>六、参考资料</h1><blockquote><p><a href="https://zhuanlan.zhihu.com/p/361716315">卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP)</a><br><a href="https://zhuanlan.zhihu.com/p/356185096">如何快速生成混合卡通光照图</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>这边文章讲述的是项目中用到的一些卡通渲染阴影相关技术。</p>
<h1 id="一、SDF面部阴影"><a href="#一、SDF面部阴影" class="headerlink" title="一、SDF面部阴影"></a</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="日式卡通渲染" scheme="http://xiaopengcheng.top/tags/%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93/"/>
</entry>
<entry>
<title>Unity下的日式卡通渲染实现-着色篇(一)</title>
<link href="http://xiaopengcheng.top/2022/01/22/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E7%9D%80%E8%89%B2%E7%AF%87%EF%BC%88%E4%B8%80%EF%BC%89/"/>
<id>http://xiaopengcheng.top/2022/01/22/Unity%E4%B8%8B%E7%9A%84%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93%E5%AE%9E%E7%8E%B0-%E7%9D%80%E8%89%B2%E7%AF%87%EF%BC%88%E4%B8%80%EF%BC%89/</id>
<published>2022-01-22T07:10:00.000Z</published>
<updated>2023-12-16T16:31:14.806Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>这篇文章讲述的是项目中二次元日式卡通着色渲染用到的一些跟着色相关的技术点。</p><h1 id="一、卡通着色"><a href="#一、卡通着色" class="headerlink" title="一、卡通着色"></a>一、卡通着色</h1><p>何谓卡通着色?大概是让角色看起来卡通的角色吧。这里说的卡通着色,实际上指的是色阶着色。即根据光照和法线计算出当前像素处于哪个色阶,色阶之间有平缓过渡。</p><h2 id="1-1-三色阶"><a href="#1-1-三色阶" class="headerlink" title="1.1 三色阶"></a>1.1 三色阶</h2><p>如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/卡通渲染三阶色.jpg"><br>这是一个三阶色的着色结果。可以看到着色结果是明显的三阶,从正对光照到背对光照是过渡分明的白色、灰色、黑色的三色阶。卡通渲染着色最基础的部分就是这种明显过渡的色阶。根据需要,项目中可能采用的是二色阶或者三色阶。我们观察过战双的角色效果,猜测采用的是三色阶。<br>实现原理:<br>选定三个颜色,作为亮色、灰色、暗色;选定2个阈值,0-灰色阈值表示亮部,灰色阈值-暗色阈值表示灰部,大于暗色阈值表示暗部;计算1-halfLambert表示着色的暗度,将暗度映射到上一个范围,选取颜色;为了色阶过渡平滑,增加过渡区域大小,使用smoothstep平滑过渡边界。</p><h2 id="1-2-暗部控制"><a href="#1-2-暗部控制" class="headerlink" title="1.2 暗部控制"></a>1.2 暗部控制</h2><p>增加一张控制贴图,用一个通道比如r通道控制对应像素的暗的程度,我们叫其为darkness。<br>限制一:如果当前像素的darkness大于暗部的阈值,那么才可能在暗部区域,否则只能是灰部或者亮部区域。<br>限制二:当前像素的dark程度是darkness和1-halfLambert的最大值,也就是像素的dark程度只能大于等于控制贴图的darkness。<br>这样的限制后,方便美术控制哪些区域是暗部,不管光照如何变化这些区域始终是暗色,同时其着色结果只能比darkness更暗。<br>效果如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/带控制贴图的三阶色.jpg"><br>可以看到原本灰部的一些区域被限制一限制为固定暗部了,同时亮部的一些区域被限制二调整为灰部了。对应限制二,其实也可以只对暗部处理,这主要是看美术的需求。</p><h2 id="1-3-色阶贴图"><a href="#1-3-色阶贴图" class="headerlink" title="1.3 色阶贴图"></a>1.3 色阶贴图</h2><p>进一步扩展,默认情况下三色阶的颜色是在材质中指定好的固定颜色。美术进一步提出,希望去从贴图中去获得变化更多的颜色。因此,可以对色阶某一个颜色增加一张颜色贴图。色阶着色时候,从贴图中通过uv取当前色色阶色,而不是使用固定的色阶色。<br>下图是一个灰部使用色阶贴图的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/三阶色的灰部色阶贴图.jpg"></p><h2 id="1-4-Ramp贴图"><a href="#1-4-Ramp贴图" class="headerlink" title="1.4 Ramp贴图"></a>1.4 Ramp贴图</h2><p>跟二色阶和三色阶对应的是,直接使用halfLambert或者1-halfLambert从ramp贴图中获得着色结果。由于Ramp贴图本身就是一个良好过渡的梯度贴图,因此也可以实现和上述三色阶类似的效果。<br>下图是一个二阶色Ramp的结果,中间的红色是Ramp贴图上的过渡色:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/二阶色Ramp着色.jpg"></p><p>Ramp贴图和三阶色实现的效果类似,不过Ramp贴图需要一张额外的Ramp贴图和以及一次贴图读取操作,但是三阶色计算量大。</p><h2 id="1-5-和颜色贴图结合"><a href="#1-5-和颜色贴图结合" class="headerlink" title="1.5 和颜色贴图结合"></a>1.5 和颜色贴图结合</h2><p>三阶色的结果乘以角色的基础颜色后就可以得到基本的卡通着色效果,如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/三阶色和颜色贴图结合.jpg"></p><h1 id="二、Pbr着色"><a href="#二、Pbr着色" class="headerlink" title="二、Pbr着色"></a>二、Pbr着色</h1><p>可以参考文章<a href="https://zhuanlan.zhihu.com/p/115238808">【04】从零开始的卡通渲染-PBR篇</a>。这篇文章讲得非常详细,包括角色的Pbr着色和卡通着色如何组合以及角色的卡通着色如何与Pbr场景角色组合等的一些思路。<br>我们的基本思路是实现一个近似的Pbr着色,包括直接光照和间接光照(近似环境光照pbr)。增加Pbr着色好处是可以实现一些Pbr材质才能达到的效果,比如Pbr的金属感等。同时通过在pbr的控制贴图中使用一个通道来作为蒙版,控制pbr着色的比例。Pbr的实现代码很多,因此具体实现思路,不再赘述。</p><h1 id="三、皮肤Ramp"><a href="#三、皮肤Ramp" class="headerlink" title="三、皮肤Ramp"></a>三、皮肤Ramp</h1><p>皮肤Ramp是一个比较简单的功能。通过计算halfLambert作为坐标去ramp皮肤贴图中获得皮肤颜色,再进行一定的缩放以及和皮肤基础颜色相乘得到一个最终的颜色。同时类似于于Pbr着色,提供一个蒙版或者混合比例,和卡通角色结果进行插值。<br>这个着色功能在某些情况下可以方便美术控制整体的皮肤梯度色调。</p><h1 id="四、高光"><a href="#四、高光" class="headerlink" title="四、高光"></a>四、高光</h1><p>卡通渲染的高光的可以采用Blinn-Phong高光或者使用近似的GGX直接光照高光。这两种高光的实现思路,请参考相关资料。</p><h2 id="4-1-各向异性高光"><a href="#4-1-各向异性高光" class="headerlink" title="4.1 各向异性高光"></a>4.1 各向异性高光</h2><p>所谓各项异性高光,指的是在不同的方向上高光表现是不一致的。<br>比如,Blinn-Phong高光不管从什么方向看过去都是一个高亮的光斑,如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/普通高光.png"><br>但是各向异性高光可能是不规则形状的,比如环形,即我们常说的天使环,如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/各项异性高光.png"><br>各向异性高光主要是应用在头发上。</p><h2 id="4-2-法线-高光控制贴图实现天使环"><a href="#4-2-法线-高光控制贴图实现天使环" class="headerlink" title="4.2 法线+高光控制贴图实现天使环"></a>4.2 法线+高光控制贴图实现天使环</h2><p>其实,即使是使用普通的Blinn-Phong高光,使用高光强度控制贴图和各项异性的法线也是可以调出类似的天使环的效果的。首先,控制贴图的强度分布映射到模型上本来就得是环形的,这个主要是靠美术去控制。另外,法线的分布要有一定的方向性。<br>另外一种方式就是下面要说的Kajiya-Kay各向异性高光。<br>下图是一个这种思路实现的效果:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/控制贴图实现各项异性高光.jpg"></p><h2 id="4-3-Kajiya-Kay-各向异性高光"><a href="#4-3-Kajiya-Kay-各向异性高光" class="headerlink" title="4.3 Kajiya-Kay 各向异性高光"></a>4.3 Kajiya-Kay 各向异性高光</h2><p>网上关于Kajiya-Kay的实现原理文章非常多,比如这篇<a href="http://tuyg.top/archives/876">卡通渲染之头发高光(anisotropy)的实现(URP)</a>。<br>这种各项异性高光的实现思路是把头发当成圆柱体,将法线用副切线替换后再计算高光,如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/Kajyiya-Kay各项异性高光.jpg"><br>如果想增加一些散乱的效果,可以使用噪声贴图让副切线沿着顶点法线方向做一个随机偏移。</p><h1 id="五、自发光"><a href="#五、自发光" class="headerlink" title="五、自发光"></a>五、自发光</h1><p>自发光比较简单,直接增加一个颜色值即可。</p><h1 id="六、边缘光"><a href="#六、边缘光" class="headerlink" title="六、边缘光"></a>六、边缘光</h1><p>边缘光有两种实现思路,一种是根据法线和视线的夹角计算边缘光的强度;一种是根据深度贴图计算当前像素处于边缘的可能性,再转化为边缘光强度。</p><h2 id="6-1-NdotV边缘光"><a href="#6-1-NdotV边缘光" class="headerlink" title="6.1 NdotV边缘光"></a>6.1 NdotV边缘光</h2><p>NdotV表示的是世界空间法线和视线的点积,反应的是从当前视线看去,像素点处于边缘的可能性。不过,通常还需要对边缘光强度做一些校正,比如根据离视点的距离等。</p><h2 id="6-2-深度贴图边缘光"><a href="#6-2-深度贴图边缘光" class="headerlink" title="6.2 深度贴图边缘光"></a>6.2 深度贴图边缘光</h2><p>如果使用深度贴图边缘光,那么需要一个额外的深度Pass,将角色的深度写入到深度贴图里。在着色渲染Pass中,采样深度贴图获得当前深度,然后沿着一个特定的方向偏移屏幕空间的深度贴图uv坐标,再采样一个偏移后的深度。对比这2个深度,如果超出一定的阈值,将差值转化为深度边缘光强度。下一篇文章讲到的深度贴图阴影也是这个原理。<br>效果如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/深度贴图边缘光.jpg"></p><h1 id="七、着色总结"><a href="#七、着色总结" class="headerlink" title="七、着色总结"></a>七、着色总结</h1><p>综上所述,一个卡通渲染的着色最终的计算结果是:<br>卡通着色结果= lerp(三阶色,Pbr着色,Ramp皮肤)+ 高光 + 自发光 + 边缘光。<br>下一篇文章,我们将讨论卡通着色的阴影实现思路。</p><h1 id="八、参考资料"><a href="#八、参考资料" class="headerlink" title="八、参考资料"></a>八、参考资料</h1><blockquote><p><a href="https://zhuanlan.zhihu.com/p/111633226">【03】从零开始的卡通渲染-着色篇2</a><br><a href="https://zhuanlan.zhihu.com/p/115238808">【04】从零开始的卡通渲染-PBR篇</a><br><a href="http://tuyg.top/archives/876">卡通渲染之头发高光(anisotropy)的实现(URP)</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>这篇文章讲述的是项目中二次元日式卡通着色渲染用到的一些跟着色相关的技术点。</p>
<h1 id="一、卡通着色"><a href="#一、卡通着色" class="headerlink" title="一、卡通着色"></</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="日式卡通渲染" scheme="http://xiaopengcheng.top/tags/%E6%97%A5%E5%BC%8F%E5%8D%A1%E9%80%9A%E6%B8%B2%E6%9F%93/"/>
</entry>
<entry>
<title>Urp相机堆栈关于后处理抗锯齿设置的问题</title>
<link href="http://xiaopengcheng.top/2021/12/28/Urp%E7%9B%B8%E6%9C%BA%E5%A0%86%E6%A0%88%E5%85%B3%E4%BA%8E%E5%90%8E%E5%A4%84%E7%90%86%E6%8A%97%E9%94%AF%E9%BD%BF%E8%AE%BE%E7%BD%AE%E7%9A%84%E9%97%AE%E9%A2%98/"/>
<id>http://xiaopengcheng.top/2021/12/28/Urp%E7%9B%B8%E6%9C%BA%E5%A0%86%E6%A0%88%E5%85%B3%E4%BA%8E%E5%90%8E%E5%A4%84%E7%90%86%E6%8A%97%E9%94%AF%E9%BD%BF%E8%AE%BE%E7%BD%AE%E7%9A%84%E9%97%AE%E9%A2%98/</id>
<published>2021-12-28T03:10:00.000Z</published>
<updated>2023-12-16T16:31:14.806Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、问题起源和影响"><a href="#一、问题起源和影响" class="headerlink" title="一、问题起源和影响"></a>一、问题起源和影响</h1><h2 id="1-1-Base相机切换导致切换场景时候闪烁"><a href="#1-1-Base相机切换导致切换场景时候闪烁" class="headerlink" title="1.1 Base相机切换导致切换场景时候闪烁"></a>1.1 Base相机切换导致切换场景时候闪烁</h2><p>问题是这样的,项目之前一直用场景相机作为Base相机,UI相机作为Overlay相机。渲染顺序是先渲染场景Base相机,然后渲染UI相机。不过,最近打包发现,在部分机器上,一切换场景时候,比如loading界面打开时候,屏幕会出现明显的闪烁,甚至还会花屏。</p><h2 id="1-2-固定Base相机解决切换场景闪烁"><a href="#1-2-固定Base相机解决切换场景闪烁" class="headerlink" title="1.2 固定Base相机解决切换场景闪烁"></a>1.2 固定Base相机解决切换场景闪烁</h2><p>尝试解决:并没有上FrameDebug或者RenderDoc去抓帧分析,比较麻烦。首先,尝试在切换场景之前就隐藏场景相机,发现花屏现象消失了,闪烁问题也大幅度减弱。猜测,是场景切换时候场景相机销毁, 导致必须切换Base相机导致整个相机堆栈都要重建的原因。<br>解决办法:固定一个空的Base相机,不渲染任何层,场景相机作为Overlay相机挂在Base相机上,然后是UI相机。<br>结果:原先loading界面闪烁的几个机器都不再闪烁。</p><h2 id="1-3-尝试强制清除颜色缓冲解决花屏和闪烁"><a href="#1-3-尝试强制清除颜色缓冲解决花屏和闪烁" class="headerlink" title="1.3 尝试强制清除颜色缓冲解决花屏和闪烁"></a>1.3 尝试强制清除颜色缓冲解决花屏和闪烁</h2><p>参考网上的文章,比如(<a href="https://blog.csdn.net/cgy56191948/article/details/103735487">二)unity shader在实际项目中出现的问题————低档机(如小米4)切换游戏场景时花屏问题</a>,猜测这篇文章的应用场景是在固定管线下。在切换场景时候强制多次清除颜色缓冲,同时Base相机设置为SolidColor清除,场景相机本来就有天空盒,以尝试解决部分机型花屏和闪烁问题,结果还是失败。故放弃治疗。沿着固定Base相机的思路继续下去。</p><h2 id="1-4-固定Base相机开启SMAA掉帧严重"><a href="#1-4-固定Base相机开启SMAA掉帧严重" class="headerlink" title="1.4 固定Base相机开启SMAA掉帧严重"></a>1.4 固定Base相机开启SMAA掉帧严重</h2><p>由于MSAA会成倍增加RT的带宽和内存,带宽又是性能非常敏感的因素,所以放弃了。刚好Unity的Urp渲染管线支持SMAA和FXAA后处理抗锯齿,因此选择了后处理抗锯齿。<br>由于为了解决切换场景闪烁问题,固定空的Base相机,然后Base相机开启后处理抗锯齿,结果发现我的红米K30 Ultra掉帧非常严重,之前可以稳定30fps,改完之后在主场景只能跑到15fps左右。一开始还怀疑是场景没有合并网格,导致批次过高,编辑器内发现视线较远甚至到800Batches,远超100-200Batches的要求。后面想想,不可能突然掉帧这么严重,结果一FrameDebug,发现SMAA跑了2次。如下图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/相机堆栈上SMAA多次执行.png"><br>Base相机一次,场景Overlay相机一次,UI相机不开后处理所以没有。而一次SMAA实际上是三个全屏Pass,性能可想而知。<br>实际上,我们只想让场景相机有抗锯齿,和之前场景相机作为Base相机的情况保持一致。那么,我们就尝试只给场景相机开启后处理抗锯齿,结果发现完全没有效果。</p><h1 id="二、Urp的相机堆栈"><a href="#二、Urp的相机堆栈" class="headerlink" title="二、Urp的相机堆栈"></a>二、Urp的相机堆栈</h1><p>可以参考Unity中国官方发在知乎上的这篇文章:<br><a href="https://zhuanlan.zhihu.com/p/351638959">URP 系列教程 | 多相机玩法攻略</a><br>简而言之,相机堆栈的意思是一系列的相机叠加在一起,Base相机作为基础设置,Base之上可以有任意的Overlay。按照叠加顺序从Base相机开始,一个个渲染,直到渲染完最后的相机,最终再把渲染结果的RT(注意,一个相机堆栈重用一个RT) Blit到屏幕上。</p><h1 id="三、SMAA无法在Overlay相机单独生效的原因"><a href="#三、SMAA无法在Overlay相机单独生效的原因" class="headerlink" title="三、SMAA无法在Overlay相机单独生效的原因"></a>三、SMAA无法在Overlay相机单独生效的原因</h1><p>Urp渲染管线默认使用的是前向渲染器ForwardRenderer,ForwardRenderer里面有两个PostProcessPass,一个是m_PostProcessPass,另一个是m_FinalPostProcessPass,后处理就是在这2个Pass里面实现的。注意,Urp定义的这种Pass只是逻辑上的,实际上可能对应多个渲染Pass。<br>PostProcessPass的Execute会判断是IsFinalPass来执行RenderFinalPass还是正常的Render。正常的Render主要对应的是UberPost相关的后处理,RenderFinalPass对应的FinalPost相关的后处理。更详细的细节不在此列,看源码吧。<br>问题在于,Render函数中如下图所示的判断,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/PostProcess的SMAA判断.jpg"><br>cameraData是传递给每个Pass的RenderingData的成员,这些都是在渲染相机时候初始化好的。因此,怀疑对于Overlay相机这个标志无法传递到PostProcess。<br>回到UniversalRenderPipeline的RenderCameraStack函数,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/overlayCameraData.jpg"><br>从这部分代码可以看到传递给overlay相机的overlayCameraData是通过baseCameraData初始化的,然后再通过InitializeAdditionalCameraData设置一些额外的参数。然后再去查看InitializeAdditionalCameraData的源码,发现没有设置抗锯齿模式的地方。再去查看InitializeStackedCameraData函数源码,如下所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/urp抗锯齿模式设置.jpg"><br>最终确定抗锯齿模式是通过base相机设置,而overlay的抗锯齿模式不会生效,。这也就解释了为什么只设置base相机的smaa会导致overlay相机也执行了smaa,单独设置overlay相机的smaa反而无法生效。<br>那么如何解决了?很简单,在InitializeAdditionalCameraData函数中增加一行代码,将overlay相机的抗锯齿设置传递到overlayCameraData即可。</p><h1 id="四、FXAA只能在最后一个相机生效(通常是UI相机)"><a href="#四、FXAA只能在最后一个相机生效(通常是UI相机)" class="headerlink" title="四、FXAA只能在最后一个相机生效(通常是UI相机)"></a>四、FXAA只能在最后一个相机生效(通常是UI相机)</h1><p>SMAA的问题解决了。结果发现FXAA也无法生效,那只能继续查源码咯。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/applyFinalPostProcessing.jpg"><br>如上截图所示,发现前向渲染器是根据标志applyFinalPostProcessing,来判断是否应用FinalPostProcessPass。而这个标志要求三个条件,相机堆栈开启了后处理、当前是最后一个相机、Base相机开启了FXAA,如果做了三的源码修改(Overlay的抗锯齿设置生效),那么需要是UI相机开启了FXAA。<br>FrameDebug的结果如下所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/FXAA.jpg"></p><p>问题:开启了FXAA,UI界面肉眼可见的变模糊了,编辑器中都能体现出来。最终打算放弃FXAA,低端机选择不开抗锯齿,中高端机器开启SMAA。由于前述只对场景相机开启抗锯齿,因此不修改urp源码的情况下,FXAA是不会被激活的。</p><h1 id="五、固定空Base相机引入的新问题"><a href="#五、固定空Base相机引入的新问题" class="headerlink" title="五、固定空Base相机引入的新问题"></a>五、固定空Base相机引入的新问题</h1><p>对比如下2个截图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/空base相机的问题.jpg"><br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/场景相机作为Base.jpg"><br>第一个有额外空的Base相机,第二个直接使用场景相机作为Base相机。对比发现,Base相机无论如何会有Clear操作;然后还有一个渲染天空盒的操作。<br>如果我们把Base相机的天空盒模式改成颜色或者未初始化,就不会渲染天空盒。但是,对比第二张截图,天空盒是在渲染场景不透明物体后渲染的。因此,引入一个固定的Base相机会造成天空盒渲染顺序不对,导致效果出现问题,以及性能也会出现问题(一开始渲染天空盒导致Early-Z无法生效,OverDraw大幅度增加)。</p><h2 id="5-1-解决天空盒渲染问题"><a href="#5-1-解决天空盒渲染问题" class="headerlink" title="5.1 解决天空盒渲染问题"></a>5.1 解决天空盒渲染问题</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/Urp天空盒渲染条件.jpg"><br>根据上述代码截图,发现Urp的前向渲染强制只有Base相机才能激活天空盒渲染。我们直接去掉这个非isOverlayCamera判断即可。不过,需要Base相机设置为SolidColor清除方式;如果场景中还有额外的相机也需要注意不要设置天空盒,同样UI相机也是。</p><h2 id="5-2-解决额外的Clear操作"><a href="#5-2-解决额外的Clear操作" class="headerlink" title="5.2 解决额外的Clear操作"></a>5.2 解决额外的Clear操作</h2><p>我们对自定义的角色、场景、特效Pass加了对Base相机的限制,可以去除额外的2个Clear操作。最终Base相机就只有一个创建RT时候的Clear操作。这样Base相机的额外销毁可以降到最低。<br>FrameDebug场景渲染结果如下:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/优化空的Base相机渲染和天空盒问题.jpg"></p><h1 id="六、最终结论"><a href="#六、最终结论" class="headerlink" title="六、最终结论"></a>六、最终结论</h1><h2 id="6-1-固定空的Base相机避免切换场景闪烁"><a href="#6-1-固定空的Base相机避免切换场景闪烁" class="headerlink" title="6.1 固定空的Base相机避免切换场景闪烁"></a>6.1 固定空的Base相机避免切换场景闪烁</h2><p>为了修复部分机型切换场景闪烁问题,固定一个空的base相机,并且ui相机固定为最后一个overlay相机。如此可以避免切换场景时候,Base相机会切换,从而避免闪烁问题。<br>为了兼容overlay相机支持SMAA和渲染天空盒,需要修改Urp的源码,如上所述。</p><h2 id="6-2-中高端机器开启SMAA"><a href="#6-2-中高端机器开启SMAA" class="headerlink" title="6.2 中高端机器开启SMAA"></a>6.2 中高端机器开启SMAA</h2><p>为了兼容固定Base相机的情况下,单独设置场景相机的抗锯齿,需要修改urp源码支持overlay相机单独设置抗锯齿,从而只对场景overlay相机开启SMAA,Base相机不跑没必要的抗锯齿。同时UI相机不开抗锯齿,以避免UI模糊以及性能压力。</p><h2 id="6-3-低端机不开启抗锯齿"><a href="#6-3-低端机不开启抗锯齿" class="headerlink" title="6.3 低端机不开启抗锯齿"></a>6.3 低端机不开启抗锯齿</h2><p>低端机不开启抗锯齿。根据上述讨论,在不修改urp源码的前提下,低端机的场景相机无法开启FXAA。UI相机开启FXAA会导致UI肉眼可见模糊。所以最终选择低端机不开启任何抗锯齿。</p><h2 id="6-4-优化结果"><a href="#6-4-优化结果" class="headerlink" title="6.4 优化结果"></a>6.4 优化结果</h2><p>之前切换场景闪烁的机器都不再有花屏和闪烁现象;开启场景相机抗锯齿的情况下,红米k30 ultra从15fps左右恢复到稳定30fps。<br>果然是后处理猛如虎,带宽猛如虎。</p><h1 id="七、参考资料"><a href="#七、参考资料" class="headerlink" title="七、参考资料"></a>七、参考资料</h1><blockquote><p><a href="https://blog.csdn.net/cgy56191948/article/details/103735487">(二)unity shader在实际项目中出现的问题————低档机(如小米4)切换游戏场景时花屏问题</a><br><a href="https://zhuanlan.zhihu.com/p/351638959">URP 系列教程 | 多相机玩法攻略</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、问题起源和影响"><a href="#一、问题起源和影响" class="headerlink" title="一、问题起源和影响"></a>一、问题起源和影响</h1><h2 id="1-1-Base相机切换</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="Urp" scheme="http://xiaopengcheng.top/tags/Urp/"/>
<category term="相机堆栈" scheme="http://xiaopengcheng.top/tags/%E7%9B%B8%E6%9C%BA%E5%A0%86%E6%A0%88/"/>
<category term="后处理抗锯齿" scheme="http://xiaopengcheng.top/tags/%E5%90%8E%E5%A4%84%E7%90%86%E6%8A%97%E9%94%AF%E9%BD%BF/"/>
</entry>
<entry>
<title>Urp下自定义特效管线和后处理特效实现</title>
<link href="http://xiaopengcheng.top/2021/10/22/Urp%E4%B8%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%B9%E6%95%88%E7%AE%A1%E7%BA%BF%E5%92%8C%E5%90%8E%E5%A4%84%E7%90%86%E7%89%B9%E6%95%88%E5%AE%9E%E7%8E%B0/"/>
<id>http://xiaopengcheng.top/2021/10/22/Urp%E4%B8%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%B9%E6%95%88%E7%AE%A1%E7%BA%BF%E5%92%8C%E5%90%8E%E5%A4%84%E7%90%86%E7%89%B9%E6%95%88%E5%AE%9E%E7%8E%B0/</id>
<published>2021-10-22T09:15:51.000Z</published>
<updated>2022-03-26T10:00:09.423Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、如何获得颜色缓冲"><a href="#一、如何获得颜色缓冲" class="headerlink" title="一、如何获得颜色缓冲"></a>一、如何获得颜色缓冲</h1><p>网上搜索Unity的后处理或者获得屏幕缓冲,大部分会提到用grabpass到一张指定纹理上或者写一个后处理脚本挂在摄像机上。但是这种方式在Urp管线下已经不生效了。urp取消了默认管线抓取颜色缓冲的grabpass,同时也取消了<a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnRenderImage.html">MonoBehaviour.OnRenderImage</a>,需要使用<a href="https://docs.unity3d.com/Packages/[email protected]/api/UnityEngine.Rendering.Universal.ScriptableRenderPass.html">ScriptableRenderPass</a> 来完成类似的功能。ScriptableRenderPass是urp中的pass基类,urp预定义的pass都继承自该类,我们自定义的pass也需要继承自该类。</p><h2 id="1-1-Urp的渲染顺序"><a href="#1-1-Urp的渲染顺序" class="headerlink" title="1.1 Urp的渲染顺序"></a>1.1 Urp的渲染顺序</h2><p>urp中通过类型RenderPassEvent定义了一些列pass的渲染顺序或者说时机,大致的顺序是ShadowPass->PrePass(Depth Or DepthNormal)->Opaques->SkyBox->Transparents->PostProcessing,这个顺序也是Urp渲染管线的大致执行顺序。每个Pass或者说每个渲染事件都分Before和After,比如BeforePostProcessing和AfterPostProcessing分别表示后处理之前和后处理之后。<br>说了这么多,现在说结论,我们的特效Pass或者说特效管线就是要插入在BeforePostProcessing这个事件范围内。对了,同一个事件,比如BeforePostProcessing事件内的pass,最终的执行顺序是已加入管线的先后为准的。</p><h2 id="1-2-Urp内置的CameraOpaqueTexture"><a href="#1-2-Urp内置的CameraOpaqueTexture" class="headerlink" title="1.2 Urp内置的CameraOpaqueTexture"></a>1.2 Urp内置的CameraOpaqueTexture</h2><p>那么,我们是一定要自定义一个Pass才能获得颜色缓冲吗?不需要,其实Urp的ForwardRenderer内会在某种情况下给我生成一个颜色缓冲存储到贴图_CameraOpaqueTexture中,通过调用函数SampleSceneColor就得获得屏幕颜色。不过,这个贴图的生成时机是固定的,只会在渲染不透明物体之后,更准确的说是在渲染天空盒之后,通过CopyColorPass把摄像机的颜色缓冲Blit到_CameraOpaqueTexture。同时,需要摄像机或者Urp设置中有开启需要OpaqueTexture或者某个Pass的Input有要求ColorTexture。<br>假如,不需要颜色缓冲中有半透明物体的信息,那么这个_CameraOpaqueTexture就已经足够了。问题是,特效基本是半透明物体,部分场景物体也可能是半透明物体。所以,默认的_CameraOpaqueTexture大概率满足不了需求。<br>因此,需要在半透明物体渲染之后再获取一次颜色缓冲。这个可以通过在AfterTransparents或者BeforePostProcessing事件中插入一个CopyColorPass来实现。</p><h1 id="二、特效渲染管线"><a href="#二、特效渲染管线" class="headerlink" title="二、特效渲染管线"></a>二、特效渲染管线</h1><p>说实话,特效同学的要求有点多,要求部分特效受到全屏效果影响部分不受到影响。那么,特效要分成两部分渲染,一部分在全屏特效前,另外一部分在全屏特效后。那么,需要至少4个Pass,全屏特效前的特效Pass->CopyColorPass->全屏特效Pass->全屏特效后的特效Pass。<br>特效渲染管线如下:</p><ol><li>EffectPass (渲染后处理特效前的特效)</li><li>CopyColorPass (拷贝屏幕颜色)</li><li>UberEffectPostRenderPass (渲染后处理特效)</li><li>EffectPass(渲染后处理特效后的特效)</li></ol><p>其中,中间2个Pass最好是能够根据是否有全屏特效来动态激活。</p><h2 id="2-1-EffectRenderFeature"><a href="#2-1-EffectRenderFeature" class="headerlink" title="2.1 EffectRenderFeature"></a>2.1 EffectRenderFeature</h2><p>Urp中需要定义RenderFeature来配置相应的Pass。因此,我们定义一个专门用于特效管线的Feature。在这个Feature中,我们按照上述的顺序加入这4个Pass,其中2和3根据全屏特效是否存在来判断是否加入渲染管线。</p><h2 id="2-2-兼容UI特效穿插UI的解决方案"><a href="#2-2-兼容UI特效穿插UI的解决方案" class="headerlink" title="2.2 兼容UI特效穿插UI的解决方案"></a>2.2 兼容UI特效穿插UI的解决方案</h2><p>由于发现自定义一个BeforeRenderingPostProcessing的特效Pass来专门渲染特效,会导致所有的特效都在半透明物体之后渲染,而UI都是在半透明Pass渲染的,ShaderTag是UniversalForward,这样子会导致根据UI的Canvas来动态计算UI特效的sortingOrder以解决UI特效穿插UI的问题失效。因此,需要去除后处理特效前的特效pass,将这个Pass对应的特效改成默认的UniversalForward的ShaderTag。<br>那么,特效渲染管线最终是:</p><ol><li>Urp默认的DrawObjectsPass(渲染后处理特效前的特效,兼容解决UI特效穿插界面问题的方案)</li><li>CopyColorPass (拷贝屏幕颜色)</li><li>UberEffectPostRenderPass (渲染后处理特效)</li><li>EffectPass(渲染后处理特效后的特效)</li></ol><p>关键代码如下,在这个Feature中还定义ColorRT的名字和采样方式、全屏后处理超级Shader的名字等。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">AddRenderPasses</span>(<span class="params">ScriptableRenderer renderer, <span class="keyword">ref</span> RenderingData renderingData</span>)</span></span><br><span class="line">{</span><br><span class="line"> renderer.EnqueuePass(mEffectBeforePostProcessRenderPass);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (UberEffectPostRenderPass.IsPostProcessEnable())</span><br><span class="line"> {</span><br><span class="line"> mCopyColorRenderTarget.Init(RenderTargetName);</span><br><span class="line"> mCopyColorPass.Setup(renderer.cameraColorTarget, mCopyColorRenderTarget, RenderTargetSampling);</span><br><span class="line"> renderer.EnqueuePass(mCopyColorPass);</span><br><span class="line"> renderer.EnqueuePass(mUberEffectPostRenderPass);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> renderer.EnqueuePass(mEffectAfterPostProcessRenderPass);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Create</span>()</span></span><br><span class="line">{</span><br><span class="line"> Instance = <span class="keyword">this</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//后处理特效前的特效pass(UniversalForward就会在后处理之前,因此不需要定义专门的Pass,专门的Pass会造成SortingOrder排序失效,UI无法遮挡特效)</span></span><br><span class="line"> <span class="comment">//mEffectBeforePostProcessRenderPass = new EffectRenderPass(new ShaderTagId("EffectBeforePostProcess"));</span></span><br><span class="line"><span class="comment">//mEffectBeforePostProcessRenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">//拷贝颜色缓冲pass</span></span><br><span class="line"> mSamplingMaterial = CoreUtils.CreateEngineMaterial(Shader.Find(<span class="string">"Hidden/Universal Render Pipeline/Sampling"</span>));</span><br><span class="line"> mCopyColorMaterial = CoreUtils.CreateEngineMaterial(Shader.Find(<span class="string">"Hidden/Universal Render Pipeline/Blit"</span>));</span><br><span class="line"> mCopyColorPass = <span class="keyword">new</span> CopyColorPass(RenderPassEvent.BeforeRenderingPostProcessing, mSamplingMaterial, mCopyColorMaterial);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//特效后处理超级Pass</span></span><br><span class="line"> mUberEffectPostRenderPass = <span class="keyword">new</span> UberEffectPostRenderPass();</span><br><span class="line"> mUberEffectPostRenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//后处理特效后的特效pass</span></span><br><span class="line"> mEffectAfterPostProcessRenderPass = <span class="keyword">new</span> EffectRenderPass(<span class="keyword">new</span> ShaderTagId(<span class="string">"EffectAfterPostProcess"</span>));</span><br><span class="line"> mEffectAfterPostProcessRenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>Urp的ForwardRender配置如图:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/EffectRenderFeature.jpg"></p><h2 id="2-3-EffectRenderPass"><a href="#2-3-EffectRenderPass" class="headerlink" title="2.3 EffectRenderPass"></a>2.3 EffectRenderPass</h2><p>特效渲染Pass用于渲染普通的特效,Pass跟Shader的对应方式是ShaderTag。关键代码如下,</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Execute</span>(<span class="params">ScriptableRenderContext context, <span class="keyword">ref</span> RenderingData renderingData</span>)</span></span><br><span class="line">{</span><br><span class="line"> DrawingSettings drawingSettings = CreateDrawingSettings(mShaderTag, <span class="keyword">ref</span> renderingData, SortingCriteria.CommonTransparent);</span><br><span class="line"> FilteringSettings filteringSettings = <span class="keyword">new</span> FilteringSettings(RenderQueueRange.all);</span><br><span class="line"> context.DrawRenderers(renderingData.cullResults, <span class="keyword">ref</span> drawingSettings, <span class="keyword">ref</span> filteringSettings);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>有个需要注意的地方是物体渲染的排序方式要用SortingCriteria.CommonTransparent,毕竟特效都是半透明物体。这个标志是Urp默认的渲染半透明物体的排序方式,理论上是从后到前的顺序渲染。</p><h2 id="2-4-UberEffectPostRenderPass"><a href="#2-4-UberEffectPostRenderPass" class="headerlink" title="2.4 UberEffectPostRenderPass"></a>2.4 UberEffectPostRenderPass</h2><p>后处理特效Pass为了兼容面片类型的扭曲特效和全屏类型的色散、黑白屏、径向模糊特效,调用了2次绘制函数。第一次是用context.DrawRenderers绘制普通的物体;第二次是用cmd.DrawMesh绘制一个全屏三角形。同时为了支持,场景中出现多个全屏特效,该Pass中保存了一个材质数组,同时根据优先级来排序,优先级高的先渲染,这样就可以实现多个全屏特效的叠加效果。<br>代码如下,</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">AddMaterial</span>(<span class="params">Material mat, <span class="built_in">int</span> order = <span class="number">0</span></span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">var</span> matOrder = mMaterialOrders.Find((temp) => temp.Mat == mat);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (matOrder == <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> matOrder = <span class="keyword">new</span> MaterialOrder();</span><br><span class="line"> matOrder.Mat = mat;</span><br><span class="line"> mMaterialOrders.Add(matOrder);</span><br><span class="line"> }</span><br><span class="line"> matOrder.Order = order;</span><br><span class="line"></span><br><span class="line"> mMaterialOrders.Sort((a, b) => a.Order - b.Order);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">RemoveMaterial</span>(<span class="params">Material mat</span>)</span></span><br><span class="line">{</span><br><span class="line"> mMaterialOrders.RemoveAll((temp) => temp.Mat == mat);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Execute</span>(<span class="params">ScriptableRenderContext context, <span class="keyword">ref</span> RenderingData renderingData</span>)</span></span><br><span class="line">{</span><br><span class="line"> DrawingSettings drawingSettings = CreateDrawingSettings(<span class="keyword">new</span> ShaderTagId(<span class="string">"UberEffectPost"</span>), <span class="keyword">ref</span> renderingData, SortingCriteria.RenderQueue | SortingCriteria.SortingLayer);</span><br><span class="line"> FilteringSettings filteringSettings = <span class="keyword">new</span> FilteringSettings(RenderQueueRange.all);</span><br><span class="line"> context.DrawRenderers(renderingData.cullResults, <span class="keyword">ref</span> drawingSettings, <span class="keyword">ref</span> filteringSettings);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mMaterialOrders == <span class="literal">null</span> || mMaterialOrders.Count == <span class="number">0</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> CommandBuffer cmd = CommandBufferPool.Get();</span><br><span class="line"> <span class="keyword">using</span> (<span class="keyword">new</span> ProfilingScope(cmd, mProfilingSampler))</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">//set V,P to identity matrix so we can draw full screen quad (mesh's vertex position used as final NDC position)</span></span><br><span class="line"> cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i < mMaterialOrders.Count; ++i)</span><br><span class="line"> {</span><br><span class="line"> Material mat = mMaterialOrders[i] != <span class="literal">null</span> ? mMaterialOrders[i].Mat : <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">if</span> (mat != <span class="literal">null</span> && mat.shader.name == mUberEffectPostShaderName)</span><br><span class="line"> {</span><br><span class="line"> cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, mat, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> cmd.SetViewProjectionMatrices(renderingData.cameraData.camera.worldToCameraMatrix, renderingData.cameraData.camera.projectionMatrix); <span class="comment">// restore</span></span><br><span class="line"> }</span><br><span class="line"> context.ExecuteCommandBuffer(cmd);</span><br><span class="line"> CommandBufferPool.Release(cmd);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h1 id="三、后处理特效"><a href="#三、后处理特效" class="headerlink" title="三、后处理特效"></a>三、后处理特效</h1><h2 id="3-1-屏幕扭曲"><a href="#3-1-屏幕扭曲" class="headerlink" title="3.1 屏幕扭曲"></a>3.1 屏幕扭曲</h2><p>屏幕扭曲的效果最简单,只是偏移uv坐标即可。实现方式很多,基本上是采样噪声或者法线贴图来偏移uv坐标,核心代码大概如下:</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">half2 screenUV = input.screenPos.xy / input.screenPos.w;</span><br><span class="line">float2 uvDiffuse = input.uv + float2(_ScreenDistortionU, _ScreenDistortionV) * _Time.y;</span><br><span class="line">float4 diffuseTex = tex2D(_ScreenDistortionDiffuse, TRANSFORM_TEX(uvDiffuse, _ScreenDistortionDiffuse));</span><br><span class="line">half2 offset = float2(diffuseTex.r, diffuseTex.g) * _ScreenDistortStrength;</span><br><span class="line">screenUV = screenUV + offset; <span class="keyword">return</span> half4(SampleScreenColor(screenUV).rgb, <span class="number">1</span>);</span><br></pre></td></tr></tbody></table></figure><p>以上代码计算了2次偏移,第一次偏移是计算噪声图的uv,第二次是计算颜色缓冲的uv,也就是屏幕uv。<br>效果如下,中间的部分放了一个扭曲面片特效。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/屏幕扭曲.jpg"></p><h2 id="3-2-色散"><a href="#3-2-色散" class="headerlink" title="3.2 色散"></a>3.2 色散</h2><p>色散的原理也很简单,计算一个偏移的uv,分别在两个方向上计算r和b,不偏移的位置计算g,合并起来作为完整的颜色输出。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"> half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV);</span><br><span class="line">result.r = SampleScreenColor(screenUV + deltaUv).r;</span><br><span class="line">result.g = SampleScreenColor(screenUV).g;</span><br><span class="line">result.b = SampleScreenColor(screenUV - deltaUv).b;</span><br></pre></td></tr></tbody></table></figure><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/色散.png"></p><h2 id="3-3-黑白屏"><a href="#3-3-黑白屏" class="headerlink" title="3.3 黑白屏"></a>3.3 黑白屏</h2><p>黑白屏的关键实现代码也很短。但是想出来不太容易。网上大部分实现,就是简单的灰度化加上和屏幕颜色的插件。后面发现特效同学要的东西其实就是网上找了位特效大佬用ASE生成的shader效果,拿到代码后,过滤掉生成的冗余代码发现核心就是下面2个插值计算。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">half luminosity = dot(screenColor.rgb, half3(<span class="number">0.299</span>, <span class="number">0.587</span>, <span class="number">0.114</span>));</span><br><span class="line"><span class="built_in">float</span> smoothstepResult = smoothstep(_BlackWhiteThreshold, _BlackWhiteThreshold + _BlackWhiteWidth, luminosity.x);</span><br><span class="line">result = lerp(_BlackWhiteWhiteColor,_BlackWhiteBlackColor, smoothstepResult);</span><br></pre></td></tr></tbody></table></figure><p>关键代码是smoothstep,在阈值和阈值+阈值范围之间曲线插值,返回的值再用来插值白屏颜色色和黑屏颜色。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/黑白屏.png"></p><h2 id="3-4-径向模糊"><a href="#3-4-径向模糊" class="headerlink" title="3.4 径向模糊"></a>3.4 径向模糊</h2><p>径向模糊的思想是沿着到中点的方向采样几个点,然后平均。代码如下,这里假定是6次采样。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"> half2 dir = screenUV - half2(_RadialBlurHorizontalCenter, _RadialBlurVerticalCenter);</span><br><span class="line">half4 blur = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i < <span class="number">6</span>; ++i)</span><br><span class="line">{</span><br><span class="line">half2 uv = screenUV + _RadialBlurWidth * dir * i;</span><br><span class="line">blur += SampleScreenColor(uv);</span><br><span class="line">}</span><br><span class="line">blur /= <span class="number">6</span>;</span><br><span class="line"> result = lerp(result, blur, saturate(_RadialBlurStrength));</span><br></pre></td></tr></tbody></table></figure><p>不过,以上代码不一定能满足美术的需求。比如dir是否需要归一化,lerp时候是否需要考虑距离中点的远近等都会影响最终的效果。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/径向模糊.png"></p><h2 id="3-5-色散和径向模糊的结合"><a href="#3-5-色散和径向模糊的结合" class="headerlink" title="3.5 色散和径向模糊的结合"></a>3.5 色散和径向模糊的结合</h2><p> 如果先计算色散的DeltaUv,再将取屏幕颜色替换为屏幕扭曲的话,就能得到一个色散和径向模糊结合的效果,关键代码如下:</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV);</span><br><span class="line">result.r = RadialBlur(screenUV + deltaUv, screenColor).r;</span><br><span class="line">result.g = RadialBlur(screenUV, screenColor).g;</span><br><span class="line">result.b = RadialBlur(screenUV - deltaUv, screenColor).b;</span><br></pre></td></tr></tbody></table></figure><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/色散和径向模糊的结合.png"></p><h2 id="3-6-黑白屏和其它后处理效果的结合"><a href="#3-6-黑白屏和其它后处理效果的结合" class="headerlink" title="3.6 黑白屏和其它后处理效果的结合"></a>3.6 黑白屏和其它后处理效果的结合</h2><p>实现方式是,如果开启了黑白屏,将屏幕颜色都应用一次黑白屏,然后再进行其它的处理,比如色散的代码修改为如下,<br></p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"> half2 deltaUv = half2(_ColorDispersionStrength * _ColorDispersionU, _ColorDispersionStrength * _ColorDispersionV);</span><br><span class="line">half4 tempScreenColor = SampleScreenColor(screenUV + deltaUv);</span><br><span class="line"><span class="meta">#<span class="keyword">if</span> _BLACKWHITE</span></span><br><span class="line"> tempScreenColor = BlackWhite(tempScreenColor);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">result.r = tempScreenColor.r;</span><br><span class="line"></span><br><span class="line">tempScreenColor = SampleScreenColor(screenUV);</span><br><span class="line"> <span class="meta">#<span class="keyword">if</span> _BLACKWHITE</span></span><br><span class="line"> tempScreenColor = BlackWhite(tempScreenColor);</span><br><span class="line"> <span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">result.g = tempScreenColor .g;</span><br><span class="line"></span><br><span class="line">tempScreenColor = SampleScreenColor(screenUV - deltaUv);</span><br><span class="line"> <span class="meta">#<span class="keyword">if</span> _BLACKWHITE</span></span><br><span class="line"> tempScreenColor = BlackWhite(tempScreenColor);</span><br><span class="line"> <span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">result.b = tempScreenColor .b;</span><br></pre></td></tr></tbody></table></figure><br>黑白屏和色散结合:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/黑白屏和色散.jpg"><br>黑白屏和径向模糊结合:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/黑白屏和径向模糊结合.jpg"><br>黑白屏和色散、径向模糊结合:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/黑白屏和色散、径向模糊结合.jpg"><p></p><h2 id="3-7-UberEffectPost超级Shader"><a href="#3-7-UberEffectPost超级Shader" class="headerlink" title="3.7 UberEffectPost超级Shader"></a>3.7 UberEffectPost超级Shader</h2><p>具体实现上,我是用一个超级shader将这些功能整合到一起(除了屏幕扭曲,特效的需求是面片)形成一个UberShader。不同的效果通过shader_feature_local的开关来控制,这样既不用增加额外的大小和内存,也更方便美术同学的使用,整合到一起也是美术提出来的。<br>材质界面如下,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectPost.jpg"></p><h2 id="3-8-UberEffectPost脚本"><a href="#3-8-UberEffectPost脚本" class="headerlink" title="3.8 UberEffectPost脚本"></a>3.8 UberEffectPost脚本</h2><p>该脚本继承自MonoBehavior,用于判断是否存在全屏特效以及全屏特效材质、全屏特效优先级设置,并且在材质改变时候将后处理材质传入Pass等。<br>另外,美术同学要求加的后处理参数控制曲线也是在该脚本中,截图如下:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UberEffectPostScript.jpg"></p><p>这些参数曲线相对于TimeLine来说,可以更快的生成动态变化的后处理效果,减少美术去编辑TimeLine的工作量,不过自由度会有所降低。</p><h1 id="四、参考资料"><a href="#四、参考资料" class="headerlink" title="四、参考资料"></a>四、参考资料</h1><blockquote><p>1、<a href="https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnRenderImage.html">OnRenderImage</a><br>2、<a href="https://zhuanlan.zhihu.com/p/419814256">仿.碧蓝幻想versus黑白闪后处理shader分享(build_in 与urp双版本)</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、如何获得颜色缓冲"><a href="#一、如何获得颜色缓冲" class="headerlink" title="一、如何获得颜色缓冲"></a>一、如何获得颜色缓冲</h1><p>网上搜索Unity的后处理</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="urp" scheme="http://xiaopengcheng.top/tags/urp/"/>
<category term="特效" scheme="http://xiaopengcheng.top/tags/%E7%89%B9%E6%95%88/"/>
<category term="后处理" scheme="http://xiaopengcheng.top/tags/%E5%90%8E%E5%A4%84%E7%90%86/"/>
</entry>
<entry>
<title>反射效果的实现总结</title>
<link href="http://xiaopengcheng.top/2021/08/22/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%BB%E7%BB%93/"/>
<id>http://xiaopengcheng.top/2021/08/22/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%80%BB%E7%BB%93/</id>
<published>2021-08-22T04:15:00.000Z</published>
<updated>2022-05-04T09:02:53.637Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、反射的实现原理分类"><a href="#一、反射的实现原理分类" class="headerlink" title="一、反射的实现原理分类"></a>一、反射的实现原理分类</h1><p>首先要说明下<strong>反射向量</strong>,指的是视线的镜面反射向量,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/视线反射方向.png"></p><p>实际上,人眼看向一个物体表面的时候,在该位置(上图O点)的反射信息,来自于视线的反射方向,因为光线会从该反射方向打到物体表面,最终进入人眼。一般情况下,我们假定反射角和入射角相等。以下所有涉及到的反射向量, 都是指的视线反射向量,不是光线反射向量。</p><h2 id="1-1-CubeMap"><a href="#1-1-CubeMap" class="headerlink" title="1.1 CubeMap"></a>1.1 CubeMap</h2><p>天空盒就是一个CubeMap,我们可以假定天空盒是一个环境反射来源,也可以指定另外的CubeMap,用反射向量采样这个CubeMap就能得到反射颜色。CubeMap虽然比较简单,但是也能出很好的效果,而且不需要实时计算反射信息,性能很好。<br>优点:实现简单,效率高,只需要额外的CubeMap存储反射信息;适用于多种情况,不仅仅限于平面反射。<br>缺点:反射信息固定,没有变化。</p><h2 id="1-2-反射探针"><a href="#1-2-反射探针" class="headerlink" title="1.2 反射探针"></a>1.2 反射探针</h2><p>这种方式需要在场景内布置反射探针,用来采集反射信息。渲染时候,在Shader内根据反射探针来获得反射信息。反射探针如果是实时的,性能就会很差,这个时候可以考虑降低反射探针的更新频率或者使用烘焙模式的反射探针。因为反射探针的输出就是CubeMap,因此烘焙模式的反射探针,本质上和CubeMap没有区别。<br>优点:直接利用引擎计算反射信息,不需要额外工作;适用于多种情况,不仅仅限于平面反射。<br>缺点:实时反射探针性能差,计算一次反射探针需要朝着6个方向渲染场景,Drawcall增加6倍,性能太差;烘焙反射探针无法变化。</p><h2 id="1-3-平面反射"><a href="#1-3-平面反射" class="headerlink" title="1.3 平面反射"></a>1.3 平面反射</h2><p>这种方式限制于只能在平面上做反射。如果要求在凹凸不平的表面上实现反射效果,则不太适合。基本思路是将场景根据平面对称镜像一次,具体实现上是将生成的反射矩阵乘以到原场景摄像机的世界到相机空间的矩阵,然后用新的相机再渲染一次场景生成RT。然后在屏幕空间内采样这个RT,得到的像素值作为反射信息。<br>其实,使用反射探针也能实现平面反射的效果,原理是将探针的位置放在摄像机在平面的对称位置。可以参考大佬的这篇文章,<a href="https://baddogzz.github.io/2020/04/22/Probe-Reflection/">关于反射探头的实时反射</a>。实现难度相对平面反射低很多,不过实时探针比平面反射性能差6倍,优化起来难度太大。<br>优点:反射效果最好,最真实接近平面反射。<br>缺点:需要额外渲染一次场景,DrawCall翻倍。</p><h2 id="1-4-屏幕空间反射(SSR)"><a href="#1-4-屏幕空间反射(SSR)" class="headerlink" title="1.4 屏幕空间反射(SSR)"></a>1.4 屏幕空间反射(SSR)</h2><p>屏幕空间反射的基本原理比较简单,也就是在屏幕空间内通过深度法线纹理恢复世界空间坐标。然后,沿着反射向量方向做步进,也就是所谓的RayMarching,检查当前深度是否已经超过深度纹理对应的值,如果超过,表面已经碰到物体了,那么取当前步进到的颜色值作为反射结果即可。<br>优点:适用于多种情况,不仅仅限于平面反射;DrawCall不变。<br>缺点:需要额外的深度和法线纹理,在前向渲染中这不是免费的,需要多渲染一次场景得到深度和法线纹理;效果一般;无法反射屏幕之外的信息;实现比较复杂,移动平台下性能差(步进相交的计算量大),很可能跑不起来;带宽增加。</p><p>SSR应该是更适合于延迟渲染的一个反射效果实现方案,毕竟可以免费得到深度和法线纹理。</p><h2 id="1-5-屏幕空间平面反射"><a href="#1-5-屏幕空间平面反射" class="headerlink" title="1.5 屏幕空间平面反射"></a>1.5 屏幕空间平面反射</h2><p>这个是平面反射在屏幕空间下的一个实现。<br>SSPR大体的实现思路如下,<br>1、用当前屏幕UV从深度图重建当前世界坐标,将世界坐标以反射平面进行对称翻转<br>2、使用翻转后的世界坐标的计算屏幕UV<br>3、对当前屏幕纹理进行采样得到ReflectColor保存到一张新的ColorRT中,保存位置是翻转后的世界坐标的屏幕UV<br>4、在反射平面的Shader中用屏幕UV对ColorRT进行采样得到反射颜。<br>5、在反射平面的Shader中将反射颜色和着色结果进行组合得到最终颜色。</p><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/SSPR.jpg"><br>如上大佬的图能够基本说明SSPR的实现思路。UAV write即是3的输出。关键点和难点是要得到步骤三的ColorRT,并且要正确高效。网上有不少博客说的是如何正确高效实现前三步,基本上要使用Computer Shader,图形接口要求是vulkan/metal。具体实现比较复杂,不在此详细说明。</p><p>优点:效果较高;性能比SSR好;DrawCall不变。<br>缺点:对硬件要求高;需要额外的ColorRT,带宽和内存增加;只适用于平面反射。</p><h1 id="二、Unity对反射效果支持"><a href="#二、Unity对反射效果支持" class="headerlink" title="二、Unity对反射效果支持"></a>二、Unity对反射效果支持</h1><h2 id="2-1-CubeMap"><a href="#2-1-CubeMap" class="headerlink" title="2.1 CubeMap"></a>2.1 CubeMap</h2><p>Unity自带的Shader或许有支持,实现起来也很简单,关键代码如下:</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">half3 reflectVector = reflect(-viewDirectionWS, normalWS);</span><br><span class="line">half3 reflectColor = SAMPLE_TEXTURECUBE(_Cubemap, sampler_Cubemap, reflectVector).rgb;</span><br></pre></td></tr></tbody></table></figure><h2 id="2-2-反射探针"><a href="#2-2-反射探针" class="headerlink" title="2.2 反射探针"></a>2.2 反射探针</h2><p>目前,Unity内置管线和Urp支持<a href="https://docs.unity3d.com/cn/2019.4/Manual/class-ReflectionProbe.html">反射探针</a>,HDRP管线还支持一种特殊的<a href="https://docs.unity3d.com/cn/Packages/[email protected]/manual/Planar-Reflection-Probe.html">平面反射探针</a>,平面反射探针猜测是针对平面反射这种特殊情况的一种优化手段。<br>场景内布置了反射探针后,Urp管线中反射信息是存储在叫做unity_SpecCube0的内置CubeMap中。Shader中需要采样该CubeMap获得反射信息,Urp代码中搜索函数GlossyEnvironmentReflection,可以得到如下代码:</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">half3 <span class="title">GlossyEnvironmentReflection</span>(<span class="params">half3 reflectVector, half perceptualRoughness, half occlusion</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="meta">#<span class="keyword">if</span> !defined(_ENVIRONMENTREFLECTIONS_OFF)</span></span><br><span class="line"> half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);</span><br><span class="line"> half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip);</span><br><span class="line"></span><br><span class="line"><span class="comment">//<span class="doctag">TODO:</span>DOTS - we need to port probes to live in c# so we can manage this manually.</span></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> defined(UNITY_USE_NATIVE_HDR) || defined(UNITY_DOTS_INSTANCING_ENABLED)</span></span><br><span class="line"> half3 irradiance = encodedIrradiance.rgb;</span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"> half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> irradiance * occlusion;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span> // GLOSSY_REFLECTIONS</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> _GlossyEnvironmentColor.rgb * occlusion;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>上述函数三个关键点,根据粗糙度计算mipmap,采样光照探针,解析HDR贴图。我们重点关注的是mipmap计算,mipmap大家都知道,越远的地方,贴图采样率越低效果才好,看起来越模糊,没有锐利的毛刺感觉;同时粗糙度刚好可以表示这个概率,粗糙度越低越接近镜面,那么反射效果更接近光滑镜子的效果,粗糙度越高,反射效果越模糊。<br><strong>计算Mipmap的概念可以应用到所有的反射效果实现中,不仅仅反射探针</strong>。</p><h2 id="2-3-平面反射"><a href="#2-3-平面反射" class="headerlink" title="2.3 平面反射"></a>2.3 平面反射</h2><p>Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。</p><h2 id="2-4-屏幕空间反射"><a href="#2-4-屏幕空间反射" class="headerlink" title="2.4 屏幕空间反射"></a>2.4 屏幕空间反射</h2><p>Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。</p><h2 id="2-5-屏幕空间平面反射"><a href="#2-5-屏幕空间平面反射" class="headerlink" title="2.5 屏幕空间平面反射"></a>2.5 屏幕空间平面反射</h2><p>Unity没有发现支持,需要自己实现或者找第三方实现。后续会写文章介绍如何实现。</p><h1 id="三、UE4对反射效果支持"><a href="#三、UE4对反射效果支持" class="headerlink" title="三、UE4对反射效果支持"></a>三、UE4对反射效果支持</h1><h2 id="3-1-CubeMap"><a href="#3-1-CubeMap" class="headerlink" title="3.1 CubeMap"></a>3.1 CubeMap</h2><p>UE4的材质编辑器可以实现。</p><h2 id="3-2-反射探针"><a href="#3-2-反射探针" class="headerlink" title="3.2 反射探针"></a>3.2 反射探针</h2><p><a href="https://docs.unrealengine.com/4.27/zh-CN/BuildingWorlds/LightingAndShadows/ReflectionEnvironment/">UE4有盒子和球形的反射探针</a>。</p><h2 id="3-3-平面反射"><a href="#3-3-平面反射" class="headerlink" title="3.3 平面反射"></a>3.3 平面反射</h2><p>UE4有<a href="https://docs.unrealengine.com/4.27/zh-CN/BuildingWorlds/LightingAndShadows/PlanarReflections/">Planar Reflection Actor</a>,放入场景中即可。不过先要在工程设置中开启平面反射。</p><h2 id="3-4-屏幕空间反射"><a href="#3-4-屏幕空间反射" class="headerlink" title="3.4 屏幕空间反射"></a>3.4 屏幕空间反射</h2><p>UE4默认是启用<a href="https://docs.unrealengine.com/4.27/zh-CN/RenderingAndGraphics/PostProcessEffects/ScreenSpaceReflection/">屏幕空间反射</a>的。不过是可以在工程设置或者配置文件中关闭的。</p><h2 id="3-5-屏幕空间平面反射"><a href="#3-5-屏幕空间平面反射" class="headerlink" title="3.5 屏幕空间平面反射"></a>3.5 屏幕空间平面反射</h2><p>目前没有发现UE4支持这个特性。</p><h1 id="四、反射颜色与物体颜色的组合"><a href="#四、反射颜色与物体颜色的组合" class="headerlink" title="四、反射颜色与物体颜色的组合"></a>四、反射颜色与物体颜色的组合</h1><h2 id="4-1-Mipmap"><a href="#4-1-Mipmap" class="headerlink" title="4.1 Mipmap"></a>4.1 Mipmap</h2><p>计算Mipmap,模拟粗糙度的效果,这个在反射探针中已经有说明。</p><h2 id="4-2-菲涅尔效果"><a href="#4-2-菲涅尔效果" class="headerlink" title="4.2 菲涅尔效果"></a>4.2 菲涅尔效果</h2><p>获得反射颜色后,可以根据菲涅尔定律与物体本身的着色结果进行一定的组合即可。不过,不一定完全照搬菲涅尔效果的近似公式,比如Schlick菲涅耳近似等式。不过关键点在于强度必须是NdotV的函数,最简单的方式是计算出NdotV,对NdotV取反或者1-NdotV,因为入射角越大反射光越强,同时提供一个最大最小值来限制强度范围。也可以自定义其它跟NdotV负相关的函数来在反射颜色和物体颜色之间进行插值,来得到想要的效果。</p><p>以下是一个同时应用了粗糙度计算Mipmap和菲涅尔效果的反射平面,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射.jpg"></p><h1 id="五、参考资料"><a href="#五、参考资料" class="headerlink" title="五、参考资料"></a>五、参考资料</h1><blockquote><p>1、<a href="https://blog.csdn.net/puppet_master/article/details/80808486">Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)</a><br>2、<a href="https://baddogzz.github.io/2020/04/22/Probe-Reflection/">关于反射探头的实时反射</a><br>3、<a href="https://zhuanlan.zhihu.com/p/150890059">Unity URP 移动平台的屏幕空间平面反射(SSPR)趟坑记</a><br>4、<a href="https://www.lfzxb.top/screen-space-plana-reflection-in-urp-study/">URP下屏幕空间平面反射(ScreenSpacePlanarReflection)学习笔记</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、反射的实现原理分类"><a href="#一、反射的实现原理分类" class="headerlink" title="一、反射的实现原理分类"></a>一、反射的实现原理分类</h1><p>首先要说明下<st</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="反射效果" scheme="http://xiaopengcheng.top/tags/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C/"/>
</entry>
<entry>
<title>Unity下平面反射实现</title>
<link href="http://xiaopengcheng.top/2021/06/15/Unity%E4%B8%8B%E5%B9%B3%E9%9D%A2%E5%8F%8D%E5%B0%84%E5%AE%9E%E7%8E%B0/"/>
<id>http://xiaopengcheng.top/2021/06/15/Unity%E4%B8%8B%E5%B9%B3%E9%9D%A2%E5%8F%8D%E5%B0%84%E5%AE%9E%E7%8E%B0/</id>
<published>2021-06-15T13:15:00.000Z</published>
<updated>2022-05-04T08:51:04.945Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>平面反射通常指的是在镜子或者光滑地面的反射效果上,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射示意图.jpg"><br>上图是一个光滑的平面,平面上的物体在平面上有对称的投影。</p><h1 id="一、平面反射的原理"><a href="#一、平面反射的原理" class="headerlink" title="一、平面反射的原理"></a>一、平面反射的原理</h1><p>对于光照射到物体表面然后发生完美镜面反射的示意图,如下所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/镜面反射.png"><br>对于平面反射,假设平面上任意一点都会发生完美的镜面反射。因此,眼睛看到物体的一点的反射信息是从反射向量处得到的,这个可以用下图来表示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/视线反射方向.png"><br>这个实际上相当于,眼睛从平面的下面看向反射向量,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射原理.png"><br>因此,如上图所示,我们可以把摄像机根据平面对称变换到A点所示的位置,然后再渲染一遍场景到RenderTexture中。当我们渲染点O的反射信息时候,就可以到这张RT中去采样了。那么如何去采样反射信息了?使用点O的屏幕空间坐标。因为,RT是从A点看到的场景,视线和平面的交点O是当前渲染的像素点,因此用O的屏幕空间坐标去采样RT就可以得到其反射信息。</p><h2 id="1-1-平面反射矩阵"><a href="#1-1-平面反射矩阵" class="headerlink" title="1.1 平面反射矩阵"></a>1.1 平面反射矩阵</h2><h3 id="1-1-1-平面方程的计算"><a href="#1-1-1-平面方程的计算" class="headerlink" title="1.1.1 平面方程的计算"></a>1.1.1 平面方程的计算</h3><p>我们现在来推导一下把摄像机关于平面对称的反射矩阵。<br>我们知道一个平面可以表示为$P*N+d=0$。P是平面上任意一点,N是平面的法向量,d是一个常数。我们首先需要求出平面方程。对于平面,其世界空间的法向量就是N。用平面的世界空间位置带入P即可求出d的值。<br></p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plane = <span class="keyword">new</span> Vector4(planeNormal.x, planeNormal.y, planeNormal.z, -Vector3.Dot(planeNormal, planePosition) - Offset);</span><br></pre></td></tr></tbody></table></figure><br>我们可以用以上的一个Vector4表示一个平面,前三个分量表示normal,第四个分量表示d。<p></p><h3 id="1-1-2-平面反射矩阵的计算"><a href="#1-1-2-平面反射矩阵的计算" class="headerlink" title="1.1.2 平面反射矩阵的计算"></a>1.1.2 平面反射矩阵的计算</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射矩阵推导.jpg"><br>如上图所示,我们需要计算点A关于平面的对称点A’。关键在于计算出点A到平面的距离AO的大小。那么$A’=A-2*n*|AO|$,负号是因为方向和法线相反。所以,关键是求出|AO|。因为AO实际上是AP在法线相反方向的投影向量,那么$|AO|=dot(AP,n)=dot(A-P,n)=dot(A,n)-dot(P,n)$。由于P满足平面方程,因此$dot(P,n)=d$,因此$|AO|=dot(A,n)+d$,因此$A’=A-2*n*(dot(A,n)+d)$。</p><p>假设n为(nx,ny,nz),已知d的值,A是(x,y,z)点作为我们要变换的点,A’为(x’,y’,z‘),那么我们可以得到:<br>$x’ = x - 2(x * nx + y * ny + z * nz + d)* nx = (1 - 2nx * nx)x +(-2nx * ny)y + (-2nx * nz)z + (-2dnx)$,<br>$y’ = y - 2(x * nx + y * ny + z * nz + d)* ny = (-2nx * ny)x + (1 - 2ny * ny)y + (-2ny * nz)z + (-2dny)$,<br>$z’ = z - 2(x * nx + y * ny + z * nz + d)* nz = (-2nx * nz)x + (-2ny * nz)y + (1 - 2nz * nz)z + (-2dnz)$,<br>改写成矩阵形式可以得到平面反射的矩阵为:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射矩阵.jpg"></p><h2 id="1-2-斜裁剪矩阵"><a href="#1-2-斜裁剪矩阵" class="headerlink" title="1.2 斜裁剪矩阵"></a>1.2 斜裁剪矩阵</h2><p>上面我们已经推导出平面反射矩阵,不过还有一种特殊情况需要处理。<br><img alt="斜裁剪矩阵" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/斜裁剪矩阵.jpg"><br>如上图所示,我们的平面是P,将摄像机从C点对称到C’点。从C’可以看到的区域包括A和B,但是B是在平面P的下部,我们从C是无法看到的。因此,从C’点渲染场景RT的时候必须排除B区域,也就是需要将平面P作为裁剪平面,裁剪掉区域B。<br>这个东西叫做斜裁剪矩阵,我们可以推导出具体的斜裁剪矩阵或者使用Unity提供的接口直接计算出来。<br>计算斜裁剪矩阵需要两个步骤,第一步是计算出摄像机空间下的平面表示,第二步是用摄像机空间下的平面和原投影矩阵一起计算斜投影矩阵。<br>具体推导可以参考文章,<a href="https://blog.csdn.net/mobilebbki399/article/details/79491825">【图形与渲染】相机平面镜反射与斜裁剪矩阵(上)-镜像矩阵</a>。<br>第二步也可以使用Unity的camera中的接口CalculateObliqueMatrix来计算,参数就是第一步得到的平面。</p><h1 id="二、平面反射的实现"><a href="#二、平面反射的实现" class="headerlink" title="二、平面反射的实现"></a>二、平面反射的实现</h1><h2 id="2-1-平面反射的脚本"><a href="#2-1-平面反射的脚本" class="headerlink" title="2.1 平面反射的脚本"></a>2.1 平面反射的脚本</h2><p>这里的脚本指的是生成RenderTexture需要的脚本,脚本继承自MonoBehaviour。</p><h3 id="2-1-1-默认管线下的实现"><a href="#2-1-1-默认管线下的实现" class="headerlink" title="2.1.1 默认管线下的实现"></a>2.1.1 默认管线下的实现</h3><p>默认管线下需要在函数OnWillRenderObject中,基本步骤是先计算反射平面,然后计算反射矩阵和斜投影矩阵,设置反射相机的斜投影矩阵,然后将反射相机变换到平面对面,调用相机的Render函数渲染RT。需要注意的是,渲染的时候需要修改物体正反旋向,即GL.invertCulling设置为true。</p><h3 id="2-1-2-Urp管线下的实现"><a href="#2-1-2-Urp管线下的实现" class="headerlink" title="2.1.2 Urp管线下的实现"></a>2.1.2 Urp管线下的实现</h3><p>Urp管线下,需要绑定 RenderPipelineManager.beginCameraRendering的回调,然后在回调中实现。回调中会接收到当前渲染的相机,反射相机就是该相机关于平面的镜像。同时,渲染RT的函数需要改成UniversalRenderPipeline.RenderSingleCamera,传入context和反射相机。其余步骤,跟默认管线的区别不大。</p><h2 id="2-2-平面反射的Shader"><a href="#2-2-平面反射的Shader" class="headerlink" title="2.2 平面反射的Shader"></a>2.2 平面反射的Shader</h2><p>平面反射的shader可以使用普通的场景shader做修改。关键在于如何采样平面反射信息和平面反射强度以及模糊等。</p><h3 id="2-2-1-平面反射信息的采样"><a href="#2-2-1-平面反射信息的采样" class="headerlink" title="2.2.1 平面反射信息的采样"></a>2.2.1 平面反射信息的采样</h3><p>这个之前已经解释过用屏幕空间坐标来采样RT。</p><h3 id="2-2-1-平面反射强度"><a href="#2-2-1-平面反射强度" class="headerlink" title="2.2.1 平面反射强度"></a>2.2.1 平面反射强度</h3><p>这个可以用菲涅尔效应计算,不过关键点在于强度必须是NdotV的函数,最简单的方式是计算出NdotV,对NdotV取反或者1-NdotV,因为入射角越大反射光越强,同时提供一个最大最小值来限制强度范围。</p><h3 id="2-2-1-模糊和Mipmap"><a href="#2-2-1-模糊和Mipmap" class="headerlink" title="2.2.1 模糊和Mipmap"></a>2.2.1 模糊和Mipmap</h3><p>可以采样周围多个像素然后做平均模糊或者高斯模糊。不过,最简单的方式是对RT强制生成Mipmap,采样RT的时候指定Mipmap级别。那么,mipmap级别如何计算了。我们可以根据shader的粗糙度来转行为mipmap级别,这个参考unity的urp内置shader函数PerceptualRoughnessToMipmapLevel的实现。</p><p>最终得到的反射效果如图,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/平面反射.jpg"></p><h1 id="三、平面反射的优化"><a href="#三、平面反射的优化" class="headerlink" title="三、平面反射的优化"></a>三、平面反射的优化</h1><p>平面反射由于需要对场景镜像渲染一遍, DrawCall会翻倍,而且由于原理限制,没有有效的优化手段,因此平面反射通常是应用在特定的场合下。<br>优化的手段,主要是降低生成反射RT的消耗。</p><h2 id="3-1-控制反射层级"><a href="#3-1-控制反射层级" class="headerlink" title="3.1 控制反射层级"></a>3.1 控制反射层级</h2><p>我们可以在反射脚本中增加层级控制,然后设置反射相机的cullingMask,指定层级的物体才会被渲染到RT中。</p><h2 id="3-2-控制反射RT的尺寸"><a href="#3-2-控制反射RT的尺寸" class="headerlink" title="3.2 控制反射RT的尺寸"></a>3.2 控制反射RT的尺寸</h2><p>可以根据反射平面的大小来调整RT的尺寸,同样我们可以在脚本中开放这个尺寸设置来方便美术来调整RT大小。</p><h2 id="3-3-降低RT的shader复杂度"><a href="#3-3-降低RT的shader复杂度" class="headerlink" title="3.3 降低RT的shader复杂度"></a>3.3 降低RT的shader复杂度</h2><p>我们可以使用Unity的shader replacement将生成RT的shader都替换为一个简单的shader,然后再渲染生成RT,这样可以大幅度降低shader计算复杂度。不过,DrawCall是无法降低的。</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><blockquote><p><a href="https://blog.csdn.net/puppet_master/article/details/80808486">Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)</a><br><a href="https://blog.csdn.net/mobilebbki399/article/details/79491863">图形与渲染】相机平面镜反射与斜裁剪矩阵(下)-斜裁剪矩阵</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>平面反射通常指的是在镜子或者光滑地面的反射效果上,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/m</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="反射效果" scheme="http://xiaopengcheng.top/tags/%E5%8F%8D%E5%B0%84%E6%95%88%E6%9E%9C/"/>
<category term="平面反射" scheme="http://xiaopengcheng.top/tags/%E5%B9%B3%E9%9D%A2%E5%8F%8D%E5%B0%84/"/>
</entry>
<entry>
<title>游戏引擎渲染管线的总结</title>
<link href="http://xiaopengcheng.top/2020/12/22/%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E7%9A%84%E6%80%BB%E7%BB%93/"/>
<id>http://xiaopengcheng.top/2020/12/22/%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E7%9A%84%E6%80%BB%E7%BB%93/</id>
<published>2020-12-22T12:15:00.000Z</published>
<updated>2022-03-26T10:13:33.610Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>需要注意的是,本文涉及的内容过多过杂,基本涉及到游戏渲染和图形管线的方方面面。内容是根据多方面的资料整理而成,比如本人的Unity和Unreal引擎相关的理解和认知,以及引擎相关官方文档等,以及DirectX和OpenGL相关官方文档等,以及网络上各种相关文章和资料等。可能有一些纰漏或者不足之处,或者有些阶段的资料来源较为单一,本人主要目的是从概念理解上对应整个游戏引擎的渲染管线,不一定和真实的游戏完全一一对应,比如应用程序阶段的知识对应到游戏引擎应该会有一些区别和取舍,几何阶段和光栅化阶段主要参考的是OpenGL和DirectX,Vulkan和Metal相关资料参考较少,可能不同的图形API会有一些出入。由于涉及内容过多,难免理解不到位,有发现比较明显错误的,请指出以尽早修正,避免造成误解。</p><h1 id="一、渲染管线的思维导图"><a href="#一、渲染管线的思维导图" class="headerlink" title="一、渲染管线的思维导图"></a>一、渲染管线的思维导图</h1><p>这是本文内容的思维导图,通过该图可以从整体上把握全文的内容,对渲染管线有整理的理解。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/渲染管线.png"></p><h1 id="二、应用程序阶段"><a href="#二、应用程序阶段" class="headerlink" title="二、应用程序阶段"></a>二、应用程序阶段</h1><h2 id="2-1-渲染数据加载"><a href="#2-1-渲染数据加载" class="headerlink" title="2.1 渲染数据加载"></a>2.1 渲染数据加载</h2><p>这个阶段指的是将渲染所需要的相关数据,比如模型、贴图、材质、Shader等加载到内存中,通常只发生一次,不需要每帧重复加载。比如,Unity游戏需要在运行时,将需要的场景或者人物从AssetBundle中加载出来,然后引擎才能显示加载的场景或者人物。</p><h2 id="2-2-物体级别的裁剪"><a href="#2-2-物体级别的裁剪" class="headerlink" title="2.2 物体级别的裁剪"></a>2.2 物体级别的裁剪</h2><p>以下描述的裁剪算法是按照粒度从粗到细的裁剪,相应复杂度和代价也是在递增。最简单的是基于距离的裁剪;然后是利用空间数据结构实现的视锥体裁剪;动态的入口裁剪是一种特殊情况,可以算在视口裁剪内也可以用于预计算;然后预计算数据的裁剪;接下来才是动态的遮挡剔除。</p><h3 id="2-2-1-基于距离的裁剪"><a href="#2-2-1-基于距离的裁剪" class="headerlink" title="2.2.1 基于距离的裁剪"></a>2.2.1 基于距离的裁剪</h3><p>思路是超过一定的视距即不渲染该物体,Unreal引擎支持这个特性,参考<a href="https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/VisibilityCulling/CullDistanceVolume/">Cull Distance Volumes</a>。对于Unity,可以使用<a href="https://docs.unity3d.com/Manual/CullingGroupAPI.html">CullingGroup</a>实现类似的功能。即使引擎没有提供类似的支持,在游戏逻辑层面,先可以每帧或者隔帧判断物体跟摄像机的距离,来动态显示隐藏物体。</p><h3 id="2-2-2-视锥体裁剪"><a href="#2-2-2-视锥体裁剪" class="headerlink" title="2.2.2 视锥体裁剪"></a>2.2.2 视锥体裁剪</h3><p>用物体跟摄像机视锥体做相交测试,将完全没有相交的物体过滤掉。为了加快速度,使用的是物体的包围盒或者包围球跟视锥体做相交测试。游戏引擎内一般都会有空间数据结构来组织物体,比如BVH,那么可以直接使用BVH来搜索加速这个计算。具体过程是用视锥体和空间数据结构去做相交测试,如果当前节点没有相交,那么不需要继续,如果有相交则继续遍历子节点直到叶子节点或者没有相交,叶子节点中存储的物体即是需要渲染的物体。</p><h4 id="基于空间数据结构的裁剪"><a href="#基于空间数据结构的裁剪" class="headerlink" title="基于空间数据结构的裁剪"></a>基于空间数据结构的裁剪</h4><h5 id="四叉树和八叉树"><a href="#四叉树和八叉树" class="headerlink" title="四叉树和八叉树"></a>四叉树和八叉树</h5><p>四叉树对应的是二维空间,下面以八叉树为例来说明。八叉树是将三维空间平均划分为八个部分作为八个子节点,重复划分到一定的粒度为止,比如叶子节点内最多存储多少个物体,物体存储在叶子节点内。<br>优点:概念和实现简单。<br>缺点:无限空间不好划分;物体可能跨越分割面;物体分布不均匀会造成层次过深,搜索效率不高。<br>适用场景:四叉树适用于基于高度场的地形管理;八叉树室适用于室外分布均匀的三维场景(有高度)。</p><h5 id="BSP"><a href="#BSP" class="headerlink" title="BSP"></a>BSP</h5><p>针对八叉树这种不均匀划分,如果将物体均匀划分成两部分,那么就是Binary Space Partition Tree,可以避免树的层次过深。注意,BSP的每个节点存储的是划分平面,而不是物体,划分平面将场景分为前后2个部分,分别对应左右子树;由于需要BSP树针对的多边形,因此可以针对物体的AABB包围盒做划分。</p><p>优点:物体分布均匀,不会出现树层次过深;支持任意空间。<br>缺点:实现复杂,构造时间长,不适合动态场景。<br>适用场景:紧凑并且分布均匀的室内场景;静态场景;自带物体排序,方便实现画家算法。</p><h5 id="KD-Tree"><a href="#KD-Tree" class="headerlink" title="KD-Tree"></a>KD-Tree</h5><p>BSP全称是K-Dimensional Tree。这是一种特殊的BSP,在BSP上进一步将划分面限制跟坐标轴垂直,但是保持从物体分布的中间划分,以尽可能得到一个物体分布均匀的树。KD-Tree不仅仅可以用来做空间划分,在其它领域经常用来组织K维度的数据来做搜索,比如K维数据查询、近邻查询。<br>优点:物体分布均匀,不会出现树层次过深;数据可以组织为数组形式的完全二叉树,缓存友好等。<br>缺点:如何确定最优或者较优的划分面?<br>适用场景:紧凑并且分布均匀的室内场景;辅助其它数据结构进行邻域查询。</p><h5 id="BVH"><a href="#BVH" class="headerlink" title="BVH"></a>BVH</h5><p>全名是Bounding Volume Hierarchy,中文翻译层次包围盒。BSP和KD-Tree的节点代表的都是分割面,但是面有可能穿过物体。层次包围盒的思想是每个节点代表一个空间,空间计算其包含物体的最小包围盒,划分空间后重新计算子空间的包围盒。与BSP最大区别是节点代表的不再是分割平面而是包含最小包围盒的子空间。因此,这些子空间可能出现一定的重叠,但是不会出现物体出现在不同的划分里面。<br>优点:节点存储的是物体,方便碰撞检测等查询;构建快,动态更新方便。<br>缺点:如何确定最优的包围盒?<br>适用场景:视锥剔除;物体碰撞检测;射线检测;光线跟踪。</p><h5 id="空间数据结构的其它应用"><a href="#空间数据结构的其它应用" class="headerlink" title="空间数据结构的其它应用"></a>空间数据结构的其它应用</h5><p>除了视锥体裁剪外,空间数据结构还有很多其它应用,比如<br>1、Ray Casting (射线检测)<br>2、碰撞检测<br>3、邻近查询 (比如查询玩家周围敌人)<br>4、光线追踪</p><h4 id="Portal-Culling(入口裁剪)"><a href="#Portal-Culling(入口裁剪)" class="headerlink" title="Portal Culling(入口裁剪)"></a>Portal Culling(入口裁剪)</h4><p>适用于将场景划分为格子,格子之间可能存在入口的情形,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/portal-diagram.png"><br>从入口只能看到部分被墙壁遮挡住的物体,因此可以借助这个特性加速视锥体和格子的相交裁剪。Unity中的<a href="https://docs.unity3d.com/Manual/class-OcclusionPortal.html">Occlusion Portal</a>即是这个特性。如果预计算出Protal Culling的结果,那么可以在运行时加快物体裁剪。</p><h3 id="2-2-3-预计算遮挡剔除"><a href="#2-2-3-预计算遮挡剔除" class="headerlink" title="2.2.3 预计算遮挡剔除"></a>2.2.3 预计算遮挡剔除</h3><p>这是一种空间换时间的算法,会增大内存占用,降低Cpu的裁剪消耗。所以是否需要预计算遮挡数据,还需要具体讨论。一般如果内存消耗不大,但是Cpu占用较高的话,可以尝试开启预计算遮挡数据。</p><h4 id="Precomputed-Visibility-UE4"><a href="#Precomputed-Visibility-UE4" class="headerlink" title="Precomputed Visibility (UE4)"></a>Precomputed Visibility (UE4)</h4><p>参考虚幻引擎的<a href="https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/VisibilityCulling/PrecomputedVisibilityVolume/">Precomputed Visibility</a>。思想是将场景划分为格子,计算每个格子内可以看到的可见物体集合相关的数据,用于运行时动态查询。</p><h4 id="预计算Occlusion-Culling-Unity"><a href="#预计算Occlusion-Culling-Unity" class="headerlink" title="预计算Occlusion Culling (Unity)"></a>预计算Occlusion Culling (Unity)</h4><p>参考Unity的<a href="https://docs.unity3d.com/Manual/OcclusionCulling.html">Occlusion culling</a>。类似于UE4的Precomputed Visibility,不过Unity的Occlusion Culling也支持动态物体,但是动态物体只能occludee(被遮挡物体)。Unity的预计算Occlusion Culling应该是入口剔除的一种预计算实现。</p><h3 id="2-2-4-动态遮挡查询"><a href="#2-2-4-动态遮挡查询" class="headerlink" title="2.2.4 动态遮挡查询"></a>2.2.4 动态遮挡查询</h3><p>这里讲的是在CPU上或者GPU上实现的遮挡查询。图形API已经提供了遮挡查询相关的接口,比如OpenGL的<a href="https://www.khronos.org/opengl/wiki/Query_Object">Query Object</a>或者DirectX的<a href="https://docs.microsoft.com/en-us/windows/win32/direct3d12/predication-queries">Predication Queries</a>。但是不是所有的硬件都能够支持,因此可以在软件层面即在CPU上做软渲染实现遮挡查询。Hierarchical Z-Buffer Occlusion则是在普通的硬件遮挡查询上的进一步优化,使用了层次Z-Buffer来进一步加快速度。</p><h4 id="软件遮挡查询"><a href="#软件遮挡查询" class="headerlink" title="软件遮挡查询"></a>软件遮挡查询</h4><p>软光栅化模仿硬件遮挡查询,因此不受设备类型限制,只是需要额外消耗CPU。</p><h4 id="硬件遮挡查询"><a href="#硬件遮挡查询" class="headerlink" title="硬件遮挡查询"></a>硬件遮挡查询</h4><p>使用图形接口本身提供的遮挡查询接口。基本思想是用物体的包围盒去渲染Z-Buffer,统计通过深度测试的像素数目,如果有通过说明当前物体没有被完全挡住,保存结果用于下一帧查询。因此,硬件遮挡查询会存在两个问题:额外的渲染消耗和延迟一帧。</p><h4 id="Hierarchical-Z-Buffer-Occlusion"><a href="#Hierarchical-Z-Buffer-Occlusion" class="headerlink" title="Hierarchical Z-Buffer Occlusion"></a>Hierarchical Z-Buffer Occlusion</h4><p>类似硬件遮挡查询,不过使用Hierarchical Z-Buffer来加快查询速度。具体实现比较复杂,请参考相关文章。</p><h3 id="2-2-5-LOD切换"><a href="#2-2-5-LOD切换" class="headerlink" title="2.2.5 LOD切换"></a>2.2.5 LOD切换</h3><p>LOD指的是Level Of Details。如果物体通过了以上的裁剪,那么说明会提交给渲染线程进行处理。LOD切换指的是这些物体的细节层次切换,比如一些不重要的或者看不清楚的物体选择更简单的模型。</p><h4 id="基于距离的LOD切换"><a href="#基于距离的LOD切换" class="headerlink" title="基于距离的LOD切换"></a>基于距离的LOD切换</h4><p>最常见的方式是根据摄像机距离来进行LOD切换,越远的物体选择更简略的LOD,Unity和UE4默认是这种方式。</p><h4 id="基于渲染分级切换LOD"><a href="#基于渲染分级切换LOD" class="headerlink" title="基于渲染分级切换LOD"></a>基于渲染分级切换LOD</h4><p>但是我们也可以主动切换LOD,比如检测到当前硬件较差,需要切换到更低的画质,那么可以根据游戏设置的渲染品质分级来切换低的LOD。</p><h4 id="LOD过渡"><a href="#LOD过渡" class="headerlink" title="LOD过渡"></a>LOD过渡</h4><p>LOD的一个常见问题是LOD的过渡问题,可能在切换LOD时候会察觉到明显的过渡。常见的方式是在切换时候混合2个LOD,比如透明度逐渐从1变化到0或者从0变化到1,避免出现明显的过渡。</p><h2 id="2-3-物体级别的渲染排序"><a href="#2-3-物体级别的渲染排序" class="headerlink" title="2.3 物体级别的渲染排序"></a>2.3 物体级别的渲染排序</h2><p>为了减少OverDraw或者实现半透明效果,所有通过裁剪的物体会按照一定的次序进行渲染。下面列举几个常见的渲染次序。游戏引擎实际的渲染过程还会跟引擎渲染管线的Pass定义顺序相关,比如不透明和透明物体在不同的Pass内渲染的,而且是先在一个Pass内渲染透明物体,再在另外一个Pass渲染透明物体。</p><h3 id="从前到后渲染(不透明物体)"><a href="#从前到后渲染(不透明物体)" class="headerlink" title="从前到后渲染(不透明物体)"></a>从前到后渲染(不透明物体)</h3><p>从前到后渲染可以利用Early Z-Test过滤掉不必要的片元处理。因此,如果先渲染近处的物体,那么后面渲染的远处物体就不会通过Early Z-Test,就不会进入片段处理阶段。不过,不是所有的硬件都需要按照从前到后的物体顺序进行渲染,这毕竟需要额外的CPU消耗来排序物体,部分支持HSV(hidden surface removal)特性的GPU,比如PowerVR是不需要做这个排序的。Unity提高了静态变量SystemInfo.hasHiddenSurfaceRemovalOnGPU来查询GPU是否支持HSV,<br>Urp渲染管线会根据这个来判断是否需要跳过从前到后排序物体。</p><h3 id="从后到前渲染(半透明物体)"><a href="#从后到前渲染(半透明物体)" class="headerlink" title="从后到前渲染(半透明物体)"></a>从后到前渲染(半透明物体)</h3><p>由于半透明物体的渲染算法要求必须从后到前渲染物体,同时关闭深度测试 ,前面的物体与后面的物体进行颜色混合。那么这个排序过程是无法省掉的,类似从前到后渲染的排序,可以采样BSP来排序物体。</p><h3 id="渲染层级或渲染队列"><a href="#渲染层级或渲染队列" class="headerlink" title="渲染层级或渲染队列"></a>渲染层级或渲染队列</h3><p>Unity同时定义了这2种排序,不过SortingLayer的优先级更高,这个是定义在物体的Renderer组件上。RenderQueue是定义在Shader和材质上,优先级在渲染层级之后。理论上,就是对所有物体进行优先级排序。</p><h3 id="最少渲染状态切换"><a href="#最少渲染状态切换" class="headerlink" title="最少渲染状态切换"></a>最少渲染状态切换</h3><p>还有一种方式是尽可能在渲染物体的时候避免渲染状态切换,这样能够尽可能减少CPU消耗。那么可以在CPU计算出来一个最优的渲染顺序来尽可能减少渲染状态切换。</p><h2 id="2-4-渲染数据绑定和状态设置"><a href="#2-4-渲染数据绑定和状态设置" class="headerlink" title="2.4 渲染数据绑定和状态设置"></a>2.4 渲染数据绑定和状态设置</h2><p>这一个阶段讲的是在CPU上设置渲染相关数据和状态,以及为了减少渲染状态切换的渲染合批的思想。</p><h3 id="视口设置"><a href="#视口设置" class="headerlink" title="视口设置"></a>视口设置</h3><p>设置窗口的渲染区域,比如OpenGL的<a href="https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glViewport.xhtml">glViewport</a>。通过这个设置,我们可以在一个窗口上渲染多个不同的视口,比如游戏的分屏。</p><h3 id="FrameBuffer设置"><a href="#FrameBuffer设置" class="headerlink" title="FrameBuffer设置"></a>FrameBuffer设置</h3><p>一般游戏引擎不会直接将物体渲染到默认的渲染缓冲上,单独的RenderTarget方便进行后处理,在后处理之后再Blit到默认缓冲上。一个FrameBuffer可以包含颜色、深度、模板三个附件,也可以将深度和模板组织成一个32位的RT。</p><h3 id="渲染合批"><a href="#渲染合批" class="headerlink" title="渲染合批"></a>渲染合批</h3><p>渲染合批指的是为了减少渲染状态切换的一种优化手段,Unity URP渲染管线的SRP技术可以大幅度优化渲染批次。这是一个在Shader变体层次的合批,与之前的材质层次的合批相比有很大的优化。</p><h3 id="顶点输入绑定"><a href="#顶点输入绑定" class="headerlink" title="顶点输入绑定"></a>顶点输入绑定</h3><p>对于OpenGL来说就是创建和绑定VAO(Vertex Array Object)。一个VAO中可以包含VBO(Vertex Buffer Object)、IBO(Index Buffer Object)。然后用glVertexAttribPointer和glEnableVertexAttribArray指定数据到Shader的输入变量。<br>顶点属性通常包括,位置、法线、切线、UV、顶点颜色等。</p><h3 id="Shader绑定"><a href="#Shader绑定" class="headerlink" title="Shader绑定"></a>Shader绑定</h3><p>渲染数据绑定好之后,需要指定当前使用的Shader,这包括Shader的编译链接和使用等(假设Shader代码已经加载进来)。</p><h4 id="Shader编译链接使用"><a href="#Shader编译链接使用" class="headerlink" title="Shader编译链接使用"></a>Shader编译链接使用</h4><p>类似于CPU上运行的程序,Shader也需要编译链接以及开始使用的过程,不过这个过程基本上是固定。<br>可以参考learnopengl的<a href="https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/">着色器</a>一节。</p><h4 id="Uniform变量绑定"><a href="#Uniform变量绑定" class="headerlink" title="Uniform变量绑定"></a>Uniform变量绑定</h4><p>Shader中通常会有很多全局变量,比如MVP、摄像机位置、光的信息等。这些都需要在CPU上传入Shader中。</p><h3 id="Output-Merger-Stage相关设置"><a href="#Output-Merger-Stage相关设置" class="headerlink" title="Output-Merger Stage相关设置"></a>Output-Merger Stage相关设置</h3><p>在渲染管线的最后(片元着色器之后),有一个Output-Merger阶段,也叫做Raster Operations。这是一个不可编程阶段,但是有很多选择可以设置。比如剪切测试、模板测试、深度测试、颜色混合因子和函数、sRGB转换等。这些都需要在应用程序阶段进行设置。</p><h2 id="2-5-DrawCall调用"><a href="#2-5-DrawCall调用" class="headerlink" title="2.5 DrawCall调用"></a>2.5 DrawCall调用</h2><p>终于到了应用程序的最后一步,即DrawCall的调用了。OpenGL对应的接口是<a href="https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glDrawArrays.xhtml">glDrawArrays</a>或者<a href="https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glDrawElements.xhtml">glDrawElements</a>。</p><h1 id="三、几何处理阶段"><a href="#三、几何处理阶段" class="headerlink" title="三、几何处理阶段"></a>三、几何处理阶段</h1><p>这是第二个大的阶段,当前阶段已经进行GPU中了。该阶段的起点和主要过程是顶点着色器。除了着色器之外,其余阶段都是硬件自动进行的,除了可选阶段之外,其余的都是固定的,应用程序无法根据配置来进行更改。</p><h2 id="3-1-顶点着色器"><a href="#3-1-顶点着色器" class="headerlink" title="3.1 顶点着色器"></a>3.1 顶点着色器</h2><p>顶点着色器的处理对象是应用程序阶段绑定的每个顶点,顶点着色器会获得顶点属性以及相应的Uniform变量。顶点着色器的输出是一个NDC Clip Space的顶点位置。NDC(Normalized device coordinates)是规范化设备坐标系的位置,OpenGL的范围[-1,1],DirectX的范围是[0,1]。之所以说是Clip Space,因为该阶段得到的顶点数据是一个齐次坐标,还需要进行透视除法,即x、y、z除以w分量才能得到NDC坐标系下的位置。</p><h2 id="3-2-曲面细分着色器"><a href="#3-2-曲面细分着色器" class="headerlink" title="3.2 曲面细分着色器"></a>3.2 曲面细分着色器</h2><p>曲面细分着色器是一个可选阶段,用于将一个简单模型细分成复杂的模型。其实该阶段是2个着色器和一个固定阶段的组合。在DirectX中叫做Hull Shader stage、Tessellator stage、Domain Shader stage;在OpenGL的<a href="https://www.khronos.org/opengl/wiki/Tessellation">Tessellation</a>中叫做Tessellation Control Shader、Tessellation Primitive eneration、Tessellation Evaluation Shader。具体的介绍和使用方式请参考相关资料。</p><h2 id="3-3-几何着色器"><a href="#3-3-几何着色器" class="headerlink" title="3.3 几何着色器"></a>3.3 几何着色器</h2><p>几何着色器也是一个可选阶段。几何着色器的输入是图元的顶点集合(比如三角形图元有三个顶点,点图元只有一个顶点),输出是一个新的图元,新的图元也要包含一个顶点集合。简单来说,几何着色器的输入和输出都是图元,输入的图元是在应用程序阶段指定的,输出的图元可以在顶点着色器中实现。</p><h2 id="3-4-Stream-Output-(Transform-Feedback)"><a href="#3-4-Stream-Output-(Transform-Feedback)" class="headerlink" title="3.4 Stream Output (Transform Feedback)"></a>3.4 Stream Output (Transform Feedback)</h2><p>这是一个可选的阶段。这个阶段在DirectX中叫做Stream Output ,在OpenGL找叫做Transform Feedback。如果该阶段开启,那么顶点数据流会输出到一个Buffer中,这个Buffer可以给顶点着色器使用也可以返回给CPU,当前渲染管线则不会进行接下来的处理。</p><h2 id="3-5-图元组装"><a href="#3-5-图元组装" class="headerlink" title="3.5 图元组装"></a>3.5 图元组装</h2><p>这一步是将之前得到的顶点数据组合成图元,比如顶点图元、线段图元、三角形图元。该阶段输出图元进行接下来的处理。</p><h2 id="3-6-透视除法和NDC裁剪"><a href="#3-6-透视除法和NDC裁剪" class="headerlink" title="3.6 透视除法和NDC裁剪"></a>3.6 透视除法和NDC裁剪</h2><p>该阶段的输入是组装好的图元,输出的是NDC裁剪之后的图元。首先对图元的顶点进行透视除法,这样得到的顶点数据都会位于NDC内,方便进行NDC裁剪。图元裁剪后可以会产生新的图元。</p><h2 id="3-7-屏幕空间映射"><a href="#3-7-屏幕空间映射" class="headerlink" title="3.7 屏幕空间映射"></a>3.7 屏幕空间映射</h2><p>该阶段是将NDC下的图元顶点坐标映射到屏幕空间。值得注意的是顶点坐标是一个齐次坐标,透视除法后得到的是NDC下的坐标;然后,通过一个缩放和平移变换将x和y映射到屏幕空间。</p><h2 id="3-8-面剔除-(Face-Culling)"><a href="#3-8-面剔除-(Face-Culling)" class="headerlink" title="3.8 面剔除 (Face Culling)"></a>3.8 面剔除 (Face Culling)</h2><p>这一个阶段指的是三角形的前后面剔除。前或者后的定义是根据正视三角形的时候定义三角形顶点的旋向,可以定义逆时针旋转或者顺时针旋转为前面。实际上,面剔除跟实际的摄像机位置没有关系,不管摄像机转到哪个地方,前后面不会改变,比如渲染立方体的时候,后面都是立方体内部看不到的面,无论摄像机如何旋转。因为,前后面的定义是固定视角正对三角形时候定义的。</p><h1 id="四、光栅化阶段"><a href="#四、光栅化阶段" class="headerlink" title="四、光栅化阶段"></a>四、光栅化阶段</h1><p>该大的阶段的输入是几何处理阶段输出的图元。该阶段主要分为四个部分,首先是光栅化图元得到片元(潜在的像素信息),然后进行Early Fragment Test,通过测试后再进行片元着色器,最终进行输出合并阶段的各种测试以及颜色混合等,再输出到颜色缓冲区。</p><h2 id="4-1-图元光栅化"><a href="#4-1-图元光栅化" class="headerlink" title="4.1 图元光栅化"></a>4.1 图元光栅化</h2><p>该阶段是将图元的顶点信息进行线性插值,然后生成片元数据。每个片元上有顶点信息线性插值而来的片元数据。需要注意的是,这个插值是线性的,如果有一些数据是非线性的,则不能在顶点着色器中计算然后输出到片元着色器,因为线性插值的结果和在片元着色器中计算的结果是不一致的。<br>这里需要特别说明的是,关于深度z’的生成。屏幕空间映射后的z’是关于摄像机空间z倒数的一个线性函数。之所以使用1/z而不是z,是为了在近处获得更好的深度缓冲精度,因为1/z在近处的变化更快,可以优化Z-Fighting这种现象。由于z’不是一个关于z的线性函数,因此z’应该是在光栅化后硬件自动根据1/z计算出来的,而不是先计算z’再光栅化。 </p><h2 id="4-2-Early-Fragment-Test"><a href="#4-2-Early-Fragment-Test" class="headerlink" title="4.2 Early Fragment Test"></a>4.2 Early Fragment Test</h2><p>参考OpenGL的<a href="https://www.khronos.org/opengl/wiki/Early_Fragment_Test">Early Fragment Test</a>,可以看到不仅仅通常所说的Early Z-Test还有其它好几个阶段都可以进行EarlyTest,一共是四个测试(Pixel ownership test、Scissor test、<br> Stencil test、Depth test)和遮挡查询更新。根据文档,Pixel ownership test和Scissor test从OpenGL4.2起会总是在EarlyTest阶段进行。那么,如果这些测试没有在EarlyTest阶段进行,则会在最终的输出合并阶段进行;如果进行了,那么输出合并阶段也不会重复处理。</p><h3 id="4-3-Early-Z-Test的限制"><a href="#4-3-Early-Z-Test的限制" class="headerlink" title="4.3 Early Z-Test的限制"></a>4.3 Early Z-Test的限制</h3><p>不要在片元着色器中改变深度,比如glsl的gl_FragDepth;也不要discard片元,通常实现AlphaTest会根据Alphadiscard片元。因为这些操作会导致硬件无法预测最终的深度,从而无法进行提前深度测试。</p><h2 id="4-4-片段着色器"><a href="#4-4-片段着色器" class="headerlink" title="4.4 片段着色器"></a>4.4 片段着色器</h2><p>片段着色器的输入是光栅化来的各种顶点属性,输出是一个颜色值。该阶段是计算光照结果的主要阶段。通常片元着色器会有比较复杂的计算,通常的优化手段是将计算转移到顶点着色器甚至CPU(应用程序阶段,用Uniform传入)上。</p><h2 id="4-5-Output-Merger-Stage-Raster-Operations"><a href="#4-5-Output-Merger-Stage-Raster-Operations" class="headerlink" title="4.5 Output-Merger Stage(Raster Operations)"></a>4.5 Output-Merger Stage(Raster Operations)</h2><p>终于进入最后的输出合并阶段,该阶段的输入是一个个的片元。片元需要进行一些列的测试和转换,最终才会将颜色输出到缓冲区上。</p><h3 id="Pixel-ownership-test"><a href="#Pixel-ownership-test" class="headerlink" title="Pixel ownership test"></a>Pixel ownership test</h3><p>根据OpenGL的文档,该阶段只对默认缓冲区生效,用于测试像素是否被其它窗口遮挡的情形。对于自定义的FrameBuffer,不存在这个测试。</p><h3 id="Alpha-Test"><a href="#Alpha-Test" class="headerlink" title="Alpha Test"></a>Alpha Test</h3><p>需要特别说明的是,Alpha测试当前是已经被废弃了,从DirectX10和OpenGL3.1开始废弃,参考<a href="https://www.khronos.org/opengl/wiki/Transparency_Sorting">Transparency Sorting</a>文档;当前需要在片元着色器用discard实现。列在这里主要是为了完整性。</p><h3 id="Scissor-test"><a href="#Scissor-test" class="headerlink" title="Scissor test"></a>Scissor test</h3><p>参考OpenGL的剪切测试文档,<a href="https://www.khronos.org/opengl/wiki/Scissor_Test">Scissor Test</a>。通过在应用程序阶段设置,可以让片元只通过视口的一个小矩形区域。根据EarlyTest的文档,推测该阶段目前都在EarlyTest阶段进行了。</p><h3 id="Multisample-operations"><a href="#Multisample-operations" class="headerlink" title="Multisample operations"></a>Multisample operations</h3><p>如果启用了MSAA,那么需要进行resolve才能够输出到默认颜色缓冲中,进行屏幕显示。假如在默认缓冲中开了MSAA,那么从MSAA的后备缓冲交换到前向缓冲就需要进行resolve操作,因为前向缓冲是single-sample的。如果是自定义的FrameBuffer开启了MSAA,那么在Blit到默认缓冲区的时候也需要进行resolve操作。</p><h3 id="模板测试"><a href="#模板测试" class="headerlink" title="模板测试"></a>模板测试</h3><p>模板测试基本思想是用一个八位的模板缓冲,一个参考值,一个比较函数,一个掩码,用该参考值和片元对应的模板缓冲值使用比较进行比较(比较之前进行掩码),通过的则片元可以继续进行深度测试,否则丢弃。另外还可以定义模板成功和失败,以及深度测试成功和失败后模板缓冲如何变化。可以参考OpenGL的<a href="https://www.khronos.org/opengl/wiki/Stencil_Test">Stencil Test</a>文档。<br>模板测试的一个常见的应用是描边或者在像素级别分类。</p><h3 id="深度测试"><a href="#深度测试" class="headerlink" title="深度测试"></a>深度测试</h3><p>深度测试是根据当前片元的深度值与深度缓冲进行比较,比较函数可以设置,通过比较的片元才会进行接下来的处理,否则丢弃当前片元。</p><h3 id="遮挡查询更新"><a href="#遮挡查询更新" class="headerlink" title="遮挡查询更新"></a>遮挡查询更新</h3><p>参考OpenGL的遮挡查询文档<a href="https://www.khronos.org/opengl/wiki/Query_Object#Occlusion_queries">Query Object</a>。<br>该阶段会更新遮挡查询的结果,因此遮挡查询的结果只能用于下一帧渲染。</p><h3 id="颜色混合"><a href="#颜色混合" class="headerlink" title="颜色混合"></a>颜色混合</h3><p>需要注意的是,容易误解半透明渲染才会有颜色混合,实际上颜色混合是管线的一个固定的阶段,不透明渲染也会有默认的混合方式。<br>理解颜色混合,首先要明白2个概念,source和dest,source指的是当前的片元,dest指的是要目标缓冲中对应的颜色。<br>颜色混合主要是需要设置2个函数,一个函数用于设置混合因子,一个函数用来设置混合函数。混合因子有四种,source rgb和dest rgb,source a和dest a,可以一起指定也可以分开指定。具体可以参考OpenGL的<a href="https://www.khronos.org/opengl/wiki/Blending">Blending</a>文档。</p><h3 id="sRGB转换"><a href="#sRGB转换" class="headerlink" title="sRGB转换"></a>sRGB转换</h3><p>1、我们知道显示器或者颜色纹理的颜色空间是sRgb,sRGB空间就是Gamma校正的颜色空间,也就是已经Gamma校正过的颜色数据,这样子在显示器上才能正常显示。如果我们使用的线性工作流,也就是在线性空间中制作资源,编写Shader计算光照结果,那么片元着色器的输出需要转换到sRgb空间。这个转换部分硬件上是自动支持,对于不支持的硬件则需要在Shader里面转换。<br>2、如果要硬件自动转换,首先要创建的必须是srgb颜色空间的FrameBuffer,在OpenGL中可以使用glEnable(GL_FRAMEBUFFER_SRGB)开启;要保证片元输出的线性空间的颜色,也就是要采用线性工作流。<br>3、需要注意的是,避免将sRGB转换和ToneMaping混合起来,ToneMaping做的是将HDR映射到LDR。这只是一个带偏向性颜色范围映射,也就是算法倾向性的增强部分颜色。而sRGB转换才是将颜色从线性空间转换到sRGB空间。</p><h3 id="Dithering"><a href="#Dithering" class="headerlink" title="Dithering"></a>Dithering</h3><p>首先说明一下,颜色格式分为Float、Normalized Integer、Integer三种,默认缓冲区就是Normalized Integer格式的颜色。根据OpenGL的文档,当将一个Float颜色写入Normalized Integer缓冲区的时候,可以开启Dithering。Normalized Integer缓冲区是一个定点数缓冲来存储浮点值,比如通常我们的颜色是定义在[0,\1]的浮点值,但是颜色缓冲是[0,254]\的Int值,OpenGL会自动进行转换。</p><h3 id="Logic-operations"><a href="#Logic-operations" class="headerlink" title="Logic operations"></a>Logic operations</h3><p>根据OpenGL的文档,当将颜色写入Integer(Normalized Or Not)缓冲区的时候,可以开启Logic operations。这是一些Bool操作。具体可以参考文档<a href="https://www.khronos.org/opengl/wiki/Logical_Operation">Logical Operation</a>。Logical Operations在sRGB颜色空间是禁止的。</p><h3 id="Write-mask"><a href="#Write-mask" class="headerlink" title="Write mask"></a>Write mask</h3><p>该阶段可以分别指定Color、Depth、Stencil的写入掩码。具体参考文档<a href="https://www.khronos.org/opengl/wiki/Write_Mask">Write Mask</a>。</p><h1 id="五、RenderPass"><a href="#五、RenderPass" class="headerlink" title="五、RenderPass"></a>五、RenderPass</h1><h2 id="5-1-Renderer"><a href="#5-1-Renderer" class="headerlink" title="5.1 Renderer"></a>5.1 Renderer</h2><p>以上所有内容在游戏引擎只是一个RenderPass,实际情况下,每帧游戏引擎会按照一定的顺序渲染多个Pass。比如,深度Pass(或者深度法线Pass)、阴影Pass、不透明物体Pass、透明物体Pass、后处理Pass等;而且后面的Pass会利用前面的Pass渲染结果来处理,比如深度Pass渲染的深度纹理可以用在后续的Pass实现一些效果。<br>总而言之,真实的游戏引擎是每帧渲染多个Pass,每个Pass对应上述的内容。</p><h2 id="5-2-CameraStack"><a href="#5-2-CameraStack" class="headerlink" title="5.2 CameraStack"></a>5.2 CameraStack</h2><p>实际上,在Unity的Urp渲染管线中,更完整的过程是渲染相机堆栈->每个相机堆栈对应一个渲染器->每个渲染器包含多个Pass。不过,Urp里面每个相机堆栈只对应一个FrameBuffer,也就是所有的相机渲染输出都是这一个FrameBuffer,避免内存和带宽浪费。如果在场景内创建多个相机堆栈,那么其它的相机堆栈的输出应该是离屏RT。</p><h1 id="六、参考资料"><a href="#六、参考资料" class="headerlink" title="六、参考资料"></a>六、参考资料</h1><blockquote><p>1、<a href="https://docs.microsoft.com/en-us/windows/uwp/graphics-concepts/graphics-pipeline">Graphics pipeline</a><br>2、<a href="https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview">Rendering Pipeline Overview</a><br>3、<a href="https://www.khronos.org/opengl/wiki/Per-Sample_Processing">Per-Sample Processing</a><br>4、<a href="https://docs.microsoft.com/en-us/windows/uwp/graphics-concepts/output-merger-stage--om-">Output Merger (OM) stage</a><br>5、<a href="https://blog.codingnow.com/2020/07/culling_space.html">裁剪和空间管理</a><br>6、<a href="https://zhuanlan.zhihu.com/p/138295496">[总结] 漫谈HDR和色彩管理(三)SDR和HDR</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>需要注意的是,本文涉及的内容过多过杂,基本涉及到游戏渲染和图形管线的方方面面。内容是根据多方面的资料整理而成,比如本人的Unity和Unreal引擎相关的理解和认知,以及引擎相关官方文档等,以及DirectX和OpenGL相</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="渲染管线" scheme="http://xiaopengcheng.top/tags/%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF/"/>
</entry>
<entry>
<title>渲染管线中的坐标空间和相关变换</title>
<link href="http://xiaopengcheng.top/2020/10/19/%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E4%B8%AD%E7%9A%84%E5%9D%90%E6%A0%87%E7%A9%BA%E9%97%B4%E5%92%8C%E7%9B%B8%E5%85%B3%E5%8F%98%E6%8D%A2/"/>
<id>http://xiaopengcheng.top/2020/10/19/%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF%E4%B8%AD%E7%9A%84%E5%9D%90%E6%A0%87%E7%A9%BA%E9%97%B4%E5%92%8C%E7%9B%B8%E5%85%B3%E5%8F%98%E6%8D%A2/</id>
<published>2020-10-19T13:20:00.000Z</published>
<updated>2022-03-28T06:37:55.187Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、思维导图"><a href="#一、思维导图" class="headerlink" title="一、思维导图"></a>一、思维导图</h1><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/坐标空间.png"></p><h1 id="二、模型空间"><a href="#二、模型空间" class="headerlink" title="二、模型空间"></a>二、模型空间</h1><p>这里的模型空间指的是建模出来的空间,也就是用建模软件输出的数据所在的坐标空间。比如,3D Max用的是右手系,输出的模型数据所在的空间就叫模型空间;由于Unity的模型空间是左手系,所以通常需要旋转90度才能对应上。</p><h1 id="三、切线空间"><a href="#三、切线空间" class="headerlink" title="三、切线空间"></a>三、切线空间</h1><p>切线空间又可以叫做纹理空间。假如纹理坐标uv构成一个二维空间,加上垂直于这个二维空间的法线,那么就是一个三维的切线空间。</p><h2 id="3-1-法线贴图的切线空间"><a href="#3-1-法线贴图的切线空间" class="headerlink" title="3.1 法线贴图的切线空间"></a>3.1 法线贴图的切线空间</h2><p>切线空间有什么应用了?我们在计算光照模型的时候,通常会有更精细表示法线的数据,比如法线贴图,法线贴图通常是建模软件用高模计算出来的。不过,法线贴图是原始切线空间下的数据。因此,法线贴图中的法线数据通常是(0,0,1),所以法线贴图表现出大部分是蓝色。我们在读取这个法线数据后,需要将其变换到计算光照模型所在的空间,比如世界空间。</p><h2 id="3-2-模型空间下的切线空间"><a href="#3-2-模型空间下的切线空间" class="headerlink" title="3.2 模型空间下的切线空间"></a>3.2 模型空间下的切线空间</h2><p>顶点上除了位置数据外,还可以有法线、切线数据。注意,这些数据都是在模型空间的。因此,法线、切线、副切线(法线和切线叉积计算出来)自然可以构成一个模型空间下的切线空间。</p><h2 id="3-3-切线变换"><a href="#3-3-切线变换" class="headerlink" title="3.3 切线变换"></a>3.3 切线变换</h2><p>假如我们想将切线空间下的法线变换到世界空间,该如何做了?我们需要得到一个世界空间下的切线空间。首先将<strong>模型空间下的切线空间变换到世界空间</strong>,这样我们就得到了一个世界空间下的切线子空间,然后用这个切线子空间构成一个切线变换,再对切线空间下的法线数据应用这个切线变换就能变换到世界空间。<br>用公式来表示这个变换是,$NormalWS=TangentMatrix*NormalTS$。当然也可以将切线变换到其它的空间,比如摄像机空间,区别是构造不同的TangentMatrix。</p><h1 id="四、关节空间"><a href="#四、关节空间" class="headerlink" title="四、关节空间"></a>四、关节空间</h1><h2 id="4-1-关节空间"><a href="#4-1-关节空间" class="headerlink" title="4.1 关节空间"></a>4.1 关节空间</h2><p><strong>这里的关节空间,指的是带骨骼的模型中,骨骼或者关节所定义的局部空间。</strong><br>以人体手指为假设,手指会受到腕关节、肘关节、肩关节影响,对应三个骨骼。那么,手指会依次受到这三个关节的牵扯影响。我们知道,虚拟的根骨骼Root所在的是模型空间,同时每个关节也定义了自己的局部空间,比如腕关节是最终的局部空间,我们把这个关节定义的局部空间叫做关节空间。</p><h2 id="4-2-关节姿势"><a href="#4-2-关节姿势" class="headerlink" title="4.2 关节姿势"></a>4.2 关节姿势</h2><p>所谓关节姿势,存储的是子关节到父关节的变换,包括旋转、缩放、平移,这个也可以叫做局部关节姿势。全局关节姿势是,将所有的局部关节姿势结合起来。<br>比如公式,$P<em>{2\to M} = P</em>{2\to 1} P<em>{1\to 0} P</em>{0\to M}$表示的是将顶点从子关节2的局部空间变换到模型空间。全局关节姿势可以表示为$P<em>{j\to M} = \prod </em>{i=j}^{0} P_{i\to p(i)}$,其中p(i)是关节i的父关节。</p><h2 id="4-3-绑定关节姿势"><a href="#4-3-绑定关节姿势" class="headerlink" title="4.3 绑定关节姿势"></a>4.3 绑定关节姿势</h2><p>我们知道,默认情况下,蒙皮骨骼都有一个T-Pose,即绑定姿势,也可以理解为初始姿势。模型空间的顶点乘以绑定姿势的逆变换就能得到关节空间的顶点。</p><h2 id="4-4-蒙皮矩阵"><a href="#4-4-蒙皮矩阵" class="headerlink" title="4.4 蒙皮矩阵"></a>4.4 蒙皮矩阵</h2><p>模型空间的顶点乘以绑定姿势的逆变换就能得到关节空间的顶点。关键点来了,这个时候再乘以骨骼的当前全局姿势矩阵,就又变换回了模型空间。所谓的蒙皮矩阵,就是这两个变换的结合。可以用公式表示骨骼i的蒙皮矩阵,$K<em>{j} = (B</em>{j\to M})^{-1} C_{j \to M}$,B代表绑定姿势,C代表当前姿势。多个蒙皮矩阵的加权,就能得到蒙皮动画。</p><h2 id="4-5-蒙皮动画"><a href="#4-5-蒙皮动画" class="headerlink" title="4.5 蒙皮动画"></a>4.5 蒙皮动画</h2><p>顶点会受到多个骨骼影响,这些骨骼的影响加权和为1,这个就是蒙皮动画。可以用公式来表示,$p’ = \sum_{i=1}^{n}W_i(p)K_ip$。其中,p是模型空间的顶点,Wi是骨骼i影响的权重,Ki是骨骼i的蒙皮矩阵。蒙皮矩阵的计算如上所示。</p><h2 id="4-6-总结"><a href="#4-6-总结" class="headerlink" title="4.6 总结"></a>4.6 总结</h2><p>根据以上五步的推导,蒙皮动画需要存储的数据是,</p><ul><li>绑定姿势下的的模型空间顶点</li><li>绑定关节姿势的逆矩阵</li><li>当前姿势数据(实时计算当前姿势矩阵)</li><li>蒙皮矩阵的权重</li></ul><h1 id="五、世界空间"><a href="#五、世界空间" class="headerlink" title="五、世界空间"></a>五、世界空间</h1><p>所谓世界空间,不需要解释了吧。放在游戏场景里面,指的是规定了场景坐标系的空间。模型空间,则指的是场景内的单个模型自身数据所在的空间。</p><h2 id="5-1-模型变换"><a href="#5-1-模型变换" class="headerlink" title="5.1 模型变换"></a>5.1 模型变换</h2><p>模型变换就是将模型空间的顶点数据变换到世界空间,通常包括对模型的平移、旋转和缩放。但是,一般要求的变换顺序是先缩放、再旋转、最后平移,如果反过来会造成平移受到前面的变换影响,与直观印象不符合。<br>所以,$ModelMatrix=TranslateMatrix*RotateMatrix*ScaleMatrix$。特别说明平移矩阵指的是,将模型从原点移动到其在世界空间的位置。</p><h1 id="六、摄像机空间"><a href="#六、摄像机空间" class="headerlink" title="六、摄像机空间"></a>六、摄像机空间</h1><p>摄像机空间也叫做观察空间。摄像机可以理解为世界空间的一个位置和朝向,比如在坐标(1,1,1)看向原点,那么射线的位置就是坐标(1,1,1),前向就是看向的方向(-1,-1,-1)。这个时候再定义一个垂直于forward的Up方向,就可以根据叉积找到垂直forward和up方向的right方向。这三个方向就可以构成一个摄像机空间。</p><h2 id="6-1-观察变换"><a href="#6-1-观察变换" class="headerlink" title="6.1 观察变换"></a>6.1 观察变换</h2><p>观察变换是将顶点从世界空间变换到新的摄像机空间。首先,需要将顶点平移,比如上述情况下的原点在观察空间下是(-1,-1,-1);然后,需要旋转顶点以匹配观察空间的坐标轴。由于观察空间的坐标轴是世界空间下的单位正交基,因此将三个轴放入矩阵即可得到旋转矩阵的逆矩阵(等于旋转矩阵的转置矩阵)。</p><h1 id="七、裁剪空间"><a href="#七、裁剪空间" class="headerlink" title="七、裁剪空间"></a>七、裁剪空间</h1><p>裁剪空间指的是观察空间下的顶点经过投影变换后所处的空间。我们知道,可视区域是摄像机前面的一个平截头体(透视投影)或者一个长方体(正交投影)。裁剪空间的用途是将可视区域外的物体裁剪,同时计算物体的二维坐标。</p><h2 id="7-1-投影变换"><a href="#7-1-投影变换" class="headerlink" title="7.1 投影变换"></a>7.1 投影变换</h2><p>投影分为透视投影和正交投影两种,透视投影会造成近大远小的效果,符合视觉效应,三维游戏一般使用透视投影,正交投影则远近一样大,通常只用于建模软件。</p><h3 id="透视投影"><a href="#透视投影" class="headerlink" title="透视投影"></a>透视投影</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/透视投影.jpg"><br>从图可以看出,透视投影后w是有值的,并不是1,结果还是一个平截头体。具体的矩阵推动,请参考相关资料。</p><h3 id="正交投影"><a href="#正交投影" class="headerlink" title="正交投影"></a>正交投影</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/正交投影.jpg"><br>从图可以看出,正交投影实际上就是缩放和平移的结合,得到的结果是一个-1到1范围的立方体。</p><h2 id="7-2-透视除法和图元裁剪"><a href="#7-2-透视除法和图元裁剪" class="headerlink" title="7.2 透视除法和图元裁剪"></a>7.2 透视除法和图元裁剪</h2><p>由于透视投影后齐次坐标的w非1,需要进行透视除法,这个是图形硬件自动进行的。如图所示:<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/透视除法.jpg"><br>透视除法后,可以得到和正交投影结果一样的规范化立方体,也叫做NDC(规范化设备坐标系)。<br>到了NDC后,就可以方便的进行图元裁剪,毕竟坐标都是-1到1了(DirectX下Z坐标是0到1)。</p><h2 id="7-3-顶点着色器的输出"><a href="#7-3-顶点着色器的输出" class="headerlink" title="7.3 顶点着色器的输出"></a>7.3 顶点着色器的输出</h2><p>顶点着色器必须输出裁剪空间下的坐标。由于DirectX的NDC的Z范围是0到1,与OpenGL的-1到1有一定区别。因此,这2者的投影矩阵在Z坐标上有一定的平移和缩放区别。</p><h1 id="八、屏幕空间"><a href="#八、屏幕空间" class="headerlink" title="八、屏幕空间"></a>八、屏幕空间</h1><p>投影变换后得到的顶点范围是-1到1。现在还需要将NDC下的顶点映射到屏幕空间。屏幕空间也可以叫做窗口空间,即窗口定义的坐标空间。</p><h2 id="8-1-视口变换"><a href="#8-1-视口变换" class="headerlink" title="8.1 视口变换"></a>8.1 视口变换</h2><p>假设,窗口坐标原点在左下角(OpenGL的原点在左下角,但是DirectX的在左上角),窗口大小为Width和Height。视口变换就是把-1到1的x和y坐标范围映射到(0,width)和(0,height),对应DirectX的话,y还需要取反。这就是一个平移和缩放的过程。</p><p>实际上,透视除法和视口变换(屏幕空间映射)都是硬件自动进行的。</p><h1 id="九、参考资料"><a href="#九、参考资料" class="headerlink" title="九、参考资料"></a>九、参考资料</h1><blockquote><p><a href="https://book.douban.com/subject/26821639/">Unity Shader入门精要</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、思维导图"><a href="#一、思维导图" class="headerlink" title="一、思维导图"></a>一、思维导图</h1><p><img alt="" data-src="https:/</summary>
<category term="渲染" scheme="http://xiaopengcheng.top/categories/%E6%B8%B2%E6%9F%93/"/>
<category term="渲染管线" scheme="http://xiaopengcheng.top/tags/%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF/"/>
<category term="坐标空间" scheme="http://xiaopengcheng.top/tags/%E5%9D%90%E6%A0%87%E7%A9%BA%E9%97%B4/"/>
<category term="坐标变换" scheme="http://xiaopengcheng.top/tags/%E5%9D%90%E6%A0%87%E5%8F%98%E6%8D%A2/"/>
</entry>
<entry>
<title>伽马校正和颜色空间</title>
<link href="http://xiaopengcheng.top/2020/08/26/%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3%E5%92%8C%E9%A2%9C%E8%89%B2%E7%A9%BA%E9%97%B4/"/>
<id>http://xiaopengcheng.top/2020/08/26/%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3%E5%92%8C%E9%A2%9C%E8%89%B2%E7%A9%BA%E9%97%B4/</id>
<published>2020-08-26T04:28:30.000Z</published>
<updated>2022-03-26T10:00:09.427Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、伽马校正"><a href="#一、伽马校正" class="headerlink" title="一、伽马校正"></a>一、伽马校正</h1><p>所谓gamma校正,实际上是一个颜色的非线性变换。下面来解释这个变换曲线存在的原因。</p><h2 id="1-1-人眼的非线性视觉效应"><a href="#1-1-人眼的非线性视觉效应" class="headerlink" title="1.1 人眼的非线性视觉效应"></a>1.1 人眼的非线性视觉效应</h2><p>为什么要有gamma校正了。一言以蔽之,人眼的生理效应。如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/gamma_correction_brightness.png"></p><p>第一行是人眼感受到的线性亮度变化,第二行是真实的非线性亮度变化。可以得出结论,首尾两端是一致的,但是中间值变化不一致;真实的中间亮度值必须更大,才能得到人眼感知的线性亮度变化。我们的目的是让人眼感受到线性的亮度变化曲线,因此输入亮度必须是第二行这种非线性的亮度变化曲线。<br>第二行的亮度变化曲线,就是<strong>伽马校正曲线</strong>。</p><h2 id="1-2-非线性显示器"><a href="#1-2-非线性显示器" class="headerlink" title="1.2 非线性显示器"></a>1.2 非线性显示器</h2><p>显示器为了应对人眼的这种非线性视觉效应,采用的也是类似的机制(也可能是历史原因,总之认为当今的显示器都是如此设计就行)。假设我们输入的颜色值,即输入给显示器的电压,那么这个电压对应的是1.1的第二行(Gamma校正曲线);人眼感受到的显示器的真实输出对应的是1.1的第一行(线性颜色输出),即gamma编码曲线。<br>如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/gamma_correction_gamma_curves.png"></p><p>这里反复强调了,人眼感受到的显示器亮度,而不是显示器的输出亮度。举个例子,<strong>输入颜色值是0.732的话,那么显示器经过gamma编码后输出的亮度是0.5,人眼感受到的亮度是0.218</strong>,刚好和人眼的视觉效应匹配。</p><p>值得强调的是,gamma指数2.2是可以变化的,在不同的场景下,可以选择不同的gamma指数。</p><h2 id="1-3-总结"><a href="#1-3-总结" class="headerlink" title="1.3 总结"></a>1.3 总结</h2><p>总结,照片是按照gamma校正曲线编码的,显示器经过gamma编码后,输出照片的亮度是线性曲线,人眼看到线性曲线的亮度后感知到的曲线是gamma曲线。<br>因此,我们需要确定输入的颜色数据是在线性曲线或者gamma校正曲线上。</p><h1 id="二、颜色空间和工作流"><a href="#二、颜色空间和工作流" class="headerlink" title="二、颜色空间和工作流"></a>二、颜色空间和工作流</h1><p>颜色空间可以理解为,颜色是在哪个空间下制作的。不需要特别多的数学曲线来描绘,但是这个说明又需要一点美术经验来理解。下面来具体分类解释。</p><h2 id="2-1-伽马颜色空间和工作流"><a href="#2-1-伽马颜色空间和工作流" class="headerlink" title="2.1 伽马颜色空间和工作流"></a>2.1 伽马颜色空间和工作流</h2><p>比如,我们拍摄的<strong>照片</strong>,人眼看起来是正确的,那么说明人眼感受到的是线性变化的,因此照片的数据是经过伽马校正的,也就是照片的数据变化是在gamma校正曲线上的。同样的,在电脑上<strong>使用软件制作的图片</strong>也是处于gamma校正曲线上的。<br>我们把这种颜色数据在gamma校正曲线上的,叫做gamma color space,也叫做sRGB。<br>那么,伽马工作流指的是所有的流程都在伽马颜色空间完成,比如输入数据,比如光照计算等。</p><h2 id="2-2-线性颜色空间和工作流"><a href="#2-2-线性颜色空间和工作流" class="headerlink" title="2.2 线性颜色空间和工作流"></a>2.2 线性颜色空间和工作流</h2><p>类似的,线性颜色空间指的是输入数据是在线性曲线上的。那么,我们如果用一张真实的图片作为输入,首先要对其进行gamma校正,也就是需要将这张贴图设置为sRGB,引擎或者图形接口自动会将其转换。<br>线性工作流指的是所有的流程都在线性颜色空间完成,比如输入数据,比如光照计算等。<br>值得强调的是,我们现在的显示器都是gamma显示器,因此我们不能在渲染管线中不能直接输出线性数据,需要转换到sRGB空间再进行输出,某些硬件支持这个自动转换,如果检测到硬件不支持,渲染引擎会在后处理流程中用shader来转换。</p><h2 id="2-3-工作流总结"><a href="#2-3-工作流总结" class="headerlink" title="2.3 工作流总结"></a>2.3 工作流总结</h2><p>下面用一张流程图来总结颜色空间的工作流,如下所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/颜色空间工作流.jpg"></p><ul><li>sRGB Texture在gamma工作流下正常显示</li><li>线性工作流的输出必须进行gamma校正,否则显示会变暗</li><li>gamma工作流的shader计算在sRGB空间中</li><li>线性工作流的shader计算在线性空间中</li></ul><p>注意,sRGB贴图移除gamma校正和shader输出进行gamma校正,都有硬件的自动支持,比如OpenGL的sRGB纹理和 GL_FRAMEBUFFER_SRGB。如果硬件不支持,那么应用(比如游戏引擎),在线性工作流中需要自己进行变换,比如加载sRGB贴图时候手动变换到线性空间和使用shader进行gamma校正。</p><h2 id="2-4-关于贴图设置为sRGB后变暗的说明"><a href="#2-4-关于贴图设置为sRGB后变暗的说明" class="headerlink" title="2.4 关于贴图设置为sRGB后变暗的说明"></a>2.4 关于贴图设置为sRGB后变暗的说明</h2><p>业界或者网上一直流传,贴图设置为sRGB后会变暗。<br>参考2.3的图,在线性工作流下,如果贴图设置为sRGB后,引擎会对贴图进行去gamma校正,变换为线性空间,颜色数值都会变小,参考1.2的曲线图。不管原始图片是否是sRGB空间下创建的,渲染时候得到的颜色值都变小了,因此不管输出时候是否进行gamma校正,我们看到的结果都会变暗。<br>如果是gamma工作流,则不会变暗,因为没有去gamma校正这个过程。</p><h1 id="三、总结"><a href="#三、总结" class="headerlink" title="三、总结"></a>三、总结</h1><p>我们讲述了人眼和显示器的视觉效应,以及两种颜色空间和对应的工作流。我们需要着重弄清楚的是,人眼的视觉效应、显示器的gamma校正、gamma颜色空间(sRGB)。</p><h1 id="四、参考资料"><a href="#四、参考资料" class="headerlink" title="四、参考资料"></a>四、参考资料</h1><blockquote><p><a href="https://docs.unity.cn/2019.4/Documentation/Manual/LinearLighting.html">Unity Color space</a><br><a href="https://learnopengl.com/Advanced-Lighting/Gamma-Correction">Gamma Correction</a><br><a href="https://zhuanlan.zhihu.com/p/66558476">Gamma、Linear、sRGB 和Unity Color Space,你真懂了吗?</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、伽马校正"><a href="#一、伽马校正" class="headerlink" title="一、伽马校正"></a>一、伽马校正</h1><p>所谓gamma校正,实际上是一个颜色的非线性变换。下面来解</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="伽马校正" scheme="http://xiaopengcheng.top/tags/%E4%BC%BD%E9%A9%AC%E6%A0%A1%E6%AD%A3/"/>
<category term="颜色空间" scheme="http://xiaopengcheng.top/tags/%E9%A2%9C%E8%89%B2%E7%A9%BA%E9%97%B4/"/>
<category term="srgb" scheme="http://xiaopengcheng.top/tags/srgb/"/>
</entry>
<entry>
<title>DrawCall、Batches、SetPassCalls的区别和联系</title>
<link href="http://xiaopengcheng.top/2020/06/08/DrawCall%E3%80%81Batches%E3%80%81SetPassCalls%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E8%81%94%E7%B3%BB/"/>
<id>http://xiaopengcheng.top/2020/06/08/DrawCall%E3%80%81Batches%E3%80%81SetPassCalls%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E8%81%94%E7%B3%BB/</id>
<published>2020-06-08T08:25:00.000Z</published>
<updated>2022-03-28T06:45:59.011Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、DrawCall、Batches、SetPassCalls的基本理解"><a href="#一、DrawCall、Batches、SetPassCalls的基本理解" class="headerlink" title="一、DrawCall、Batches、SetPassCalls的基本理解"></a>一、DrawCall、Batches、SetPassCalls的基本理解</h1><p>我们先从图形渲染的角度对这些概念做一个基本的理解。</p><h2 id="1-1-DrawCall"><a href="#1-1-DrawCall" class="headerlink" title="1.1 DrawCall"></a>1.1 DrawCall</h2><p>DrawCall实际上指的是一次图形渲染接口的调用,比如OpenGL的glDrawArrays或者glDrawElements的一次调用,以及DirectX的DrawPrimitive或者DrawIndexedPrimitive。因此,DrawCall可以简单理解为一次渲染指令调用。</p><h2 id="1-2-Batches"><a href="#1-2-Batches" class="headerlink" title="1.2 Batches"></a>1.2 Batches</h2><p>我们知道,在调用DrawCall之前,需要设置渲染状态,比如当前使用的Shader、当前shader的参数(材质参数)、深度测试是否开启、模板测试设置等,设置完这些状态后,才会调用DrawCall。我们把设置渲染状态,加载网格数据,然后调用DrawCall这一个过程,叫做一个批次。理论上,我们可以在设置完渲染状态后,调用多个DrawCall,假如一个DrawCall的绘制数量有限制的话,但是通常一个批次也就调用一次DrawCall。<br>那么所谓合批,就是想办法尽量减少批次。减少批次的关键是减少场景中不同的渲染状态组合,也就是渲染状态切换尽可能少。这样子批次自然最少。批次少了,批次对应的DrawCall自然少了,每个批次需要的渲染状态切换也少了。注意,渲染状态切换类似于DrawCall都是一次渲染指令调用。</p><h2 id="1-3-SetPassCalls"><a href="#1-3-SetPassCalls" class="headerlink" title="1.3 SetPassCalls"></a>1.3 SetPassCalls</h2><p>那么什么是SetPassCalls了。在Shader中有一个Pass的概念,比如一个Shader有2个Pass,那么实际上应用这个Shader的物体会按照Shader的Pass定义顺序渲染2遍,每一遍都是用对应的Pass渲染。Unity的官方文档里面解释SetPassCalls就是Shader中的Pass被切换的次数,因为每个渲染批次都会设置一个Pass,一个Pass就会对应一些渲染状态,当渲染状态变化时候就必须开始新的批次,但是新的批次下Pass可能没有变化</p><h1 id="二、Unity的DrawCall、Batches、SetPassCalls区别和联系"><a href="#二、Unity的DrawCall、Batches、SetPassCalls区别和联系" class="headerlink" title="二、Unity的DrawCall、Batches、SetPassCalls区别和联系"></a>二、Unity的DrawCall、Batches、SetPassCalls区别和联系</h1><p>我们以一个没有开启静态合批的场景运行时的统计数据为例子来说明。我们打开Unity场景的Statistics窗口,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UnityStatistics.jpg"><br>以及Profile窗口,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UnityProfileRendering.jpg"><br>FrameDebug窗口,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UnityFrameDebug.jpg"></p><p>我们可以得出这个场景的DrawCall是584,Batches也是584,SetPassCalls是192。Statistics中是不会显示DrawCall的,只有在Profile窗口下选中Rendering才能看到。</p><h2 id="2-1-Unity的DrawCall"><a href="#2-1-Unity的DrawCall" class="headerlink" title="2.1 Unity的DrawCall"></a>2.1 Unity的DrawCall</h2><p>根据运行数据,可以得出结论DrawCall数目基本等于Batches。为什么说基本了?因为同一个Batch下,可能分多次调用DrawCall,比如网格过于巨大,可能拆分成多个DrawCall,这个也是符合批次的定义的,因为渲染状态没有切换,这发生在静态合批和动态合批的情况下。<br>如果没有静态合批和动态合批,那么DC等于Batches,如果有那么DC没有变化,但是Batches等于合并之后的渲染状态切换。</p><h2 id="2-2-Unity的Batches"><a href="#2-2-Unity的Batches" class="headerlink" title="2.2 Unity的Batches"></a>2.2 Unity的Batches</h2><p>Unity的批次实际上就是前面解释的Batches。不过,Batches实际上包含有三类:Static Batches、Dynamic Batches、Instancing Batches,分别对应Unity的静态合批、动态合批、实例化渲染。</p><h2 id="2-3-Unity的SetPassCalls"><a href="#2-3-Unity的SetPassCalls" class="headerlink" title="2.3 Unity的SetPassCalls"></a>2.3 Unity的SetPassCalls</h2><p>根据FrameDebug窗口可以看到,一共是197+24+1+1=233个渲染事件。其中,Clear事件有14个。除去Clear事件后还生效219的事件,不过我们的SetPassCalls是192,还多了17个。我们观察到UI相机有18个DrawMesh事件,点击后发现这个事件使用的都是同样的Pass,如下图所示,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/UnityUIPass.jpg"><br>,这些Pass之间除了材质属性外的渲染状态都是一致的,因此还要减去17。<br>注意,FrameDebug窗口的截图中折叠的部分基本是SRP Batch。<br>根据这些数据我们可以得出结论,如果支持SRP Batch,一个SetPassCall等于一个SRP Batch;如果不支持SRP Batch,那么一个SetPassCall就是一次Shader的Pass切换。由于Pass切换实际上指的是Shader关键字或者ROP阶段的设置改变,那么其实这个跟SRP是一致的。SRP本质上也是Shader变体切换,而非传统的材质切换。传统的材质切换对应的是Batches。<br>注明:实验引擎版本是Unity2020.3.12。</p><h2 id="2-4-总结"><a href="#2-4-总结" class="headerlink" title="2.4 总结"></a>2.4 总结</h2><p>至此可以得出最终结论,Unity的DrawCall和Batches数目在没有静态合批和动态合批时候相等,Batches对应的是传统的材质切换,DrawCall是一次Batch内一次到多次的渲染命令调用。SetPassCalls一般会大幅度少于Batches,对应的是SRP Batch或者Pass切换,数目等于FrameDebug中的事件数目减去Clear事件、Draw Mesh事件中重复的Pass数目。</p><h1 id="三、DrawCall相关的性能优化"><a href="#三、DrawCall相关的性能优化" class="headerlink" title="三、DrawCall相关的性能优化"></a>三、DrawCall相关的性能优化</h1><h2 id="3-1-为什么需要降低DrawCall"><a href="#3-1-为什么需要降低DrawCall" class="headerlink" title="3.1 为什么需要降低DrawCall"></a>3.1 为什么需要降低DrawCall</h2><p> 一谈起游戏优化,尤其是渲染优化,大家就说降低DrawCall,降低批次。实际上,大部分人都没法正确区分,Unity引擎下DrawCall、Batch、SetPassCall这三个概念。DrawCall或者批次高,并不是性能低下的直接原因,真正的原因是批次高,导致渲染状态切换过多。而渲染状态切换实际上是发生的渲染管线的CPU阶段,使用图形API,比如OpenGL或者DirectX来完成的。这样CPU会花费大量的时间提交渲染指令给GPU,CPU占用过高,但是GPU的渲染指令队列并没有饱和,GPU执行渲染指令的速度很快,因此GPU的负荷可能还没上来,GPU在等待CPU提交渲染指令,整个渲染流水线没有最高速的跑起来。当然如果GPU也忙不过来,那么不仅仅需要降低批次,Shader复杂度和OverDraw应该是重点关注对象。</p><h2 id="3-2-如何降低批次"><a href="#3-2-如何降低批次" class="headerlink" title="3.2 如何降低批次"></a>3.2 如何降低批次</h2><h3 id="3-2-1-静态合批"><a href="#3-2-1-静态合批" class="headerlink" title="3.2.1 静态合批"></a>3.2.1 静态合批</h3><p> 静态合批实际上是引擎在打包或者烘焙时候,将同材质的物体合并成一个更大的物体,这样相同材质的物体只需要一次渲染状态设置和一次DrawCall调用,也就一个批次。由于合并生成大的模型后,会占用额外的内存空间,比如三个同材质的立方体的网格就是一个简单的立方体,合并后的网格占用是三个世界空间立方体的组合,因此有时候需要考虑静态合批带来的内存增长。</p><h3 id="3-2-2-动态合批"><a href="#3-2-2-动态合批" class="headerlink" title="3.2.2 动态合批"></a>3.2.2 动态合批</h3><p> 动态合批是静态合批在运行时的体现。Unity对动态合批有一些限制,比如限制模型顶点属性不能超过900等,具体可以参考<a href="https://docs.unity3d.com/2021.2/Documentation/Manual/dynamic-batching.html">Dynamic batching</a>。动态合批由于是运行是合并网格,因此不仅会增大内存,还会占用CPU时间。动态合批一般应用在一些小物体的合并上,比如小的道具或者特效等。</p><h3 id="3-4-3-Instancing-Draw"><a href="#3-4-3-Instancing-Draw" class="headerlink" title="3.4.3 Instancing Draw"></a>3.4.3 Instancing Draw</h3><p> Instancing Draw实际上是图形接口支持的一种技术,可以翻译为实例化渲染,可以参考文档:<a href="https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/">实例化</a>。这种技术通常应用在重复的物体大量出现的情况下,比如说草地、树木、星星,这种只有位置或者朝向、缩放等不一样。实例化渲染可以通过指定每物体属性(正常的每顶点属性是每个顶点不一样)来传入这种每个物体不一样的属性,从而避免使用不同的材质。在OpenGL中是使用glVertexAttribDivisor来设置属性的更新速度,从而指定每物体属性。<br> 至于Unity的Instancing,参考文档:<a href="https://docs.unity3d.com/Manual/GPUInstancing.html">GPU instancing</a>。关键点:GPU instancing不能和SRP Batcher、Static Batcher并存,SRP Batcher、Static Batcher的优先级更高;GPU instancing不支持 SkinnedMeshRenderers(蒙皮); Graphics.DrawMeshInstanced或者Graphics.DrawMeshInstancedIndirect是主动Instancing,如果不调用这2个函数,那么Unity会尝试Instancing(如果Shader支持Instancing,且没有开启SRP Batch),这会有额外的CPU消耗。</p><h3 id="3-4-4-SRP-Batcher"><a href="#3-4-4-SRP-Batcher" class="headerlink" title="3.4.4 SRP Batcher"></a>3.4.4 SRP Batcher</h3><p> 参考文档:<a href="https://docs.unity.cn/2019.4/Documentation/Manual/SRPBatcher.html">Scriptable Render Pipeline (SRP) Batcher</a>。关键点:只有可编程管线才支持,默认管线不能支持;Shader必须支持SRP Batcher;只支持Mesh和SkinMesh,不支持粒子系统;不能与 Instancing Draw兼容;如果使用了MaterialPropertyBlock,SRP Batcher无法开启。<br> SRP Batcher本质上是Shader变体级别的合批优化,根据前面的分析等价于一次SetPassCall。具体原理还是参考Unity 的官方文档。</p><h3 id="3-4-6-合批方法的优先级"><a href="#3-4-6-合批方法的优先级" class="headerlink" title="3.4.6 合批方法的优先级"></a>3.4.6 合批方法的优先级</h3><p> 根据Unity优化DC的官方文档<a href="https://docs.unity3d.com/2021.2/Documentation/Manual/optimizing-draw-calls.html">Optimizing draw calls</a>,合批方法的优先级如下:</p><p> 1.SRP Batcher and static batching<br> 2.GPU instancing<br> 3.Dynamic batching<br> 其中SRP和静态合批是最高优先级,并且是可以兼容的(对于使用SRP Batcher兼容Shader的物体),因此可以同时启用静态合批和SRP Batcher。不过,经过实验发现上述实验场景在开启了SRP Batcher后,再去打开静态合批,Batches并没有多少什么变化,猜测是场景内使用同样材质的物体过少,相反使用同样Shader变体的物体较多。</p><h3 id="3-4-5-合批总结"><a href="#3-4-5-合批总结" class="headerlink" title="3.4.5 合批总结"></a>3.4.5 合批总结</h3><p> 对于目前的可编程管线,优先使用的都是SRP,因此Shader要尽可能兼容SRP Batcher。对于特殊情况,比如渲染草地这种,才需要舍弃SRP Batcher去使用实例化渲染。对于不支持SRP Batcher的Shader,动态合批和静态合批才可能会被开启。动态合并和静态合批都要增大内存,动态合批还会占用CPU,限制条件还非常多。所以,首选SRP Batcher和Instancing。<br>由于SRP Batcher不能降低DrawCall和Batcher,实际上降低的是SetPassCall;但是静态合批和动态合批可以降低Batcher,但是不能降低DrawCall。所以,在一些低端机器上,Batcher过多可能引起问题的话,还是得开启传统的静态合批,不过这会需要打开网格读写,合并网格也会增大包体和内存。因此出现这种情况的话,最好的选择应该是只开启SRP Batcher,然后让美术手工合并网格和贴图。</p><h1 id="四、参考资料"><a href="#四、参考资料" class="headerlink" title="四、参考资料"></a>四、参考资料</h1><blockquote><p><a href="https://blog.csdn.net/qq_30259857/article/details/110062397">DrawCall,Batches,SetPass calls是什么?原理?【匠】</a><br><a href="https://docs.unity.cn/cn/2022.1/Manual/RenderingStatistics.html">The Rendering Statistics window</a><br><a href="https://blog.csdn.net/yudianxia/article/details/79398590">Unity Profiler中常见的WaitForTargetFPS、Gfx.WaitForPresent 和 Graphics.PresentAndSync</a><br><a href="https://docs.unity3d.com/2021.2/Documentation/Manual/DrawCallBatching.html">Draw call batching</a><br><a href="https://docs.unity3d.com/2021.2/Documentation/Manual/optimizing-draw-calls.html">Optimizing draw calls</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、DrawCall、Batches、SetPassCalls的基本理解"><a href="#一、DrawCall、Batches、SetPassCalls的基本理解" class="headerlink" ti</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="Unity" scheme="http://xiaopengcheng.top/tags/Unity/"/>
<category term="DrawCall" scheme="http://xiaopengcheng.top/tags/DrawCall/"/>
<category term="Batches" scheme="http://xiaopengcheng.top/tags/Batches/"/>
<category term="SetPassCalls" scheme="http://xiaopengcheng.top/tags/SetPassCalls/"/>
</entry>
<entry>
<title>左右手坐标系和相关定则的总结</title>
<link href="http://xiaopengcheng.top/2020/03/06/%E5%B7%A6%E5%8F%B3%E6%89%8B%E5%9D%90%E6%A0%87%E7%B3%BB%E5%92%8C%E7%9B%B8%E5%85%B3%E5%AE%9A%E5%88%99%E7%9A%84%E6%80%BB%E7%BB%93/"/>
<id>http://xiaopengcheng.top/2020/03/06/%E5%B7%A6%E5%8F%B3%E6%89%8B%E5%9D%90%E6%A0%87%E7%B3%BB%E5%92%8C%E7%9B%B8%E5%85%B3%E5%AE%9A%E5%88%99%E7%9A%84%E6%80%BB%E7%BB%93/</id>
<published>2020-03-06T12:15:30.000Z</published>
<updated>2022-03-28T06:38:04.250Z</updated>
<content type="html"><![CDATA[<html><head></head><body><p>左手坐标系和右手坐标系是三维空间下两种不同的坐标系,而且无法通过旋转将左手坐标系转换到右手坐标系。与其相对应的,有左手定则和右手定则,主要是用来确定叉积的朝向或者说旋向。<br>首先,规定二维坐标,X轴朝右、Y轴朝上,推广到三维空间,需要确定的是Z轴是朝前还是朝后。</p><h1 id="一、左手坐标系"><a href="#一、左手坐标系" class="headerlink" title="一、左手坐标系"></a>一、左手坐标系</h1><p>所谓左手坐标系,指的是通过左手来确定的一个三维空间坐标系。</p><h2 id="1-1-确定左手坐标系的方式"><a href="#1-1-确定左手坐标系的方式" class="headerlink" title="1.1 确定左手坐标系的方式"></a>1.1 确定左手坐标系的方式</h2><p>下面总结了三种可以确定左手坐标系的方法。</p><h3 id="1-1-1-拇指、食指、中指相互垂直确定法"><a href="#1-1-1-拇指、食指、中指相互垂直确定法" class="headerlink" title="1.1.1 拇指、食指、中指相互垂直确定法"></a>1.1.1 拇指、食指、中指相互垂直确定法</h3><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/左右手坐标系.png"><br>如图,伸出左手,拇指朝上代表Y轴、食指朝前代表Z轴、中指朝右代表X轴。注意,中指这个时候是只能往右边弯曲的。</p><h3 id="1-1-2-左手定则确定法"><a href="#1-1-2-左手定则确定法" class="headerlink" title="1.1.2 左手定则确定法"></a>1.1.2 左手定则确定法</h3><p>伸出左手,手指朝着右边X轴,握向Y轴,这个时候拇指指向的方向就是Z轴(朝前)。</p><h3 id="1-1-3-人站立的正面朝向确定法"><a href="#1-1-3-人站立的正面朝向确定法" class="headerlink" title="1.1.3 人站立的正面朝向确定法"></a>1.1.3 人站立的正面朝向确定法</h3><p>人朝前站立着,右手伸出的朝向是X轴,头顶的方向是Y轴,面向Z轴。</p><h2 id="1-2-左手定则"><a href="#1-2-左手定则" class="headerlink" title="1.2 左手定则"></a>1.2 左手定则</h2><p>假设,叉乘计算,C=A叉乘B。如何确定在C的朝向了?如果A和B都在左手坐标系下,那么使用左手定则来确定C的朝向。<br>类似1.1.2,伸出左手,手指朝着A,握向B,这个时候拇指指向的方向就是C。</p><h1 id="二、右手坐标系"><a href="#二、右手坐标系" class="headerlink" title="二、右手坐标系"></a>二、右手坐标系</h1><h2 id="2-1-确定右手坐标系的方式"><a href="#2-1-确定右手坐标系的方式" class="headerlink" title="2.1 确定右手坐标系的方式"></a>2.1 确定右手坐标系的方式</h2><h3 id="2-1-1-拇指、食指、中指相互垂直确定法"><a href="#2-1-1-拇指、食指、中指相互垂直确定法" class="headerlink" title="2.1.1 拇指、食指、中指相互垂直确定法"></a>2.1.1 拇指、食指、中指相互垂直确定法</h3><p>参考1.1.1,伸出右手,拇指朝上代表Y轴、食指朝前代表Z轴、中指朝左代表X轴。注意,中指这个时候是只能往左边弯曲的。<br>但是,我们一般假定X轴朝右,因此需要握着Z轴旋转180度。这个时候,拇指朝上代表Y轴、食指朝后代表Z轴、中指朝右代表X轴。注意,左右手坐标系旋转后不会改变。</p><h3 id="2-1-2-左手定则确定法"><a href="#2-1-2-左手定则确定法" class="headerlink" title="2.1.2 左手定则确定法"></a>2.1.2 左手定则确定法</h3><p>伸出右手,手指朝着右边X轴,握向Y轴,这个时候拇指指向的方向就是Z轴(朝后)。</p><h3 id="2-1-3-人站立的正面朝向确定法"><a href="#2-1-3-人站立的正面朝向确定法" class="headerlink" title="2.1.3 人站立的正面朝向确定法"></a>2.1.3 人站立的正面朝向确定法</h3><p>人朝前站立着,右手伸出的朝向是X轴,头顶的方向是Y轴,背后的是Z轴。</p><h2 id="2-2-右手定则"><a href="#2-2-右手定则" class="headerlink" title="2.2 右手定则"></a>2.2 右手定则</h2><p>类似1.1,如果A和B都在,右手坐标系下,那么使用右手定则来确定C的朝向。<br>类似1.1.2,伸出右手,手指朝着A,握向B,这个时候拇指指向的方向就是C。<br>因此,左手定则和右手定则的区别是使用左手还是右手。</p><h1 id="三、图形API的左右手坐标系"><a href="#三、图形API的左右手坐标系" class="headerlink" title="三、图形API的左右手坐标系"></a>三、图形API的左右手坐标系</h1><p>图形管线中,存在多个坐标系,每个坐标系都可以使用左手或者右手坐标系。下面按照,物体坐标系->世界坐标系->摄像机坐标系->裁剪坐标系->窗口坐标系来说明。</p><h2 id="3-1-OpenGL"><a href="#3-1-OpenGL" class="headerlink" title="3.1 OpenGL"></a>3.1 OpenGL</h2><p>OpenGL默认是右手坐标系。不过到了窗口坐标系,OpenGL使用的是左手坐标系。为什么了?<strong>因为OpenGL的深度范围是[0,1],而且是摄像机越远,深度越大,这就是左手坐标系啦</strong>。<br>由于物体坐标系、世界坐标系、摄像机坐标系都是右手坐标系,但是窗口坐标系是左手坐标系,那么投影矩阵就需要乘以右手坐标系变换到左手坐标系这个变换,也就是Z变换成-Z。不过这个变换也可以放在摄像机坐标系,也就是MVP的V中。现在假定,都乘到P中了。<br>最终结论是:物体坐标系、世界坐标系、摄像机坐标系是右手坐标系;裁剪坐标系和窗口坐标系是左手坐标系,窗口坐标系实际上只是裁剪坐标系进行齐次除法后再平移缩放而已。</p><h2 id="3-2-DirectX"><a href="#3-2-DirectX" class="headerlink" title="3.2 DirectX"></a>3.2 DirectX</h2><p>DirectX默认是左手坐标系。<br>类似3.1,物体坐标系、世界坐标系、摄像机坐标系是左手坐标系。<strong>注意,DirectX的窗口坐标系是以左上角为原点的,深度是朝前的,那么跟OpenGL的反过来,是右手坐标系。</strong><br>因此,裁剪坐标系和窗口坐标系是右手手坐标系。投影变化同样要乘以,右手坐标系变换到左手坐标系这个变换,也就是Z变换成-Z。</p><h2 id="3-3-Vulkan"><a href="#3-3-Vulkan" class="headerlink" title="3.3 Vulkan"></a>3.3 Vulkan</h2><p>Vulkan的窗口坐标系和DirectX的一致,因此推测其余坐标系和DirectX的一致。</p><h2 id="3-4-Metal"><a href="#3-4-Metal" class="headerlink" title="3.4 Metal"></a>3.4 Metal</h2><p>Vulkan的窗口坐标系和DirectX的一致,因此推测其余坐标系和DirectX的一致。</p><p>看来只有,历史遗留的奇葩OpenGL的窗口坐标系,原点在左下角啊。原点在哪,这个跟纹理的v坐标是否需要取反也有关系。</p><h1 id="四、游戏引擎的左右手坐标系"><a href="#四、游戏引擎的左右手坐标系" class="headerlink" title="四、游戏引擎的左右手坐标系"></a>四、游戏引擎的左右手坐标系</h1><p>游戏引擎中,物体和世界坐标系是固定的,对于所有的图形API都会一样。</p><h2 id="4-1-Unity"><a href="#4-1-Unity" class="headerlink" title="4.1 Unity"></a>4.1 Unity</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/Unity物体坐标系.jpg"><br>根据上图,Unity的物体和世界坐标系可以推测都是左手系。<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/Unity坐标系旋向.jpg"><br>根据上图,出自Shader入门精要,Unity的窗口坐标系和OpenGL的一致,是左手系。但是摄像机空间变换到了右手系。那么,在V中需要乘以Z到-Z的变换。同时,P中再乘以-Z到Z的变换变回左手系。<br>为啥多次一举了,怀疑这个结论的正确性。下面做实验,用IMGizmos绘制坐标轴。代码如下,</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">using UnityEngine;</span><br><span class="line"></span><br><span class="line">namespace GYGame</span><br><span class="line">{</span><br><span class="line"> /// <summary></span><br><span class="line"> /// 出生点</span><br><span class="line"> /// </summary></span><br><span class="line"> public class PlayerStart : MonoBehaviour</span><br><span class="line"> {</span><br><span class="line"> public float GizmosHeight { get; set; } = 2.0f;</span><br><span class="line"></span><br><span class="line"> void OnDrawGizmos()</span><br><span class="line"> {</span><br><span class="line"> IMGizmos.Line3D(transform.position, transform.position + transform.up * GizmosHeight, Color.green);</span><br><span class="line"> IMGizmos.Line3D(transform.position, transform.position + transform.right * GizmosHeight, Color.red);</span><br><span class="line"> IMGizmos.Line3D(transform.position, transform.position + transform.forward * GizmosHeight, Color.blue);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></tbody></table></figure><p>选中场景相机,可以得到下面结果,<br><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/Unity摄像机坐标系.jpg"><br>可以看到右下角的场景相机画面里面有显示PlayerStart的Gizmos,Gizmos显示的坐标系是左手系,跟右上角显示的坐标系是一致的。同时,引擎自带的Gizmos显示的摄像机前向也是Z轴正向。<br>因此,推测我实验的Unity版本是2020,与UnityShader入门精要使用的Unity5.X版本,摄像机空间的旋向性已经发生了变化。</p><h2 id="4-2-UnrealEngine"><a href="#4-2-UnrealEngine" class="headerlink" title="4.2 UnrealEngine"></a>4.2 UnrealEngine</h2><p><img alt="" data-src="https://raw.githubusercontent.com/xpc-yx/markdown_img/master/小书匠/虚幻坐标系.jpg"><br>虚幻和Unity一样也是采用左手坐标系,不过其是Z轴朝上,Y轴朝外。沿着X轴旋转90度,可以得到Z轴朝内,Y轴朝上,那么和Unity的是一致的。<br>推测,其余的空间的坐标系旋向和Unity的是一致。摄像机空间的旋向也可以用类似4.1的方式绘制Gizmos,然后选中摄像机,查看摄像机的绘制结果。</p><h1 id="五、参考资料"><a href="#五、参考资料" class="headerlink" title="五、参考资料"></a>五、参考资料</h1><blockquote><p><a href="https://book.douban.com/subject/26821639/">Shader入门精要</a><br><a href="https://zhuanlan.zhihu.com/p/269621383">图形坐标系的跨平台适配</a></p></blockquote></body></html>]]></content>
<summary type="html"><html><head></head><body><p>左手坐标系和右手坐标系是三维空间下两种不同的坐标系,而且无法通过旋转将左手坐标系转换到右手坐标系。与其相对应的,有左手定则和右手定则,主要是用来确定叉积的朝向或者说旋向。<br>首先,规定二维坐标,X轴朝右、Y轴朝上,推广到</summary>
<category term="图形学" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
<category term="Rendering" scheme="http://xiaopengcheng.top/categories/%E5%9B%BE%E5%BD%A2%E5%AD%A6/Rendering/"/>
<category term="左右手坐标系" scheme="http://xiaopengcheng.top/tags/%E5%B7%A6%E5%8F%B3%E6%89%8B%E5%9D%90%E6%A0%87%E7%B3%BB/"/>
<category term="左右手定理" scheme="http://xiaopengcheng.top/tags/%E5%B7%A6%E5%8F%B3%E6%89%8B%E5%AE%9A%E7%90%86/"/>
</entry>
<entry>
<title>Unity3D代码规范</title>
<link href="http://xiaopengcheng.top/2018/12/19/Unity3D%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/"/>
<id>http://xiaopengcheng.top/2018/12/19/Unity3D%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/</id>
<published>2018-12-19T09:15:51.000Z</published>
<updated>2022-03-28T06:46:34.820Z</updated>
<content type="html"><![CDATA[<html><head></head><body><h1 id="一、命名法"><a href="#一、命名法" class="headerlink" title="一、命名法"></a>一、命名法</h1><p>Pascal命名法:每个单词首字母大写。<br>Camel命名法:第一个单词首字母小写,其余单词首字母大写。<br>C++标准库命名法:全小写,单词用下划线分割。</p><h2 id="1-1-CSharp"><a href="#1-1-CSharp" class="headerlink" title="1.1 CSharp"></a>1.1 CSharp</h2><p>函数和类采用Pascal命名法,变量采用Camel命名法。<br>代码目录和文件采用Pascal命名法。</p><h2 id="1-2-Lua"><a href="#1-2-Lua" class="headerlink" title="1.2 Lua"></a>1.2 Lua</h2><p>类采用Pascal命名法,其余采用C++标准库命名法。<br>代码目录和文件采用C++标准库命名法。</p><h2 id="1-3-其它"><a href="#1-3-其它" class="headerlink" title="1.3 其它"></a>1.3 其它</h2><p>其它目录和文件采用Pascal命名法。</p><h1 id="二、C-代码规范"><a href="#二、C-代码规范" class="headerlink" title="二、C#代码规范"></a>二、C#代码规范</h1><h2 id="2-1-命名的基本约定"><a href="#2-1-命名的基本约定" class="headerlink" title="2.1 命名的基本约定"></a>2.1 命名的基本约定</h2><p>函数用动词命名,其它的用名词或者形容词命名。</p><h3 id="避免使用拼音"><a href="#避免使用拼音" class="headerlink" title="避免使用拼音"></a>避免使用拼音</h3><p>原则上避免使用拼音命名代码。</p><h3 id="尽量避免缩写"><a href="#尽量避免缩写" class="headerlink" title="尽量避免缩写"></a>尽量避免缩写</h3><p>尽量不要缩写名字,名字长没关系,尽可能描述清楚。</p><h3 id="类型前缀"><a href="#类型前缀" class="headerlink" title="类型前缀"></a>类型前缀</h3><p>类和变量前一般不要加前缀。模板类型加前缀T,接口加前缀I,枚举加前缀E。</p><h3 id="类型后缀"><a href="#类型后缀" class="headerlink" title="类型后缀"></a>类型后缀</h3><p>特殊类型可选加后缀。<br>List:可选加List后缀。<br>Dictionary:可选加Dict后缀。<br>delegate:加上后缀Event。</p><h3 id="命名空间"><a href="#命名空间" class="headerlink" title="命名空间"></a>命名空间</h3><p>使用Pascal命名法。<br>命名空间采用GY开头,比如GYEngine、GYGame。</p><h3 id="类"><a href="#类" class="headerlink" title="类"></a>类</h3><p>使用Pascal命名法。<br>类名要用名词。模板类开头用T。</p><h3 id="接口"><a href="#接口" class="headerlink" title="接口"></a>接口</h3><p>使用Pascal命名法。<br>接口开头用I。接口名要用名词或者形容词。</p><h3 id="枚举"><a href="#枚举" class="headerlink" title="枚举"></a>枚举</h3><p>枚举类型采用Pascal命名法,需要加上前缀E,比如EMessageType。<br>枚举常量不需要加前缀,采用Pascal命名法,特殊情况下可以拆成两部分用下划线区分,比如Message_Start。</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Enum EWeaponType</span><br><span class="line">{</span><br><span class="line">Knife,</span><br><span class="line">Pistol,</span><br><span class="line">MachineGun,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Enum EMessageType</span><br><span class="line">{</span><br><span class="line">Message_Start,</span><br><span class="line">Message_End,</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h3><p>使用Pascal命名法。<br>函数名最好用动词开头。</p><h3 id="委托和事件"><a href="#委托和事件" class="headerlink" title="委托和事件"></a>委托和事件</h3><p>使用Pascal命名法。<br>使用动词短语命名,delegate类型的命名需要加上后缀Event。<br>event类型的实例需要加上On前缀,Event后缀。</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">delegate</span> <span class="keyword">void</span> <span class="title">KillMonsterEvent</span>()</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">event</span> KillMonsterEvent OnKillMonsterEvent = <span class="literal">null</span>;</span><br></pre></td></tr></tbody></table></figure><h3 id="属性"><a href="#属性" class="headerlink" title="属性"></a>属性</h3><p>使用Pascal命名法。<br>属性是对Get和Set的语法封装,一般是public或者protected采有意义。</p><h3 id="特性-Attribute"><a href="#特性-Attribute" class="headerlink" title="特性(Attribute)"></a>特性(Attribute)</h3><p>使用Pascal命名法。<br>用名词或名词短语+Attribute方式命名特性。<br>比如,</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">ObsoleteAttribute</span></span><br><span class="line">{</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="局部变量"><a href="#局部变量" class="headerlink" title="局部变量"></a>局部变量</h3><p>采用Camel命名法。</p><h4 id="函数参数"><a href="#函数参数" class="headerlink" title="函数参数"></a>函数参数</h4><p>采用Camel命名法。</p><h3 id="成员变量"><a href="#成员变量" class="headerlink" title="成员变量"></a>成员变量</h3><p>类非公有非静态成员变量用m开头。比如mActorId。<br>类的公有成员变量大写开头,不需要加m前缀,尽量用属性代替公有变量。</p><h3 id="静态变量"><a href="#静态变量" class="headerlink" title="静态变量"></a>静态变量</h3><p>类的静态成员变量用s开头。<br>函数内的静态变量用s开头。<br>比如,</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Actor</span><br><span class="line">{</span><br><span class="line"><span class="keyword">private</span> <span class="built_in">int</span> mActorId = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> sActorNumInClass = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> <span class="built_in">int</span> mActorClassId = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="built_in">string</span> ActorName = <span class="string">"name"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="built_in">int</span> ActorId</span><br><span class="line">{</span><br><span class="line"><span class="keyword">get</span></span><br><span class="line">{ </span><br><span class="line"><span class="keyword">return</span> mActorId;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">set</span></span><br><span class="line">{ </span><br><span class="line">mActorId = <span class="keyword">value</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">delegate</span> <span class="keyword">void</span> <span class="title">KillMonsterEvent</span>()</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">event</span> KillMonsterEvent OnKillMonsterEvent = <span class="literal">null</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">Actor</span>()</span></span><br><span class="line">{</span><br><span class="line">mActorId = <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">int</span> <span class="title">GetActorNum</span>(<span class="params"><span class="built_in">bool</span> isFirstTime</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">int</span> sActorNumInFun;</span><br><span class="line"><span class="built_in">int</span> addNum = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">return</span> sActorNumInFun = (isFirstTime ? <span class="number">0</span> : sActorNumInFun + addNum);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="常量"><a href="#常量" class="headerlink" title="常量"></a>常量</h3><p>所有单词大写,多个单词之间用下划线隔开,比如public const int MAX_NUM = 10。</p><h2 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h2><p>原则上,尽量写可读性良好、自解释的代码,避免写冗余的注释。</p><h3 id="文件注释"><a href="#文件注释" class="headerlink" title="文件注释"></a>文件注释</h3><p>文件开头必须要有注释,如果是单个类的文件,可以将用类注释替代。</p><h3 id="类注释"><a href="#类注释" class="headerlink" title="类注释"></a>类注释</h3><p>单个类的文件,必须有类注释。<br>类注释说明该类是做什么的,可选包含怎么实现以及为什么这么实现的原因。</p><h3 id="函数注释"><a href="#函数注释" class="headerlink" title="函数注释"></a>函数注释</h3><p>简单函数不需要注释,难以使用的函数需要加注释,想想为什么难以使用,这个时候往往需要重构或者拆分函数代码了。</p><h3 id="语句注释"><a href="#语句注释" class="headerlink" title="语句注释"></a>语句注释</h3><p>关键难以理解的代码语句,需要加上注释说明。</p><h3 id="变量注释"><a href="#变量注释" class="headerlink" title="变量注释"></a>变量注释</h3><p>关键变量加上注释,普通的不需要加注释。</p><h2 id="2-2-代码风格"><a href="#2-2-代码风格" class="headerlink" title="2.2 代码风格"></a>2.2 代码风格</h2><h3 id="类成员排列顺序"><a href="#类成员排列顺序" class="headerlink" title="类成员排列顺序"></a>类成员排列顺序</h3><ol><li>属性:公有属性 、受保护属性 </li><li>字段:受保护字段、私有字段(公有字段当作属性对待)</li><li>事件:公有事件、受保护事件、私有事件</li><li>构造函数:参数数量最少的构造函数,参数数量中等的构造函数,参数数量最多的构造函数</li><li>方法:重载方法的排列顺序与构造函数相同,从参数数量最少往下至参数最多。方法按照功能分块,尽可能按照公有、保护、私有的访问级别来分布。</li></ol><h3 id="变量"><a href="#变量" class="headerlink" title="变量"></a>变量</h3><ol><li>一行只能声明一个变量,尽量避免用var定义变量类型,除非类型写起来很冗余。</li><li>尽量在声明的同时初始化。</li><li>变量定义在开头,比如类开头或者函数开头。除非是根据条件定义的块变量。</li></ol><p>比如,</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">People</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">private</span> <span class="built_in">string</span> mName = <span class="string">"PeopleName"</span>;</span><br><span class="line"><span class="keyword">private</span> <span class="built_in">int</span> mAge = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ChangeAge</span>(<span class="params"><span class="built_in">int</span> newAge, <span class="built_in">bool</span> needAddAge</span>)</span></span><br><span class="line">{</span><br><span class="line">mAge = newAge;</span><br><span class="line"><span class="keyword">if</span> (needAddAge)</span><br><span class="line">{</span><br><span class="line"><span class="built_in">int</span> tempAddAge = <span class="number">1</span>;</span><br><span class="line">mAge += tempAddAge;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="语句"><a href="#语句" class="headerlink" title="语句"></a>语句</h3><ol><li>一行只能有一条语句。</li><li><p>单行复合语句必须加大括号。原则上,即使只有一行语句,也需要加大括号包起来,防止后续修改代码破坏忘记语句范围。比如,</p></li><li><p>else if等必须新起一行。比如,</p><figure class="highlight c#"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (isWorkday)</span><br><span class="line">{</span><br><span class="line">Work();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (isHoliday)</span><br><span class="line">{</span><br><span class="line">Rest();</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure></li></ol><h3 id="缩进"><a href="#缩进" class="headerlink" title="缩进"></a>缩进</h3><p>代码缩进使用Tab键实现,最好不要使用空格,为保证在不同机器上使代码缩进保持一致,设置Tab键宽度为4个字符。</p><h3 id="大括号"><a href="#大括号" class="headerlink" title="大括号"></a>大括号</h3><ol><li>大括号需要占一行对齐,而不是将左大括号放在行尾。</li><li>Lambda函数可以将左大括号放在同一行,不需要另起一行。</li></ol><h3 id="空格"><a href="#空格" class="headerlink" title="空格"></a>空格</h3><ol><li>if、while、for、return等关键词后应有一个空格[eg. “if (a == b)”]。</li><li>运算符前后应各有一个空格[eg. “a = b + c;”]。</li><li>函数调用后不需要加空格。</li><li>左括号后面和右括号前面不需要加额外的空格。</li></ol><h3 id="空行"><a href="#空行" class="headerlink" title="空行"></a>空行</h3><ol><li>函数之间必须加空行。</li><li>较长函数的代码块直接用空行分割。</li><li>变量定义可以分块加空行分割。</li></ol><h3 id="行长度"><a href="#行长度" class="headerlink" title="行长度"></a>行长度</h3><p>每一行代码的行长度,建议不要超过110个字符或者说不超过屏幕宽度。如果超过这个长度,可以按照以下规则换行:</p><ol><li>在逗号后换行。</li><li>在操作符前换行。</li><li>第一条优先于第二条。</li></ol><h3 id="函数长度"><a href="#函数长度" class="headerlink" title="函数长度"></a>函数长度</h3><p>建议单个函数长度不要超过80行。越简短越好。<br>超过80行,可以考虑拆分函数重用代码。</p><h3 id="类长度"><a href="#类长度" class="headerlink" title="类长度"></a>类长度</h3><p>单个类文件原则上不超过1000行。接近或者超过,考虑拆分类或者多个文件实现类。</p><h2 id="2-3-示例代码"><a href="#2-3-示例代码" class="headerlink" title="2.3 示例代码"></a>2.3 示例代码</h2><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> <span class="title">YMGame</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">public</span> Enum EWeaponType</span><br><span class="line">{</span><br><span class="line">Knife,</span><br><span class="line">Pistol,</span><br><span class="line">MachineGun,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Actor</span><br><span class="line">{</span><br><span class="line"><span class="keyword">private</span> <span class="built_in">int</span> mActorId = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> sActorNumInClass;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> <span class="built_in">int</span> mActorClassId;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="built_in">string</span> ActorName;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="built_in">int</span> ActorId</span><br><span class="line">{</span><br><span class="line"><span class="keyword">get</span></span><br><span class="line">{ </span><br><span class="line"><span class="keyword">return</span> mActorId;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">set</span></span><br><span class="line">{ </span><br><span class="line">mActorId = <span class="keyword">value</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">int</span> <span class="title">GetActorNum</span>(<span class="params"><span class="built_in">bool</span> isFirstTime</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">int</span> sActorNumInFun;</span><br><span class="line"><span class="built_in">int</span> addNum = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">return</span> sActorNumInFun = (isFirstTime ? <span class="number">0</span> : sActorNumInFun + addNum);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">SetActorId</span>(<span class="params"><span class="built_in">int</span> classId, <span class="built_in">int</span> actorId</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">int</span> sNonClassIdActorNum = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">mActorClassId = classId;</span><br><span class="line">mActorId = actorId;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (mActorClassId <= <span class="number">0</span> )</span><br><span class="line">{</span><br><span class="line">sNonClassIdActorNum++;</span><br><span class="line"><span class="built_in">bool</span> isNonClassIdActor = <span class="literal">true</span>;</span><br><span class="line">actorId = <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h1 id="三、Lua代码规范"><a href="#三、Lua代码规范" class="headerlink" title="三、Lua代码规范"></a>三、Lua代码规范</h1><p>除了以下特殊提及到的,Lua的代码规范参照C#的代码规范。</p><h2 id="3-1-命名规则"><a href="#3-1-命名规则" class="headerlink" title="3.1 命名规则"></a>3.1 命名规则</h2><h3 id="文件(类)名"><a href="#文件(类)名" class="headerlink" title="文件(类)名"></a>文件(类)名</h3><p>采用Pascal命名法。</p><h3 id="函数-1"><a href="#函数-1" class="headerlink" title="函数"></a>函数</h3><p>采用Pascal命名法。</p><h3 id="文件的local变量"><a href="#文件的local变量" class="headerlink" title="文件的local变量"></a>文件的local变量</h3><p>下划线开头,采用Camel命名法。比如_classType。</p><h3 id="函数的local变量"><a href="#函数的local变量" class="headerlink" title="函数的local变量"></a>函数的local变量</h3><p>采用Camel命名法。</p><h3 id="函数参数-1"><a href="#函数参数-1" class="headerlink" title="函数参数"></a>函数参数</h3><p>采用Camel命名法。</p><h3 id="C-代码导出到Lua"><a href="#C-代码导出到Lua" class="headerlink" title="C#代码导出到Lua"></a>C#代码导出到Lua</h3><p>必须增加Cs前缀以做区分,比如CsFileManager = CS.GYEngine.FileManager.Instance。</p><h3 id="双下划线"><a href="#双下划线" class="headerlink" title="双下划线"></a>双下划线</h3><p>双下划线用于一些特殊函数的前缀,比如类的初始化和销毁函数。</p><h3 id="日志打印"><a href="#日志打印" class="headerlink" title="日志打印"></a>日志打印</h3><p>使用项目规定的log函数。比如使用log.l,可以通过个人logid来过滤其他人日志;警告使用log.w;错误使用log.e,避免使用默认的error。</p><h1 id="四、编程技巧"><a href="#四、编程技巧" class="headerlink" title="四、编程技巧"></a>四、编程技巧</h1><h2 id="避免使用魔数"><a href="#避免使用魔数" class="headerlink" title="避免使用魔数"></a>避免使用魔数</h2><p>代码里面不要出现魔法数字,用常量来替代。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">double</span> <span class="title">CalculateCircularArea</span>(<span class="params"><span class="built_in">double</span> radius</span>)</span> </span><br><span class="line">{</span><br><span class="line"><span class="keyword">return</span> (<span class="number">3.1415</span>) * radius * radius;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 常量替代魔法数字</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> final Double PI = <span class="number">3.1415</span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="built_in">double</span> <span class="title">CalculateCircularArea</span>(<span class="params"><span class="built_in">double</span> radius</span>)</span> </span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> PI * radius * radius;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="解释型变量"><a href="#解释型变量" class="headerlink" title="解释型变量"></a>解释型变量</h2><p>如下所示,用bool变量代替复杂的条件判断,bool变量的命名可以解释条件判断的意思。</p><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (date.after(SUMMER_START) && date.before(SUMMER_END))</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">} </span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 引入解释性变量后逻辑更加清晰</span></span><br><span class="line"><span class="built_in">bool</span> isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);</span><br><span class="line"><span class="keyword">if</span> (isSummer)</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">} </span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">} </span><br></pre></td></tr></tbody></table></figure><h2 id="避免函数参数过多"><a href="#避免函数参数过多" class="headerlink" title="避免函数参数过多"></a>避免函数参数过多</h2><p>参数过多时候,可以将参数组合成一个结构体传入,方便后续对参数的修改。</p><h3 id="避免函数参数控制函数内部逻辑"><a href="#避免函数参数控制函数内部逻辑" class="headerlink" title="避免函数参数控制函数内部逻辑"></a>避免函数参数控制函数内部逻辑</h3><p>可以考虑拆分成多个函数,保证函数职责单一。</p><h2 id="避免嵌套过深"><a href="#避免嵌套过深" class="headerlink" title="避免嵌套过深"></a>避免嵌套过深</h2><p><code>可以考虑使用</code>continue、break、return关键字,提前退出嵌套。</p><h2 id="分割代码和单一职责"><a href="#分割代码和单一职责" class="headerlink" title="分割代码和单一职责"></a>分割代码和单一职责</h2><p>如果函数或者类的代码过长,考虑拆分成多个函数或者类,保证职责单一。</p><h2 id="预计算和缓存"><a href="#预计算和缓存" class="headerlink" title="预计算和缓存"></a>预计算和缓存</h2><p>比如Component或者UI控件的获得等,可以在初始化的时候获取然后缓存引用,避免重复查询。</p><h2 id="避免频繁创建字符串"><a href="#避免频繁创建字符串" class="headerlink" title="避免频繁创建字符串"></a>避免频繁创建字符串</h2><p>由于C#中的string是独一无二的,无法修改,所以字符串操作会创建新的字符串,不像C++可以就地初始化或者重复利用对象,因此避免大量使用string的操作符构建字符串,改成使用StringBuilder。</p><h1 id="五、安全性编程"><a href="#五、安全性编程" class="headerlink" title="五、安全性编程"></a>五、安全性编程</h1><h2 id="5-1-安全性编程原则"><a href="#5-1-安全性编程原则" class="headerlink" title="5.1 安全性编程原则"></a>5.1 安全性编程原则</h2><h3 id="判空"><a href="#判空" class="headerlink" title="判空"></a>判空</h3><p>C#中的对象都是引用,使用前需要判空,空引用会造成异常。这个是良好的编程习惯。可以用空值传播操作符等,简略代码。</p><h3 id="参数检查"><a href="#参数检查" class="headerlink" title="参数检查"></a>参数检查</h3><p>对传入的参数要进行安全性检查,比如空引用,索引范围等,非法情况提前返回,然后再进行正常的逻辑处理。</p><h3 id="尽可能使用错误处理而不是异常处理"><a href="#尽可能使用错误处理而不是异常处理" class="headerlink" title="尽可能使用错误处理而不是异常处理"></a>尽可能使用错误处理而不是异常处理</h3><p>异常有额外的性能消耗,加上异常会破坏调用链,应该尽可能用错误判断得方式处理各种可以预测的问题,而不是抛出异常。游戏引擎内一般不使用异常,比如UE4的源码内就禁用异常。</p><h2 id="5-2-示例代码"><a href="#5-2-示例代码" class="headerlink" title="5.2 示例代码"></a>5.2 示例代码</h2><figure class="highlight csharp"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> IEnumerator <span class="title">SpawnObjectAsync</span>(<span class="params"><span class="built_in">string</span> assetPath, Vector3 position, Quaternion rotation, Vector3 scale, Transform parent = <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="params"><span class="function"> <span class="built_in">string</span> name = <span class="string">""</span>, Action<GameObject> onSpawnObjectDone = <span class="literal">null</span></span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">string</span>.IsNullOrEmpty(assetPath))</span><br><span class="line"> {</span><br><span class="line"> GYLog.LogError(<span class="string">"GameObjectPool SpawnObjectAsync assetPath is IsNullOrEmpty"</span>);</span><br><span class="line"> <span class="keyword">yield</span> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> GameObjectPooledItemList pool = <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">if</span> (mAssetPathLookup.TryGetValue(assetPath, <span class="keyword">out</span> pool) == <span class="literal">false</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="function"><span class="keyword">yield</span> <span class="keyword">return</span> <span class="title">WarmPoolAsync</span>(<span class="params">assetPath, <span class="number">1</span>, (tempPool</span>)</span> => pool = tempPool);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (pool == <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> GYLog.LogError(<span class="string">"GameObjectPool SpawnObjectAsync Get GameObjectCollection return null"</span>);</span><br><span class="line"> <span class="keyword">yield</span> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> GameObject clone = pool.GetItem();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (clone == <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> GYLog.LogError(<span class="string">"GameObjectPool SpawnObject Get GameObject from GameObjectCollection return null"</span>);</span><br><span class="line"> <span class="keyword">yield</span> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> clone.SetActiveEx(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (parent != <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> clone.transform.parent = parent;</span><br><span class="line"> }</span><br><span class="line"> clone.transform.position = position;</span><br><span class="line"> clone.transform.rotation = rotation;</span><br><span class="line"> clone.transform.localScale = scale;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (name != <span class="string">""</span>)</span><br><span class="line"> {</span><br><span class="line"> clone.name = name;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> mInstanceLookup.Add(clone.GetInstanceID(), pool);</span><br><span class="line"> mIsDirty = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> onSpawnObjectDone?.Invoke(clone);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>比如示例代码,首先做了输入参数检查,然后在执行过程中做了条件检查,检查失败直接主动报错,马上返回。</p><h1 id="六、改动权限"><a href="#六、改动权限" class="headerlink" title="六、改动权限"></a>六、改动权限</h1><p>项目中可以通过SVN或者Git的权限限制,避免过多人改动底层或者关键代码。下面举例说明,</p><h2 id="C-的Engine代码"><a href="#C-的Engine代码" class="headerlink" title="C#的Engine代码"></a>C#的Engine代码</h2><p>原则上,Engine代码不做改动,主程或者指定的人有权限改动,其它人需要改动需要事先跟主程沟通后才能改动。</p><h2 id="C-的Game代码"><a href="#C-的Game代码" class="headerlink" title="C#的Game代码"></a>C#的Game代码</h2><p>在游戏发布之前,Game代码允许改动;在游戏发布之后,改动Game层的C#代码需要热更新二进制包或者打补丁更新,有改动需求需要事先跟主程沟通。</p><h2 id="Lua的框架代码"><a href="#Lua的框架代码" class="headerlink" title="Lua的框架代码"></a>Lua的框架代码</h2><p>框架代码改动之前需要考虑清楚,客户端程序都有改动权限,改动大的部分最好同步主程或者执行主程等,并且负责跟踪和修复改动后引入的问题</p><h2 id="Lua的业务代码"><a href="#Lua的业务代码" class="headerlink" title="Lua的业务代码"></a>Lua的业务代码</h2><p>客户端程序一直有改动权限,需要遵守代码规范。</p></body></html>]]></content>
<summary type="html"><html><head></head><body><h1 id="一、命名法"><a href="#一、命名法" class="headerlink" title="一、命名法"></a>一、命名法</h1><p>Pascal命名法:每个单词首字母大写。<br>Camel命名法:</summary>
<category term="游戏开发" scheme="http://xiaopengcheng.top/categories/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
<category term="Unity" scheme="http://xiaopengcheng.top/categories/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/Unity/"/>
<category term="Unity" scheme="http://xiaopengcheng.top/tags/Unity/"/>
<category term="代码规范" scheme="http://xiaopengcheng.top/tags/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/"/>
</entry>
</feed>