写这一系列回顾文档,一方面是对自己之前一段时间做的东西的回顾总结,看看哪些做得不好,汇集总结一下开发经验,用在下次更好的开发上面;另一方面也希望对有类似开发需求的伙伴们能够提供一点微薄的帮助。文章写得不算好,请多见谅。如大佬有发现写错的,或是有更好建议的内容,也感谢能够指出!

渲染管线总览

项目所用的管线整体是URP自带的基于Stencil的延迟渲染管线。一开始的方案我把角色和场景分开绘制,角色用前向管线,场景用延迟管线,但在后面算SSR时,由于角色已经进行了光照计算,而场景还只有Albedo信息,导致反射出的角色和场景一个受光一个不守光,表现不够统一,所以角色后面也改成了同样使用的延迟管线进行绘制,和场景一样统一在Deferred Pass进行光照计算。

管线重要Pass概览

Shadow

Shadow Cast这一部分我暂时没有进行自定义处理,使用Unity默认的Cascade Shadow Map和Tile形式的点光源、聚光灯阴影。后续打算再尝试一下分离处理高清角色自阴影与场景投影、角色Decal投影,以及Neverwind老师在UFSH2024中分享的半程向量自阴影的方案。

https://www.bilibili.com/video/BV1rW2LYvEox

Shadow Sample这一部分,主光使用屏幕空间阴影,在一个额外的Pass中将主光阴影信息绘制到一张RT上,再Lighting Pass中直接采样这张RT来获得主光的阴影信息;额外光则是在Lighting计算时逐光源采样进行ShadowMap计算。在URP关于屏幕空间阴影的文档里还提及,Screen Space Shadow也能用于前向管线,可以减少Cascade Shadow Maps的访问次数,不过会引入一张额外RT的内存占用。

我在毕设项目里通过Stencil Test分离了场景阴影和角色阴影的绘制,第一个Pass先通过常规的ShadowMap计算场景的阴影,第二个Pass计算角色的阴影,除了通过ShadowMap计算场景投影外,还会采样GBuffer 1混合R通道中存储的角色贴图自阴影,以及面部GBuffer Pass中计算的SDF阴影信息。

分离角色和场景的双Pass ScreenSpaceShadow

GBuffer

GBuffer 0存储场景绘制对象的Albedo,即基础颜色信息;

GBuffer 0

GBuffer1在场景和角色上分别表示不同的信息。对于场景来说,GBuffer存储的信息和URP默认的相同,GBuffer1的R通道存储绘制对象的金属度,G通道存储绘制对象的菲涅尔项,B通道暂时没有使用,A通道存储绘制贴图Occlusion信息。

GBuffer 1

对于角色来说,如下图所示,GBuffer 1的R通道存储角色的贴图阴影遮罩信息,G通道存储材质ID,用于在后续光照流程中区分角色不同材质部位,B通道存储高光遮罩,A通道存储材质标签信息,和材质ID用途类似,在后续流程中用于区分部分效果的影响部位。

角色GBuffer 1各通道信息

GBuffer2存储法线信息。GBuffer3存储烘焙GI信息,同时角色的BackFace描边也是画到了GBuffer3上,后续描边不参与光照计算。

GBuffer3存储BakedGI和角色描边(图片进行了提亮)

Character Pre Pass

在毕设项目里,这个Pass用来画角色的高清刘海投影,最早是从这个大佬的文章里看到的方案。

【Unity URP】以Render Feature实现卡通渲染中的刘海投影 - 流朔的文章 - 知乎

实现原理就是画出角色没有被遮挡部分的头发Mask,在阴影采样时,基于光源的方向偏移屏幕空间坐标采样Mask,以得到相比于自投影更干净的刘海投影表现。同时由于最终阴影的采样基于屏幕空间,只是偏移屏幕空间坐标采样Mask信息作为刘海投影信息,所以需要在绘制头发Mask的时候就处理身体其他部位与头发的遮挡关系。一般做法是给这张RT一定的DepthBit,先绘制一遍面部和身体的深度信息到Depth Target上,再绘制头发,通过ZTest来实现和脸的前后关系。在这里,我把CharacterMask的绘制放到了GBuffer绘制之后,就可以利用GBuffer的深度信息进行ZTest,通过把绘制CharacterMask这个Pass的DepthTarget绑定为GBuffer得到的CameraDepthTexture,再将ColorTarget绑定给创建的CharacterMask,这样就能省去额外的面部、身体绘制,以及省去CharacterMask的DepthBit。

发影Mask的绘制和最终角色的阴影

上述的CharacterMask实际上只需要R通道的信息,在之前联创三的项目中,我还额外开了GB通道,用来做边缘光的Mask。简单讲一下思路,第一个Pass先绘制面部和身体原本的Mask到G通道中;第二个Pass绘制头发遮罩到R通道,用于后续绘制刘海投影;第三个Pass再利用角色身上存储的平滑法线信息,向角色内部进行收缩,绘制到B通道中。然后最终在光照计算时,只需要采样一次CharacterMask,就可以通过R通道获得刘海投影信息,通过G通道减去B通道获得边缘光遮罩信息。不过实际使用上来,光源对头发投影的偏移和对边缘光采样的偏移可能会需要不一样的方向和强度,所以可能还是需要做两次不同的偏移,分开采样刘海投影和边缘光。最终在毕设里我还是用屏幕空间等距边缘光,在光照计算时靠深度来计算边缘光的Mask。

RT中额外存储边缘光Mask

SSR

这次毕设的SSR效果我不够满意,最终在毕设的场景里也没有反射的表现,这一块的记录打算等到打磨好反射的效果,多尝试几个SSR方案后,再做梳理。这里简单提一下在开发SSR时对我帮助最大的一篇文章,文章里介绍了一个通过几个参数进行Lerp代替每一次步进都需进行空间变换的Tracing方法,能够省去一些循环内矩阵运算的开销。

屏幕空间反射 - zznewclear13

Deferred Pass

毕设项目里我用的是Unity默认的Stencil based deferred,我对其的原理是,通过两个Pass进行绘制,第一个Pass进行一次不写入Color双面渲染,双面前后对Stencil的同一位执行Invert操作,标记屏幕空间上需要计算光照的像素;第二个Pass只绘制球体的背面,利用Stencil Test判断需要计算光照的像素,计算光照结果,并清空上一个Pass标记的Stencil位数。

Stencil based deferred中每多一个光照模型,逐光源计算时就会多一个Lighting Pass。由于我们毕设中场景和角色使用不同的光照模型,所以Lighting Pass会有两个。

Stencil deferred shading光照计算流程

Post Process

后处理这边,除了常规的场景中调色、Bloom的后处理外,还加入了一些在特殊效果中使用的后处理效果,例如KuwaharaFilter制作油画效果、黑白闪、六边形像素化和一些故障艺术效果。

六边形像素化和故障艺术效果参考自毛佬的XPL库,里面有很多高质量的后处理效果,大家如有需要可以康康:https://github.com/QianMo/X-PostProcessing-Library

KuwaharaFilter的油画效果,则是寻找当前像素所在周围像素集合中,颜色方差最小的集合的平均颜色,以此来获得一个不同颜色之间边界清晰,同一颜色中有涂抹感的效果。具体的算法和原理可以看这个大佬的分享:UE4卡通渲染基础教程 Part4:Paint Filter - 呆晒晒的文章 - 知乎

风格化角色渲染

Ramp二分

角色的明暗二分通过Ramp图实现,在基础灯光颜色上,再叠加一层Ramp进行二分和调色的处理。具体实现上,以光照计算得到的NdotL作为采样Ramp贴图的U轴坐标,采样不同光照情况下的调色颜色,用以混合材质基础颜色和灯光颜色,得到最终的光影表现。在项目里,不同于常规把Ramp放在材质上,我把角色的Ramp和场景的Ramp统一放在了后处理中,在一个后处理环境下不同角色的Ramp是相同的,这样更适合我们这样产能有限的项目,同时也能更好地控制不同角色在同一场景中光照的统一性。

普通PBR、简单二分和利用Ramp的光照效果对比

边缘光

在基础的事实光影之外,我还加入了夸张化处理的边缘光,用于强调角色的轮廓表现。因为赛璐璐风格使用大色块铺色,容易使得主体对象与背景区分得不够清楚,尤其是在灯光不强的区域。如下图所示,边缘光的使用可以加大主体对象与背景的区分,同时我觉得边缘光可以让卡通角色显得更立体,添加带有二分感的边缘光可以让角色更有二次元味。在实现上,如同之前提及的,我使用屏幕空间等距边缘光来实现,利用GBuffer Pass渲染的场景深度信息来计算二分边缘光的遮罩。具体处理上,我在延迟光照计算时,对模型沿法线进行一定程度的外扩,对比外扩前后像素的深度差距,如果像素深度差距达到一定值,则判定为边缘光区域,对这个区域内的像素再叠加上一层光照表现,做进一步的边缘光效果处理。

边缘光添加前后对比

光源ID

同时,为了提升引擎中光源的可控性,我还在光源上添加了额外参数,用以控制角色的光照类型,为二次元风格渲染最重要的角色部分提供更高可控性的打光控制方式,这一点也是参考自Nerverwind大佬的分享。项目中,我提供了以下几种类型的角色灯光,分别是Coloring晕染灯光、Specular高光灯光、Rim Light边缘打光和Cel-Lighting二分灯光。其中晕染灯光不产生任何的阴影,仅仅是作为亮度和颜色去晕染角色,一般不会作为场景中影响角色的主光,而是作为一些氛围灯存在;高光灯光则不影响角色材质的漫反射表现,仅仅影响角色材质上的高光表现,只打出高光而不产生其他的亮度;边缘打光只在角色身上产生边缘光的效果,用于某些需要极强边缘光的场景,去构建镜头下的特定氛围;二分灯光会在角色身上产生二分色阴影,一般作为场景中明确出现的影响角色的主光,用于确保场景与角色在明暗关系上的统一。

不同光照类型灯光与组合效果

手绘阴影

在上述流程中的实时光影处理外,还加入了一些手绘阴影,用于表现模型上相互遮挡的自阴影和AO效果。通过在角色基础颜色贴图上手绘自阴影和阴影遮罩,在渲染GBuffer时,中读取阴影遮罩的信息,存储进GBuffer 1的R通道中,并在屏幕空间阴影计算时与场景阴影结合,以此来处理角色材质上的自阴影。

头发贴图上的手绘阴影

在给角色添加自阴影后,角色表现的层次感获得了不少的提升,并且在保证二分色块的基础上,进一步丰富角色材质上的细节表现。

自阴影贴图添加前后对比

面部SDF阴影

对于面部光影,使用的是目前比较常见的借助SDF计算的阴影。大概的原理是先手绘多个光照角度下面部的阴影情况,利用SDF来合并这些二分的阴影光照得到一张灰度图,渲染时用这张灰度图作为阴影阈值,与Front向量和Light向量的点积做比较,从而精确地控制光线在不同角度下脸部的阴影形状。由于《泉流心》使用的是延迟渲染管线,在光照计算时无法获取模型本身的信息,因此不能将面部阴影的处理放在光照计算中进行。为此,我将面部光影的计算前置到了GBuffer Pass中,并将面部的光影信息存储进前文提及的GBuffer 1的R通道,后续处理流程就和自阴影的处理一致。如下图所示,相较于使用常规的明暗计算,使用SDF得到的面部阴影表现能够有更多的动画感。

面部使用普通阴影和SDF阴影效果对比

刘海投影

目前《泉流石》中由于没有特殊处理角色的投影Pass,直接使用默认的Shadow Map得到的头发投影表现精度不足,容易在面部形成大量的锯齿,影响观感。最后使用了通过预渲染头发遮罩的处理方式,来采样得到高精度的发影表现。具体实现上,如下图所示,在绘制完成GBuffer后,创建一张额外RT,在这张贴图上只进行头发的绘制,由此来得到头发的遮罩,并和自阴影的处理一样,在屏幕空间阴影计算时与场景阴影结合,用于后续延迟光照计算时采样头发的投影信息。额外值得一提的是,常规处理流程中,需要提前额外绘制角色面部和深度,写入深度缓冲,以避免背后头发透视的异常表现。但是如前文所说,由于管线之前绘制了GBuffer,已经获取了角色其他部位的深度信息,所以可以将GBuffer的深度贴图绑定到头发遮罩绘制的深度缓冲上,在绘制头发时使用GBuffer中的深度信息进行比较,以此来节省渲染其他身体部位的开销。

不同渲染阶段的阴影处理与后续光照表现

角色描边

《泉流石》中使用的是BackFace描边。基本原理是借助一个额外的Pass,在VertexShader中将模型沿着表面法线方向进行一定程度的外扩,并在FragmentShader中对描边颜色进行处理,以此在模型外加上一层描边轮廓线。在细节上,模型硬表面上的顶点具有多个法线,导入Unity后引擎会将其视为具有各自不同法线朝向的不同顶点,导致直接使用法线外扩后,硬边部位的描边会有断裂的情况。所以在计算描边偏移时,需要使用平滑计算后的法线,用以确保进行法线外扩时,硬边部位的多个顶点法线朝向一致。为此,我在引擎中使用了喵刀老师的自动计算平滑法线的工具,在导入指定模型时计算平滑法线信息,并存储到顶点色的RG通道中。在进行外扩时,不再使用模型的法线,而是使用存储到顶点色中的平滑法线进行计算,得到如下图所示更为平滑的轮廓线效果。

不同描边处理方式效果对比

总结

很开心,也很感谢最早确定方向时组员们愿意和我一起去尝试做3D、尝试做二次元风格的项目,一方面自己确实收获到了很多经验,一开始我还没体会到,在现在慢慢开始正式工作的时候才意识到,很多共性的问题在做泉流石的时候就已经遇到过了,已经提前面对过这些问题,处理起来也得心应手的很多;另一方面,也算是实现了我想要做一个二次元美少女游戏的梦,虽然模型质量、渲染效果比起平均水准来说都差许多,但是因为基本是自己跑的角色全流程,所以还是持有很强的滤镜,觉得很好看。等到下次,再带着更多的经验,去突破更好看的次元壁吧!