前言
本文初发布于zhihu
https://zhuanlan.zhihu.com/p/170241589
近日,笔者在进行项目开发时,美术提出了一个需求,要让主角Player有个描边,而且被建筑遮挡的时候能有斜线使其显示出来。
听到这个需求,笔者首先想到的是用后处理把两个效果都做掉,然后便想起了URP的Render Feature,因为笔者曾经看过以Render Feature实现遮挡半透明效果的教程。不过不同的是,我们的项目使用的是HDRP,而HDRP中与URP的Render Feature相对应的强大功能为Custom Pass。
在学习相关内容时发现,目前关于Custom Pass的资料实在稀少,笔者便决定在自己完成效果并理解原理后写一篇文章作为笔记,也权且当做一个简单的Custom Pass入门教程提供给各位。
关于CustomPass
如上文所言,Custom Pass是个HDRP所独有的强大功能,它能够支持用户自定义Pass,并将其插入渲染管线的绝大部分位置。
而HDRP默认有两种Custom Pass,分别为DrawRenderersCustomPass与FullScreenCustomPass
两者的用途都正如其名,前者为专门渲染某类物体的Pass,后者为全屏后处理的Pass。
而在这次的效果实现中,我们就是要使用这两个Pass来完成。
如果你想知道更多关于CustomPass的内容,可以参考官方文档:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.3/manual/Custom-Pass.html
效果实现原理
虽然Custom Pass是个新功能,但是我们效果的基本原理其实跟传统的后处理做法是一样的,具体可参考下文
https://zhuanlan.zhihu.com/p/98663995
简单来说,遮挡高亮的原理就是以两个深度图来进行对比,一个深度图所存的是Player的深度,一个是场景的深度,当场景的深度值大于Player的深度值(此时的深度图均为Reverse-Z的,即“近大远小”)时,我们即可判定Player被遮挡,便可以在这个像素上进行一些操作了。
而后处理描边的原理可以参考这篇文章,言简意赅地说,就是想办法对我们需要绘制描边的那些物体额外绘制出一个“纯色”(物体为纯白或其他颜色,背景为纯黑)的buffer,然后对那个纯色buffer做边缘检测来进行描边。
https://zhuanlan.zhihu.com/p/95747680
只是这篇文章是利用Stencil Buffer来进行纯色buffer的绘制,我们则直接使用CustomPass即可。
可以说,本文的方法是典型的新瓶装旧酒。
注
*笔者所用Unity版本为2019.4.6f1,HDRP 7.3.1
*笔者经验甚少,难以避免文中出现错误,还请大家不吝斧正,只求轻喷。
本文代码参考自:https://github.com/alelievr/HDRP-Custom-Passes
且使用了其中的InnerColor图片(被遮挡时的那个斜线Texture):
这个工程中还有许多CustomPass的使用示例,相当值得学习。
绘制“纯色Buffer”及Player深度
那么我们正式动工。
在HDRP默认场景中创建一个Empty Object,然后给它添加Custom Pass Volume
之后它点击右下角的"+",选择DrawRenderersCustomPass,
我们暂时不去修改它的默认值。
那么以Create/Shader/HDRP/Custom Renderers Pass创建我们所需的Pass,并以它创建Material,放进刚刚创建的CustomPass的Overrides/Material中,当然,现在还没什么变化。
那么打开Shader,让我们来粗略地看一下它
1 | Shader "Renderers/NewRenderersCustomPass" |
粗略地看一看,就可以发现这个Shader的写法还是挺古怪的,颜色的输出在GetSurfaceAndBuiltinData()中,而具体的vert和frag都不涉及。
官方也在这个默认Shader中添加了许多注释,如果你想要更加深入地学习,推荐把注释都读一遍。
那么回想一下我们的目的,我们打算“将Player以白色渲染到一张黑底图片上”。那么写起来也非常简单了,我们只需要让输出的color永远为白色(1, 1, 1)即可。
因此,我们只需要将GetSurfaceAndBuiltinData()修改一下:
1 | void GetSurfaceAndBuiltinData(FragInputs fragInputs, float3 viewDirection, inout PositionInputs posInput, out SurfaceData surfaceData, out BuiltinData builtinData) |
哦差点忘了,我们还没写入深度!要记得把Pass中的ZWrite Off改成Zwrite On~
(当然,我们可以尽可能地把Shader中没用到那些内容删除干净,不过这并不是本文的重点,就不多提了)
保存Shader之后,我们会发现场景内所有物体都变成了白色
由于一些后处理的存在,这些物体的颜色不是纯白
此时我们便需要调整Custom Pass中的一些设置了
将Target Color Buffer 和Target Depth Buffer都设置为Custom,前者的设置是因为我们要让物体的“白色”绘制在另一张图上,而Depth Buffer的设置便是“单独绘制Player深度”了,相当便利。
而ClearFlag的设置,就是让我们在每次调用这两个Buffer之前,都将这两个Buffer清空。
之后,我们便需要为Player单独创立一个层,并且将我们要当做Player的物体的层设置为"Player",我选择的是场景中的Workbench。
此时我们看场景,依旧与最初没有区别,但这并不代表我们之前是白干的,让我们打开Frame Debugger,找到Custom Pass的内容,便可以看见我们已经切实地“将Player以白色画在了黑底的图上”。
而Custom Depth Buffer在Frame Debugger中看不到,笔者这里通过RenderDoc来查看,可以看见Workbench的深度被单独绘制了。
而图像上下翻转,应当是DX与OpenGL的纹理坐标轴不同的缘故,无伤大雅。
绘制描边与遮挡显示
那么我们来到了第二步,为了让效果更明显,我们先放一个Plane挡住一半的Workbench,就跟头图一样。
和第一步类似,我们要先在Custom Pass中新建一个FullScreenCustomPass,然后新建一个Create/Shader/HDRP/Custom FullScreen Pass,并以其创建一个Material放入Custom Pass中,依旧没什么效果。
那么我们打开Shader看看
1 | Shader "FullScreen/NewFullScreenCustomPass" |
和之前的Custom Renderers Pass写法很是相似,我们只需要关注FullScreenPass()即可,不过这次这个函数就是Fragment Shader。
笔者最初使用时以为跟以前的OnRenderImage一样,以为我们是拿到了后处理前的CameraTexture,然后在它的基础上进行处理。但是眼尖的朋友可以发现,这个Shader中有一个Blend SrcAlpha OneMinusSrcAlpha,那就说明我们渲染的东西是个半透明的Texture。
更通俗来讲,Custom Renderers Pass就是渲染出一个图片与我们的场景图混合,默认的混合方式就是这个透明度混合。
描边方式的选择
那么我们先进行描边操作,后处理描边可以使用Sobel/Prewitt等算子进行卷积计算,我最初也是那么打算的,之前那个使用Stencil Buffer做描边的文章也确实是那么做的,但是笔者参考了前言中的那个Github工程后发现,即便是只“上下左右采样四次取最大值”带来的效果也同样不错。
而这个似乎就叫Max Filter,也就是“对 data 进行滤波,用邻域 r 内的最大值替换图像中的值”。
但是如果只是如此,会导致物体内部一片纯色,因此的时候还需要减去原本Custom Color所绘制的纯色区域,留下的就是描边了。正因为还需要这一步,Max Filter并不能通用在大部分以法线或深度来检测边缘的场合,而反过来讲,Max Filter也是在本项目中的特化方法。
笔者将两个方法都做了一次:
可以看见,基本没什么区别,但如果仔细看的话,可以发现Sobel的描边会比Max的更圆滑一些,但要看出这个也确实是需要写轮眼的功力了,而且这也只是Max用上下左右4次采样而已,它也可以再加斜着的四个方向,还是比Sobel少采样一次呢。
不过,即便Max Filter比我们常用的Sobel Filter更风光,但别忘了我们还有Roberts算子(),它也只用采样四次。那么同样的,笔者也尝试了一下:
显然,效果也不错
好吧,Roberts确实也不错,而它也不用减去纯色的那步骤……
即便如此,我相信以这种常见方式进行描边大家也都看腻了,笔者在此还是选择Max Filter,权当一次思维拓展吧。各位在实践的时候大可以选择以Roberts算子来描边。
正式动工
首先,我们要定义每次采样的不同方向。
以下的代码部分写在FullScreenPass之前。
1 |
|
offsets数组便是我们所存的各次采样的方向,前四个值代表上下右左四个方向,之后的四个为斜向45°的四个方向,c45即为cos(45°)。而c225则是代表cos(22.5°),s即为sin。这个采样数组的设置也是为了方便之后对采样次数的调节,我们接下来就马上会看到。
那么为了描边,我们需要一个调整采样次数的变量,以及调整描边宽度及描边颜色的变量
因此在开头添加Properties:
1 | Properties |
记得要将它们再声明一次。
那么描边的内容比较少,也比较简单,大家光看注释相信也能懂:
1 | float4 FullScreenPass(Varyings varyings): SV_Target |
要注意的是LoadCustomColor()使用的是positionSS(Screen Space),而SampleCustomColor()使用的是positionNDC。当然如果想用SampleCustomColor()取得该像素的颜色也是可以的,只是笔者这里示范一下两个函数的用法。
那么我们保存Shader,便可以看见场景中Player层的物体已经被描上一层黄边了。
接下来就是遮挡显示的内容了,为了实现被遮挡的内容,我们需要定义斜线填充图的颜色、斜线图的Texture以及这Texture的重复次数。
1 | Properties |
实际的代码量同样很少,也比较简单:
1 | ...... |
那么保存Shader之后,就大功告成了。
笔者把遮挡颜色调为了橙色,效果还是不错的
如果之后美术提出,要让敌人也有描边,而且描边颜色还得不同,那简直是轻而易举,相信读完本文的人是一定会做的。
总结
做了这个简单的案例之后,我们便学会了Custom Pass的基本用法,也了解了它的基本原理,虽然它显得很高大上,不过用起来其实还是比较方便的。
希望这篇文章能够对将来学习HDRP Custom Pass相关内容的人有所帮助。
也感谢在此领域不断开拓的前人,让我们能够比较容易地学到这些内容。
参考资料
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.3/manual/Custom-Pass.html
https://www.bilibili.com/video/BV1iE411i7Ei
https://zhuanlan.zhihu.com/p/98663995
https://zhuanlan.zhihu.com/p/95747680
https://github.com/alelievr/HDRP-Custom-Passes
https://reference.wolfram.com/language/ref/MaxFilter.html
感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。